docs(01-foundation): create phase plan
This commit is contained in:
335
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
335
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified: [index.html]
|
||||
autonomous: true
|
||||
requirements: [VFX-05, CORE-07]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Canvas fills the entire browser window with no scrollbars or white space"
|
||||
- "Canvas resizes immediately when the window is resized, maintaining minimum 4:3 aspect ratio"
|
||||
- "Graphics render sharply on HiDPI/Retina displays (no blurry ball or paddle edges)"
|
||||
- "Pressing W moves Player 1 paddle up; pressing S moves it down — responsively with no OS key-repeat lag"
|
||||
- "Game loop runs continuously at device refresh rate; GameLoop, Renderer, Input objects exist in global scope"
|
||||
artifacts:
|
||||
- path: "index.html"
|
||||
provides: "Complete HTML scaffold with embedded CSS, canvas element, and all four module objects"
|
||||
contains: "GameLoop, Physics, Renderer, Input"
|
||||
- path: "index.html"
|
||||
provides: "HiDPI canvas setup — devicePixelRatio scaling applied on init and resize"
|
||||
contains: "devicePixelRatio"
|
||||
- path: "index.html"
|
||||
provides: "Full-window canvas with aspect ratio enforcement and resize handler"
|
||||
contains: "window.addEventListener('resize'"
|
||||
- path: "index.html"
|
||||
provides: "Keyboard input state tracking for W/S keys"
|
||||
contains: "KeyW, KeyS"
|
||||
key_links:
|
||||
- from: "GameLoop.main()"
|
||||
to: "requestAnimationFrame"
|
||||
via: "window.requestAnimationFrame(this.main.bind(this))"
|
||||
pattern: "requestAnimationFrame"
|
||||
- from: "Renderer.init()"
|
||||
to: "devicePixelRatio"
|
||||
via: "canvas bitmap scale + ctx.scale(dpr, dpr)"
|
||||
pattern: "devicePixelRatio"
|
||||
- from: "Input.getVerticalInput()"
|
||||
to: "GameLoop update cycle"
|
||||
via: "Physics.update() queries Input.getVerticalInput() each frame"
|
||||
pattern: "getVerticalInput"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the complete HTML scaffold for Super Pong Next Gen: a single index.html with embedded CSS, a full-window HiDPI-aware canvas, and the four core module objects (GameLoop, Renderer, Input, Physics skeleton). After this plan, the canvas renders sharply on Retina displays, fills the window with correct aspect ratio on resize, the game loop is running, and Player 1 can move a visible paddle with W/S keys.
|
||||
|
||||
Purpose: Every subsequent phase builds on this foundation. The module structure established here is the architecture all later phases extend.
|
||||
Output: index.html — runnable directly in a browser with no build step.
|
||||
</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/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: HTML scaffold, HiDPI canvas, and full-window Renderer</name>
|
||||
<files>index.html</files>
|
||||
<action>
|
||||
Create index.html from scratch. It must contain exactly:
|
||||
|
||||
**HTML structure:**
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Super Pong Next Gen</title>
|
||||
<style>...</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<script>...</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**CSS (inside `<style>`):**
|
||||
- `* { margin: 0; padding: 0; box-sizing: border-box; }`
|
||||
- `body { background: #000; overflow: hidden; width: 100vw; height: 100vh; }`
|
||||
- `#gameCanvas { display: block; background: #000; }`
|
||||
|
||||
**Renderer object (inside `<script>`):**
|
||||
```javascript
|
||||
const Renderer = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
logicalWidth: 0,
|
||||
logicalHeight: 0,
|
||||
|
||||
init() {
|
||||
this.canvas = document.getElementById('gameCanvas');
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
},
|
||||
|
||||
resize() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
// Enforce minimum 4:3 aspect ratio — canvas fills window
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const minAspect = 4 / 3;
|
||||
const currentAspect = w / h;
|
||||
|
||||
// Always fill entire window; use logical coords for game
|
||||
this.logicalWidth = w;
|
||||
this.logicalHeight = h;
|
||||
|
||||
// Enforce minimum 4:3: if narrower than 4:3, treat as 4:3 height
|
||||
// (window fills screen; minimum 4:3 means we never let height dominate width)
|
||||
if (currentAspect < minAspect) {
|
||||
// Too tall — game uses 4:3 minimum: constrain logical height
|
||||
this.logicalHeight = Math.floor(w / minAspect);
|
||||
}
|
||||
|
||||
// Step 1: Set CSS size to logical dimensions
|
||||
this.canvas.style.width = this.logicalWidth + 'px';
|
||||
this.canvas.style.height = this.logicalHeight + 'px';
|
||||
|
||||
// Step 2: Scale bitmap to device pixel ratio
|
||||
this.canvas.width = Math.floor(this.logicalWidth * dpr);
|
||||
this.canvas.height = Math.floor(this.logicalHeight * dpr);
|
||||
|
||||
// Step 3: Scale context so all draw calls use logical pixels
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.ctx.scale(dpr, dpr);
|
||||
|
||||
// Notify Physics of new dimensions (Physics may not exist yet)
|
||||
if (typeof Physics !== 'undefined' && Physics.onResize) {
|
||||
Physics.onResize(this.logicalWidth, this.logicalHeight);
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
|
||||
},
|
||||
|
||||
drawRect(x, y, w, h, color) {
|
||||
this.ctx.fillStyle = color || '#fff';
|
||||
this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
|
||||
},
|
||||
|
||||
drawCircle(x, y, radius, color) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(Math.round(x), Math.round(y), radius, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = color || '#fff';
|
||||
this.ctx.fill();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Place the full Renderer object in the script tag. Do not initialize yet (initialization happens at bottom of script after all objects are defined).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>Open index.html in browser — canvas fills window, background is black, no scrollbars. Resize window — canvas adjusts. No JS console errors.</automated>
|
||||
</verify>
|
||||
<done>index.html exists with valid HTML5 structure, black full-window canvas, Renderer object with HiDPI-aware resize() method using devicePixelRatio. Canvas fills browser window on load and on resize.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: GameLoop, Input module, and Player 1 paddle rendering</name>
|
||||
<files>index.html</files>
|
||||
<action>
|
||||
Add to the existing `<script>` tag in index.html (after Renderer, before initialization code):
|
||||
|
||||
**GameState object** — holds all mutable game state:
|
||||
```javascript
|
||||
const GameState = {
|
||||
paddle1: {
|
||||
x: 0, y: 0, // Set on Physics.init()
|
||||
width: 12, height: 80,
|
||||
speed: 400, // Logical pixels per second
|
||||
color: '#fff'
|
||||
},
|
||||
ball: {
|
||||
x: 0, y: 0, // Set on Physics.init()
|
||||
radius: 8,
|
||||
vx: 0, vy: 0, // Set on Physics.init()
|
||||
speed: 0, // Current scalar speed
|
||||
color: '#fff'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Input object:**
|
||||
```javascript
|
||||
const Input = {
|
||||
keys: { w: false, s: false },
|
||||
|
||||
init() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyW') { this.keys.w = true; e.preventDefault(); }
|
||||
if (e.code === 'KeyS') { this.keys.s = true; e.preventDefault(); }
|
||||
});
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.code === 'KeyW') this.keys.w = false;
|
||||
if (e.code === 'KeyS') this.keys.s = false;
|
||||
});
|
||||
},
|
||||
|
||||
getVerticalInput() {
|
||||
if (this.keys.w) return -1;
|
||||
if (this.keys.s) return 1;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Physics object (skeleton — full physics in Plan 02):**
|
||||
```javascript
|
||||
const Physics = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
||||
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;
|
||||
// Ball positioned center — physics will drive it in Plan 02
|
||||
GameState.ball.x = width / 2;
|
||||
GameState.ball.y = height / 2;
|
||||
},
|
||||
|
||||
onResize(width, height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
},
|
||||
|
||||
update(deltaTime) {
|
||||
// Move paddle1 based on input
|
||||
const dir = Input.getVerticalInput();
|
||||
const paddle = GameState.paddle1;
|
||||
paddle.y += dir * paddle.speed * deltaTime;
|
||||
// Clamp to canvas bounds
|
||||
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**GameLoop object:**
|
||||
```javascript
|
||||
const GameLoop = {
|
||||
stopHandle: null,
|
||||
lastTime: 0,
|
||||
|
||||
start() {
|
||||
this.lastTime = performance.now();
|
||||
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (this.stopHandle) cancelAnimationFrame(this.stopHandle);
|
||||
},
|
||||
|
||||
main(currentTime) {
|
||||
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
|
||||
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // Cap at 50ms
|
||||
this.lastTime = currentTime;
|
||||
|
||||
Physics.update(deltaTime);
|
||||
|
||||
Renderer.clear();
|
||||
// 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)
|
||||
const b = GameState.ball;
|
||||
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Initialization block (at bottom of script):**
|
||||
```javascript
|
||||
// Initialize all modules
|
||||
Renderer.init();
|
||||
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
|
||||
Input.init();
|
||||
GameLoop.start();
|
||||
```
|
||||
|
||||
The deltaTime cap of 50ms prevents the physics from exploding if the tab loses focus and resumes (a single large delta would fling the ball off screen).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>Open index.html in browser — white paddle visible on left side, white ball dot visible at center. Press W: paddle moves up smoothly. Press S: paddle moves down smoothly. Paddle stops at top/bottom canvas edges. No console errors. Holding W while resizing window should not break anything.</automated>
|
||||
</verify>
|
||||
<done>index.html renders a visible paddle on the left side that responds immediately to W/S keyboard input with no OS key-repeat lag. Ball dot is visible at center. Game loop running at device refresh rate. All four objects (GameLoop, Physics, Renderer, Input) exist and are initialized. Canvas remains HiDPI-correct on resize.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Open index.html in a browser:
|
||||
1. Canvas fills entire window, black background, no scrollbars
|
||||
2. White paddle visible on left side (x~30), white ball dot at center
|
||||
3. Press W — paddle moves up immediately (no lag)
|
||||
4. Press S — paddle moves down immediately (no lag)
|
||||
5. Paddle stops at top and bottom edges (clamped)
|
||||
6. Resize window — canvas resizes, paddle stays visible
|
||||
7. On Retina/HiDPI display — paddle and ball edges are crisp, not blurry
|
||||
8. Browser console shows zero errors
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- index.html exists and opens without errors in Chrome/Firefox/Safari
|
||||
- Canvas fills window, renders sharp on HiDPI (VFX-05)
|
||||
- Player 1 W/S input moves paddle responsively (CORE-07)
|
||||
- GameLoop, Renderer, Physics, Input, GameState objects all exist in script scope
|
||||
- Resize does not break layout or throw errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` documenting:
|
||||
- What was built (objects created, their responsibilities)
|
||||
- Key implementation decisions (deltaTime cap, HiDPI resize approach, logical vs bitmap coords)
|
||||
- Patterns established for Phase 2 to follow
|
||||
- Any deviations from the plan and why
|
||||
</output>
|
||||
329
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
329
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
@@ -0,0 +1,329 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user