Let the player walk between points, and fix seven retest gripes
All eight issues from Oliver's 35-game retest. The freeze (1 and 6 — one root cause): the movement block, pose update included, lived inside `if (G.phase === 'rally')`, so every 'over' beat and every pretoss stranded the player mid-animation while the zombies walked on. It now also runs whenever splatter is in any phase but 'toss' — 'toss' is the aim, and the thumb is busy. Tennis keeps its old gating. The rest: - the mop boy is waved off the sudden-death duel and barred for its duration - triple-serve clones inherit the serve's topspin gravity, so the wings stop floating while the centre ball dives - "Time." belongs to the changeover's own countdown, not to whatever beat happened to expire while the breather ran - the tutorial illustration is a finger pressing and pulling back now, not a curved arrow that read as a scroll - aiming holds the pretoss stance; the smash plays when the shot fires - GAME N sits on the scoreboard permanently HANDOFF records all eight as closed with what the fix was, plus two ways a soak rig lies to you and why the CPU-return benchmark can't be reproduced from the number alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8dca9b6d45
commit
e00886f57e
7 files changed
+89
-52
No files matched your search
+63
-44
@@ -5,7 +5,9 @@ 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-27 after Oliver's 35-game retest. **Open issues are at the bottom — start there.**
|
||||
Last updated 2026-07-30. All eight issues from Oliver's 35-game retest are fixed and verified in the
|
||||
browser; they are recorded at the bottom as **CLOSED** with what the fix was, because each one is a
|
||||
trap worth not re-digging. **Awaiting Oliver's next phone playtest.**
|
||||
|
||||
## What this is
|
||||
SNES-style pseudo-3D tennis (Super Tennis reference) that transitions into a zombie splatter game
|
||||
@@ -116,9 +118,22 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
|
||||
**Benchmarks that must keep holding** (re-run after any physics/aim change):
|
||||
- CPU returns ~11/12 serves (91.7%). Recent 150-sample runs: 90.7%, 89.3%, 90.7%, 88.7% (±2.5%).
|
||||
Watch this — the last few sit under the bar rather than scattered around it.
|
||||
**The harness for this number was never written down and I could not reconstruct it.** A rig that
|
||||
counts "did the CPU get a racket on the served ball" scores 100/100, because after a flub the CPU
|
||||
gets another swing at the SAME ball (`aiSwingCool = 0.5`) and connects — 91.7% ≈ 1 − aiFlub, so the
|
||||
recorded number must count only the FIRST swing opportunity, or only returns that land in.
|
||||
Until someone writes the rig down, prove "no tennis regression" **differentially** instead: run
|
||||
your rig, `git stash` the change, reload the modules, run the identical rig, compare. Equal
|
||||
numbers on the same rig is the real claim; a bare percentage against this line is not.
|
||||
- 30 human-tapped points (tap when the incoming ball is 2.2–3.2 m away) = **50 hits / 0 whiffs**.
|
||||
This one DOES reproduce exactly — drive it off `G.missLog` (HIT vs MISS counts).
|
||||
- A 26-game state-machine soak with bombs/bursts/fire must finish with no exceptions, no stall,
|
||||
`mgT` back to 0, and gore/piles arrays bounded.
|
||||
`mgT` back to 0, and gore/piles arrays bounded (gore caps at 40, piles at 30).
|
||||
**Two ways a soak lies to you**, both cost me a run: force-spawning zombies during `G.clearing`
|
||||
makes the hand-clear un-completable and reads as a 30-minute stall; and reviving the player after
|
||||
each `eaten` inflates gore past its cap, because the death push (`zombies.js`, the `eatenR` hit) is
|
||||
the one uncapped `gore.push` in the file — fine at one death per run, not at 260. Respect
|
||||
`clearing`, and either end the soak on death or set `TUNE.eatenR = 0` to make him untouchable.
|
||||
- **Never trust a single sample.** aiFlub is 9%; a 20/24 run is normal noise. Use 150.
|
||||
|
||||
## Asset pipeline
|
||||
@@ -170,52 +185,56 @@ Known generator quirks, all still true:
|
||||
|
||||
---
|
||||
|
||||
# OPEN ISSUES — from Oliver's 35-game retest (2026-07-27)
|
||||
Nothing below is fixed. Diagnoses are from reading the code this session; verify before trusting.
|
||||
# CLOSED — Oliver's 35-game retest (2026-07-27), all fixed 2026-07-30
|
||||
Kept because each one is a trap worth not re-digging. Every fix was verified in the browser via
|
||||
`window.T`; the numbers quoted are from those runs.
|
||||
|
||||
### 1. Player frozen (and stuck mid-animation) during the between-game pauses — HIGH
|
||||
During the ~5s changeover the player cannot move but the zombies keep walking. The whole movement
|
||||
block, including the `P.pose = moving ? 'run' : 'idle'` update, lives inside
|
||||
`if (G.phase === 'rally')` in `update()`. Every 'over' beat (changeover, game beat, set card)
|
||||
therefore freezes the player in whatever pose he held while `updateZombies` runs on.
|
||||
**This is almost certainly also issue 6** (freeze after the horde) — `endHorde` hands back to a
|
||||
phase where the movement block doesn't run.
|
||||
Oliver's stated options: *drop the 5s entirely, or stop the zombies for its duration.* Stopping the
|
||||
zombies is the more characterful of the two (the boys mop, everyone waits) but letting the player
|
||||
walk during 'over' in splatter is the smaller diff and fixes the stuck pose too. Consider both.
|
||||
### 1 + 6. Player frozen (and stuck mid-animation) during the between-game pauses — was HIGH
|
||||
Same root cause, one fix. The whole movement block — including `P.pose = moving ? 'run' : 'idle'` —
|
||||
lived inside `if (G.phase === 'rally')`, so every 'over' beat (changeover, game beat, set card) and
|
||||
every `pretoss` froze the player while `updateZombies` ran on. That `pretoss` case is issue 6:
|
||||
`endHorde` calls `setupPoint()`, which hands back to `pretoss`.
|
||||
The block now also runs whenever `G.mode === 'splatter' && G.phase !== 'toss'`. `'toss'` stays frozen
|
||||
on purpose — that is the aim, and the thumb is busy drawing the bow. **Tennis is deliberately
|
||||
untouched** (verified: a keyboard-held ArrowLeft moves the player 0.00 m during a tennis 'over',
|
||||
−2.70 m during a splatter one).
|
||||
Oliver's other option — stopping the zombies for the breather — was NOT taken. `startChangeover`'s
|
||||
"it's calm, not safe" is still the design; being able to walk was the actual complaint.
|
||||
|
||||
### 2. The mop boy walks on during the 1:1 duel — LOW
|
||||
`sendBoy` has no `G.interlude` guard and `startDuel` doesn't clear `G.boy`. The duel is meant to be
|
||||
clean tennis; nobody should be mopping through it.
|
||||
### 2. The mop boy walks on during the 1:1 duel — was LOW
|
||||
`sendBoy` now returns early on `G.interlude`, and `startDuel` sets `G.boy = null`. Note the ordering
|
||||
that makes this safe: `resolveInterlude('P')` clears `G.interlude` **before** calling `deliverTin()`,
|
||||
so the boy still comes out with the tin.
|
||||
|
||||
### 3. Triple serve: the two wing balls lob off at the wrong height — MEDIUM
|
||||
Confirmed cause. The real serve carries topspin gravity `ball.g = TUNE.gravity * (1 + quality *
|
||||
TUNE.serveSpin)` — up to **2.3×** `TUNE.gravity` at full power. The clones in `extras` are
|
||||
integrated at flat `TUNE.gravity` (`zombies.js`, the `for (let i = extras.length - 1 …)` loop:
|
||||
`e.vy -= TUNE.gravity * dt`). So the centre ball dives and the wings float long.
|
||||
Fix: copy `g: ball.g` onto each extra where they're pushed (`gameplay.js` in `hitServe`, and the
|
||||
re-fire clone lower down) and integrate with `e.g || TUNE.gravity`.
|
||||
### 3. Triple serve: the two wing balls lob off at the wrong height — was MEDIUM
|
||||
The diagnosis in the old text was right. `g: ball.g` is now copied onto each clone (both push sites:
|
||||
`hitServe`'s triple fan and `dragServe`'s re-fire clone) and `zombies.js` integrates
|
||||
`e.vy -= (e.g || TUNE.gravity) * dt`. Verified at full pull: clones carry g = 21.16 (2.3 × the 9.2
|
||||
base) instead of 9.2, and centre and wing apex heights now match exactly (2.90 m both).
|
||||
|
||||
### 4. The umpire calls "Time" on pauses that aren't changeovers — LOW
|
||||
`voice('time')` fires in the `over` branch of `update()` whenever `G.changeover` clears. Needs to
|
||||
distinguish a real changeover from other forced waits, or the line needs to move.
|
||||
### 4. The umpire calls "Time" on pauses that aren't changeovers — was LOW
|
||||
`G.changeover` is now the **seconds left**, not a flag (still truthy, so every `!G.changeover` guard
|
||||
reads the same), and it counts itself down near the top of `update()`, calling `voice('time')` when it
|
||||
hits zero. It no longer rides the generic `G.overT` expiry — anything that overwrote `overT` mid-
|
||||
breather (a melee kill taking a game, say) used to make the umpire call time on a pause that was
|
||||
never a changeover. Verified with a `fetch` spy on `assets/audio/time.m4a`: an unrelated `overT`
|
||||
expiry at t=0.4 s says nothing and leaves 4.6 s on the breather; the line fires exactly once, when
|
||||
the breather's own clock runs out.
|
||||
|
||||
### 5. The transition tutorial illustration confuses people — MEDIUM
|
||||
`assets/x_howto.png`. The curved arrow plus thumbprint reads as a rounding/scroll gesture rather
|
||||
than a pull-back. Oliver: *"maybe display a pulling and aiming finger?"* Worth regenerating as a
|
||||
literal finger pressing down and dragging back with a motion trail, and re-checking it at the
|
||||
modal's drawn size (~89×120 logical px in portrait) rather than at full resolution.
|
||||
Note `drawPullHint()` now demos the gesture live in-game, so the illustration can lean on that.
|
||||
### 5. The transition tutorial illustration confuses people — was MEDIUM
|
||||
`assets/x_howto.png` regenerated (Gemini 3 Pro, edit mode with the old sheet as `--input`, then
|
||||
`assets/_key.py … 240`). The curved arrow and thumbprint are gone; it is now a press ring at the top,
|
||||
a straight vertical dashed track, two chevrons pointing DOWN, and a fingertip at the bottom — with the
|
||||
yellow shot arrow flying the opposite way to the zombie, which is the inversion lesson. Aspect is
|
||||
unchanged (240×321 vs 240×322), so the modal still draws it at 90×120 in portrait. Checked at that
|
||||
drawn size, not at full resolution.
|
||||
|
||||
### 6. Sprite freeze after the horde — HIGH
|
||||
See issue 1; very likely the same root cause.
|
||||
### 7. Aiming plays the toss animation; it should stay in the pretoss stance — was MEDIUM
|
||||
`update()`'s forced `sv.pose = 'serve'` for `phase === 'toss'` is now gated on `!G.aim`: while the
|
||||
slingshot is drawn the server holds the pretoss stance, and only `hitServe` puts him into the
|
||||
toss/smash. Verified: plain tap → pose `serve`; drawn bow → pose `idle`.
|
||||
|
||||
### 7. Aiming plays the toss animation; it should stay in the pretoss stance — MEDIUM
|
||||
`input.js` touchmove calls `serveToss()` on the first drag, which sets `G.phase = 'toss'`, and
|
||||
`update()` forces `sv.pose = 'serve'` for the whole of `phase === 'toss'`. So the player is in the
|
||||
toss/serve pose the entire time he is aiming. Oliver wants the **pretoss** stance while aiming, with
|
||||
the toss/smash playing when the shot actually fires.
|
||||
|
||||
### 8. Show the current game number persistently — LOW
|
||||
Today it only appears as a 1.1s `GAME N` flash when the next game arms. Wants it visible somewhere
|
||||
permanent — the scoreboard is the obvious home (it already has a sets column in splatter).
|
||||
### 8. Show the current game number persistently — was LOW
|
||||
`GAME N` now sits permanently under the scoreboard, right-aligned to the board's width, on the same
|
||||
row as the NEXT pips and in the same dim tone (`render.js`, just before the pips block). The 1.1 s
|
||||
flash when a game arms is still there.
|
||||
Reference in new issue
Block a user