Files
splatter-tennis/HANDOFF.md
T
wuwiandClaude Fable 5 e7aab295d4 WIP splatter feedback round + HANDOFF.md for session continuation
Done (unverified): faster serve cadence in splatter (overT 0.25, serve from
where you stand, flick = instant aimed quick serve), aim-anywhere targeting
relative to the player. Not done: zombie speed ramp (G.splatT exists but is
never incremented), eaten-on-touch death + restart screen. Full state and
plans in HANDOFF.md.

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

112 lines
7.7 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 — Oliver's last message (his exact words):
> much better!!!
> the time between each serve is too long in zombie mode.
> right now it auto aims to the closest. i should also be able to aim anywhere includibg over the net.
> the speed of zombies should increase over time
> when they reach me i should be eaten
Status per item (working tree runs clean, no console errors, but items 1–2 are UNVERIFIED):
1. **Serve cadence — code DONE, not verified.**
- gameplay.js dead-ball branch: splatter overT 0.7 → 0.25, new_balls voice freq 0.2 → 0.12.
- state.js setupPoint splatter branch no longer repositions P (serve from where you stand).
- keyPressed pretoss+splatter: a FLICK now fires an instant quick serve (no toss, quality
0.62, aimed by the flick via hitServe(P, 0.62, flick)); a plain TAP still does the toss
ritual (perfect serves remain possible). hitServe gained an `aim` param.
2. **Aim anywhere incl. over the net — code DONE, not verified.**
- `zombieTarget(aim)` in gameplay.js rewritten: no aim → most advanced zombie (max z);
with aim → lane relative to the PLAYER (`P.x + ax*4`), az<0 prefers far zombies,
az>0 prefers close ones on your own side. All z candidates included.
- NOTE Oliver's complaint was that it "auto aims to the closest" — if the rewrite still
feels wrong, the next step is true free-aim: flick direction → landing point directly
(no zombie snap), with a generous splash/hit radius. Ask him which feel he wants if
the lane system still fights him.
3. **Zombie speed ramps over time — NOT DONE.**
- `G.splatT` field was added to state.js but is NEVER incremented — wire it up in
updateZombies (only while `G.mode==='splatter'`).
- Planned approach (my edit failed on a stale string, zombies.js current content is in
this repo, read it): in `spawnZombie`, multiply speed by
`1 + Math.min(1.5, G.splatT / 70)` (ramp at spawn time keeps the shot-lead math exact),
and optionally shrink the spawn interval / grow zMax with splatT.
4. **Eaten when they reach you — NOT DONE.** Plan agreed with Oliver:
- In updateZombies: `G.mode==='splatter' && Math.hypot(zb.x-P.x, zb.z-P.z) < 0.7` →
eat: gore decal + big particle burst at P, splat(1), voice('game_set_massacre', 0.8),
`G.mode='eaten'`, ball.active=false. Zombies keep shambling for ambience (guard the
eat check to mode==='splatter' so it fires once).
- gameplay.update: early-exit branch for mode 'eaten' (still tick idleT + updateZombies).
- render.js: skip drawChar(P) when eaten; dark overlay with 'EATEN.', final games score,
'TAP TO PLAY AGAIN'. Also gate the serve prompt on mode!=='eaten'.
- main.js: in the cv pointerdown listener (and a keydown), if mode==='eaten' →
location.reload() (laziest honest restart; fine for now).
- Existing baseline-cross rule (splatPoint('O') at z >= L+0.7) stays: crossing without
touching you is a point for THEM, touching you is death.
## 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.