Files
splatter-tennis/HANDOFF.md
T
wuwiandClaude Fable 5 943b7da9fd Zombies get faster over time; when one reaches you, you're eaten
splatT now ticks while in splatter mode and scales spawn speed by
1 + min(1.5, splatT/70). A zombie within 0.7m of the player ends the
run: gore burst, 'game, set, massacre', dark EATEN. overlay with final
games and a tap-to-restart (gated 0.7s so death-spam doesn't skip it).
Zombies keep shambling on the end screen but no longer score. Also
verified the two prior unverified items: quick-serve cadence (0.25s
between balls, flick = instant aimed serve) and flick aiming across
the net (far/near lane selection works as designed). HANDOFF updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:54:09 +02:00

91 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — Splatter Tennis (for the next Claude session)
Read this first, then `~/.claude/projects/.../memory/splatter-tennis-history.md` (auto-loaded)
for the long history. This file is the precise mid-task state as of 2026-07-11.
## What this is
SNES-style pseudo-3D tennis (Super Tennis reference) that transitions into a zombie splatter
game while keeping all tennis aesthetics (umpire voice, 15/30/40 scoring, ball boys, applause).
Zero-dependency vanilla JS, native ES modules, no build step. Mobile portrait one-handed play
is the primary target (tap = swing/serve, flick = aimed shot, auto-run assist does positioning).
- **Run locally**: `preview_start` name `splatter-tennis` (python http.server :8641, .claude/launch.json)
- **Deploy**: push to `git@gitea.wuwi.one:wuwi/splatter-tennis.git` (main) → Coolify webhook
auto-builds the Dockerfile (nginx, everything served no-cache). Oliver tests on his phone at
splatter-tennis.wuwi.one within ~30s of a push.
- **Oliver edits files himself** (tuning values, sometimes sprites/sheets). ALWAYS Read current
file state before editing; never assume your last version is what's on disk.
## Module map (all in repo root)
`config.js` TUNE (live panel, T key) + constants · `state.js` G object + P/O/ball/score + setupPoint
· `gameplay.js` ballistics/swings/serve/rules/AI/update + transition + splatPoint · `zombies.js`
zombie/gore/particle state + updateZombies · `render.js` camera/palette-lerp/sprites/drawing
· `input.js` keys/touch/flick · `audio.js` sfx synth + recorded umpire (assets/audio/*.m4a,
made with macOS `say -v Daniel`) · `panel.js` · `main.js` boot, loop, umpire-tap cheat, window.T.
Two runtime-only import cycles (documented in headers): input↔gameplay, zombies↔gameplay.
## Zombie mode — how it works today
- Trigger: win your first game in tennis mode, OR **triple-tap the umpire** (x=7.1, netZ) within
1.6s (cheat; listener in main.js → `forceZombies()` in gameplay.js).
- Transition (`G.mode='transition'`, timeline in gameplay.update via `G.transT`): applause cut,
palette lerp (PAL in render.js), umpire "Quiet… please" at low playbackRate (voice() has a
`rate` param), CPU opponent swapped for zombie-CPU (variant 'c'), scripted first spawns,
then `G.mode='splatter'`.
- Splatter: you serve at zombies; every ball carries `ball.pow` (= shot quality); hit with
pow >= TUNE.zSplatQ → splatter (gore decal + particles + splat() sound + splatPoint('P')),
else stagger/pushback and the ball is spent (`lastHit='X'`). Zombie crossing your baseline →
splatPoint('O'). Tennis scoring machinery keeps running (deuce, advantage, "Game, player").
Ball boys mop gore they pass. HUD shows THEM instead of CPU.
- Balance knobs in TUNE (T panel): zSpeed, zSpawnEvery, zMax, zSplatQ, zPushback.
## ACTIVE TASK — all four items from Oliver's feedback round are DONE and VERIFIED
(serve cadence · flick-aim anywhere incl. over the net · zombie speed ramp · eaten):
1. **Serve cadence — verified.** Dead splatter ball → 15 frames (0.25s) to next pretoss;
setupPoint leaves P where he stands; FLICK in pretoss fires an instant quick serve
(no toss, pow 0.62, aimed), plain TAP still does the toss ritual.
2. **Aim anywhere — verified.** Flick up targeted a far-side zombie (marker z=5.5 for a
zombie at z=5, over the net), flick down a near-side one (z=18.1). If the lane system
still fights Oliver, next step is true free-aim (flick → landing point, splash radius).
3. **Speed ramp — done.** G.splatT ticks in updateZombies (splatter only); spawnZombie
multiplies speed by `1 + min(1.5, splatT/70)`. Verified 0.76 m/s fresh vs ~1.5 at splatT=140.
Spawn interval / zMax do NOT ramp yet — add if Oliver wants more pressure.
4. **Eaten — done.** updateZombies: zombie within 0.7m of P (splatter only) → gore + 30
particles + splat(1) + voice('game_set_massacre') + mode 'eaten', ball dead, flash cleared.
gameplay.update early-exits in 'eaten' (ticks idleT/eatenT, still updates zombies —
they shamble on; baseline crossings no longer score). render: P not drawn, dark overlay
EATEN. / games score / TAP TO PLAY AGAIN (restart prompt appears after G.eatenT > 0.7 so
death-spam taps don't skip the screen). main.js: tap/key while eaten → location.reload().
ALSO: tennis volley fix (Oliver's follow-up "impossible to volley when the marker is behind
me"): pre-bounce player contact widens reach x1.4 and swing window x1.8 (VOLLEY in config.js,
applied in trySwing + the expiry in update). Root cause was no hard block, just a ~0.28s
no-cue timing window at full ball pace; now ~0.5s. doHit also clears G.wasServe so volleying
a serve back doesn't fault-check the return. CPU still returns 11/12 serves (benchmark holds).
## Verification recipe (the regression net — use it after the above)
Drive the game deterministically via `window.T` with javascript_tool: the Browser-pane eval
context reports `document.visibilityState==='hidden'`, so RAF is paused and `T.step(1/60)`
gives clean manual stepping. `T.press('Space')` = tap. Dismiss the intro overlay first:
`document.getElementById('intro')?.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true}))`.
- Tennis regression: serve → CPU returns most balls → points/games advance.
- Splatter: `T.G.mode` / force via 3 synthetic pointerdown on the umpire's projected position
(see main.js listener; coords = proj(7.1,0,11.885), remember logical = cv.width/2 because RES=2).
`T.zombies`, `T.spawnZombie(x, z, 'a')`, TUNE.zMax=0 to stop spawns during tests.
- Prior benchmarks that must keep holding: CPU returns ~11/12 serves; assist gets the idle
player in reach 8/8; constructed perfect corner (s=28, g×1.9) is a WINNER, medium center
ball is returned (run scenarios ~12x — single runs are flaky due to 9% aiFlub).
## Gotchas learned the hard way
- Sprite pipeline: generate via generate-image skill (needs OPENROUTER_API_KEY in repo .env,
gitignored) → magenta background → scratchpad slice.py (chroma-key, segment, strip ground
lines/dividers, common-scale per sheet, IDLE_TARGET_H=112). Generator sometimes ignores
magenta (then: edit-mode the image back to magenta) or draws near-identical animation
frames (then: re-prompt with strongly contrasted pose descriptions).
- drawChar mirrors sprites by ch.facing — any stance that must not mirror needs facing reset
(setupPoint pins facing=1).
- The scratchpad venv/scripts get wiped between sessions — recreate venv (requests+pillow)
and slice.py lives only in scratchpad; its logic is described above if gone.
- Don't trust single-sample gameplay sims; randomness (aiFlub 9%, aim error) needs ~12 runs.
- Commit style: imperative summary + Co-Authored-By Claude line; push = deploy.