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>
|
<body>
|
||||||
<canvas id="gameCanvas"></canvas>
|
<canvas id="gameCanvas"></canvas>
|
||||||
<script>
|
<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 = {
|
const Renderer = {
|
||||||
canvas: null,
|
canvas: null,
|
||||||
ctx: null,
|
ctx: null,
|
||||||
@@ -127,9 +133,9 @@
|
|||||||
// Position paddle1 on left side, vertically centered
|
// Position paddle1 on left side, vertically centered
|
||||||
GameState.paddle1.x = 30;
|
GameState.paddle1.x = 30;
|
||||||
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
|
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
|
||||||
// Ball positioned center — physics will drive it in Plan 02
|
GameState.paddle1.speed = GameConfig.paddleSpeed;
|
||||||
GameState.ball.x = width / 2;
|
// Serve ball from center
|
||||||
GameState.ball.y = height / 2;
|
this.serveBall();
|
||||||
},
|
},
|
||||||
|
|
||||||
onResize(width, height) {
|
onResize(width, height) {
|
||||||
@@ -137,13 +143,93 @@
|
|||||||
this.height = height;
|
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) {
|
update(deltaTime) {
|
||||||
// Move paddle1 based on input
|
const ball = GameState.ball;
|
||||||
const dir = Input.getVerticalInput();
|
|
||||||
const paddle = GameState.paddle1;
|
const paddle = GameState.paddle1;
|
||||||
|
|
||||||
|
// --- Move paddle1 ---
|
||||||
|
const dir = Input.getVerticalInput();
|
||||||
paddle.y += dir * paddle.speed * deltaTime;
|
paddle.y += dir * paddle.speed * deltaTime;
|
||||||
// Clamp to canvas bounds
|
|
||||||
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
|
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);
|
Physics.update(deltaTime);
|
||||||
|
|
||||||
Renderer.clear();
|
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
|
// Draw paddle1
|
||||||
const p1 = GameState.paddle1;
|
const p1 = GameState.paddle1;
|
||||||
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
|
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;
|
const b = GameState.ball;
|
||||||
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user