feat(01-02): add ball physics — movement, wall bounce, zone deflection, and GameConfig
- Add GameConfig with initialBallSpeed (220px/s), speedIncrement (18px/s), paddleSpeed (400px/s) - Add Physics.serveBall(): centers ball, sets random angle (-45 to +45 deg), random left/right dir - Replace Physics.update() skeleton with full ball movement and wall bounce logic - Add Physics._checkPaddleCollision() with 5-zone angle deflection and speed increment per hit - Ball exits left/right bounds → immediate re-serve from center - Ball pushed out of paddle on collision to prevent double-hit next frame - Add on-canvas speed debug display (Phase 1 verification aid)
This commit is contained in:
106
index.html
106
index.html
@@ -13,6 +13,12 @@
|
||||
<body>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<script>
|
||||
const GameConfig = {
|
||||
initialBallSpeed: 220, // px/s — starting speed per serve
|
||||
speedIncrement: 18, // px/s added per paddle hit
|
||||
paddleSpeed: 400 // px/s — Player 1 paddle movement
|
||||
};
|
||||
|
||||
const Renderer = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
@@ -127,9 +133,9 @@
|
||||
// Position paddle1 on left side, vertically centered
|
||||
GameState.paddle1.x = 30;
|
||||
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
|
||||
// Ball positioned center — physics will drive it in Plan 02
|
||||
GameState.ball.x = width / 2;
|
||||
GameState.ball.y = height / 2;
|
||||
GameState.paddle1.speed = GameConfig.paddleSpeed;
|
||||
// Serve ball from center
|
||||
this.serveBall();
|
||||
},
|
||||
|
||||
onResize(width, height) {
|
||||
@@ -137,13 +143,93 @@
|
||||
this.height = height;
|
||||
},
|
||||
|
||||
serveBall() {
|
||||
const ball = GameState.ball;
|
||||
ball.x = this.width / 2;
|
||||
ball.y = this.height / 2;
|
||||
ball.speed = GameConfig.initialBallSpeed;
|
||||
|
||||
// Random vertical angle between -45 and +45 degrees, always heading right
|
||||
const angle = (Math.random() * 90 - 45) * Math.PI / 180;
|
||||
// Randomly serve left or right
|
||||
const dir = Math.random() < 0.5 ? 1 : -1;
|
||||
ball.vx = Math.cos(angle) * ball.speed * dir;
|
||||
ball.vy = Math.sin(angle) * ball.speed;
|
||||
},
|
||||
|
||||
update(deltaTime) {
|
||||
// Move paddle1 based on input
|
||||
const dir = Input.getVerticalInput();
|
||||
const ball = GameState.ball;
|
||||
const paddle = GameState.paddle1;
|
||||
|
||||
// --- Move paddle1 ---
|
||||
const dir = Input.getVerticalInput();
|
||||
paddle.y += dir * paddle.speed * deltaTime;
|
||||
// Clamp to canvas bounds
|
||||
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
|
||||
|
||||
// --- Move ball ---
|
||||
ball.x += ball.vx * deltaTime;
|
||||
ball.y += ball.vy * deltaTime;
|
||||
|
||||
// --- Wall bounce (top and bottom) ---
|
||||
if (ball.y - ball.radius < 0) {
|
||||
ball.y = ball.radius;
|
||||
ball.vy = Math.abs(ball.vy);
|
||||
}
|
||||
if (ball.y + ball.radius > this.height) {
|
||||
ball.y = this.height - ball.radius;
|
||||
ball.vy = -Math.abs(ball.vy);
|
||||
}
|
||||
|
||||
// --- Ball out of bounds (left or right) → reset ---
|
||||
if (ball.x + ball.radius < 0 || ball.x - ball.radius > this.width) {
|
||||
this.serveBall();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Paddle collision (only check when ball moving left toward paddle1) ---
|
||||
if (ball.vx < 0) {
|
||||
this._checkPaddleCollision(ball, paddle);
|
||||
}
|
||||
},
|
||||
|
||||
_checkPaddleCollision(ball, paddle) {
|
||||
// Speed is uncapped per CONTEXT.md. Tunneling risk begins when ball travels
|
||||
// more than paddle.height px/frame. At 60fps, that's paddle.height * 60 px/s.
|
||||
// With height=80: tunneling risk above ~4800 px/s. Current rallies max ~800px/s.
|
||||
// If Phase 5 introduces higher speeds, add substep physics here.
|
||||
|
||||
// AABB: check ball center against paddle bounds (with ball radius buffer)
|
||||
const inX = ball.x - ball.radius < paddle.x + paddle.width &&
|
||||
ball.x + ball.radius > paddle.x;
|
||||
const inY = ball.y + ball.radius > paddle.y &&
|
||||
ball.y - ball.radius < paddle.y + paddle.height;
|
||||
|
||||
if (!inX || !inY) return;
|
||||
|
||||
// Zone-based deflection: divide paddle into 5 zones
|
||||
const relativeHitPos = (ball.y - paddle.y) / paddle.height;
|
||||
const hitZone = Math.max(0, Math.min(4, Math.floor(relativeHitPos * 5)));
|
||||
|
||||
// Map zone to angle (degrees) — positive = downward
|
||||
const anglesDeg = [
|
||||
-60, // Zone 0: top edge — steeply upward
|
||||
-30, // Zone 1: upper — angled upward
|
||||
5, // Zone 2: center — nearly flat (slight downward to avoid infinite horizontal)
|
||||
30, // Zone 3: lower — angled downward
|
||||
60 // Zone 4: bottom edge — steeply downward
|
||||
];
|
||||
|
||||
const angleRad = anglesDeg[hitZone] * Math.PI / 180;
|
||||
|
||||
// Increment speed
|
||||
ball.speed += GameConfig.speedIncrement;
|
||||
|
||||
// Decompose into velocity components, always going right after hitting paddle1
|
||||
ball.vx = Math.cos(angleRad) * ball.speed;
|
||||
ball.vy = Math.sin(angleRad) * ball.speed;
|
||||
|
||||
// Push ball out of paddle to prevent double-collision next frame
|
||||
ball.x = paddle.x + paddle.width + ball.radius + 1;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,10 +254,16 @@
|
||||
Physics.update(deltaTime);
|
||||
|
||||
Renderer.clear();
|
||||
|
||||
// Debug: show ball speed (keep for Phase 1 verification)
|
||||
Renderer.ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
Renderer.ctx.font = '14px monospace';
|
||||
Renderer.ctx.fillText('speed: ' + Math.round(GameState.ball.speed), 10, 20);
|
||||
|
||||
// Draw paddle1
|
||||
const p1 = GameState.paddle1;
|
||||
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
|
||||
// Draw ball placeholder (will move in Plan 02)
|
||||
// Draw ball
|
||||
const b = GameState.ball;
|
||||
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user