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>
|
||||
Reference in New Issue
Block a user