The ad strip sat 12px above the far baseline, but the out-area lawn reaches z=-6, whose projected edge is ~15.5px above the baseline in portrait (camF is larger there) — so the lawn painted over the bottom of the boards. Pinning the strip's bottom to proj(0,0,-6).y clears them in both orientations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tins now accumulate on a courtside rack (cap 3) instead of replacing each other; a PLAIN slot banks specials for later, and GUN is a charge you pop for an auto-fire burst. Tap the rack or press 1-5 to choose what your serve throws. A fresh delivery auto-equips but never overrides a deliberate PLAIN, and the 1:1 sudden-death duel forces plain balls.
HUD gets real art: generated pixel-art weapon icons, an ornate Wimbledon scoreboard PNG backing the live text (no more code-drawn boxes on top), and a bundled Silkscreen pixel font replacing the system serif. Rack tiles get padding, count-over-label alignment, and an infinity marker on the ball.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the rounded-panel version with a bevelled bottle-green box (light
top/left, dark bottom/right), a purple club spine, cream Georgia serif
names, and the live game sunk into a recessed slot — purple with gold
digits for the server. Sets/games stay quiet in divided columns; the
kills-to-next-point strip sits in a recessed well. Hard edges, no radius.
Designed in an offline harness, then ported; verified in tennis and
splatter modes. (Local const W/H were shadowing the module canvas W/H —
renamed to bW/bH.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bottle-green board with a purple flash down the left, cream Georgia serif
names, a yellow ball marking the server, and the live game in a purple
points cell. Sets/games in divided columns; the kills-to-next-point strip
tucks into a darker well below. Verified in tennis and splatter modes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- start screen: SPLATTER TENNIS logo + slogan, controls kept
- scoreboard redrawn as a real broadcast board (server ball, name, sets,
games, highlighted points cell); weapon stock moved to a status strip
- kills-to-next-point pips (NEXT ○○○) so the K-kill gate is visible;
TUNE.killPips toggles it
- miss-debug HUD off by default (TUNE.missHud 0), dev-only now
- eaten screen crows "NEW BEST" when the run beats the stored high score
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runner counter (no new weapon needed — one you don't hold when it shrieks
can't save you): the sprinter is now a GLASS CANNON — any ball contact at
any quality bursts it (main ball and triple-serve side balls; melee and
pellets already killed it). The hold-to-aim auto-lock targets the most
advanced zombie = the charging runner, so touch-and-release is the panic
button. Plus runnerVMax 4.2: the late-game speed ramp is capped so runners
stay humanly reactable.
Battery (iPhone drain):
- TUNE.fps = 30 (T-panel tunable, 0/60 = uncapped): frame-skip cap in the
RAF loop — halves paint work on a 810x1440 canvas, the dominant cost.
dt clamp raised 0.033 -> 0.04 so 30fps doesn't run 1% slow-motion.
- Gore piles pre-rendered to per-pile cached canvases (rebuilt on growth /
orientation change): 1 drawImage per pile per frame instead of ~26
ellipse paths — late-game courts had ~800 path fills per frame.
- matchMedia('(pointer: coarse)') hoisted out of the render loop (was
allocating a MediaQueryList every pretoss frame).
Verified: weak (pow .05) ball kills a runner; max-ramp runner v = 4.2
exactly; piles render from cache (screenshot, pl.cv set); frame-cap
arithmetic yields clean every-other-tick at 60Hz RAF (RAF itself is paused
in this pane — logic deterministic); tennis serve benchmark 30/36 across
runs (flub-band; diff is provably tennis-neutral); no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Umpire chair moved courtside (UMP_X 5.6, was 7.1 — which projected to
x=272 on a 270px-wide portrait canvas: literally one pixel off-screen).
Tap-cheat coords follow the constant.
- Generated pixel-art night crowd (x_crowd.png) replaces the tier stripes +
random dots; drawn in 16 columns that bounce independently while the crowd
cheers. Old procedural crowd kept as the loading fallback.
- Generated courtside sponsor boards (x_ads.png): ACEADE, TOPSPIN & CO,
VOLLEX, NETTO GRIP, SLAMWICH — tiled at native aspect so lettering is
never half-cut at any viewport width. Replaces the 'SPLATTER OPEN' text.
- One rot tint over crowd + boards driven by the shared paletteT() (new
helper), so the stadium dims into splatter and brightens back for duels.
Verified: fresh origin; tennis + splatter screenshots (boards tile cleanly,
crowd reads, tint dims, umpire on-screen beside the court); umpire triple-tap
works at the new chair position; tennis serve benchmark 33/36 across two runs
(first 12 was flub noise); no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RES 2 -> 3: phone screens now DOWNSCALE the canvas (crisp pixels) instead
of upscaling it (soft). Umpire-tap coords un-hardcoded from RES=2.
- Flatter lens: camZ 36 -> 42 with camF scaled per orientation (406/580) so
the PLAYER stays the same size — far-court zombies render ~25% bigger and
finally read as individuals from the baseline.
- Smoother animation: 4-frame walk cycles for both classic shamblers and the
runner's sprint (new sheets, same style anchor; z_a1-4, z_b1-4, z_f1-4).
Same cycle frequency — frames subdivide the stride. Render falls back to
2 frames per variant until its sheet loads.
- Tiered gore (GORE_MUL): the more advanced the zombie, the more absurd the
mess — classics 1x (base bumped 24 -> 30 particles), runner 1.8x, shield
2.5x, racket boss 3x, brute 4x (120 particles, a scatter of stains, an
instant size-3 mound).
Verified: fresh origin, canvas 1440px, umpire triple-tap works at RES 3,
all 4 anim frames cycle at the old stride rate, brute kill = 120 parts /
2 stains / size-3 pile, far-court lineup screenshot clearly readable,
portrait layout numbers correct (810x1440, camF 580), tennis 11/12,
no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE FREEZE (root cause found): the game-winning kill fires INSIDE the zombie
update loop — killZombie -> splatPoint -> gameOver -> sweepCourt guts the
zombies array mid-iteration, the loop reads zombies[i] === undefined, throws,
and the un-rescheduled RAF chain dies: a permanent hard freeze. Crowded courts
(later games, brutes) make the mid-loop game-winner likelier — hence 'freezes
around game 3 when the fat zombies arrive'. Fixed at the root (null guards in
every loop whose body can cascade into a sweep: zombie walk loop, meleeSwipe,
explode, pellet + extras inner loops) AND made frame() reschedule RAF FIRST so
no single bad frame can ever kill the loop again.
Aim awareness: the reticle now appears the moment the thumb lands (mag 0 =
auto-target the most advanced zombie, red lock immediately). A pure HOLD
(>250ms, no drag) releases into the auto-target serve; dragging aims as
before; the serve prompt switches to 'RELEASE TO SERVE / RELEASE FIRES THE
BOMB - DRAG BACK TO CANCEL' while aiming.
Duel presentation: the interlude now opens with a 2.4s intro beat — serving
blocked, centered card (SUDDEN DEATH / ONE POINT - THE LINES ARE LIVE / WIN
IT FOR THE NEXT TIN), and the palette lerps back to the TENNIS colors for
the duration of the duel (duelLerp, 0.8s each way) — order visibly restored,
then the rot returns.
Machine-gun steering: absolute now — the finger x IS the player x, zero lag
(was sign-quantized velocity). Clamped to what the camera can see
((W/2-14)/proj-scale — portrait's world bounds are wider than its viewport),
keyboard steering clamped the same during auto-fire.
Verified: the exact pre-fix crash scenario (game-winning kill mid-loop,
9 zombies + brute) survives and sweeps clean; intro blocks taps then the
duel plays; hold-release fired at the auto-lock and won a tin end-to-end;
screenshot shows tennis-green court + centered card + GAME 2; MG jumps
P.x to the finger in one frame, clamped; tennis 12/12; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flick-to-aim was aim-by-faith — you learned where you aimed after the ball
was gone. Now, at a serveable pretoss, touching and dragging shows a LIVE
reticle (same mapping: direction + length = landing point); release serves
there. The magnet is finally visible: within zSnap the ring turns red and
rides the locked zombie (plus a marker over its head) — you see the lock
BEFORE you commit the tin.
- Context locked at touchstart: a movement drag started mid-rally never
morphs into a serve when the ball dies (sub-300ms releases still count
as the rapid flick re-serve — the Phase 1 cadence survives)
- Drag back home = cancel: no serve, the tin stays in stock
- Aimed (drag) serves commit specials at fixed 0.62 quality; the tap-toss
ritual keeps the PERFECT-timing power ceiling and the stock — placement
vs power is now a real choice
- Rally / horde / machine gun untouched: drag steers, flick swings
- Serve prompt: 'TAP TO SERVE - HOLD + DRAG TO AIM' / 'AIMED SERVES FIRE
THE BOMB - TAP KEEPS IT'
Verified with synthetic touch sequences: drag arms the reticle without
serving and it tracks the thumb; release serves at the marker with the bomb
committed; cancel keeps the tin; rally-started drags never morph; locked
reticle renders red on the zombie (screenshot); tennis 12/12; no console
errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oliver's six complaints, addressed:
1. Opponent never came — a racket zombie now spawns on EVERY game you win
(if none alive); endHorde no longer double-spawns
2. No wave-cleared feeling — sweepCourt: at game end the surviving wave
collapses, every game gets a real beat (gameBeatT 1.6 / changeover 4.5),
and 'GAME N' announces the next wave when play re-arms
3. Serve-posture lockup during machine gun — root cause: any 'over' beat
expiring into setupPoint() -> pretoss during auto-fire (no movement outside
rally, no serving during horde/GUN). The over-expiry now returns auto-fire
modes to 'rally', never the serve loop
4. Forced bomb spending — specials commit ONLY on deliberate serves: flick
(or arrow-aimed keyboard serve) fires the tin, plain tap keeps it; serve
prompt teaches it ('FLICK FIRES THE BOMB - TAP KEEPS IT')
5. Auto-aim — free-aim: the flick vector maps to a landing point (length =
depth, up to 22m), zombies within zSnap 1.4 magnet the shot, otherwise the
ball lands exactly where flicked; taps keep the old auto-target assist;
flicks now also work at the toss apex
6. Zombies walking through you — option (a): touch is honest. eatenR 0.9
(brute +0.2) matches the visual sprite overlap
Phase 13 — SUDDEN DEATH interlude: court clear + un-dueled racket zombie +
about to serve -> 'Quiet, please. Play will resume.' / 'Sudden death.' One
point under REAL tennis rules (line calls live, the boss plays every ball,
spawns paused). Win (line call, double bounce, or regicide by any weapon) =
the next tin delivered on the spot; lose (out, or its return passes you) =
a game on their scoreboard + an instant 5-shambler surge. One duel per
opponent. New lines: play_resume.m4a, sudden_death.m4a.
Verified in browser: horde 'over' beat -> rally (lockup dead); game win ->
sweep + boss + beat -> SUDDEN DEATH chains naturally; duel lose path (GAME.
THEM., surge 5, scoreboard), no re-duel, fresh-boss duel, bomb regicide ->
tin on the spot; tap serve keeps stock, real touch-flick consumes + lands
at the flicked point, magnet snaps when near; eaten at 0.85m; tennis 11/12;
no console errors. PLAN.md/HANDOFF updated: plan v2 complete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deliveries cycle BOMB -> TRIPLE -> FIRE -> GUN (G.deliveries indexes the
ladder); one tin at a time, a fresh delivery replaces unspent stock. All
weapons stay one-thumb: the serve auto-consumes, zero new inputs.
- Triple serve (x3): one real serve fans two ballistic clones +-6deg (extras[]
in zombies.js — NOT the reverted rapid-fire: each fan costs a proper serve).
Side balls respect variant rules (wound brutes, clank off chairs, pass the boss)
- Flame ball (x3): kills on ANY contact regardless of pow, chair ignored,
full pierce (splatter pace tax undone), ember trail, white-hot render
- Machine gun (8s): horde auto-fire as a pickup — no serving, drag steers,
no assist, spawner keeps running, ammo bar drains on the HUD, then the
serve loop resumes
- HUD pips per type (black/yellow/orange) at the bomb slot; flash names the
tin ('NEW BALLS: FIRE')
Verified: ladder cycles across 5 deliveries; triple spawns 2 extras and the
fan kills flanked targets; pow-0.1 flame ball clears shambler+shield+brute in
one pass with TRIPLE KILL chaining; GUN tin arms at changeover, blocks taps,
streams pellets (kill in 5 frames), expires back to pretoss; tennis 11/12;
no weapon-flag leaks into tennis; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Spawn mix (pickVariant): roster grows one type per set — runner from set 1
game 3+ (12% slot, at most one alive, shriek on spawn = the fairness contract),
brute from set 2 (15%), shield from set 3 (15%); classics fill the rest
- Runner ('f'): 3.2x shamble speed, faster leg cycle; new shriek() synth in audio
- Brute ('g'): hp 2 vs splat-quality balls AND pellets, 0.55x speed, 0.75m target,
pushback-immune, 0.9m eaten radius, dies to double gore + size-2 pile, towers
at 1.5x height
- Shield ('s'): blocks front balls with the chair (clank, ball spent), the block
drops the chair for shieldDown 1.5s — any splat-quality ball kills in the
window (z_s3 staggered pose renders); chair recovers after; always dies to
bombs, melee, pellets
- TUNE: runnerSpeed 3.2, bruteHp 2, shieldDown 1.5
Verified in browser: per-variant stats; brute soft-ball immunity, 2-hit kill,
2-pellet kill, size-2 pile; shield block->window kill->chair recovery->blocks
again; melee kills f/s but not the racket boss; tier-2 spawn mix over 1500
spawns = 14/15/8/63%; all five sprites render with correct scale+occlusion;
tennis regression 11/12; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Camera shake (G.shake, decays over 0.35s; HUD stays steady via transform reset)
- One white flash frame (G.boomFlash overlay)
- Scorch crater: near-black stain the boys mop 10x slower — the lawn scars
- Hitstop 0.1 -> 0.18
- Umpire loses composure: 'Order. Order, please.' (new order.m4a), rate-limited
so bomb chains don't spam him
Verified: explode kills cluster of 4 (MASSACRE x4 chains), shake/flash set and
decay clean, 2nd bomb order-call suppressed, 1s of mopping erases blood (0.9->0)
but not the crater (1->0.84); screenshot mid-shake; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Spawn director: gap and court cap scale with games finished (runGames);
game 1 is three lonely shamblers, late runs approach zSpawnMin/zMaxCap
- Changeover after odd games: 4.5s forced breather, no spawns, no serving
(melee still works), boys mop, umpire calls 'Time.' (new time.m4a)
- 6 games = a set; set end launches the horde (replaces the splatGames>=2 trigger)
- Zombies taking a game sets shameGame (phase 9 withholds the delivery)
- Eaten = the run is over: GAME, SET, MASSACRE summary with games/sets survived
and best run (localStorage); HUD shows sets in splatter (SETS·GAMES PTS)
- spentBall() helper so dead-ball paths can't clobber a changeover's beat
- T surface: splatPoint + explode exposed for verify scripts
Verified: game-1 cap 3 held 25s; changeover blocks taps+spawns then resumes;
6 games -> set+horde -> racket zombie; runGames=6 tempo ~6 spawns/10s;
eaten writes best; tennis regression 11/12 CPU returns; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Survive a horde and a racket zombie takes up residence on the far court
(variant 'r', one per horde survived) — the tennis rally skill lives on
inside zombie mode. It reuses the turned-CPU z_c sprite (already holds a
racket) and a slimmed copy of the tennis AI: holds station deep, chases a
ball laterally at TUNE.racketSpeed (2.8), and returns your SOFT shots
(pow < zSplatQ) as live lastHit:'O' balls with quality capped at
racketQMax (0.55) and racketFlub (0.2) whiffs. trySwing gained an
optional quality-cap arg (defaulted, so tennis is untouched).
It's a mini-boss with TUNE.racketHp (3): only splat-quality balls hurt it
(-1 hp, staggers the ball; the killing blow runs the full splatter). Soft
balls pass through for it to rally; melee and pellets don't touch it. Its
return that gets past your baseline is a point for THEM; you can rally it
back with the existing player hit. A red HP bar floats over its head.
Verified via window.T: endHorde spawns exactly one (hp 3, z 3.5); three
splat hits kill it (2→1→dead); a pow-0.3 ball is returned toward you; an
'O' ball past your baseline scores for THEM; melee spares the boss but
cleaves the shambler beside it; tennis still 12/11/12 and a normal
shambler is still one-shot; renders with its HP bar; no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Kills now grow 3D-ish gore mounds, not just flat stains. zombies.js gains
a piles array {x,z,size,seed}: a kill within 0.6m of a pile grows it
(size +1, cap 4), else starts a new one (cap 30, oldest shifts out).
Ball boys shave overlapping piles at size -= dt*0.25 — far slower than
stain mopping (dt*1.6) — so net growth wins over a long match. Splatter
particles bumped 14→24, ~1/3 dark and longer-lived (settling gore),
render reads pt.dark for colour.
render.js draws each pile as seeded pixel-art blobs (mulberry32 from
p.seed): N=6+size*5 blobs, offset within size*0.28m, lifted up to
size*0.14m so the pile has visible height, weighted gore colours + ~10%
bone flecks. Blobs are built once and rebuilt only when the pile GROWS
(builtSize guard) — never on the per-frame mop-shrink — and each pile
costs a single proj() call (blob screen positions offset from the base).
Piles draw split far/near of the net like zombies so height occludes
right. window.T exposes render/piles/gore/parts for verification.
Verified via window.T: clustered kills build tall dense mounds while
singles stay small (screenshot), avg render 0.61ms with 10 piles, 0 blob
rebuilds across 120 frames, mop shrinks a size-4 pile at exactly 0.25/s,
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>
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>