Commit Graph
97 Commits
Author SHA1 Message Date
wuwiandClaude Fable 5 4e80bd8cb2 Cards say one thing: trimmed copy, and a title that fits the band
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:14:32 +02:00
wuwiandClaude Fable 5 9fab26eec9 The court keeps no score: the landing ring is gone
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:05:32 +02:00
wuwiandClaude Opus 5 1bb528f7e0 The back room: five taps on the logo, not three on the scoreboard
The weapon-test cheat sat on the SCOREBOARD, where a single tap is the miss-log
overlay — so the thing you tap to read a log was also the thing that opened a
sandbox mid-run, three taps in, by accident. That tap does one job again.

The cheats now live behind five taps on the splash logo, where nothing marks
them and nothing in play can reach them:

  WEAPON TEST  the old sandbox. It stocked only the BARREL weapons, on a screen
               called the weapon test — it stocks every weapon in the game now,
               eight of the nine slots.
  BOSS TEST    a portrait of every chapter boss, built by asking bossArt() what
               each chapter gets, so the list cannot drift from what a run
               actually spawns. Pick one and you land in a real fight at that
               chapter's game number with the full arsenal at ×99.

Neither is a special mode: both ride the transition out with laserScene's trick
(force the turn, then step the ten-second cinematic out in one frame) and hand
you the ordinary update loop, ordinary scoring, ordinary death. A test that
plays by different rules tells you nothing about the game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:19:12 +02:00
wuwiandClaude Opus 5 04ee0174ce Three more chapters, and at game 100 the champion returns everything
The roster ran out at chapter 6 and then repeated itself; game 70 got the
groundskeeper again, bigger. bossArt() now walks nine designs, one per chapter,
and hands out THE CHAMPION on every tenth — the floor(ch/10) term takes his
chapters back out of the count so chapter 11 opens the second lap on the
groundskeeper rather than skipping him.

  70 THE STRINGER — leather apron, bandoliers of gut string, racket frames on
     his back. He takes your lane and puts two dead-straight ones on it.
  80 THE TOURNAMENT DIRECTOR — blazer, lanyard, radio. `adds` is new: he calls
     stewards onto the court while you fight him, which is the one thing a boss
     fight could not do before, since the stage is cleared for it.
  90 THE STRAWBERRY SELLER — tray of cream, apron soaked red, ladle. A tank
     that sprays four at once and then comes for you.
 100 THE CHAMPION — torn whites, a laurel of strapping tape, the trophy still
     in one fist. `rally`: anything under splatter quality comes straight back
     at your head, so he is the one body you have to out-HIT rather than
     out-shoot. `phases` lays three rows over his own as his health falls — he
     holds the baseline and rallies, then runs the line drilling volleys, then
     drops to a charge — and each turn is an event: cry, shake, the stand up.

Thirty-six frames through assets/_regen.py, the mascot's three-batch recipe
(2 stride, 3 wounds, 4 throws). The trap that cost the re-rolls: a throw batch
references its own wound frames, so a wounds batch that fails leaves the throw
sheet drawn against the SEED — two of the four came back half groundskeeper.
Wounds first, verify, then throws.

And the physio, who Oliver reported "does basically nothing but move towards
the net": he kept 4.2 of every 6.3 m he charged, so the fight was always over
before he arrived. `withdraw` is his own number now (0.2) and he charges half
again as fast — measured, he is on top of you in 11 s instead of never — and he
drills three balls a cycle on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:08:57 +02:00
wuwiandClaude Opus 5 f4c6f408e7 A stolen finger no longer welds you to the edge, and the stack is yours
The right edge is where iOS keeps Control Center, and it is exactly where the
horde's absolute finger-steering parks your thumb. When the system takes that
gesture it delivers neither touchend nor touchcancel: `stick` survives forever,
so every later touchstart falls through to the extra-finger branch (swish, and
only swish) while touchmove ignores the new finger's id. steerPX stays frozen at
0.97 and the player is pinned to the screen edge, melee firing, for the rest of
the run. Any new touch event proves the old finger is gone if it is not in
e.touches — drop the stick then. Verified against the bug: pinned at x=6.11
before, steers to -5.20 after, and a genuine second finger still only swishes.

And wCap 9 -> 99, which is no cap in practice: game 70 wants a bigger stack, not
a rationed one. The barrel weapons move from "paid out only once all four tins
are full" (a branch an uncapped rack would never reach again) to rungs of the
same ladder, so a late duel still hands out a rocket instead of a fifth bomb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 15:35:53 +02:00
wuwiandClaude Opus 5 5de42648ec The slice stops steering: each kill strikes the ball once, then physics
Oliver, watching a sliced ball circle a shambler it had only wounded: "an
orbiting tennis ball midair, nobody knows what is going on." He is right,
and not only about that shot. Every ball in this game flies a parabola you
can read, so the one that thinks reads as broken even on the shots where it
works. The hook was the wrong primitive, not a hook with two bugs in it.

So there is no hook. At the instant of a kill the ball is struck again —
ONE velocity out of solveShot, the same solver the serve uses, aimed at the
next body's chest — and from the next frame it is an ordinary ball under
ordinary gravity. It cannot orbit, cannot hover and cannot outlive its shot,
because after the strike nothing about the slice is still running.

That also deletes the wound bug rather than fixing it. Six sites spend the
ball (lastHit = 'X') and not one of them cleared the hook, so a ball that
merely WOUNDED its target was left steering at 30% pace toward a point that
body had already walked off — the loop he saw. With no persistent state
there is nothing to clear: a ball spent on a body it failed to kill just
drops, like every other spent ball.

The hop draws its own band, white, so a chain leaves the same chalk streak
every other shot leaves. sliceTurn and sliceHookT are gone with the steering
that needed them; sliceArc stays, no longer as a turn-radius limit but to
keep a carom from doubling back down-court, which reads as a stray
deflection rather than as a cut.

Net 24 lines lighter than the thing it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 14:15:24 +02:00
wuwiandClaude Opus 5 e11ad30a63 The hook turns as hard as the geometry needs, or not at all
A slice hooking onto a body two metres to the side flew a closed loop around
it forever: 285 frames of the same circuit, never arriving, never falling.
A constant turn rate has a constant turn radius (v/w = 2 m), and anything
inside that circle cannot be reached however long the ball steers — while
the hook pins vy at 0, so nothing else ends it either. A crowd is nothing
but close bodies, which made the one place this weapon should shine the one
place it hung.

The rate is no longer a constant. An arc leaving along the current heading
and passing through a body at distance d, bearing a, has radius d/(2 sin a),
so the ball needs 2 v sin(a) / d — solved every frame, because the body is
walking while the ball travels. sliceTurn stays as the FLOOR, which is what
it was always really for: how wide the curl looks when there is room.
Turning faster than needed is safe, the aim being re-solved each frame.

