Field data (miss log) showed the dominant unfair miss: the assist parks
you on the landing spot, the ball spends the whole swing window below
minY rising off the bounce at your feet, and becomes hittable one frame
after the window closes. Root fix: the below-ankles rejection now only
applies to a DROPPING ball — rising post-bounce balls can be scooped at
any height (a half-volley). Feet-bounce taps go from ~0 to 19/30 winning
frames; 30 simulated real points now log 50 hits, 0 whiffs.
Also: swing-log snapshots record the same swing-timer value trySwing
tests (removes the one-frame misclassification behind 'unclassified'),
and tapping the scoreboard toggles an on-screen dump of the last 9 log
entries so live situations can be screenshotted from the phone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Whiffed swings sample the ball's approach each frame and classify the
miss at swing expiry: tap too early (with ms), tap too late, ball line
wide, too high/low, out of reach. Balls that die on your side without
a swing log as PASSED with closest-approach data. Ring buffer of 50 at
T.G.missLog; the latest miss reason flashes on the HUD for 3.5s
(TUNE.missHud to disable). CPU serve-return benchmark still 11/12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Win your first game and the applause cuts off mid-clap. The palette drains,
the umpire lowers his voice ('Quiet... please'), your opponent turns — torn
red polo, dragging his racket — and shamblers come over the billboard wall.
The scoreboard rewrites CPU to THEM. 'Play.'
- zombies.js: shamblers walk your way; quality shots splatter them (gore
decal + particle burst + wet synth splat), weak sitters only stagger them
back; one crossing your baseline is a point for THEM
- tennis scoring machinery keeps running untouched: fifteen, thirty, deuce,
and 'Game, set... massacre' on a game win, all in recorded umpire English
- ball boys mop the gore they pass (their destiny fulfilled)
- transition: palette lerp, scripted spawns incl. the turned opponent,
new recorded lines (quiet_dark, remain_seated, play, new_balls)
- moan ambience, drained splatter palette, all zombie params in TUNE
Verified: transition triggers on first game win, splat/stagger/baseline
scoring all correct, dead-ball flow feeds fresh serves, tennis mode
regression clean (6/6 returns, 6/6 points resolved).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
game.js (830 lines) -> 9 modules, native ES modules so still zero build step:
dom canvas handles (imports nothing; keeps cycles harmless)
config TUNE (live-tunable) + structural constants
state all mutable game state (G) + setupPoint/sendBoy
audio sfx synthesis, recorded umpire voice, reverb, speech fallback
input keyboard + touch, exports axes and takeFlickAim()
gameplay ballistics, swings, serve, rules, AI, update loop
render camera, viewport, sprites, crowd, all drawing
panel T tuning UI
main boot, loop, intro, window.T test hooks
Deduped into named constants: bounce tempo (was in 3 places), contact
height band, intercept zone depth, home positions, player bounds, toss
params, shot depths, pose hold time, sprite scale, actor heights. Also
extracted drawShadow/drawBall/expirePose from repeated code.
One intentional import cycle (input <-> gameplay), safe: both sides only
call functions at runtime, never during module evaluation.
Regression-verified in browser sims: point cycle, CPU returns 11/12,
assist contract 8/8, corner-winner gradient intact (0/12 perfect corners
returned, 10/12 center balls returned).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>