---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on: [01-01]
files_modified: [index.html]
autonomous: true
requirements: [CORE-01, CORE-02, CORE-03, CORE-04]
must_haves:
truths:
- "Ball moves continuously across the canvas — it never stops"
- "Ball bounces off the top wall and bottom wall predictably (angle of incidence = angle of reflection)"
- "Ball deflects off Player 1 paddle when they collide"
- "Ball deflects at different angles depending on which zone of the paddle it hits (top edge ~60 degrees, center ~5 degrees)"
- "Ball speed increases after each paddle hit and resets when it passes the left or right edge"
- "Ball serves automatically in a random direction on start and after each reset"
artifacts:
- path: "index.html"
provides: "Complete Physics.update() with ball movement, wall bounce, paddle collision, zone deflection, speed increment"
contains: "ball.vx *= -1, zone, angles, ball.speed"
- path: "index.html"
provides: "Ball serve function that initializes ball velocity"
contains: "serveBall"
- path: "index.html"
provides: "Speed increment constant tunable via GameConfig"
contains: "GameConfig"
key_links:
- from: "Physics.update()"
to: "ball position"
via: "ball.x += ball.vx * deltaTime; ball.y += ball.vy * deltaTime"
pattern: "ball\\.x.*deltaTime"
- from: "wall bounce logic"
to: "ball.vy"
via: "Math.abs(ball.vy) inversion on top/bottom boundary crossing"
pattern: "Math\\.abs.*vy"
- from: "zone deflection"
to: "ball.vx / ball.vy"
via: "relativeHitPos -> hitZone -> angle -> cos/sin decomposition"
pattern: "relativeHitPos|hitZone|getPaddleAngle"
- from: "speed increment"
to: "ball.speed"
via: "ball.speed += GameConfig.speedIncrement on each paddle hit"
pattern: "speedIncrement"
---
Complete the physics simulation: ball moves continuously, bounces off walls and paddles, deflects at zone-based angles, and accelerates with each rally. After this plan, the full Phase 1 experience is playable — Player 1 can hit a moving ball with angle control and watch speed build up over a rally.
Purpose: Completes Phase 1. All 6 success criteria become verifiable.
Output: Fully updated index.html with working ball physics replacing the Plan 01 skeleton.
@/home/dabit/.claude/get-shit-done/workflows/execute-plan.md
@/home/dabit/.claude/get-shit-done/templates/summary.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
GameState (from Plan 01):
```javascript
GameState.ball = {
x, y, // current position (logical pixels)
radius: 8,
vx, vy, // velocity (logical pixels per second)
speed, // current scalar speed (px/s)
color: '#fff'
};
GameState.paddle1 = {
x, y, // current position
width: 12, height: 80,
speed: 400, // movement speed (px/s)
color: '#fff'
};
```
Physics object (skeleton from Plan 01 — replace update() and init() fully):
```javascript
Physics.init(width, height) // called once at startup
Physics.onResize(width, height) // called on window resize
Physics.update(deltaTime) // called every frame — REPLACE THIS
```
Renderer helpers available:
```javascript
Renderer.drawRect(x, y, w, h, color)
Renderer.drawCircle(x, y, radius, color)
Renderer.logicalWidth
Renderer.logicalHeight
```
Task 1: Ball physics — movement, wall bounce, serve, and GameConfig
index.html
Replace the Physics.init() and Physics.update() skeleton from Plan 01 with the full implementation. Also add a GameConfig object and serveBall() function.
**Add GameConfig object** (place before GameState in the script):
```javascript
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
};
```
**Update Physics.init()** — initialize ball with a served velocity:
```javascript
init(width, height) {
this.width = width;
this.height = height;
// Position paddle1 on left side, vertically centered
GameState.paddle1.x = 30;
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
GameState.paddle1.speed = GameConfig.paddleSpeed;
// Serve ball from center
this.serveBall();
},
```
**Add serveBall() method to Physics:**
```javascript
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;
},
```
**Replace Physics.update()** with full ball physics:
```javascript
update(deltaTime) {
const ball = GameState.ball;
const paddle = GameState.paddle1;
// --- Move paddle1 ---
const dir = Input.getVerticalInput();
paddle.y += dir * paddle.speed * deltaTime;
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);
}
},
```
**Add _checkPaddleCollision() method to Physics:**
```javascript
_checkPaddleCollision(ball, paddle) {
// 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;
},
```
After implementing, also update the GameLoop.main() render block to draw the ball using `GameState.ball` (it already does this from Plan 01 — confirm it still works with the new state).
Open index.html in browser — ball should immediately start moving from center. Observe: (1) ball bounces off top wall, (2) ball bounces off bottom wall, (3) when ball exits left or right edge it reappears at center with a new serve direction. Console.log not required but no errors should appear.
Ball moves continuously at ~220 px/s initial speed, bounces off top and bottom walls, and serves from center on load and after going out of bounds left/right. Browser console shows zero errors.
Task 2: Paddle-ball collision refinement and speed acceleration verification
index.html
With ball physics running, verify and refine collision behavior:
**1. Test and tune zone deflection angles:**
Hit the paddle at the top edge — ball should deflect upward steeply (~60 degrees from horizontal).
Hit the paddle at center — ball should deflect nearly flat (~5 degrees).
Hit the paddle at the bottom edge — ball should deflect downward steeply (~60 degrees).
If angles feel wrong, adjust the `anglesDeg` array values within these constraints from CONTEXT.md:
- Top/bottom edge: ~60 degrees
- Upper/lower zone: ~30 degrees
- Center: ~5 degrees (not exactly 0 — avoids infinite horizontal loop)
**2. Add speed display for verification (debug only):**
In GameLoop.main(), after Renderer.clear() and before drawing objects, add:
```javascript
// Debug: show ball speed (remove after 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);
```
This lets the tester confirm speed increases by reading the on-canvas number during play.
**3. Verify speed resets on serve:**
After ball exits bounds, `serveBall()` sets `ball.speed = GameConfig.initialBallSpeed`. Confirm the debug display resets to 220 (or the configured initial speed) after each out-of-bounds.
**4. Confirm no tunneling at high speed:**
After ~10 paddle hits the ball will be moving at ~220 + (10 * 18) = 400 px/s. At 60 FPS, that's ~6.7 px/frame. Paddle height is 80px — well within safe range (max ~4800 px/s before tunneling risk at 60 FPS). Document in code comment:
```javascript
// 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.
```
Place this comment inside `_checkPaddleCollision()`.
**5. Remove debug speed display before final commit** — or keep it if the plan verifier needs to confirm speed increase. Keep it for Phase 1 verification; it costs nothing.
Open index.html in browser and perform these manual checks:
1. Ball bounces continuously without stopping — CORE-01
2. Move paddle into ball path — ball deflects off paddle — CORE-02
3. Hit ball at top of paddle — steep upward angle. Hit at center — flat. Hit at bottom — steep downward — CORE-03
4. Rally 10+ times — speed display number increases each hit, resets to 220 on out-of-bounds — CORE-04
5. Press W — paddle moves up immediately. Press S — paddle moves down immediately — CORE-07
6. On HiDPI display — ball edges are crisp — VFX-05
All Phase 1 success criteria verified:
- Canvas renders sharply on Retina/HiDPI (VFX-05)
- Ball moves continuously and bounces off top/bottom walls (CORE-01)
- Player 1 paddle deflects ball (CORE-02)
- Ball angle changes per paddle hit zone (CORE-03)
- Ball speed increases each paddle hit, resets on serve (CORE-04)
- W/S keys move paddle responsively (CORE-07)
Open index.html in browser and verify all 5 Phase 1 success criteria:
1. Canvas renders sharply on HiDPI — no blurry edges on ball or paddle
2. Ball moves continuously from first serve — bounces off top and bottom walls without slowing
3. Player 1 paddle moves up on W, down on S — immediate response, no lag
4. Hit ball at top/center/bottom of paddle — observe three distinctly different deflection angles
5. Rally 10 times — on-canvas speed display shows increasing value; on serve, resets to initial speed
Also verify: window resize does not break ball trajectory or throw console errors.
- Ball moves and bounces continuously (CORE-01)
- Paddle deflects ball correctly (CORE-02)
- Zone-based angle deflection produces 5 visually distinct trajectories (CORE-03)
- Speed increments per hit, resets per serve — confirmed via on-canvas debug display (CORE-04)
- All requirements CORE-01, CORE-02, CORE-03, CORE-04 met
- index.html runs as a single static file with no build step