Oliver's feedback on the feel pass:
1. Rapid-fire serving was way too easy. Reverted splatter serving to the
proper model: a tap does the toss ritual (apex timing = quality), a
flick is a quick aimed serve. Dropped the multi-ball barrage and
removed the extraBalls plumbing (array, update, render, hooks). Melee
swish and zombie homing stay. If multi-ball is wanted back, it should
require a proper serve per ball.
2. The player was stuck in the serve/swing pose when a horde started
(the triggering shot's pose lingered) — startHorde now clears it.
3. A horde re-triggered immediately: machine-gun kills scored points, so
games were won mid-horde and the next kill after it ended won another
game → instant new horde. A horde is now survival: it doesn't score
(splatPoint skipped while G.horde), so it can't chain. After a horde
you must win a full game again to summon the next.
Verified via window.T: a tap tosses (not an instant serve) and the full
toss→serve→rally completes; melee still kills adjacent shamblers; pose is
idle/-1 at horde start; no scoring during a horde and the next kill after
scores 15 without restarting it; tennis 11/12, bomb consume, homing all
still green; no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From Oliver's feedback round:
- Racket melee: any swish (tap) in splatter cleaves shamblers within
TUNE.meleeRange (1.7m) — a close-range survival tool. Works during a
horde too (tap = swish only, still no serve). The racket-zombie boss
(Phase 6, not built) is immune by design.
- Zombies home in on you: on YOUR half (z > netZ) they now drift toward
P.x instead of walking a straight lane; far half stays an aimless
shamble.
- Multi-ball / rapid fire: splatter is now a shooter — every tap/flick
fires immediately (dropped the toss ritual there), and serving while a
ball still flies stashes the old one into a new extraBalls array so it
keeps arcing and killing (cap 6). Refactored the kill into a shared
killZombie() with no ball coupling, used by ball hits, pellets, melee,
and extra balls.
- Machine-gun mode clarity: confirmed free-walk (drag/arrows move you,
auto-assist off) and tap-doesn't-serve during a horde; added a
code-drawn "rambo" racket held forward with a muzzle flash over the
player, and a splatter-specific serve prompt.
REVIEW on your phone — two opinionated calls, both easy to revert:
1. Dropped the toss in splatter (tap = instant quick-serve, pow 0.62).
Rapid fire, but no apex-timing quality/pierce. Want the toss back as
an option?
2. MG posture is a code-drawn forward racket, not a real body sprite —
good enough, or worth a proper p_mg sprite later?
Also: each rapid serve still auto-consumes a stocked bomb, so bombs
deplete fast now — flag if that feels wrong.
Verified via window.T: melee kills a 1.0m zombie and spares a 4.5m one;
a near-half zombie drifts x 3→1.9 toward you; firing twice leaves 1
stashed ball + a fresh serve (barrage screenshot); extra balls kill and
pierce; free-walk during a horde (x 0→2.7); horde-tap melees without
serving; tennis toss-serve and Phase-4 bomb consume still work; no
console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Winning your 2nd splatter game (G.splatGames) — or triple-tapping the
umpire in splatter (forceZombies now calls startHorde) — launches a 12s
horde (TUNE.hordeLen). During a horde: the match ball is gone and taps do
nothing; the racket auto-fires pellets straight up-court at TUNE.mgRate
(6/s, ±4° spread, 26 m/s, flat, 1.2s life) and the thumb DRAG steers you
(auto-assist disabled). Zombies pour in at TUNE.hordeRate (2.5/s) from the
far baseline and the sides, ignoring zMax; the speed ramp still applies.
Pellets live in a new zombies.js `pellets` array, kill on contact (always
splatter-quality). splatter() gains a fanfare flag — pellet kills make
gore/piles/points but skip the multi-kill streak so a horde doesn't spam
MASSACRE. splatPoint() gains a quiet flag: during a horde the umpire only
calls a score every >2s (game-win lines always speak); the HUD score keeps
ticking. Horde ends → setupPoint() resumes serving, voice('quiet_dark');
leftover zombies stay for you to mop up. Eaten stays lethal during hordes
(a zombie reaching you ends it and clears the swarm). Pellets render as
2×2 yellow tracers with a ground shadow; serve prompt suppressed while
G.horde. window.T exposes pellets + forceZombies.
NOTE for Oliver: per spec every game won after the 2nd triggers a horde,
and games won by the machine gun mid-horde count too, so play gets very
horde-heavy after game 2 — flag if you want a cooldown or a knob.
Verified via window.T: cheat + 2nd-game both launch the horde (1st does
not); ~29 spawns over exactly 12s; pellets stream (7 in flight) and clear
a lane, building piles/gore; 4 points scored but the umpire speaks once
(rate-limited); horde ends cleanly back to pretoss; eaten still fires
mid-horde; regression ladder green — tennis 11/12, Phase 1 flick-serve,
Phase 2 DOUBLE→TRIPLE, Phase 4 MASSACRE ×4; no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ball boys deliver a bomb ball every TUNE.bombEvery (8) splatter kills, up
to TUNE.bombMax (3) in stock (G.bombs / G.killMeter in state). The next
serve auto-consumes one (hitServe, splatter-only) — no extra input, keeping
one-handed play. A bomb detonates on first zombie contact (regardless of
pace) or, if it misses everyone, on its first bounce: explode() splatters
everything within TUNE.bombRadius (2.5m), each kill feeding ball.kills so
the Phase-2 fanfare escalates naturally, and leaves a crater — big stain,
size-3 pile, 40 particles, 0.1s hitstop, boom(). New synth boom() in
audio.js. Bomb ball renders black with a flickering orange fuse; the HUD
shows stocked bombs as black/orange pips by the YOU line. Both explode
sites set phase='over' so the serve loop doesn't stall on the killed ball.
Alternative delivery ideas Oliver floated but not built: umpire throws it,
or court pickup.
Verified via window.T: 8th kill earns a bomb (meter resets); a serve
consumes one and renders #111 black + orange fuse; a pow-0 bomb amid 4
clustered zombies kills all 4 with 'MASSACRE ×4', boom, hitstop 0.1,
crater stain (r1.4) + size-4 pile; a bomb that misses still craters on
first bounce; bombs never leak into tennis (mode guard); 3 HUD pips; no
console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single shot that splatters ≥2 zombies now escalates: DOUBLE KILL /
TRIPLE KILL / MASSACRE ×N flash, louder crowd, and a new umpire line
(double_kill / triple_kill / unbelievable m4a, recorded say -v Daniel).
ball.kills tracks the streak, reset on every fresh shot (doHit + hitServe).
zombies.js imports cheer (now exported from gameplay). The splatter pace
cut is now TUNE.zPierce (0.75, was hardcoded 0.5) so a hot ball plows
through a lined-up lane instead of stalling on the first kill.
Verified via window.T: a low ball down a one-side lane kills 3→TRIPLE and
5→MASSACRE ×5 with escalating flash, streak resets on the next serve, the
three new lines serve 200 and match the existing m4a format, no crash when
the ball dies mid-streak, no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Input during the 'over' beat is no longer swallowed: a tap/flick while
G.phase==='over' (splatter only) resets inline and falls through to the
serve logic, so the finger is the only cadence limit. Spent balls
(lastHit 'X' after a stagger bounce) and hopeless balls (out of the
z/x bounds) are retired immediately instead of lingering until they roll
out. New TUNE.splatterOverT (0.1) replaces the hardcoded 0.25 over-timer.
The out-of-bounds z-bounds gate on travel direction so a quick-serve born
behind the baseline (P.z up to 26, heading vz<0 to the far court) isn't
killed on frame one.
Verified via window.T: flick/tap during 'over' serves same frame; spent
ball dies the frame it bounces; serve->serve latency 0 frames with
immediate flicks; tennis 'over' unchanged (2.2s pause, tap ignored);
tennis CPU still returns 11/12 serves; no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A ball passing pre-bounce moves at full pace with no bounce cue to time
against, so marker-behind volleys needed frame-tight taps. Pre-bounce
player contact now widens reach x1.4 and the swing window x1.8 (VOLLEY
in config.js). Also clear G.wasServe in doHit so a serve returned out
of the air doesn't fault-check the return, and launch.json uses
autoPort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- doHit/hitServe in splatter mode lock onto zombies instead of tennis court
depths: your flick picks the lane and depth band, the shot targets the best
match anywhere on the court (including your own side) with a walking lead;
serves auto-track the most advanced shambler
- hidden shortcut: tap/click the umpire three times within 1.6s during tennis
mode to summon the transition immediately
Verified: near-side and far zombies killable, cheat triggers transition,
tennis mode targeting untouched.
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>