Two things the maths cannot cover, so they are ruled out instead: a body
behind the ball, which no tangent arc reaches at any rate (sliceArc), and a
target that walks out of reach mid-hook (sliceHookT, a hard 0.7 s licence to
steer). An endless orbit is now impossible by construction rather than by
the geometry happening to work out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:53:20 +02:00
wuwiandClaude Opus 5 99d84b2c9d A perfect serve is a cut ball: it hooks from kill to kill
Slice is not a tin and not a rack slot. It is what a perfect serve DOES —
the toss ritual's 45 ms window, or a pull that stops inside perfectBand.
Every other weapon is handed to you; this one is a technique, and it hands
the plain ball a real route to the DOUBLE/TRIPLE KILL ladder that until now
only a bomb or a lucky line-up could reach. Banking your specials stops
meaning playing worse in the meantime.

Only a kill hooks, so a wound ends the chain and no brute, steward or boss
ever extends one. It is a crowd skill shot, which is the half of the arsenal
the tins do not cover.

The pull had to learn a third state to say this. Quality SATURATES at
pullFull, so an overpull and a perfect pull drew an identical band: the game
was asking you to hit a window it never showed you. The band now goes chalk
white inside it and the mark on the target grows a second ring — the fill
says where the ball lands, the ring says it will not stop there.

The chain path is deliberately NOT previewed. It is a scallop laid over the
exact patch of court you need to read, it solves the shot before you take
it, and the bodies walk — any chain drawn at pull time is a lie by the time
the ball arrives, which is the one thing the band exists not to be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:44:17 +02:00
wuwiandClaude Opus 5 655b86d27f Fire that looks like fire, a rack you can leave, and a boss you can read
The band work from f352612 rides along; everything else is this round's list.

- proj returns a NEGATIVE scale behind the camera, so four ellipse call sites
  could be handed negative radii — ctx.ellipse throws on those, and the throw
  killed the rest of the frame (no HUD, no rack, no band). All four now skip
  points behind the camera.
- a burst tin is no longer a commitment: tapping another slot aborts it, spends
  it and hands back the serve loop. Lives in selectWeapon, so the rack tap and
  the 1-0 keys both get it. A horde still locks the rack — that is the one place
  your finger is already spoken for. Spending splices the rack, so the tap
  resolves to a weapon id and re-finds it rather than trusting the index.
- beacon 1.6 -> 2.4 rounds/s: a lob cannon you could not leave AND could not
  feel working was two problems.
- FIRE. Flamer throws fat embers instead of a one-pixel thread; burning bodies
  get nine tongues over a heat bloom plus smoke; the fire ball gets a halo and a
  real tail; an ash death gets twice the motes at twice the life over a ground
  bloom and a column. The flamer had no sfx case at all and was using the gun's
  dry tick — it has its own roar now.
- chapter bosses carry an HP bar over their head. The no-HP-bar rule stands for
  everything else and the comment says why a boss is the exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:21:57 +02:00
wuwiandClaude Opus 5 f352612de3 The band became one axis: fitted plates, and cards that travel
Every transient message — milestone card, RESUMING clock, one-line flash — now
centres its ink on CARD_MID and only one is ever up. Three shared pieces do all
of it: inkSpan measures, plate() sizes the fill to what it holds, slideDraw
swipes it in from the left and off to the right with a four-ghost smear sampled
back along its own easing curve.

- weapon card is icon + name + count, one centred row, no prose
- plain cards centre as a block instead of hanging off the band's top edge
- the clock left the net for the band; it takes over when a card or flash clears
- band moved up 20px into the flash's old row, which it can now that they never
  draw together
- hold times +0.8s: the swipe eats ~0.55s that the old numbers didn't budget for

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:55:04 +02:00
wuwiandClaude Fable 5 911035af00 The card lies flat, and a popup owns its screen
- icon LEFT of the text: every card is 62px at a fixed top, fully over
  the crowd — the stacked layout hung its lower third over live court,
  and the art draws bigger this way, not smaller
- nothing draws or ticks under a modal: a card raised in the same beat
  used to burn out dimmed beneath it — it holds and plays after

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:10:11 +02:00
wuwiandClaude Fable 5 1337048a85 The reaction lands where the ball does
- praise was firing at RELEASE, half a second before impact (measured:
  32-frame flight, word on frame 0) — it now arms on the ball and pops
  on the first body it meets, or its first bounce
- DOUBLE KILL / MASSACRE / RETURNED rise off the thing that earned them,
  as world-anchored pop texts — the announcement band is for milestones
- the band got rows: a card can no longer print over the flash line
- GAME N flash deleted (the scoreboard bar already says it), CHANGEOVER
  folded into the countdown panel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 20:50:46 +02:00
wuwiandClaude Fable 5 a89e8ff419 Seven verdicts from the test group
- opening CPU beatable in a game or two (softer cap, more whiffs, rubber band)
- modals are for calm courts only; live-court news is a card now
- the transition howto is gone: a ghost hand teaches the pull, the toss,
  the apex and the swing, each on the exact struggle it answers
- a shot barrel keeps your weapon and glows its slot instead
- switching weapons clicks, the live tile is unmistakable
- barrels come faster the deeper the run goes
- the umpire triple-tap cheat is deleted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:49:27 +02:00
wuwiandClaude Fable 5 b504cb3565 Three verdicts from the phone
The PERFECT spark ring is out — the hitstop, the click and the praise word
already carry the moment. The net-cord wobble drops to ~1 px. And the
floodlight stutter is gone entirely: on the iPhone a random dark dip over
the stands reads as the crowd flickering black — a rendering bug, not
ambience.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:58:11 +02:00
wuwiandClaude Fable 5 8ab7ac0c25 Ten small flourishes, all borrowed machinery
Kills stutter the world a touch more per streak; a massacre borrows the
boss's slow-mo; a boss death gets the transition's zoom punch. PERFECT
contact flashes a spark ring. The sidestep and every real bounce kick up
court-tinted dust. The net cord ripples where the ball clips it. The
landing ring contracts on time-to-touchdown. Hot rally balls drag three
fading ghosts. And every 8-15 seconds the floodlights stutter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:39:10 +02:00
wuwiandClaude Fable 5 d847c2e4a8 Sweep the dead code off the court
EATEN_GRAB (write-only marker), ball.firstLand (written five places, read
nowhere), zombies' unused VOLLEY import, three scoreboard palette fields
from the pre-PNG board, the x_tin.png nothing references, and a dozen
export keywords on functions no module ever imported.

