330 lines
13 KiB
Markdown
330 lines
13 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/dabit/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/dabit/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Key contracts from Plan 01 that this plan extends -->
|
|
|
|
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
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Ball physics — movement, wall bounce, serve, and GameConfig</name>
|
|
<files>index.html</files>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>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.</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Paddle-ball collision refinement and speed acceleration verification</name>
|
|
<files>index.html</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>
|
|
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
|
|
</automated>
|
|
</verify>
|
|
<done>
|
|
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)
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
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.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` documenting:
|
|
- Physics implementation details (speed values, angle map, tunneling comment)
|
|
- GameConfig constants and their rationale
|
|
- How Phase 2 should extend Physics (add second paddle, scoring — without touching zone deflection logic)
|
|
- Any tuning done to angles or speed increment during verification
|
|
</output>
|