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>
predictLanding stepped the trajectory in 10ms chunks and returned the first
point past ground level — an overshoot of up to ~25cm that varied per frame,
which is what actually jittered (the earlier pixel-snap only addressed
sub-pixel shimmer). The landing point of a parabola has an exact solution;
computing it directly makes the prediction identical every frame.
Measured: max frame-to-frame movement 5.5mm, was ~25cm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hand-raised frame while the ball rises, pat-down frame from the apex on —
synced to the ball's direction instead of a height threshold. Includes
Oliver's hand-tuned ball offset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- p_bounce/p_bounce2 re-extracted from Oliver's corrected _sheet_pbounce.png
(racket properly in the right hand)
- bounce ball offset 0.4 -> 0.32 so it sits under the reaching hand
- landing rings draw with integer-snapped center/radii and a solid 2px
stroke — the sub-pixel anti-aliasing shimmer read as flickering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sprite file was fine — drawChar mirrors sprites by P.facing, which
persisted from the last run direction, so the pre-serve bounce stance
randomly flipped (racket in the wrong hand, ball beside the wrong hand).
setupPoint now resets both players to the canonical righty stance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- boy runs on a proper 4-phase cycle (stride, pass, mirrored stride, pass)
- server holds the crouched bounce stance through pretoss instead of
flickering to standing idle; hand-down/hand-up frames sync with the ball,
which now bounces floor-to-hand height
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- fake topspin: quality shots dive under extra effective gravity, so real pace
(up to 28 m/s) can clear the net AND land in — without it the net-clearance
loop silently slowed every shot to a coverable lob, which is why the CPU
returned everything regardless of aim
- no grass-scraping: balls below ankle height (0.24m) are past you, both sides
- CPU: 0.45s shot-read delay, 4.3 m/s legs, half-speed recovery between shots.
Verified gradient: perfect/good corner shots are winners, center balls and
weak shots still come back
- player aimed shots go wider (3.3m vs 2.7m)
- ball boy: nano-banana mirrored stride frames (was stride-vs-hop), height
1.2m, z-ordered against umpire and CPU (no more running over the chair)
- umpire voice through a synthesized convolver reverb — center-court PA echo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- intercept() now picks a point the runner can actually reach in time
(travel-time feasibility), falling back to the deepest hittable point
- tapping no longer slows the auto-run: anticipating a shot doesn't lose it
- assist legs slightly superhuman (1.15x) so positioning is guaranteed
- ball boy run frames regenerated with contrasting stride/gather poses
(previous pair was near-identical, which read as no animation)
Verified: 10/10 CPU shots end inside the honest 1.3m contact radius with
zero manual movement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ball boy redesign: spawns at the sideline on point end, sprints across at
the ball's depth with a proper side-view run animation, scoops the ball
mid-run, despawns off the other side (no more net-post idling/sliding)
- hit feel: ball snaps to the racket at the contact frame so hits visually
connect; contact radius tightened 1.7 -> 1.3 (assist does the positioning,
the tap must be honest); missed swings now play an audible swish
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- white ring shows where the incoming ball will land; yellow/red ring shows
where your shot will land (red = going out) — Mario-Tennis-style feedback
- ball boys wait at the net posts, fetch every dead ball, and trot home
- canvas renders at 2x density, all sprites re-sliced at 112px; run/swing
sheets regenerated in the finer style
- assist: 0.35m deadzone + facing hysteresis kills the left/right flicker;
player reach 1.5 -> 1.7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 23 pre-recorded umpire lines (macOS say, en-GB Daniel) played via WebAudio,
works on mobile; speechSynthesis stays as fallback only
- umpire chair + two ball boys (generated sprites) dress the court
- court redrawn: doubles alleys, white perimeter border, clearly darker out area
- intro overlay with control symbols; first tap dismisses it and unlocks audio
- nginx now serves everything Cache-Control: no-cache so a reload always
revalidates and picks up new deploys
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same predictive brain as the CPU: simulate the trajectory, run to where the
ball will be hittable (x and z), and drift back to home position after your
own shot. Full speed while chasing, 55% while recovering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new 5-pose sheets per character: idle weight-shift x2, ball-bounce crouch,
serve trophy pose (toss), overhead smash
- serve shows toss stance through the ball toss, smash frame on contact
- auto-assist: full player speed, adds depth tracking after the bounce,
and drives the run animation + facing like manual movement
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- doHit now trades pace for loft when contact is low, margin scales with
shot quality; AI prefers waist-high interception instead of ankle-height
- serve stance held through toss, run cycle with bob, receiver split-step,
server bounces the ball while waiting
- speechSynthesis unlocked inside first touch gesture for iOS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AI armed its swing at aiReach+0.4 but contact required aiReach, so it
always swung one guaranteed-whiff too early and the 0.4s cooldown consumed
the whole approach window. Also: predictive interception instead of ball-chasing,
shorter whiff recovery, slightly faster legs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>