Kept on purpose: o_run3/o_run4.png — the SPR list excludes them so both
tours run on two frames (the women's set has no 3/4 twins), per the comment
at the preload list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:37:08 +02:00
wuwiandClaude Fable 5 f1f0d05a61 Six calls the umpire got wrong
- The duel misread a spent ('X') ball: bounce out = instant win, two bounces
  = a loss plus a surge, for a shot that should just be re-served.
- Duel serves routed through the temp weapon riding slot 0 — a laser fired
  instead of the ball. Tennis rules now mean plain balls, temp or no temp.
- A tap during the death animation tossed a ball that hung frozen midair.
- The EPIC boss card was clobbered by the SET card when game 30 was both.
- The telegraph glow divided by the wrong wind: negative alpha, no fade-in.
- The crowd clapped forever over the death screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:33:08 +02:00
wuwiandClaude Opus 5 1f22d3d5a0 Say the weapon's name once
A first-seen tin announced itself twice after a won duel: the modal that
teaches its rules, and then the delivery card right behind it, same title
same icon. weaponModal now reports whether it spoke, and the card (and the
barrel's flash) only speak when it didn't.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:07:28 +02:00
wuwiandClaude Opus 5 a1a8a61422 Answer the playtest: a net for the lock, and a duel that pays
The hang: I could not reproduce it or pin it on one latch, so this is two
things. A root-cause fix — iOS does not guarantee a touchend when you switch
apps mid-drag, and the held G.aim it leaves behind blocks the uncaught-toss
retoss forever — and a watchdog for whatever else is out there. The watchdog
does not time how long you have been blocked, because a horde blocks you for
fifteen honest seconds; it watches whether the clock that should release you is
moving, and logs the frozen state before letting go.

The duel: past about game 20 all four tins sit at wCap and a won duel handed
over literally nothing. It falls through to the barrel weapons now.

The prize also stops borrowing the praise channel — arriving in the same slot
as PERFECT made it read as another compliment. It gets a card with its own icon.

The turn into zombie mode was unwatchable because the zoom started ON it, and
the curve render applies is zero at t=0, so the camera only arrived half a
second after he had changed. It peaks on the turn now, and the swap lands on a
hitstop, a flash and a burst so it reads as an event rather than a glitch.

Laser down from 0.55 to 0.22 — zap() sums four voices, so its vol was never one
voice's level.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:02:11 +02:00
wuwiandClaude Opus 5 f3b0b791d6 Trade the tutorial for a screen that counts the dead
The menus were placeholders next to the art. Three screens now: a title over
an empty, dimmed, blood-stained court; a MATCH SETUP screen where each court
wears meters read straight off its own bounce numbers; and BODY COUNT, which
needed the game to start measuring anything at all.

stats.js keeps one capped array of per-run records — all time, last match and
seven days are three filters over it rather than three counters that can drift
apart. Kills file under the body AND the weapon: killZombie only ever knew an
index, so every damage loop stamps the cause on its way past.

The tutorial is gone. Marvin explained what the in-game modals already teach,
and his stage is where the stats live now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:01:59 +02:00
wuwiandClaude Opus 5 c75bd5b9ba Pay out every won duel, and stop the popup eating the announcement
Two separate faults, both of which read as "the duel gave me nothing".

The first is mine, from making weaponModal first-sight-only. G.flash ticked
ABOVE the `if (G.modal) return` in update(), so a flash raised in the same beat
as a popup spent its whole 1.1 s counting down invisibly behind the dim. That was
harmless while every tin opened its own modal — but once the modal went quiet the
label flash WAS the announcement, and the trainer's WOUNDS TREATED popup ate it
every single time you won a duel while wounded, which late in a run is every
time. Measured: flash null after 1.5 s of reading, nothing left on dismiss. A
modal freezes the world and a transient HUD message is part of the world, so the
tick moved below the return. It now holds at 1.1 for as long as you read, and
starts counting when you tap.

The second is older and Oliver felt it directly. deliverTin CYCLED the ladder —
TINS[G.deliveries++ % 4] — handing you whatever rung came next even when that
type was already at wCap, where grantWeapon's Math.min clamp makes the delivery a
silent no-op. Bomb and fire fill in three duels each, triple in one, so past
about the thirteenth duel three rungs of four are full and most late duels paid
out in literally nothing. It walks to the first rung with ROOM now. Simulated
twenty wins with nothing ever spent: every one delivers through #16, where before
#10, #13, #14 and #15 were already empty handovers. Past that it says RACK FULL,
which is a real state and not a bug, so it says so plainly instead of miming a
delivery — no boy, no umpire line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 06:49:41 +02:00
wuwiandClaude Opus 5 0c12157702 Find a burst weapon wherever it is on the rack, not just in the first five
endBurst looked its tin up with [0, 1, 2, 3, 4].find(...) — correct for exactly
as long as the rack held five slots. Growing RACK_MAX to 9 meant a burst weapon
picked up into slot 5..9 could never be found, so it was never spent: you fired
it and the count stayed where it was, forever. Oliver hit it with the beacon, the
rocket and the flamer, which together with the gun are precisely the four
kind:'burst' weapons — endBurst is the only spend path that searched for its slot
instead of being handed one.

The serve weapons were never affected, and that is the tell: bomb, triple, fire
and the laser all spend through spendSlot(G.weapon), the live index. Checked the
laser end to end at slot 8 rather than reasoning about it — 6, 5, 4, 3, 2, 1,
gone.

Fixed at the invariant rather than at the call site. The rack has always had a
comment saying everything reads through the slot accessors so nothing else needs
to know how it is stored; slotOf(id) is now the fourth one, and it is the single
place that knows the range. Verified all four burst weapons arm from a real tap
in slots 5-8 and leave the rack when the burst expires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:15:14 +02:00
wuwiandClaude Opus 5 ebce08199f Turn the praise down, and stop the footnotes shouting
The band made PERFECT earnable, which was the fix — but you learn a band, and
then the HUD is talking on every other serve again whatever the word says. So
the praise goes behind a shared cooldown (TUNE.praiseEvery, 14s), the same model
the umpire's asides have used all along for exactly this problem. Eight
band-perfect serves back to back now yield one word. The bright click in the pull
is the reliable feedback for a good draw; the word was only ever garnish, and
garnish on every plate is just food.

And the free-floating slot drops 24 -> 18. It shares that slot with CLEAR THE
COURT, DOUBLE KILL, RETURNED and GAME N — half of which are a footnote to
something you already watched happen. Sharing the loud FACE with the milestone
cards is right; sharing their SIZE made every footnote read as a milestone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:53:10 +02:00
wuwiandClaude Opus 5 9c7ee42bfa Earn the praise, and give the quiet face a shadow too
PERFECT fired on almost every serve, and it was not a threshold that had drifted
— it was the wrong test entirely. pullFrac CLAMPS at pullFull, so pullQuality is
exactly 1.0 for any pull of 130 px or more, and 130 px is an ordinary comfortable
thumb drag. "quality > 0.999" therefore meant "you pulled far enough", which is
not an achievement and is why it read as noise.

So hitServe takes `perfect` as its own argument now. The toss ritual keeps
earning it the honest way, on the 45 ms perfectMs window. The slingshot earns it
by STOPPING in a band at the top of the draw (pullFull..pullFull*perfectBand,
130-166 px) — a target you can miss, rather than a ceiling you cannot. The pull
already clicks when it crosses the splatter threshold; it clicks brighter when it
enters the band and goes silent when you pull past, so the window is learnable by
feel instead of being invisible skill.

And the word rotates: PERFECT, TEXTBOOK, IMMACULATE, OH LOVELY, UNPLAYABLE, SHOT
OF THE DAY. Cycled rather than random — the same word twice running is what makes
one read as a stuck HUD element rather than as praise. The umpire has still never
acknowledged the massacre, so a clean shot gets the line it would get at Wimbledon.

Also: the card band grew 72 -> 80 px, because the second sub line ended 4 px off
the bottom edge and looked like it had fallen out. And the quiet face floats as
much as the loud one does, so it gets the same 1 px shadow — the control hints,
the gesture-demo labels, the card subs, the modal body and prompt, the death
summary lines. The scoreboard and the rack keep theirs off: both already sit on
dark panels, and a shadow under 6 px type on a flat fill only smears it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:56 +02:00
wuwiandClaude Opus 5 55c0f75c37 Give him a sidestep, and split the messages into two voices
He died at every chapter boss because he could not move, and there were two
independent reasons for it. The movement block is 'rally'-only — a settled feel
decision the six boss AI styles were written around — and a boss is fought from
'pretoss'. On top of that canAimServe() is true through a boss fight, so his one
finger was already spent on the slingshot: there was no gesture left over that
could have meant "move".

So it is a SIDESTEP, not a walk. One committed step on a cooldown, carrying
invulnerability that beats both damage channels — the chip damage in hurtPlayer
and the instant-death contact test. The rule about walking stands untouched.

The gesture is picked by WHICH WAY the drag leaves the deadzone, decided once and
never revisited for that touch: down-ish is the pull the game already teaches,
sideways is the step. No timing window, no invisible mode, so "one gesture means
one thing" still holds. The finger-steer modes are untouched — stick.aimable is
false in a horde, so the drag is the absolute steer it always was. The apron demo
now alternates PULL DOWN and SWIPE TO DODGE, and Marvin has a page for it.

The rest of the playtest list:

- Bangers is bundled beside Silkscreen, and the register IS the category. The
  quiet face keeps the HUD, the rack, the control hints and RESUMING IN; the loud
  one takes SET, CHAPTER, EPIC!, PERFECT and every free-floating flash. Floating
  text gets one shared y and a drop shadow, so it survives crowd, clay and blood.
- Modals queue instead of being dropped. That was why the trainer and the weapon
  had to be crammed into one card after a won duel; the heal gets its own popup
  with the x_cross tin over it — a sprite loaded since that merge and drawn by
  nothing.
- weaponModal's first-sight gate moved inside the function. The barrels gated
  themselves and the tin ladder did not, so four weapons re-explained themselves
  every three games while a barrel weapon explained itself once.
- Ten rack slots. Past five it grows a second row upward rather than shrinking
  ten tiles under the thumb target; slots 0-4 stay welded to the bottom edge.
- The boss win holds for its own 3.6s and says EPIC!, with no GAME n NEXT line —
  the bar under the scoreboard already prints that permanently. That bar went
  from 6px to 7px, which is the smallest type in the game becoming words.
- The turn punches the camera in on him and the ground shakes harder at each beat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:48:24 +02:00
wuwiandClaude Opus 5 8f16d085f0 Stop bodies blinking out, and give the cone a core and edges
The blinking was drawZombie validating its frame count against the LAST frame
only: a set with (say) z_h2 missing but z_h4 present kept N = 4, the walk cycle
stepped onto a name that had never loaded, and drawStatic silently drew nothing.
The body vanished for that frame of the cycle and came back on the next one,
forever. One cached 404 -- this project's oldest trap -- is enough to cause it.
It holds frame one now: a body that stops animating is a bug you can see, one
that flickers in and out reads as the game breaking. SPRV bumped to flush
whatever his phone has cached.

And no ctx.filter anywhere. The cut line was briefly the sprite redrawn clipped
to a band and flattened by a canvas filter, which put it exactly on the
silhouette -- and on the iPhone the line did not draw at all. Unsupported or
leaking past its restore, a rendering feature that fails silently on the target
device is not worth a pixel of polish. It is a plain bar again, brighter, and it
crosses the odd gap between the legs, which is the trade.

The cone gets the reference's look: a horizontal gradient across the width with a
near-white core down the centre line falling to blue at the edges, plus the depth
falloff along its length, plus stroked outer edges. Canvas has no 2D gradient, so
it is filled in eight slices each carrying its own across-gradient -- enough that
the seams vanish, which is what the old nested wedges got wrong.

Test mode: barrels gone (the rack already holds everything they would give you)
and the crowd raised to 16 bodies at a spawn every 0.9s, so it does not run dry
between kills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:58:16 +02:00
wuwiandClaude Opus 5 6e88fd5bb9 Burn them to ashes, and give the burst back its bar
A burnt body does not burst. Fire kills by cooking, so there is nothing left to
spray: a body that dies alight -- flamethrower or flame ball -- leaves dark grey
ash instead of blood. Same decal, mound and particles, recoloured and smaller,
with a puff of smoke and a dull thud instead of the wet splat and the headshot
crack. The flame BALL has to mark itself, because that kill lands before the body
ever catches.

The flamethrower is a lighter, not a gun: no impact damage at all, it pierces,
and it only IGNITES -- the burn does the killing, so a lit crowd walks a few
steps and goes down together. 26 m of reach against a 22.8 m court, so it
crosses to the far baseline. The guard matters: hp -= 0 fell straight through to
killZombie, which made the cone delete the crowd on contact.

Every burst weapon had no progress bar and the cause was the spend. Arming spent
the tin at once, so the rack entry vanished -- and the bar draws on the slot
holding G.gunId, which no longer existed. The tin is spent when the burst ENDS
now, so icon, count and bar are all there while it runs, and every path that
ends a burst routes through endBurst or it would hand out free ammo.

Auto-fire movement again: the previous fix read moving as mx||mz||assist, and
finger-steering zeroes mx/mz, so sliding left and right still counted as standing
still. And the oval on his back was the mg racket -- its head projected almost
exactly onto the sprite; offset by facing it reads as held out beside him.

The laser's far edge is pinned in SCREEN space to the middle of the ad strip
rather than running out to beamLen on the ground, which landed on their top line.
beamLen goes back to being the kill range instead of a number tuned to one camera.

Beacon thumps, rocket swooshes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:05:56 +02:00
wuwiandClaude Opus 5 91053b4c35 Fire the laser as light, and stop him jogging on the spot
The auto-fire jog was one guard. gameplay skipped the pose update in mgSteer mode,
so the pose stuck on 'idle' -- and render covered for it by faking a run cycle off
G.idleT, which animated whether he was moving or not. That fake is what read as
"this hectic run animation... odd all the time". The pose follows MOVEMENT in every
mode now and the fake is deleted, so auto-fire, the horde and the melee racket all
use the ordinary run animation and standing still is standing still. The comment
that justified the fake -- two rackets when idle -- was solving a smaller problem
than it caused; at sprite scale the overlap does not read.

The laser, second pass, all of it "it must read as light he fired":
 - it draws AFTER the net and every body. It was drawn before the far-side pass,
   so the net and the crowd painted over it, which is exactly why it looked like
   it was lying on the floor and sitting under the net.
 - beamLen 26 -> 34: at 26 it stopped just past the far baseline and read as cut
   off in mid-court. It carries into the courtside ads now.
 - beamW0 0.35 -> 0.06, a POINT at the strings. At 0.35 it left the racket as a
   0.7 m bar hanging in mid-air instead of something emitted from it.
 - it DELETES ordinary bodies, hp irrelevant. A plane of light that left a roller
   man standing made it a worse ball, not a weapon. A boss is the exception and
   takes dmgBeam 8 -- severe, survivable.
 - four nested wedges composited 'lighter' (violet bloom, blue, cyan, white core),
   each fading along its length. One quad with a gradient was a coloured shape.

Rocket: speed 17 -> 38, and a real jet plume laid tight ON the flight line rather
than scattered sparks, which looked like a firework.

Beacon: a finer one-pixel thread and a much brighter ball, so the BALL is the
bright thing and the trail only says where it has been.

And the bug in the middle of that: firePellet stamps a whitelist of fields onto
the round, and jet/trailR went onto the WEAPONS rows without being added to the
list -- so the rocket flew with no plume and nothing errored. Any new pellet field
has to be added in both places.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:44:57 +02:00
wuwiandClaude Opus 5 46e268973d Triple-tap the scoreboard for a sandbox
A test mode for trying the new content without playing twenty games to reach it.
Three taps on the scoreboard toggles it, three more turns it off, and a single
tap is still the miss-log overlay. Same shape as the umpire triple-tap cheat it
sits next to.

It OVERRIDES the difficulty director rather than editing it, so the wave is the
same whether you opened it in game 1 or game 40 and the run keeps running
underneath: 8 bodies, a fixed 1.15 m/s shamble, a spawn every 1.6 s, and
pickVariant pretending to be at game 20 so every body type can turn up. All four
barrel weapons sit on the rack and are topped back up each frame, because
committing a burst spends the whole tin and a sandbox that runs dry is not one.
A barrel rolls out every 14 s so the pickup itself is testable too.

The third tap must NOT toggle logShow: taps one and two already toggled it twice
and it is back where it started, so toggling again left the miss-log overlay
switched on behind you every single time.

Also records the trap that cost a debugging round getting here: index.html loads
main.js?v=N, and fetching 'main.js' does not bust that URL -- so main.js stayed
stale through a reload while every other module was fresh, and the tap handler
appeared to do nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:20:14 +02:00
wuwiandClaude Opus 5 282f5145a9 Make the pink weapons throw themselves, and let the big bodies soak
Oliver's playtest of the barrel weapons. Four notes, all four real defects.

"The bigger guys were way too easy to kill at the beginning." A bug, and an old
one. The ball-vs-body chain handled boss, g/m, s, f and a/b, and everything NOT
named in it fell through to "splat-quality kills outright" -- so the STEWARD has
died to one serve since the day he shipped with stewardHp 5 and a comment calling
it a brute's worth of hp, and the three new bodies inherited that the moment they
existed. Pellets and melee always read hp; only the ball skipped it. One generic
hpMax > 1 branch now, and measured after: shambler 2, steward 5, brute 5, ball
boy 3, bouncer 5, roller man 7 -- each exactly its hp.

"I have seen all of them by round 10." The new three move 3/8/15 -> 5/12/20. The
original four keep their gates: that curve was already tuned through game 12.

"The laser fired only when I tapped, that was confusing." The whole tap-per-round
kind is gone. A barrel weapon is now either the normal ball gesture made
spectacular or something that fires itself -- nothing waits for a tap.
 - laser is a serve, and the RAY is the weapon: no ball anywhere in it. The
   preview IS the wedge, it leaves the RACKET rather than lying on the grass, the
   release erases the plane along its own path behind a bright front, and the
   edges are hairlines instead of the heaviest lines on the court.
 - beacon and rocket are LOBBED bursts -- `arc` turns a flat pellet into a thrown
   one, solved onto the nearest body ahead, and landing is itself a hit so a
   rocket that misses still goes off where it lands. The beacon changes colour
   every ball and drags that colour behind it.
 - flamer got "way more pronounced": 30 rounds/s, a ~10 m cone, embers.

"I got the laser almost every time." A flat 1-in-4 over four items does not feel
uniform. BARREL_WEAPONS carries weight and a `from` gate now, never repeats twice
running, and the beacon is what you meet first. Measured: laser 16-28%, rocket
unlocked at game 12 so it is genuinely a late find.

And the barrel keeps its pink halo -- that was always procedural, painted by
drawBarrel, and never went near the chroma key. Three rings and a pool on the
lawn now, because one flat disc read as a smudge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:13:05 +02:00
wuwiandClaude Opus 5 25f915f236 Fill the rack yourself, and roll barrels out that fill it
The rack had five hardcoded slots where the index WAS the weapon, spelled out in
five places: SLOTS, slotStock, WDESC, drawRack's three parallel arrays,
deliverTin's w+1, and bare wsel === 1|2|3 / G.weapon === 4 tests across five
files. config.WEAPONS is the one table now, G.rack holds up to four {id, n} in
the order you GATHERED them, and slot 0 stays BALL. Everything reads through
slotId/slotStock/slotEntry, so nothing else knows how the rack is stored.

A fifth weapon is always allowed, per Oliver: it rides slot 0 as G.temp until
spent, then slot 0 goes back to the infinite ball. A barrel arms on the spot and
is spent quickly, so refusing it would mean a barrel you shot did nothing. And a
spent weapon LEAVES its slot -- with gathered slots, an exhausted weapon holding
one is a quarter of the rack you can never pick up into. (The old rack kept its
empties dimmed, which was right when a slot meant "BOMB goes here".)

Four new weapons, and they are all the ONE existing pellet loop. firePellet(w)
stamps the WEAPONS row onto the round -- r, dmg, hp, pierce, boom, ignite, col,
speed, life -- so the beacon cannon, flamethrower, laser and rocket needed no new
array and no new update; the break after the first body was the only thing making
a pellet single-target. All four go through the same isBoss branch, which is why
none can silently no-op on a boss the way three of five once did. Measured:
gun/beacon/laser/rocket reach past 16 m, the flamer is a ~6 m cone that ignites.

G.mgT stays the mode timer for all of them, so the eighteen !(G.mgT > 0) guards
across five files did not have to change. kind:'burst' auto-fires at its own rate
for len seconds; kind:'shot' holds the same braced stance and fires one per tap.

The barrels reuse the ball machine's shape: a rolling, shootable non-body. All
three of the registrations that fail silently are done -- blocksClear (loot must
not gate the hand-clear), the contact guard (furniture cannot eat you), and
hitR/GORE_MUL/zombieH. It scores nothing.

The pink had to become procedural. The chroma key deletes every pixel where red
and blue both beat green by 40, so a magenta drum and a magenta laser pistol both
came back as hollow outlines -- 19% and 15% solid against 50-56% for the icons in
other colours. The drum is grey steel with hazard chevrons and every bit of its
pink is the halo drawBarrel paints; the laser went electric blue. Both are solid
now (92% and 58%), and the rule is written down in HANDOFF.

tinTriples 3 -> 10 as asked, and the four dead TUNE ammo constants are gone --
nothing read them once the table landed, which is a panel value you can tune all
day for no effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 12:09:28 +02:00
wuwiandClaude Opus 5 7284ca08f8 Pace back to 1.2x, and unweld the racket from over his head
PACE 1.3x -> 1.2x on all four numbers: 19.2 / 14.4 rally, 22.8 / 26.4 serve.

The strength-to-velocity relationship you suspected was already there is: a
splatter serve runs 26 m/s at the minimum pull, 36 at the kill threshold and
49.2 at a full draw. The draw very nearly DOUBLES the pace (1.89x), because
serveQualitySpeed carries more of the pair than serveSpeed does. Nothing to
change unless you want it steeper, and that is the one knob.

THE RACKET-ALWAYS-UP LOOK WAS A LATCH, not the art. hitServe sets pose 'serve'
with poseT 0; that only expires after POSE_TIME (0.38 s); and p_pull, the
wind-up frame, is only ever selected from pose 'idle'. So firing faster than
0.38 s reset the timer before it could expire, he never returned to idle, the
pull frame was never drawn once, and he stayed welded into p_smash — arm
extended overhead with balls appearing out of the strings. Exactly the late
game, because that is where the fast successions are.

serveToss now clears a stale 'serve' pose: a new wind-up ends the last shot's
follow-through. At ~4.6 shots/sec the cycle is now pull x5 frames, smash x8,
pull x5 — a real swing. Slow fire still plays the whole ritual: bounce, pull,
smash, idle, bounce.

Caveat: past roughly 8 shots/sec each pose gets 1-2 frames and will read as a
flicker rather than a swing. There is no fix for that short of drawn
follow-through frames; the poses are stills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:14:12 +02:00
wuwiandClaude Opus 5 2edabe6c64 Aim the ball at the chest, and stop lobbing at a net that isn't there
THE BALL WAS SOLVED TO THE GROUND. solveShot's terminal height was a hardcoded
0.08 — ball-on-the-grass — so a splatter serve came down past the shambler and
registered at the ankles however the mark was drawn. It now takes an arrival
height, and a lane shot aims at the body: zombieH x Z_CHEST, scaled off the
same height table render draws with, so a brute is struck on the brute rather
than at the height a shambler's chest happens to be. Measured at the frame the
wound registers: shambler 1.11 m of 1.8, brute 1.64 m of 2.7, and an empty lane
still lands on the grass. The aim mark reads the same `ty`, so mark, arc end
and impact are now one point instead of three guesses.

AND A BUG FOUND ON THE WAY. The net-clearing loop ran for shots that never
reach the net. A shambler on your own side sits short of the tape, so `tN` is
still positive — the net is ahead in the direction of travel, the ball simply
lands first — the height test there is far below the tape, and the loop spent
all twelve cuts. Every same-side shot left the racket at 28% of nominal pace as
a slow loop. That is exactly the shot that needs to be fast: those are the ones
about to eat you. Guarded on the shot actually crossing the net, in serveArc
and in doHit's copy. Flight time to a body at z=16 drops 0.71 s -> 0.20 s.

THIS ONE IS A FEEL CHANGE YOU DID NOT ASK FOR — near shots are now fast and
flat where they used to loop. Say if you want the lob back.

Tennis and the duel still serve into the box, far-side shots still clear the
tape, 900 frames of splatter render clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:49:47 +02:00
wuwiandClaude Opus 5 ff9e09e3eb The body mark was behind the body; the yellow ring was never mine
Two bugs, one report.

THE MARK WAS DRAWN UNDER THE SPRITE. The whole band, target mark included, is
painted before the sprite pass — which is right for the arc, since a nearer
shambler should occlude the part of the flight behind it. But the mark is a
mark ON a shambler, so it went down first and the body was then drawn straight
over the top of it. It has never once been visible. Split out as drawAimMark
and called after the near-side actors, which is the only place a highlight on
a body can go.

THE YELLOW CIRCLE WAS G.outMarker, not the aim at all. It is the tennis in/out
call — "did that land in?" — and splatter never asks: `out` could not be true
there, so it was an unconditional yellow ring on the grass carrying no
information, sitting under the shambler for 1.3 s. That is longer than the gap
before the next pull, which is why it was there "most of the time" and why it
looked like the aim marking the feet. Now set only in tennis, from both the
serve and the rally swing.

Checked: tennis keeps its ring on serves AND rally shots, splatter has none
anywhere, and the mark now draws over the torso at every pull strength.

Note this leaves a splatter VOLLEY with no landing feedback at all. It had a
ring that said nothing before, so nothing was lost, but say if the volley now
feels blind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:38:08 +02:00
wuwiandClaude Opus 5 0ce857fedc Let the ball eat its own aim band, mark the body, grow the machine
Four things from the phone.

THE BAND NO LONGER BLINKS OUT. Cutting it at the instant of release made a
fast rally strobe — band, gone, band, gone. hitServe now records the arc it
actually fired (error included, so the ball tracks what it is consuming to
within a centimetre) and the ball eats it from behind as it flies. The aim
resolves into the shot instead of vanishing. It ends early when the ball does,
because a shambler swallowing the shot is a cut you want.

THE TARGET MARK SITS ON THE BODY. A ring on the grass pointed at the one part
of a shambler the shot has nothing to do with — the ball strikes anywhere under
1.9 m and the wound lands on the torso. So the mark rides at chest height when
there is a body to hit and lies flat only when the lane is empty. Filled and
washed out in the band's own colour instead of a hard 2px ring, which stops it
reading as a decal stuck to the lawn.

x_howto REDRAWN for the current gesture: thumb dragging back to a print, the
shot leaving as a curved band onto the shambler's chest, shadow dots beneath.
The old one still taught a straight arrow.

MACH_H 1.15 -> 1.55. Waist high read as a prop you could ignore.

Verified on a cache-busted build (the pane was serving a stale config.js, so
the first pass measured nothing): preview velocity and gravity still identical
to the fired ball, same flight time in the trail, unravel smooth to the hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:41:18 +02:00
wuwiandClaude Opus 5 3a91d87b08 Show the shot itself: one solver, one band, no meter
The reticle was a chevron trail on the ground and a draw meter across the
bottom. The trail was a RAY — it pointed at the target but said nothing about
the ball getting over the net — and the meter sat in the apron, which is
exactly where your thumb is while you pull, so the one readout that mattered
mid-gesture spent the whole gesture underneath your hand.

Now the aim IS the flight. previewServe runs the same targeting and the same
loft solve hitServe fires with (both extracted, neither duplicated), so the arc
you see is bit-identical to the ball you get — verified: vx/vy/vz/g all match
to zero at quality 1.0, where the aim error vanishes. A preview drawn from a
second copy of the maths is worse than no preview; it lies with confidence.

One object now carries all three readings, up-court where you are looking:
the parabola is WHERE, the colour is WHETHER (cool blue still means it only
staggers), and the band's thickness and opacity are HOW MUCH — which is the
part a hue cannot carry on its own. Because the arc ends at the auto-picked
target, a weak pull and a full one at the same shambler draw the same curve in
different colours: "pulling further does not reach further" made visible.

A shadow tracks the arc along the lawn. An arc hanging in the air is ambiguous
about where it comes down, and that trail is what pins it.

The apron is left to the gesture demo alone, drawn bigger now that it owns the
space, with its chevrons lifted clear of the player's legs.

Tennis and the duel still serve to the box, not at shamblers — checked both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:16:21 +02:00
wuwi 8ea749f380 Revert "Point at them, then wind up: the lane comes from where the thumb lands"
This reverts commit 1798fc67b8.
2026-08-02 14:35:25 +02:00
wuwiandClaude Opus 5 1798fc67b8 Point at them, then wind up: the lane comes from where the thumb lands
The lane used to come from the pull's own direction, so aiming up-court was
always a downward drag. Every shot ended lower than it began, the next touch
starts where the last one ended, and over a horde the thumb walks itself into
the iOS bottom edge — where a pull down is Reachability and the system takes
the gesture out from under you. A web page cannot refuse that.

So touch-down picks the lane now (a ray from his chest through the thumb) and
the pull after it is power and nothing else, in whatever direction there is
room for. Aiming is what drags the thumb UP the screen, because that is where
the shamblers are.

The cost, and the thing to feel: the lane locks when the thumb lands. You can
no longer sweep the aim while drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:22:55 +02:00
wuwi 1337c1f47d Revert "Aim by dragging at them, so the thumb stops walking into the iOS edge"
This reverts commit d4bbcfc080.
2026-08-02 14:10:12 +02:00
wuwiandClaude Opus 5 d4bbcfc080 Aim by dragging at them, so the thumb stops walking into the iOS edge
The slingshot pulled back, which meant every shot ended with the thumb lower
than it started — and the next touch begins where the last one ended. Over a
horde it walks itself into the bottom edge strip, where a pull down is
Reachability and iOS takes the gesture. A web page cannot refuse that.

So the drag now points AT the target: the thumb ends high and drifts up-screen,
away from the edge. It also matches the flick-swing, which always aimed this way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:05:04 +02:00
wuwiandClaude Opus 5 d3aabc4bd6 Fold the menu into the title screen, and stop the theme restarting
The menu stage was the same logo on the same backdrop with two buttons, so it
read as the title screen failing to advance. The buttons move onto the title
screen in place of the control list, which is the tutorial's job now.

Death no longer reloads the page. The reload was an honest full reset, but it
also tore down the AudioContext — so the theme restarted from silence every
time you died, when it should carry straight on from the death summary into the
title screen. restartRun() puts G, P, O, ball and score back from a
structuredClone taken at module load (all plain data, so the clone is the whole
job and a field added later is covered for free), and the two modules with
timers of their own reset themselves: every zombies array plus spawnT/moanT/
mgTick, and input's held keys and half-finished drags, which would otherwise be
inherited by the next run's first frame.

The overlay is hidden rather than removed so it can come straight back, which
needs its own [hidden] rule to beat the display:grid on #intro.

The theme starts on the first tap and never restarts: loopBed's beds.has guard
already makes every later menuMusic() a no-op.

Verified: one background-music fetch across launch -> run -> death -> title ->
second run, and a full second run through the transition after the in-place
reset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:25:20 +02:00
wuwiandClaude Opus 5 60a74f6a99 Give the menus a theme
background-music.wav loops under the title screen and the court picker, ducks
out when a court is picked, and fades back in at the death summary — the end
screen is a menu too, so the zombie bed ducks out as this one comes in.

It starts at BOOT, not on the first tap, so the title screen already has music.
That needed loopBed to call audio() before its already-started early return: the
theme plays into a context the autoplay policy holds suspended, and the call
that wakes it is a second menuMusic() from the first tap, which used to return
at the beds.has guard without reaching AC.resume().

fadeBed also now records a duck for a bed that is still loading, and loopBed
starts that bed silent. An 8 MB file ducked mid-fetch would otherwise come up at
full level, seconds later, over the match. Same latent bug applied to zombieDuck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:42:10 +02:00
wuwiandClaude Opus 5 bcff17812f Draw the whole serve right-handed, and let the pull wind it up
The racket kept changing hands because the art disagreed with itself: p_idle,
p_bounce and p_toss held it in his LEFT hand while p_smash and p_swing used his
right, so every new frame copied whichever reference it was handed. The idle, the
bounce pair, the toss and the four wind-up frames are all regenerated with the
side named — drawn from behind, his right hand is the right-hand side of the
image, and only the racket HEAD crosses behind his back.

What separates the four wind-up stages is how far the racket has fallen, and that
needed the racket head pinned to a body landmark per frame — above his head,
beside his ear, between his shoulder blades, at the back of his thighs. Described
as "further into it each time" it drew four near-identical poses twice running.
They are wired late in the pull, Oliver's call: frame 1 owns the first half and
the three coiled frames are packed into the top half.

assets/_derive.py is deleted. Fixing generated art by mangling it afterwards is
always worse than regenerating; three guards in _regen.py turn the bad rolls that
used to install silently into automatic re-rolls — a portrait reference canvas
(the generator answered with a 2x2 grid), a non-magenta background (a green sheet
keyed away four racket heads), and a mirrored frame.

The bounce pair is one drawing from one sheet now, differing only in the free
hand, so BOUNCE_IDLE moved to his left side and was re-measured off the sprite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:49:28 +02:00
wuwiandClaude Opus 5 01498eddad Turn the bodies around, calm the racket zombie, break up the pack
Three from the playtest, all code:

Every side-on sprite is DRAWN facing left, and nothing ever mirrored them, so a
zombie walking right moonwalked — obvious the moment they close on you across
your own half. They face where they last walked now. The front-facing ones (the
turned CPU, the machine, a chapter boss) have no left or right and are left alone.

The racket zombie holds a position and only moves when the ball makes him, so a
time-driven walk cycle flicked him between two frames on the spot. His stride
steps off ground covered, like the brute's — standing still is standing still.

And the pack no longer stands on the body twitching for as long as the summary is
up: once it appears they break up and shamble away up-court. Measured — feeding
at 3s, dispersed to 3m by 6.5s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:24:11 +02:00
wuwiandClaude Opus 5 4d6b47dadd Die the way people die in the films
The old death was a red tint and a shake for half a second, then a puff of gore.
That reads as the sprite having a moment, not as being eaten.

Now it takes four and a half seconds and it is a crowd doing it. You are dragged
to your knees fighting it (new frames, p_die1/p_die2, alternating), blood coming
out of you the whole time — a constant spray and a stain that keeps widening,
with the wet work on a loop underneath. Every body on court drops what it was
doing and converges: each claims a spot in a ring around you, kept from wherever
it was standing, so they crowd in from all sides instead of stacking on one
pixel. Then you are pressed flat into the ground while the ring closes over you —
you are drawn INSIDE the crowd's z order, so the ones between you and the camera
hide you exactly as they arrive — and what is left is the mound, with them still
feeding over it when the summary finally fades in.

Also: the wounded shamblers were stepping their two frames at half the cadence of
the healthy four, which reads as frozen next to the crowd they walk in. Same
clock now. (They were animating — verified frame by frame — just too slowly.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:36:25 +02:00
wuwiandClaude Opus 5 b7899f122c Stop the brute pulsing, and let a boss die where you can see it
The pulse was drawStatic being told a height instead of reading one. _slice.py
common-scales a sheet, so a crouched frame is genuinely shorter — 211 px against
its neighbours' 224 — and forcing every frame to the same on-screen height
stretched it back up, so the body swelled and shrank once per stride. Worst on
the brute because it is the biggest and slowest. The rule already existed for the
wound stages; it now applies to every frame, once, which also settles the
shamblers and the steward (their cycles vary by up to 9%).

A chapter boss now dies in its own beat instead of under the card announcing it:
the world drops to a third speed for 1.2 s, the screen kicks, a shockwave ring
blows out through 90 extra particles and six stains, and the CHAPTER DOWN card
holds off until the pieces have landed — about 1.4 s, measured.

Cards can hold off in general now (`wait`). First cut only delayed the card's
countdown while still drawing it, which is worse than no delay: caught it in a
screenshot before it shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:22:55 +02:00
wuwiandClaude Opus 5 7803c83564 Never let a pending horde pour onto a boss stage
Game 30 is both a set and the third chapter boss. pendingHorde is a LATCH: the
set arms it and the next 'over' beat to expire cashes it — and with the boss
outranking the set at that clear, the next beat to expire was the chapter card.
The wave arrived on top of the boss.

The latch is now left alone while a boss fight or a duel is running, so the beat
after the fight cashes it: the set still gets its horde, just not on top of the
thing that outranked it. Verified both halves — ten seconds of boss fight with
the latch armed and no spawn, then the wave arriving once the boss is down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:52:03 +02:00
wuwiandClaude Opus 5 e904dfefa6 Let the horde gun keep up with the horde
The run ended on the second wave every time: the gun fired 6 flat rounds a
second, hit one body each, and every shambler soaked one — while hordeGrow
made each wave longer and denser. Doubled the rate and scaled it with the
wave (14/s at one set banked, 21/s at four), gave a round 0.5m of extra
reach, a wider cone, and enough damage to drop a shambler outright. Raised
the spawn rate to match, so surviving a wave still leaves a standing crowd
on your half — that pressure is the point of it.

Also: he no longer carries a second racket at his hip while holding one
forward (the run frames are the racket-less art), the umpire stopped talking
over himself and saying "remain seated" twice, and a timed freeze now shows
what it is counting down to instead of reading as dead controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:07:35 +02:00
wuwiandClaude Opus 5 ae64daa0fb Fill the screen, and let the swish keep up with a late horde
The portrait canvas is adaptive now. A fixed 270x480 is 9:16 while a modern
iPhone is taller, so width bound the fit and ~150 CSS px of screen went unused
at the bottom. Height is derived from the device aspect, and TUNE.horizon rides
with it — proj() places the court in absolute pixels, so leaving horizon fixed
just grew a dead green band between the court and the rack instead of moving
the court down. RES 3 to 4 for the extra pixels: an iPhone gives ~1125 device
px across and we were rendering 810. That is the first knob to turn back down
if the phone stutters.

The swish cooldown scales with how swarmed you are — 1.00s at one body, 0.47 at
four, 0.22 from fourteen. Oliver died on every game-20 horde because the wave
grows with hordeGrow while the panic button stayed a flat second; he chose
scaling the melee over shrinking the horde. The first version measured the
crowd after meleeSwipe had already killed it, so it counted survivors and
barely moved.

Winning a duel while wounded lost the weapon popup entirely: showModal never
clobbers an open modal, so the heal modal opened first and the tin's was
dropped. One popup carries both now.

And the mop boy stays off the court unless a stain actually passes the test
mopRound uses to pick a target. He was walking on, finding nothing reachable,
and walking straight off again.

Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:43:48 +02:00
wuwiandClaude Opus 5 49d391dc64 Make it readable: 2x sprites, a flatter camera, honest hit timing
The umpire is situational again. Each line is tied to the thing that justifies
it — a new variant appearing, the court filling, one crossing your baseline, a
game lost, a horde, a changeover — behind one shared cooldown, so several
triggers at once still yield one remark. The random timer pool is gone, the
interval went 26s to 55s, and ump_out_of_order, ump_let and ump_no_appeal are
deleted.

"Visually the ball is not yet at the player but somehow the player already is
hit" was real and measurable. The hostile-ball hit test reused the parry's
hypot(dx, dz*0.8), and weighting depth shrinks it: contact fired while the ball
was still 0.68 m up-court and drawn up around his head. It has its own
unweighted test now, and contact lands at 0.25 m with the ball on his chest.
The parry keeps its weighting — a racket sweep really is wider than it is deep.

On "everything is a tad too small": measured first, because higher-resolution
sprites are only half the answer. At RES 3 the player rasterises into 170
device pixels from a 112px source — upscaled 1.5x — while a far shambler is
downscaled to 78. So near bodies needed resolution and far bodies needed the
camera.

Camera: pushed back and lengthened (camZ 42->48, camF 580->760) to flatten the
perspective so distant bodies grow relative to near ones rather than everything
zooming, camY raised for a steeper look-down, horizon re-centred. Far shambler
26->30px, boss 39->45px, player unchanged, court in the same band.

Sprites: the original sheets were still in the repo at full resolution, so
nothing needed regenerating — IDLE_TARGET_H went to 224 and every sheet was
re-sliced. The sheet-to-output mapping was never recorded, so it was recovered
by matching sliced frames to existing assets by image signature: 58 came back
from sheets, the 29 without a confident match were 2x-upscaled to stay on the
shared scale, since a sprite off that scale renders at half height.

Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:23:04 +02:00