# GeekFighter — build plan & autonomous-work guide

A Street Fighter II–style 1v1 fighter. Goal: play as close to the original arcade game
as possible, with Ryu and Blanka as the test roster (sprites are placeholder rips the
owner will replace). Move/mechanic target spec: **`docs/sf2-moves.md`** — read it before
tuning combat.

## How this app is built & served

- Static site served by nginx (`docker-compose.yml`), files in `public/`, live-bind-mounted
  — **editing `public/**` takes effect on reload; no redeploy needed.** Only run
  `srv-gw deploy --project geekfighter` if the container is down or compose changed.
- URL: https://geekfighter.rian.ca  (also `http://172.17.0.1:3133/` from the host).
- No backend, no DB, no build step. Pure HTML/CSS/JS + PNG sprites.

## Code map

- `public/index.html` / `style.css` — page shell, 2× pixelated canvas, control legend.
- `public/game.js` — the engine: fixed-timestep loop, `Fighter` state machine, input +
  motion/charge parsing, hit detection (reach + high/mid/low bands), blocking, projectiles,
  round/match flow, simple AI, rendering + HUD. **All globals** (`G`, `step`, `render`,
  `setupMatch`, `keys`, `edgeState`, `updateAI`) are reachable for testing.
- `public/data/ryu.json`, `public/data/blanka.json` — data-driven per character: `anims`,
  `moves` (normals), `specials`, each with frame lists + timing/damage/reach/band.
- `public/sprites/<char>/f###.png` + `atlas.json` — extracted frames + per-frame bbox
  (used for bottom-center foot alignment).
- `public/testkit.js` — `TEST` harness (see Testing below).
- `tools/extract.py` (+ `tools/.venv`) — the sprite extractor. Source sheets are the two
  `Arcade - Street Fighter 2 ... .png` files in the project root.

## Sprite extraction

`tools/.venv/bin/python tools/extract.py "<sheet>.png" assets/<char> <contact.png>`
keys out the sheet background (border + globally-frequent flat colors) and the green
ground-shadow, then splits by row-band / column-gutter projection. Output: `assets/<char>/`
frames + `atlas.json` + a numbered **contact sheet** (open it to map frame index → pose).
Re-copy into `public/sprites/<char>/` after re-extracting.

Contact sheets already generated once; regenerate anytime. Known good gameplay ranges:
- **Ryu**: frames ~4–132 are individual poses (idle/walk/crouch/all normals/jumps/
  hit/dizzy/knockdown/hadoken wind-up 127–132), 133–158 = blue fireball, 215–221 =
  alt-palette Ryu, 222–226 = art/portrait.
- **Blanka**: frames ~3–84 are gameplay poses, 85 = blue electric, 51–55 = rolling ball,
  87+ = big versus-portrait art.

## Testing recipe (IMPORTANT — the rAF loop is throttled in a hidden/automation tab)

When the tab is not the visible foreground tab, the browser throttles `requestAnimationFrame`,
so the game appears frozen in screenshots. **Do not conclude the game is broken from a static
screenshot.** Drive it deterministically via injected JS using the `TEST` harness:

```js
TEST.reset(); TEST.toFight();          // clean round, skip intro (also sets G.paused=true)
TEST.neutral(170,232);                 // BOTH grounded idle, AI off, keys cleared, at x=170/232
                                        //   ALWAYS call before a controlled single-input test —
                                        //   otherwise the AI may leave a fighter mid-jump/attack.
TEST.smoke(1500);                      // AI-vs-AI 1500 frames; returns {err, ...snapshot}
TEST.tap(2,'hp');                      // P2 single heavy punch (edge press + 1 step)
TEST.motion(1,'qcf','hp'); TEST.frames(20); TEST.snap();   // Ryu hadoken
TEST.hold(1,'right',40); TEST.snap();  // P1 walks; x should increase toward opponent
TEST.paint();                          // force ONE render() so a screenshot reflects state
```

**Cache / reload:** nginx now sends `no-store` and `index.html` loads `game.js`/`testkit.js` with a
`?v=Date.now()` cache-bust, so a normal reload runs fresh code. If you ever suspect stale code
(e.g. an edit "not taking"), reload with a unique query — `https://geekfighter.rian.ca/?v=<n>` —
and confirm with `Array.isArray(G.p1.punchPresses)` (a field only the current code sets).
**The live rAF loop is paused during tests** (`G.paused`, set by `TEST.reset`) so manual `step()`
has full control; call `TEST.resume()` to hand control back to the live loop.

`TEST.smoke(n)` is the fastest regression check: it runs the whole engine AI-vs-AI for n
frames and reports any thrown error plus the end state. Run it after every change.
For a visual check: drive with `TEST`, call `TEST.paint()`, then screenshot.

