CLAUDE.md (9189B)
1 # KGames — Claude Guide 2 3 ## What this project is 4 5 Free educational games for young kids. No ads, no signup, no backend. 6 Static HTML + CSS + JavaScript served from any web host. Phaser 3 and 7 Tone.js loaded from CDN. GPL-3.0. 8 9 No build tools — double-clicking `index.html` is a valid development 10 workflow. Keep it that way. 11 12 ## Testing — run before every commit 13 14 ```bash 15 npm install # one-time; downloads playwright (~112 MB Chromium) 16 npm test # unit tests + browser smoke tests; must all pass 17 npm run test:unit # fast, no browser (~0.2 s) 18 npm run test:browser # headless Chromium (~10 s) 19 ``` 20 21 **Never commit if `npm test` fails.** The browser tests catch broken 22 rendering that syntax checks miss (see the projection-bug incident). 23 24 ## Architecture 25 26 | Path | Purpose | 27 |---|---| 28 | `index.html` + `css/style.css` + `js/main.js` | Portal — game tile grid | 29 | `games/<slug>/index.html` | HTML shell: CDN scripts, HUD buttons, game container | 30 | `games/<slug>/lib.js` | Pure logic (projection math, route gen) — dual-export for unit tests | 31 | `games/<slug>/game.js` | Phaser scene — all graphics via `generateTexture`, audio via Web Audio / Tone.js | 32 | `js/shared/game-shared.js` | Shared `KGames.*` helpers (AudioSystem, win screen, confetti, progress bar) | 33 | `js/shared/touch-controls.js` | On-screen keyboard / D-pad for phones — see "Mobile / touch controls" | 34 | `assets/thumbnails/<slug>.svg` | 160×130 viewBox, rx=18, stroke from palette, `#FFF5F5` fill | 35 | `tests/unit/` | `node --test` unit tests — no browser, no Phaser | 36 | `tests/browser/runner.js` | Playwright smoke tests — no JS errors + canvas renders | 37 | `tests/serve.js` | Tiny Node HTTP server used by browser tests (and for local dev) | 38 39 ## Top-down rendering (fire-truck) 40 41 Fire-truck is a **top-down** driving game: the camera zooms 1/`SCALE` (SCALE=5) 42 and follows the truck over a 50×50-cell island. `lib.js` `buildIsland()` lays 43 out water/beach ring roads + BSP-subdivided interior blocks; the road graph 44 drives the route and fire pathing. 45 46 **Chunk bake pipeline** (`renderIsland()` in game.js). The static island is 47 baked once into 16×16-cell chunk textures at `BAKE_CELL = 96` px/cell, drawn 48 ×SCALE (zoom 1/SCALE) → **~1:1 on screen**, so ~96 px of detail per cell costs 49 nothing per frame and Phaser culls offscreen chunks for free. Each chunk runs 50 named passes into one `Graphics`, in order: `_bakeGround` → `_bakeSidewalks` 51 (incl. road corner masks) → `_bakeRoadMarkings` → `_bakeBuildings` → `_bakeParks` 52 → `_bakePalms`. 53 54 - **1-cell apron rule** (highest visual-bug risk): every pass iterates a 1-cell 55 margin around the chunk (clamped) at true local coords so overhanging art — 56 SE drop shadows, palm fronds, block-spanning buildings — that crosses a chunk 57 seam is re-drawn by the neighbour; `generateTexture` clips the overflow. 58 - Buildings are drawn **per block, not per cell**: `FT.findBuildingBlocks(grid)` 59 covers all building cells with disjoint rectangles (BSP guarantees rectangular 60 road-bounded blocks) so each block reads as one building with one roof. 61 `FT.assignParks()` tags ~15% of blocks `cell.park = true` (**type stays 62 `building`**, so grid/route tests are untouched; parks are excluded as fire 63 destinations). 64 65 **Ocean underlay**: a screen-fixed (`scrollFactor 0`) `TileSprite` at depth −1 66 drifts each frame. A world-sized TileSprite would allocate a multi-GB fill 67 canvas — keep it viewport-sized. 68 69 **Dual cameras** (game.js): `cameras.main` is the world; `uiCamera` is the HUD. 70 Every new **world** object must be hidden from the HUD camera — 71 `this._addWorld(obj, depth)` sets depth + `uiCamera.ignore`. HUD objects are 72 added to `cameras.main.ignore(...)`. 73 74 **Depth ordering** (exact values live in game.js): ocean < chunks < 75 boats/buoys < peds < bikes < cars/buses < burning overlay < fire glow < fire < 76 spray < smoke < cloud shadows < truck < birds < HUD. 77 78 **Hose minigame** (`FireHoseScene`): when the truck reaches the fire, the 79 driving scene enters state `minigame`, pauses itself, and launches a 80 street-view scene — building facade with a window grid (`FT.buildFacade` in 81 lib.js, pure + unit-tested), arrow keys move an aim reticle, SPACE sprays from 82 the truck's rotating ladder. Quenched windows reveal waving residents/a cat; 83 when all are out the scene resumes the driving scene and calls 84 `_onMinigameComplete()`. **Headless-RAF rule**: gameplay logic lives in 85 `_step()` driven from both `update()` and a wall-clock `setInterval` fallback 86 (same trick as the driving scene's `fallbackTimer`), and all scene transitions 87 use wall-clock `setTimeout` — never `this.time.delayedCall`, which starves in 88 throttled/headless browsers and hangs the tests. 89 90 **Shared cartoony art** (file-scope helpers in game.js): `bakePeopleTextures` 91 (`kg-person-0..7`, racially diverse cartoon townspeople), `bakeSideTruckTexture` 92 (`kg-truck-side`), `bakeLadderTexture`, `bakeCritterTextures` (dog/cat/parrot/ 93 iguana), `drawSidePalm`. Used by both `FireHoseScene` and `FireTruckEndScene` — 94 reuse these rather than drawing new people/trucks. 95 96 **Ambient life** (`_initAmbient` / `_updateAmbient`, driven from `update()` — 97 never `step()`, which tests call directly): baked-texture Images for traffic 98 (`FT.advanceCarPlan` graph walk), pedestrians, cloud shadows, boats, birds. 99 Guard with `this.ambientReady`; clean up in `shutdown()`. All decor variety is 100 deterministic via `FT.hashCell(x, y, salt)` so the island bakes identically 101 every run. 102 103 ## Mobile / touch controls 104 105 Phones have no physical keyboard and a Phaser canvas can't summon the native 106 one, so `js/shared/touch-controls.js` renders an on-screen control bar whose 107 buttons dispatch **synthetic `KeyboardEvent`s to `window`**. Phaser's keyboard 108 plugin already listens on `window`, so every game's existing key handling works 109 unchanged — game logic never references this module. 110 111 - Opt in from the HTML shell, after `game.js`: 112 `<script>KGames.initTouchControls({ layout: 'alphabet' });</script>` 113 - Layouts: `alphabet` (A–Z), `text` (A–Z + Space/Backspace/Enter, e.g. work), 114 `drive` (arrows + Siren/Lights/Spray, e.g. fire-truck). 115 - Shown only on touch / coarse-pointer devices. Force with `?kbd=1` / `?kbd=0` 116 or `window.__FORCE_TOUCH_CONTROLS__` (tests use the latter). 117 - The bar is a flex child below `#game-container`, so the canvas shrinks to fit 118 above it. Phaser's `Scale.RESIZE` ignores a synthetic window `resize` when the 119 window size is unchanged, so the module calls `game.scale.refresh()` directly. 120 **This requires the game instance on `window.__KG_GAME__`** — assign it where 121 you call `new Phaser.Game(...)`. 122 123 ## Adding a new game 124 125 1. Create `games/<slug>/index.html` (clone from `games/fire-truck/index.html`) 126 2. Create `games/<slug>/lib.js` for pure (unit-testable) logic 127 3. Create `games/<slug>/game.js` for the Phaser scene 128 4. Add `render: { preserveDrawingBuffer: true }` to the `Phaser.Game` config 129 (required for the Playwright pixel-reading tests to work), and assign the game 130 to `window.__KG_GAME__` so touch controls can resize it 131 5. Create `assets/thumbnails/<slug>.svg` (160×130, rx=18 rounded rect) 132 6. Replace a `Coming Soon` placeholder in `js/main.js` 133 7. Wire up touch controls: `KGames.initTouchControls({ layout: ... })` in the 134 HTML shell (pick or add the layout that matches the game's keys) 135 8. Add unit tests in `tests/unit/<slug>-*.test.js` 136 9. Add browser test cases to `tests/browser/runner.js` 137 10. Run `npm test` — all must pass before committing 138 139 ## Hosting & deploy 140 141 The project is self-hosted on NearlyFreeSpeech — there is no Codeberg/GitHub forge. 142 143 - `origin` is `ssh://…@ssh.nyc1.nearlyfreespeech.net/home/private/kgames.git` 144 (a bare repo outside the document root). 145 - A **`pre-push` hook** runs `deploy.sh` on every push to `origin`, which rsyncs 146 the site, refreshes the restic-backed mirror at 147 `/media/bespin/kyle/backups/gits/kgames.git`, and publishes a static 148 [stagit](https://codemadness.org/stagit.html) code browser + dumb-HTTP clone 149 source to `https://www.keyboard.games/code/`. Skip with `KG_SKIP_DEPLOY=1`. 150 - `deploy.sh` and the hook are **untracked**, so a fresh clone does not reproduce 151 this setup — copies plus restore notes live in 152 `/media/bespin/kyle/backups/gits/kgames-untracked/`. 153 - The mirror is deliberately **never repacked** so restic dedups its objects. 154 - `tools/stagit-style.css` is the code browser's stylesheet (site palette). 155 156 ## Color palette 157 158 `#E63946` red · `#1D7CF2` blue · `#FFD23F` yellow · `#2EC4B6` teal 159 160 Tint equivalents: `0xE63946 · 0x1D7CF2 · 0xFFD23F · 0x2EC4B6` 161 162 ## Key conventions 163 164 - **No external image assets** — all textures via `make.graphics({ add: false }).generateTexture()` 165 - **Audio**: Tone.js for looping music; Web Audio API oscillators for SFX 166 (lazy-init on first user interaction to satisfy browser autoplay policy) 167 - **Font**: Fredoka, 400 + 600 weight (Google Fonts) 168 - **Scale**: `Phaser.Scale.RESIZE` + `applyLayout(W, H)` / `onResize` pattern 169 - **No `console.log` in production code** — browser tests assert zero console errors 170 - **`window.__FT_SCENE__ = this`** exposed in `create()` for browser test inspection