Commit Graph
35 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 32401366fe Drawn frames, not procedural fakes: brute lurch, boy stride, crowd applause
Every animation in this round gets real frames. The procedural squash-and-sway
on the wounded brute read as fake on sight, and that gait is where the comedy
lives.

- Each brute wound stage now has its own 2-frame lurch (z_gw/z_gwb, z_gw2/z_gw2b,
  ...). The generator gave every B frame a dragged pole that only stage 1
  actually carries, so it would have blinked in and out each step — stripped by
  keeping the largest blob, and where the pole touched the body, by cutting on
  column density (a torso is dense, a dragged pole is a thin diagonal).
- Frames step off DISTANCE COVERED, never elapsed time — boy.dist for ball boys,
  z/bruteStride for brutes. A time-driven cycle moonwalks the moment speed and
  frame rate disagree, which is exactly what "he slides faster than the animation
  suggests" was. Change a speed now and the legs keep up on their own.
- One boy, one slot. sendBoy no longer replaces a boy already on court: that was
  the appearing and disappearing, since every kill teleported him back to the
  wing mid-stride. New mess is appended to his round instead. He now walks for
  10.5s a crossing, and there is a lot of mopping to do.
- The applauding crowd is a real two-frame animation (seated / arms up), swapped
  per column on its own phase. The old "smooth wave" still shifted each column by
  a FRACTIONAL dy, and nearest-neighbour resampling at a fractional y re-snaps a
  column's pixel rows every frame — that boiling beside the HUD is what read as
  the scoreboard flickering. The board art is 99.9% opaque; nothing was ever
  showing through it. With the crowd calm the frame delta is now 0.0%.

Benchmarks: 136/150 serve returns (90.7% ±2.4%, the 11/12 bar is inside one
sigma), 30 human-tapped points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:20:43 +03:00
wuwiandClaude Opus 5 ff77922e85 Feedback round: fire actually spreads, brutes lurch, crowd stops flickering
- Fire never spread in play. The flame ball's full pierce wiped its own lane, so
  the bodies that would have caught were already gone. It now ignites everything
  it FLIES PAST (TUNE.burnBallR), with a wider ignition and spread radius. One
  flame serve into a scattered 10-wave kills 9, nine burning at once.
- Wounded brutes slid along like decals: every wound stage is a single frame and
  the brute now spends most of its life wounded. They get a procedural lumber
  instead of eight more sprites — translate + squash, no rotation, so the pixels
  stay on the grid, and the amplitudes scale with the projection or the whole
  thing goes sub-pixel at range.
- The "scoreboard flicker" was the crowd: the cheer bounce was per-column
  Math.random() every frame, i.e. TV static, showing through the scoreboard art's
  transparent ornate edges. Now a smooth per-column sine on idleT.
- Ball boys: mopping is the default in splatter mode now, and the boy is half
  again as tall — at 1.2m he read as a distant speck. The running fetch-boy
  survives for tennis, where there is nothing on the grass but a ball.