## Status (updated by the loop — newest at bottom of `.logs/progress.md`)

### DONE (foundation, verified in browser 2026-07-19)
- Extraction pipeline; 227 Ryu + 175 Blanka frames extracted & served.
- Engine runs error-free (560+ and 1500-frame drives, no exceptions).
- Movement (walk/crouch), jump arc, gravity, pushboxes/separation & stage clamp.
- Six normals + crouch variants + sweep (knockdown); reach + high/mid/low band hits.
- Blocking (stand vs high/mid, crouch vs mid/low), block spark, blockstun/pushback.
- Specials + input parsing: qcf/qcb/dp motions, back/down charge, mash. Hadoken projectile
  verified connecting for damage.
- Health bars, countdown timer, round pips, ROUND/FIGHT/K.O. messages, best-of-3 flow,
  match-over + rematch, victory pose. Basic AI (approach / attack / block / jump / retreat).

### TODO — priority order (do these; check off and log each)
1. **Frame-mapping accuracy (biggest visual win).** [DONE — both characters: idle/walk/crouch/
   jump + all normals + crouch normals + sweep + all specials mapped & verified in-browser
   (Iters 1-4). Future polish only: per-frame anchors (#3), hit/dizzy fine-tuning.]
2. **Remove baked-in text labels from frames.** [MOSTLY DONE — every frame currently USED by an
   anim/move/special is text-free (verified). OPTIONAL: improve `text_flag` to catch faint
   light-gray labels (missed Blanka 3,6,32,35,36,38,39) + full re-extract, only if a future
   frame pick needs a contaminated one.]
3. **Per-frame foot/anchor offsets.** Bottom-center alignment is close but attacks that
   extend a limb shift the body. Add optional per-frame `anchor` (dx,dy) in the atlas or
   move data so the pivot (front foot) stays planted across an animation.
4. **Projectile art & travel.** Confirm Hadoken uses the blue-fireball frames (133–145),
   sized/positioned at hand height; add spark/impact. Verify it destroys on hit and offscreen.
5. **Fill the full movesets** from `docs/sf2-moves.md`: [MOSTLY DONE — all normals + specials
   mapped/working; throws (close 4/6+MP/HP, unblockable) DONE incl. AI. REMAINING optional:
   jump-ins forced stand-block nuance, Shoryuken startup invuln, Tatsumaki fireball-immunity,
   Blanka ball unsafe-on-block frames, Electric mash-scaled hits/chip, dedicated grab-toss art.]
6. **Hitstop / juggle / knockdown polish**: [DONE — hitstop scaled by damage; getup state +
   wake-up invulnerability; dizzy/stun meter → skeleton-flash. Verified. REMAINING optional:
   air juggles, throw-tech.]
7. **Round intro & KO polish** [DONE — KO slow-mo, PERFECT / TIME OVER / DOUBLE K.O., win pose,
   best-of-3 flow. Optional: win-quote text screen.]
8. **Character select screen** [DONE — arcade select w/ cursors, F/Num4 confirm, [2] 1P/2P toggle,
   CPU auto-confirm, returns to select after match. Verified. Optional: AI difficulty setting.]
9. **Better AI** [DONE — reaction-gated state-aware AI: anti-air reversals, anti-fireball
   jump/block, whiff-punish, okizeme, jump-ins, fireball zoning, spacing. skill=0.7 (beatable),
   verified. Optional: per-difficulty skill from an AI-difficulty menu setting.]
10. **Audio** [DONE — all SFX synthesized in WebAudio (public/audio.js), no external files,
    unlocks on first keypress. Hooked to whiff/hit/block/jump/fireball/special/KO/dizzy/round.]
    (superseded) Only
    self-authored/CC0 sounds — do NOT fetch copyrighted audio.
11. **Stage** [DONE — sunset arena: gradient sky+sun, parallax mountains + lit skyline, plank
    floor w/ perspective, height-scaled shadows, subtle midpoint parallax.]
12. **Mobile/touch controls** (stretch): on-screen d-pad + buttons.

## Guardrails for autonomous work

- After EVERY change: reload the tab, run `TEST.smoke(1500)`, confirm `err` is null and
  both fighters still take/deal damage. Then `TEST.paint()` + screenshot for a visual pass.
- Keep `game.js` readable and data-driven — new moves/characters should be JSON, not code.
- Don't break the running container. Never edit `/srv/.private/**` or run privileged cmds.
- Sprites are placeholder rips for a private project; the owner will replace them. Do not
  fetch additional copyrighted assets (art, audio, fonts). Mechanics/inputs are fine.
- Log meaningful progress to `.logs/progress.md` (what changed, what was verified, what's next).
