diff --git a/HANDOFF.md b/HANDOFF.md index 01efb01..8c9ff6d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,9 +5,10 @@ everything they specified is shipped, and what still matters from them is record long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the detail of any individual change, read the git log. -Last updated 2026-07-30, after Oliver's FOURTH playtest round (viewport, horde survivability). All playtest -rounds are closed at the bottom. Step 6 (crowd turning, ball-boy catcher) stays PARKED by his -decision. **Awaiting his next phone playtest.** +Last updated 2026-07-30, after Oliver's fourth playtest round. **Start with "How to work on this" +and "OPEN — leftovers"** (both just above the CLOSED sections) — the first tells you how his feedback +should be handled, the second is everything known to be outstanding. All four playtest rounds are +closed below them, newest first. **Awaiting his next phone playtest.** ## What this is SNES-style pseudo-3D tennis (Super Tennis reference) that transitions into a zombie splatter game @@ -31,7 +32,10 @@ transition + `splatPoint` + ball-boy routines · `zombies.js` zombie/gore/partic `updateZombies` · `render.js` camera/palette-lerp/sprites/all drawing · `input.js` keys/touch/flick · `audio.js` sfx + recorded umpire (`assets/audio/*`, macOS `say -v Daniel`) · `panel.js` the T panel · `main.js` boot, loop, umpire-tap cheat, `window.T` test surface. -Two runtime-only import cycles (documented in headers): input↔gameplay, zombies↔gameplay. +THREE runtime-only import cycles (documented in headers): input↔gameplay, zombies↔gameplay, +and state↔zombies (`sendBoy` reads `gore` to decide whether a mopping round is worth walking out for). +All three are safe for the same reason: the imported binding is only ever touched inside a function at +runtime, never during module evaluation. ## How the game flows Tennis → win a game → **transition** (`G.mode='transition'`, timeline on `G.transT`) → **splatter**. @@ -371,6 +375,78 @@ Known generator quirks, all still true: --- +## How to work on this — the meta-lesson from four playtest rounds + +**Every single "this feels odd" note Oliver gave this session turned out to be a real defect, not +taste.** Not one was a tuning preference. The score so far: + +| what he said | what it actually was | +|---|---| +| "the racket zombie doesn't seem harmable, but that could just be a feeling" | 3 of 5 weapons silently no-oped on it | +| "I could not return a single ball" | the parry was unreachable — the tap only became a swing in `rally` | +| "the first boss is way too easy" | a flame ball re-wounded it every frame; one killed a 16 hp boss | +| "the ball is not yet at the player but he is already hit" | contact fired 0.68 m early (weighted depth) | +| "one of the endboss sprites is used for a normal zombie" | a case-insensitive filename collision | +| "everything is a tad too small" | near sprites were being upscaled 1.5x from a 112px source | +| "I die all the time at the game-20 horde" | the horde scales, the panic button was a flat 1 s | + +So: **when he reports a feel problem, go measure it before touching a tuning value.** The measurement +is usually cheap (project both objects to screen space at the moment of the event; count device pixels +against source pixels; print the schedule for 45 games) and it has found a bug every time. + +**Verify with instrumentation, not with screenshots.** Small pixel art at phone scale is genuinely +unreadable in a screenshot — during the boss-design work I twice concluded from screenshots that the +sprites were fine when they were all silently falling back to design 1. What settled it in one call: + +```js +const o = CanvasRenderingContext2D.prototype.drawImage; +CanvasRenderingContext2D.prototype.drawImage = function (img, ...a) { log(img.src); return o.call(this, img, ...a); }; +``` + +Same idea elsewhere: project to screen coords and compare numbers, don't eyeball. A silent fallback +(`SPR[wanted] ? wanted : `) is convenient in production and *poisonous* while debugging, +because a wiring bug renders as perfectly working code. + +**Assert your scripted edits matched.** A `str.replace` that finds nothing is a silent no-op. One of +those left `drawZombie` on hardcoded sprite names while everything else moved to the `BOSS_ART` table, +and the fallback above hid it completely. Every scripted edit in this repo should `assert a in s`. + +--- + +# OPEN — leftovers, in rough priority order + +Nothing here is broken; these are known gaps and unverified risks. + +1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080 + device px across). Oliver has not reported stutter, but he also has not played a late horde since. + **If the game ever stutters, `RES` back to 3 is the first move** — not reducing zombie counts. +2. **The game-20 horde fix is unplayed.** The crowd-scaled swish (1.00 s at one body -> 0.22 s at + fourteen) is measured but not yet felt. If it now trivialises hordes, `meleeCoolMin` is the knob. +3. **`p_idle1`/`p_idle2` and most `o_*` frames are 2x-upscaled, not re-sliced.** They are on the right + scale but gained no detail in the remaster, and they are the two most-seen poses in the game. + Fix by hand-slicing `_sheet_player2/3` and `_sheet_opp2/3` at `IDLE_TARGET_H` 224 rather than going + through the signature matcher, which assigned those frames elsewhere. +4. **Boss designs are asymmetric.** Design 1 (GROUNDSKEEPER) has 3 wound stages and 4 throw frames; + designs 2 (LINE JUDGE) and 3 (CHAIR UMPIRE) have 1 wound skin and 2 throw frames each. Legal — the + `BOSS_ART` indices are clamped — but a chapter-2 boss degrades in one visible step instead of three. +5. **Only 3 boss designs, cycling.** Chapter 4 repeats the groundskeeper at 43 hp and 3.67 m. A fourth + design is a sheet plus one row in `BOSS_ART`. +6. **Landscape is under-maintained.** The readability pass (camZ/camY/camF/horizon), the adaptive + height and the safe-area work are all portrait-only; landscape still runs the old camera and a fixed + 480x270. Portrait is the stated primary target, but landscape has drifted and is untested. +7. **`drawCard` (the chapter/set cards) still cuts in** with no entrance, while modals swoosh + (`TUNE.modalIn`). Offered and not yet requested. +8. **A chapter can be skipped.** `G.bossDue` takes the max, so going a long stretch without ever + clearing the court means you face the current chapter, not a backlog. Deliberate; noted in case it + ever reads as a bug. +9. **Step 6 stays PARKED** by Oliver's decision: the crowd turning into zombies (spawning from the + stands — attacks from behind, high spectacle, `x_crowd` sheets already exist) and the ball-boy zombie + that catches your shots. Recorded so the ideas are not lost, not as a to-do. +10. **`ump_no_deal` does not exist.** Oliver asked for its removal; `ump_no_appeal` was removed as the + closest match. Worth one confirmation that no other line is still unwanted. + +--- + # CLOSED — Oliver's fourth playtest (2026-07-30) ### The game did not use the full height of his iPhone