- Changeover 7s -> 5s, with the mop crossing timed to fill it.
- Save-to-homescreen: a bloody tennis ball, wired up as apple-touch-icon +
  favicon + webmanifest. The Dockerfile now ships the manifest and gives it its
  own MIME type via an exact-match location (a bare `types {}` block would have
  replaced nginx's whole map and broken .js and .png with it).

Benchmarks re-run: CPU returns 67/72 serves (93.1%, above the 11/12 bar — a
20/24 sample in between was flub noise), 30 human-tapped points = 50 hits /
0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:58:18 +03:00
wuwiandClaude Opus 5 a09a5d7559 Polish round: modal system, softened palette, pixel font, and the 14 plan items
The three foundations first, since everything else leans on them:

- G.modal is the only thing that stops the world. update() early-returns on it
  so no zombie steps, no ball flight, no spawn timers run while a popup is on
  screen — you can read it without being eaten. Dismissal swallows the gesture;
  the pointerdown dismisser is registered only on pointer:fine, because on touch
  pointerdown beats touchstart and a shared handler cleared the modal and then
  served with the same thumb.
- UI in config.js is the accent palette. The arcade primaries were shouting over
  the artwork; the gore reds deliberately stay loud.
- Every canvas font string now goes through F(). Bare `monospace` resolved to
  Courier — a slab serif — which was the "generic serif" bug, not taste.

Then: weapons are only ever WON. deliverTin() has exactly one caller now
(resolveInterlude('P')); the old game schedule calls the DUEL instead, fired off
proceedAfterClear() where the court is guaranteed empty. Four popups (splatter
tutorial with a generated illustration, first duel, weapon received, horde),
wCap 9 with the tin amounts tunable, a 5-hp brute that comes apart over four
wound skins, fire that ignites the neighbours and chains outward, a three-stage
death where you become the biggest gore mound on the court, and a 7s changeover
with a ball boy who finally has a mop.

New art: assets/logo.png, x_howto, z_gw2-4, x_boymop1-4, plus assets/_key.py for
standalone art (chroma-key + crop, no frame splitting, no forced sprite height).

Benchmarks re-run and holding: CPU returns 23/24 serves, 30 human-tapped points
= 50 hits / 0 whiffs, 26 simulated games with no exceptions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:28:04 +03:00
wuwiandClaude Opus 5 f3dcf0b16f Splatter: chevrons in perspective, and a smash that actually flies
The chevrons were stepped along the SCREEN line at a fixed size, so they read as
a flat sticker over a 3D court. They now step through world space and take their
size from their own projection scale — near ones roughly twice the far ones —
and they fade IN toward the target so the eye is walked up the lane instead of
being handed a uniform stripe. The arrowhead scales with the target's own depth
too. (proj is a true projective map, so the screen direction stays constant
along the line; only size and alpha needed to vary.)

Hard shots now fly hard: serve pace is weighted onto quality (19 base + 22×q,
so 21 -> 41 m/s) instead of 21 -> 28. Two things had to give way for it.

Serve topspin was a fixed 1 + 0.35q, which meant a fast flat ball could not
both clear the tape and land on a target, so the net-loft loop throttled every
smash back to walking pace — measured 21.8 m/s whether you drew 60px or 95px.
TUNE.serveSpin 1.3 lets it leave flat and DIVE, which is what a smash is.

And the loft loop's margin was doHit's quality cushion, which taxed exactly the
shots meant to hurt. It's now sized to the real error it exists to cover: the
sim samples the net plane once a frame, so a diving ball can test below the tape
when the closed form clears it. Margin = descent rate × the dt clamp. A flat
drive pays almost nothing, a lob pays what it must.

Measured, target 20m out: 24.7 / 32.2 / 41 m/s for weak / mid / full draw. At
the net it's 9.6 / 11.2 / 11.6 — still the slow floaty lob, as it should be.

Ball-vs-shambler contact gets a crude sweep (radius + half a frame's travel),
because a 41 m/s ball steps 0.7m per frame at 60fps and 1.6m at main.js's dt
clamp, both wider than a shambler. Verified the full smash connects at both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:36:06 +03:00
wuwiandClaude Opus 5 f32cac6a70 Splatter: still camera, and the aim reads as arrows + a notched draw meter
Camera dynamics off (aimCamY/aimCamZ 0) — the knobs stay, so either direction
is a value away in the T panel, but the default holds still now.

The aim graphics carry the read instead. A chevron trail runs down the lane and
plants an arrowhead on the target: the chevrons multiply and fatten as the draw
builds, so the trail reads as pressure rather than as a static dashed line, and
the direction is unmistakable now that the angle actually selects a lane.

The new part is the draw meter on the apron in front of your feet, because the
kill threshold was the one rule with no picture: you could hear the click and
see the colour flip, but never see the line COMING. Now a white notch marks it,
the fill runs cold below and hot past it, and a full draw flashes. Anchored at
the feet rather than over the head — up there it sat right in the lane with the
chevrons running through it — and clamped so advancing up-court can't push it
into the rack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:15:43 +03:00
wuwiandClaude Opus 5 e85345e0d8 Splatter: invert the aim camera, and make the pull's angle actually matter
The aim camera now drops and pushes IN while you draw instead of lifting and
leaning back — you crouch into the shot. Both knobs just flip sign, so the old
behaviour is a sign change away in the T panel.

And the reason the pull's angle felt like it did nothing: aimCone 0.45 meant a
lane half-width of ±10 m at the far baseline, which is the entire court. Every
downward pull locked the same body, so the indicator's angle was decoration.
At 0.10 (±2.9 m out there, ±0.9 m at your feet) the angle picks a lane for
real — swept in 4° steps against three shamblers 18 m out, each one owns a
~12° window and past ±20° the ball flies into an empty lane. Thumb-sized, but
you have to point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:57:24 +03:00
wuwiandClaude Opus 5 b61004a18d Splatter: one gesture always, a serve that clears the net, an aim camera
Three things from the phone test.

The drag meant two different things depending on invisible state: aiming from
pretoss, but walking the player around while the last ball was still in the
air. That "mode between the hits" read as a bug, so it's gone — canAimServe now
covers every phase of splatter, and firing over a live ball is allowed again
(the old one flies on as a ballistic clone). That was the mid-rally re-toss
that used to be the spam vector, and it's safe now because the brake moved into
the pull: a rhythm of lazy 30px pulls fires 100 shots in 30s and kills NOTHING
(the court fills up instead), while 85 committed full pulls kill 11.

hitServe never had doHit's net-clearance loop, so every shot at a shambler
hugging the tape died in the net — not balance, a missing loop. Both paths now
trade pace for arc until the ball is genuinely over, and the ceiling goes 8 ->
12 tries because a target right behind the net needs a near-vertical drop
(measured: clears the tape by 0.20m and connects). The trade is real: the loft
arrives slower and the walk-lead was computed for the original pace, so a lob
is honestly the less reliable shot.

And the camera now rises and leans back a little while the slingshot is drawn.
That steeper look is what makes the gap between the net and the bodies behind
it readable — the shot that used to be blind. Eased once per frame before
anything projects, since one sheared frame would be visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:45:45 +03:00
wuwiandClaude Opus 5 ff973cf6bb Splatter: the pull IS the shot — direction aims, length is power
The ball economy made it harder in the wrong way: waiting for a ballboy is
friction, not difficulty, and walking with the finger was one gesture too many
for anyone to discover. Both are out — a spent ball hands you a fresh one
again, and the thumb only ever does the slingshot.

The brake moves into the gesture instead. The pull's LENGTH is now the shot's
power and nothing else: under TUNE.zSplatQ it merely staggers, at full draw
it's quality 1.0 with the PERFECT flash and hitstop. A lazy rhythm of short
pulls can't kill anything, so tempo stops being a strategy.

The pull's DIRECTION is a ray, not a landing point, which is the ergonomic fix
for having to drag off the bottom of the phone: distance to the target no
longer costs drag length. The first shambler standing in the lane eats the
ball, whether it's 5 or 21 metres away, and the cone widens with range so
distant lanes stay pickable. Full power now sits at 95px of travel.

Also gone: the free predictive auto-lock on a directionless aim (zombieTarget
returns null with no direction, and a bare hold no longer fires at all). Melee
goes back to cleaving the cluster — it's the panic button — with the 1s
cooldown left in as the brake. The reticle reads power throughout: cool blue
only staggers, hot red kills, the drawn band grows behind you, and the pull
clicks the moment it crosses the kill threshold.

Verified by simulated touch gestures: 10px cancels, 35px = 0.31 (stagger),
60px = 0.60, 95px = 1.00; the same 60px pull reaches shamblers at 5m, 21m and
diagonally; 22 serves in 30s, so the pace is back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:31:37 +03:00
wuwiandClaude Opus 5 2787778ad2 Splatter: ball economy — one ball, fetch it back, no more toss-spam
The slingshot serve didn't fix the gameplay because four things were free at
once: pure-hold auto-target with predictive lead, a hardcoded 0.62 serve
quality, infinite ammo with an instant mid-rally re-toss, and a melee swish on
every single tap that cleaved the whole cluster. Perfecting the pretoss/toss
rhythm beat the game; no skill required.

The ball is scarce now. A spent ball LIES where it died (G.loose, ringed on
court) and there is no serve until it's back in your hand: walk it down
(instant, and the only reason to leave the baseline), let a boy scoop it, wait
out TUNE.looseMax, or watch a shambler swallow it — the one real punishment.
Fire rate is a positioning decision instead of a thumb-tempo one.

Also: the far backstop rebounds a ball with pace left, still lethal on the way
home and volleyable (trySwing lets P re-hit their own ball in splatter); melee
drops to nearest-one-only on a 1s cooldown; and the mid-rally re-toss is gone.
With no ball in hand the thumb-drag steers your legs instead of the reticle —
without that, walking your ball down is impossible on touch.

TUNE.looseMax is the fire-rate knob. Measured: ~4s per serve standing still,
~0.4s if you go and get it. 600 taps in 10s now yield exactly one serve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 14:18:07 +03:00
wuwiandClaude Opus 4.8 6df0ddb00f Splatter: clear the wave by hand, tougher 2-hit shamblers, set cards, free depth
Feedback batch from a phone playtest:

- Winning a game no longer auto-sweeps the court. The survivors stay and must
  be killed by hand (no new spawns, no scoring during the mop-up); only once the
  court is clear does play advance to the changeover / next game / set. Replaces
  sweepCourt() with a G.clearing gate + proceedAfterClear(). "CLEAR THE COURT".
- Classic shamblers (a/b) now soak one splat-quality wound before they drop
  (shamblerHp=2) — swapping to new z_aw/z_bw wound skins that bleed the thin
  dynamic trickle plus a small impact pop. Only the runner stays one-shot glass;
  brute/boss unchanged. Ball, pellets and triple side-balls all respect hp.
- Winning a SET shows a centered milestone card (a real pause) before the horde;
  one reusable drawCard() now backs both it and the reworded sudden-death card.
- Player is no longer pinned to the baseline: free to advance up-court on
  keyboard (bounded at the net); touch keeps the baseline (drag is the serve-aim)
  and the horde/GUN burst re-brace there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 22:13:59 +02:00
wuwiandClaude Opus 4.8 37e5bfd10f Weapons: a courtside rack you pick from + pixel-art HUD
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>
2026-07-14 16:11:12 +02:00
wuwiandClaude Opus 4.8 148da4713c Zombies: organic approach, honest eaten radius; restore CPU strength
- Personal lane offset (homeOff ±0.8m) + a per-zombie shamble wobble so
  on your half they fan around you instead of stacking into one column.
- Eaten check weights depth by 0.75 (z is foreshortened in the projection),
  so a head-on shambler kills you when it LOOKS on top of you — fixes
  "they touch me and I don't die." Lateral reach unchanged.
- Revert aiFlub 0.14 -> 0.09 (undo the CPU weakening).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:32:44 +02:00
wuwiandClaude Opus 4.8 eacad1ffca Sound: recorded SFX layer + a touch-easier CPU
Wire the new assets/audio/*.wav samples over the synth SFX:
- Ball hits: per-side tennis-player / tennis-opponent on serve & return,
  now with a stadium reverb tail (same convolver as umpire/aww).
- Crowd: tennis-applaus on your points/games (louder on big moments);
  tennis-aww when the opponent scores or you're eaten (reverb tail).
- Ambient bed: tennis-ambient-loop from first tap, 500Hz lowcut, low vol.
- Zombie music: zombie-mode loop when splatter kicks in (800Hz lowcut),
  fades out for the 1:1 sudden-death duel and back in after.
- Headshots: random headshot-1/-2 on every zombie kill (killZombie).

audio.js: sfx() one-shot player w/ rate+reverb opts, loopBed() beds with
duck/fade, aww() wrapper, hotter reverb send on the umpire voice.

Also: normal opponent aiFlub 0.09 -> 0.14 (a little easier to beat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:33:29 +02:00
wuwiandClaude Opus 4.8 ae8c04d413 UI polish: logo start screen, broadcast scoreboard, kill pips, best-run
- 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>
2026-07-13 15:10:29 +02:00
wuwiandClaude Opus 4.8 aa8d2fff36 Zombie mode: kills-per-point knob so a game isn't 4 pot-shots
1 kill = 1 point made a tennis game trivially short (4 kills). Gate
player scoring behind TUNE.killsPerPoint (default 3) — K kills bank one
point, the rest just splatter. Opponent breaches stay 1-for-1, so the
asymmetry is where the pressure comes from. Live-tunable in the T panel;
set to 1 to restore the old behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 14:39:18 +02:00
wuwiandClaude Fable 5 2e5af224b8 Back to 60fps — the 30fps cap was too noticeable
The knob stays (TUNE.fps in the T panel) for anyone who wants to trade
smoothness for battery; the pile cache and GC fixes keep their savings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:19:34 +02:00
wuwiandClaude Fable 5 70882d2dd4 Runner is glass; battery pass (30fps cap, cached piles, GC fixes)
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>
2026-07-12 18:04:51 +02:00
wuwiandClaude Fable 5 1b97905217 Stadium pass: real crowd, sponsor boards, umpire visible in portrait
- 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>
2026-07-12 17:48:39 +02:00
wuwiandClaude Fable 5 e948e8b36e Aesthetics: 3x render res, flatter lens, 4-frame walk cycles, tiered gore
- 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>
2026-07-12 17:30:46 +02:00
wuwiandClaude Fable 5 2f1419b1f2 Feedback round + Phase 13: the duel, free-aim, honest stakes
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>
2026-07-12 11:50:18 +02:00
wuwiandClaude Fable 5 06bd24e6ea Phase 12: the weapon ladder — every tin opens something nastier
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>
2026-07-12 10:58:28 +02:00
wuwiandClaude Fable 5 7bc26aec1c Phase 11: enemy roster wired — runner, brute, shield zombie
- 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>
2026-07-12 10:39:10 +02:00
wuwiandClaude Fable 5 eb66376033 Phase 9: weapons ride the new-balls rule — kill meter retired
- Deliveries are scheduled by the rules of tennis: game ballChangeFirst (3),
  then every ballChangeEvery (4) games; the tin lands at the NEXT changeover
  (a due tin survives hordes and even-game endings until a changeover comes)
- Fresh tin = bombMax (3) bombs; boy run + 'new balls, please' + NEW BALLS flash
- Shame rule: zombies took a game since the last delivery -> NO NEW BALLS,
  slate wiped either way
- killMeter/bombEvery deleted from state/config/zombies

Verified: delivery at game 3 (bombs 3); due-but-even games hold the tin until
the next changeover; NO NEW BALLS withholds and clears shame; bomb serve still
arms and detonates; killMeter gone; no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:00:10 +02:00
wuwiandClaude Fable 5 5a7f71fdba Phase 7: the scoreboard is the progression — spawn director, changeovers, sets, permadeath
- 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>
2026-07-12 09:54:01 +02:00
wuwiandClaude Opus 4.8 41a6d2a034 Phase 6: racket zombies (rally mini-boss)
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>
2026-07-12 09:18:43 +02:00
wuwiandClaude Opus 4.8 9100bd5a23 Splatter feel pass: melee, homing, multi-ball, clearer MG mode
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>
2026-07-11 22:54:31 +02:00
wuwiandClaude Opus 4.8 bf9553bd49 Phase 5: horde waves + machine-gun racket
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>
2026-07-11 22:26:22 +02:00
wuwiandClaude Opus 4.8 06025bdccf Phase 4: bomb balls
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>
2026-07-11 22:13:06 +02:00
wuwiandClaude Opus 4.8 b6119c937b Phase 2: multi-kill applause + umpire appreciation
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>
2026-07-11 21:57:19 +02:00
wuwiandClaude Opus 4.8 795ddeb425 Phase 1: tighten the splatter shooting loop
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>
2026-07-11 21:49:50 +02:00
wuwiandClaude Fable 5 d6d00929bb Miss log: every tennis swing logs why it failed, last reason shown on HUD
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>
2026-07-11 21:02:11 +02:00
wuwiandClaude Fable 5 a5a6f550a3 Volleys get wider reach and swing window; serve volleys no longer fault
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>
2026-07-11 19:46:36 +02:00
wuwiandClaude Fable 5 69e3a0c310 Zombie town: the match continues
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>
2026-07-11 16:26:49 +02:00
wuwiandClaude Fable 5 f221ff84df Taller umpire chair; umpire proportionally smaller
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:58:18 +02:00
wuwiandClaude Fable 5 c506a631c2 Split the monolith into ES modules; hoist scattered magic numbers
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>
2026-07-11 15:38:15 +02:00