15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-complete-experience | 01 | execute | 1 |
|
true |
|
|
Purpose: Replace Phase 2's plain white temporary screens with a coherent visual identity. Add the title screen (first thing player sees) and settings screen (palette, difficulty, sound). Wire arrow-key menu navigation. Overhaul game over to show full score format.
Output: index.html with palette system, 5 neon-styled game states (title, settings, modeSelect, diffSelect, gameover), and complete keyboard navigation flow.
<execution_context> @/home/dabit/.claude/get-shit-done/workflows/execute-plan.md @/home/dabit/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-complete-experience/03-CONTEXT.md @.planning/phases/03-complete-experience/03-RESEARCH.mdGameConfig (current structure):
const GameConfig = {
initialBallSpeed: 220,
speedIncrement: 18,
paddleSpeed: 400,
WIN_SCORE: 7,
AI_EASY: { speed: 200, reactionDelay: 0.3, errorMargin: 20 },
AI_MEDIUM: { speed: 320, reactionDelay: 0.1, errorMargin: 5 },
AI_HARD: { speed: 400, reactionDelay: 0.05, errorMargin: 2 }
};
// ADD: PALETTES object with MAGENTA, CYAN, LIME, WHITE presets
// ADD: PALETTE_KEYS array for cycling ['MAGENTA','CYAN','LIME','WHITE']
GameState (current structure):
const GameState = {
paddle1: { x,y,width:12,height:80,speed:400,color:'#fff' },
ball: { x,y,radius:8,vx,vy,speed,color:'#fff' },
paddle2: { x,y,width:12,height:80,speed:400,color:'#fff' },
score1: 0, score2: 0,
mode: null,
difficulty: 'medium',
gameState: 'modeSelect', // Change initial value to 'title'
winner: null
};
// ADD: activePalette (reference to GameConfig.PALETTES.MAGENTA)
// ADD: soundEnabled: true
// ADD: selectedMenu: 0
// ADD: previousState: null
Input._handleKeyDown (current):
- Handles W/S/ArrowUp/ArrowDown for paddle movement
- Digit1/2/3 for mode and difficulty selection
- KeyR restart in gameover state // EXTEND: Add ArrowUp/Down for menu navigation in title/settings/modeSelect/diffSelect // EXTEND: Enter/Space to confirm selection // EXTEND: Escape/Backspace to go back (GameState.previousState) // ADD: Audio.init() call on first keydown (for Plan 02 — add the call even though Audio // module doesn't exist yet; guard with: if (typeof Audio !== 'undefined' && !Audio.isInitialized) Audio.init())
GameLoop.main() render switch (current states):
- 'modeSelect' → plain white text prompt (Press 1/2)
- 'diffSelect' → plain white text prompt (Press 1/2/3)
- 'playing'/'scored'/'gameover' → gameplay with white colors // ADD: 'title' render case — arcade title screen // ADD: 'settings' render case — settings screen // RESTYLE: 'modeSelect' → neon arrow-key navigation // RESTYLE: 'diffSelect' → neon arrow-key navigation // RESTYLE: 'gameover' overlay — "PLAYER 1 WINS 7 – 4" format + Play Again / Main Menu // UPDATE: all hardcoded '#fff' colors → GameState.activePalette.accent
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add palette system to GameConfig and extend GameState</name>
<files>index.html</files>
<action>
In GameConfig, add after the AI_HARD entry:
```javascript
PALETTES: {
MAGENTA: { accent: '#ff00cc', glow: 'rgba(255,0,204,0.6)', name: 'Magenta' },
CYAN: { accent: '#00ffff', glow: 'rgba(0,255,255,0.6)', name: 'Cyan' },
LIME: { accent: '#00ff00', glow: 'rgba(0,255,0,0.6)', name: 'Lime' },
WHITE: { accent: '#ffffff', glow: 'rgba(255,255,255,0.6)', name: 'White' }
},
PALETTE_KEYS: ['MAGENTA', 'CYAN', 'LIME', 'WHITE']
In GameState, change gameState: 'modeSelect' to gameState: 'title' and add:
activePalette: null, // Set after GameConfig defined — see init sequence
soundEnabled: true,
selectedMenu: 0,
previousState: null
After the GameState object definition (before Physics), add:
GameState.activePalette = GameConfig.PALETTES.MAGENTA;
// Also update paddle/ball color references to use palette at draw time (not stored on objects)
Do NOT store palette color on paddle1.color, paddle2.color, or ball.color — those properties stay as fallback '#fff'. The palette is applied at draw-call time by reading GameState.activePalette.accent in the renderer. This keeps the physics/AI logic untouched.
Open index.html in a browser. The game starts at a blank black screen (title state). Open the browser console and run: console.log(GameState.activePalette.accent) — should log '#ff00cc'. console.log(GameConfig.PALETTE_KEYS) — should log ['MAGENTA','CYAN','LIME','WHITE'].
GameConfig has PALETTES and PALETTE_KEYS. GameState has activePalette pointing to MAGENTA preset, soundEnabled:true, selectedMenu:0, previousState:null. gameState starts at 'title'.
Replace the current keydown handler body with:
-
Audio init guard (for Plan 02 integration — harmless no-op now):
if (typeof Audio !== 'undefined' && !Audio.isInitialized) Audio.init(); -
Continuous-hold keys (keep as-is):
if (e.code === 'KeyW') { this.keys.w = true; e.preventDefault(); } // ... etc -
Title screen navigation (gameState === 'title'):
- ArrowUp → selectedMenu = Math.max(0, selectedMenu - 1)
- ArrowDown → selectedMenu = Math.min(1, selectedMenu + 1) (two items: Play=0, Settings=1)
- Enter OR Space → if selectedMenu===0: previousState='title', gameState='modeSelect', selectedMenu=0 if selectedMenu===1: previousState='title', gameState='settings', selectedMenu=0
-
Mode select navigation (gameState === 'modeSelect'):
- ArrowUp/Down → selectedMenu cycles between 0 (Solo vs AI) and 1 (2-Player)
- Enter OR Space → if selectedMenu===0: mode='ai', previousState='modeSelect', gameState='diffSelect', selectedMenu=1 (default Medium) if selectedMenu===1: mode='2p', gameState='playing', Physics.serveBall()
- Escape OR Backspace → gameState='title', selectedMenu=0
- KEEP existing Digit1/Digit2 for backward compat (just in case)
-
Difficulty select navigation (gameState === 'diffSelect'):
- ArrowUp/Down → selectedMenu cycles 0 (Easy), 1 (Medium), 2 (Hard)
- Enter OR Space → difficulty=['easy','medium','hard'][selectedMenu]; AI.init(); gameState='playing'; Physics.serveBall()
- Escape OR Backspace → gameState='modeSelect', selectedMenu=0
- KEEP existing Digit1/2/3 for backward compat
-
Settings navigation (gameState === 'settings'): Items: 0=AI Difficulty, 1=Sound, 2=Color Scheme
- ArrowUp/Down → selectedMenu cycles 0-2
- ArrowLeft OR ArrowRight OR Enter OR Space → modify the selected setting:
- If selectedMenu===0: cycle difficulty: easy→medium→hard→easy (ArrowLeft goes reverse)
- If selectedMenu===1: toggle soundEnabled
- If selectedMenu===2: cycle activePalette through PALETTE_KEYS array
ArrowLeft goes (idx - 1 + length) % length
const idx = GameConfig.PALETTE_KEYS.indexOf( Object.keys(GameConfig.PALETTES).find(k => GameConfig.PALETTES[k] === GameState.activePalette) ); const nextIdx = (idx + 1) % GameConfig.PALETTE_KEYS.length; GameState.activePalette = GameConfig.PALETTES[GameConfig.PALETTE_KEYS[nextIdx]];
- Escape OR Backspace → gameState = previousState (which is 'title'), selectedMenu=0
-
Game over navigation (gameState === 'gameover'): Items: 0=Play Again, 1=Main Menu
- ArrowUp/Down → selectedMenu cycles 0-1
- Enter OR Space →
- if selectedMenu===0 (Play Again): reset scores, winner, mode, difficulty; gameState='modeSelect'; selectedMenu=0; AI.init(); Physics.init(...)
- if selectedMenu===1 (Main Menu): reset scores, winner, mode, difficulty; gameState='title'; selectedMenu=0; AI.init(); Physics.init(...)
- KEEP KeyR restart (goes to modeSelect for backward compat)
GameLoop.main() render overhaul:
Helper function (add before GameLoop object): function drawNeonText(ctx, text, x, y, font, palette, blurRadius) { ctx.save(); ctx.shadowColor = palette.glow; ctx.shadowBlur = blurRadius || 15; ctx.fillStyle = palette.accent; ctx.font = font; ctx.fillText(text, x, y); ctx.restore(); }
Replace ALL occurrences of hardcoded '#fff' and 'rgba(255,255,255,0.8)' in render cases with GameState.activePalette.accent where appropriate. Score display, divider line (keep dim white), paddles, ball — all use palette.
Render cases to add/replace:
'title' case (add before modeSelect):
- Renderer.clear()
- Large glowing title: "SUPER PONG NEXT GEN" — bold 52px monospace, centered at logicalHeight/2 - 80, shadowBlur=20
- Subtitle: "ARCADE EDITION" — 18px, shadowBlur=8, half-alpha glow, centered at logicalHeight/2 - 40
- Menu items (Play at index 0, Settings at index 1) centered at logicalHeight/2 + 20 and +70
- Selected item: bold 24px + shadowBlur=12 + leading
>marker - Unselected: 20px + shadowBlur=0 + leading
spaces
- Selected item: bold 24px + shadowBlur=12 + leading
- Footer hint: "ARROW KEYS to navigate, ENTER to select" — dim white, 14px, bottom of screen
- early return
'settings' case (add after title):
- Renderer.clear()
- "SETTINGS" title — bold 36px, centered at y=80, shadowBlur=15
- Items at y=170, 240, 310 (3 items with 70px spacing):
- AI Difficulty: display current GameState.difficulty.toUpperCase() on right
- Sound: display GameState.soundEnabled ? 'ON' : 'OFF' on right
- Color Scheme: display GameState.activePalette.name on right
- Selected item highlighted with shadowBlur=12, bold font,
>marker - Unselected: normal weight, no glow
- Footer: "ARROW KEYS to select, LEFT/RIGHT or ENTER to change, ESC to return" — dim white, 14px
- early return
'modeSelect' case (restyle existing):
- Renderer.clear()
- "SELECT MODE" neon title — bold 36px, shadowBlur=15, centered at logicalHeight/2 - 90
- Two items: "Solo vs AI" (index 0) and "2-Player Local" (index 1) — same highlight pattern as title menu
- Footer hint: "ARROW KEYS + ENTER to select, ESC to go back"
- early return (remove old Digit1/2 instructions from display, but keep handler)
'diffSelect' case (restyle existing):
- Renderer.clear()
- "SELECT DIFFICULTY" neon title
- Three items: Easy / Medium / Hard — selectedMenu starts at 1 (Medium) when entering from modeSelect
- Footer hint
- early return
'gameover' overlay (restyle existing):
- Build winner label:
gs.winner === 'player1' ? 'PLAYER 1' : gs.winner === 'ai' ? 'AI' : 'PLAYER 2' - Score line:
winnerLabel + ' WINS ' + gs.score1 + ' – ' + gs.score2 - Large neon text centered, shadowBlur=20
- Two menu items below: "Play Again" (index 0), "Main Menu" (index 1) — with selection highlight
- Remove old "Press R to play again" text
Gameplay rendering (playing/scored):
- Change all
p1.color,p2.color,b.colorin drawRect/drawCircle calls toGameState.activePalette.accent - Score display: change
'rgba(255,255,255,0.8)'toGameState.activePalette.accent - Center divider: keep dim
rgba(255,255,255,0.2)(neutral, not palette-colored) Open index.html. Verify: (1) Title screen renders with neon magenta glow on black background, menu shows Play and Settings. (2) Arrow keys change which item is highlighted. (3) Enter on Play goes to mode select with neon style. (4) Enter on Settings goes to settings screen showing difficulty/sound/palette. (5) Changing Color Scheme updates the palette (ball/paddle/text all change on next gameplay screen). (6) Completing a game shows "PLAYER X WINS N – M" format with Play Again and Main Menu options. (7) Escape from settings/modeSelect returns to previous screen correctly. All 5 game states (title, settings, modeSelect, diffSelect, gameover) render with neon palette. Arrow-key navigation works. Palette switching affects all game colors. Game over shows winner + full score in the specified format.
<success_criteria>
- Title screen renders with neon branding on first load
- All screens use consistent neon palette from GameState.activePalette
- Arrow-key + Enter navigation works across all menu screens
- Settings palette change propagates to ball/paddle/UI colors in gameplay
- Game over displays "PLAYER X WINS N – M" with functional navigation options
- No hardcoded '#fff' in the actively-rendered gameplay/menu paths (all reference activePalette.accent) </success_criteria>