Commit Graph
76 Commits
Author SHA1 Message Date
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 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 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 5a4c6e4359 Give the laser its own voice, and let a body sound like its size
The laser was playing boom() — the BOMB — so the showiest weapon in the game
sounded like the least showy one. zap() is its own: a highpassed crack, two
detuned saws diving through a resonant lowpass, weight underneath so it lands on
a phone speaker, and a triangle that rings after the plane has gone. The dive is
the whole reading; a rising sweep is a charge-up and this thing never charges.

And the NORMAL hit — ball meeting a body without killing it — was borrowing
pock(), the racket thock. That is a bright, dry, stationary-band click: exactly
right for gut on felt and exactly wrong for felt on a torso, which is why hitting
a zombie sounded like hitting a wall. thud() falls instead of sitting still,
which is where the wetness comes from, with one quiet bright tick just behind the
slap: something gave. The machine keeps its pock — it is furniture and it rings.

Both thud() and splat() now take a size, zombieH/1.8, so 1 is a shambler, 1.5 a
brute and 2.04 a chapter-4 boss. Everything pitches down and lengthens with it.
Measured through a patched AudioParam rather than assumed: the body thump under a
hit runs 150 Hz on a shambler, 100 on a brute, 59 on the boss.

splat() got the same treatment while I was in there — a resonant band collapsing
down the spectrum instead of a plain lowpass, plus a second brighter burst a beat
later. One clean hit is a thud; a thud followed by spatter is a body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:23:47 +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 99774ab4be Shape the cone by hand, and give the playground its knobs back
Oliver's tuned numbers become the defaults: beamY 0.95, beamLen 23, beamW0 0.08,
beamW1 3.1, beamOriginLift 0.44, beamBorder 0.18.

Three knobs earned their keep on the way there.

beamLen is the DRAWN length now, not only the kill range. The drawn length was
solved every frame so the far end landed on the middle of the ad strip, which
made this a control that visibly did nothing. One meaning beats one clever
behaviour -- and it pairs with beamY, since a lower plane meets the ads sooner.

beamOriginLift replaces beamOriginY: the apex is a lift ABOVE the plane rather
than an absolute height, so lowering beamY brings the near end down with it
instead of leaving it hanging at chest height with the beam sloping away.

beamBorder exposes the edge width, which was hardcoded.

And the playground is back, deliberately NOT paused. The first version froze the
world so the picture would hold still, and then nothing redrew, so every value
changed did nothing -- a still frame you cannot change is worth nothing. The
bodies are held still individually and spawning is off instead.

Also: a boolean living in TUNE has to be tested with >= 0.5. The panel steps
every key by 0.02, so one nudge set beamBand to 0.02 -- still truthy -- and the
renderer switched variants under him mid-tune.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:58:51 +02:00
wuwiandClaude Opus 5 ac1d5fbccd Draw the laser like the ball's aim band, and let the two be compared
TUNE.beamBand flips between the two looks live from the T panel. 1, the new
default, draws the laser the way the BALL's aim trajectory is drawn: a path down
the lane whose width comes from each point's own projection scale, so it narrows
into the distance like everything else on the court, with a shadow on the lawn to
pin it there. 0 is the solid widening cone.

The cone was a true projection, which is why it was not obviously wrong to me --
but it GROWS in world width, so on screen it opens out while the court recedes,
and the eye reads that as a shape stuck to the screen rather than a plane lying
in the world.

The kill follows whichever look is on, in both fireBeam and drawBeamCuts.
Otherwise the comparison would be a lie: a narrow band that deletes a cone-wide
crowd tells you nothing about how the narrow band plays. Measured down one lane,
the cone takes 9 of 9 and the band 5 of 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:26:18 +02:00
wuwiandClaude Opus 5 337def8526 Cut them with the laser plane instead of painting over them
The beam is a real plane now, holding TUNE.beamY (1.15 m) all the way out instead
of sloping to the ground. That one change is what makes the rest possible: sloping,
it lay flat on the grass across the whole far half, so there was nothing to cut
exactly where the crowd stands.

One flat translucent triangle, apex at the player's CHEST, opening out down the
court with the alpha falling away. It was four nested wedges composited 'lighter'
and that layering WAS the banding -- additive light blowing out to white where the
layers overlapped. Flat alpha also means a body under the plane is tinted rather
than brightened, which is what lets the cut read at all.

Bodies are sliced by it: the part above the plane is redrawn clipped so it sits on
top, the part below stays tinted underneath, and it happens while AIMING as well as
on the shot. Measured, the plane takes a shambler at 64% of its height, a brute at
43% and the roller man at 36% -- bigger bodies visibly take it lower.

The cut line follows the SPRITE rather than a rectangle. A fillRect spanned the
body's bounding width, so it crossed the gaps between the legs and under a held-out
arm and read as a bar lying behind him; the line is the sprite itself, redrawn
clipped to a thin band and flattened to one colour by ctx.filter. It thins with
distance on s squared, because this camera's scale only spans about 1.5x across the
court and a linear falloff was invisible.

Aiming a filter at a specific colour turned out to be a measurement, not a guess:
brightness(0) invert(1) goes to WHITE, and sepia adds almost no chroma to white, so
the first chain rendered a grey. Leaving a mid grey for sepia to tint lands on
(32,154,255) against the laser icon's (29,153,232). The probe fills a rect and reads
the pixel back.

The pass is bounded to the eight nearest bodies above 14 px, because it draws each
one twice more through a filter and runs every frame you are aiming. 0.27 ms/frame
with 20 bodies on desktop, against a 16.7 ms budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:48:08 +02:00
wuwiandClaude Opus 5 da9cca2f23 Hide the racket, and light the beam all the way to its edge
The mg racket is gone, not moved. It was a procedural racket drawn over a sprite
that already holds one: straight up-court it projected onto the player's back as
a floating oval, and offset to the side it just stuck out.

The laser's far edge was pinned to the middle of the ad strip two commits ago and
nothing appeared to change, twice. The geometry was always right -- each wedge
layer faded to alpha 0 along its length, so the beam went invisible before it
reached its own far corners and the VISIBLE end sat well short of the pinned one.
The far stop is lit now, so the two are the same line. Verified by recomputing
the projection: far corners at y 113.8, the ads' middle, against 110.2 unpinned.

It also falls off with distance now, brightest at the strings and thinning down
the court -- but never to zero, which is the trap above.

Fixes a third instance of the empties-under-the-iterator bug: a rocket blast can
bank the last kill of a game, and the duel or chapter boss that follows clears
pellets[] mid-loop. shots, pellets, extras and the body loop all carry the guard.

Also records that two consecutive render() calls do not produce the same frame --
render mutates camera smoothing as a side effect -- so frame-diffing to find where
something was drawn measures the camera drift instead. It claimed the beam reached
16 px above where its own geometry could go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:20:33 +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 4cda6bde76 Make the bosses move, and put six of them and three new bodies on court
A chapter boss had no AI at all: it matched no branch in the variant chain and
fell through to the crowd's aimless wobble, so "the boss AI owns its x" was a
comment describing code that did not exist. Movement is now data on the BOSS_ART
row -- pace, shadow, surge, charge, bounce -- and bossAI is the one place it
happens. Every behaviour field is optional, so a row carrying only art behaves
exactly as a boss did before.

Three earlier drafts of those styles failed the same way: written as "walk toward
the player's x", which in splatter means arrive in four seconds and stand still,
because the player is rooted in pretoss for nearly all of a boss fight. Every
style now moves under its own power and only biases toward him. Measured over a
simulated minute each: 50-136 m travelled, none static.

Three new designs -- THE HEAD COACH (a 5-shot drill 0.14s apart), THE PHYSIO
(charges, and withdraws less than it charges, so it reaches you at 38s and that
is the fight's clock), THE SPONSOR MASCOT (gap 0, so the whole wall of five
leaves in ONE frame and is dodged rather than parried). Harder to kill: 16 -> 20
hp, 9 -> 10 per chapter, plus per-design multipliers.

Three new bodies: the ball boy (3 hp, 2 skins, from game 3), the bouncer (6/3,
game 8) and the roller man (9/4, game 15), each with a 4-frame walk and a drawn
two-frame lurch for every wound stage.

Three wound rules became one. The brute had a hardcoded stage array, everything
else a single skin behind 'abd'.includes(...), and the boss a fraction of hpMax.
The boss's rule reproduces the other two exactly -- verified: the brute still
draws z_gw/gw2/gw3/gw4 at hp 4/3/2/1 -- and keeps working when hp is not
one-per-skin, which all three new bodies need. So a multi-hit body is now a row
in config.ZWOUND and no code. bleed() reads the same call, which also gave the
steward the trickle his wound skin had always implied.

Fixes a real crash on the way past: killPlayer empties shots[] while the shots
loop is mid-iteration, so every remaining index read undefined and the frame
threw. The mascot's five-at-once volley makes it easy to hit; the shipped 3-shot
volley could always have hit it.

_regen.py gains two style blocks and a check tier. One constant used to say
"keep every colour exactly as the reference" -- right for a remaster, and it
fights every word of the subject otherwise. newart is for a new character, state
for the same character wounded or mid-throw; using newart for the wound sheets
licensed a repaint and got one, five came back with living tan skin. And the
detail gate ratchets against the file it replaces, so check: any exists for
re-rolling already-fine art to fix something the gate cannot see, such as colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:45:35 +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 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 661ad90be5 Let the ball machine roll, dent, and blow up
It was furniture in the most literal sense: bolted to one spot, three hits from
scrap, and when it died it bled and a ball boy came out to mop it.

- It trundles to a new spot every seven seconds or so, and CANNOT fire while it
  rolls. That is honest (a rolling cart cannot aim) and it is the window you use
  to close on it — but it also means it stops being a landmark you can ignore.
- Six hits, not three, and it wears them: dented at two thirds, split open and
  sparking at one third, smoking where it stands once it is that far gone.
- It comes apart as MACHINERY. No blood decal, no gore mound, no mop boy — a
  flash, sparks, torn metal and a column of dark smoke over a scorch mark. Smoke
  is a new particle flavour that rises and spreads instead of falling.

Damaged sheets keep the healthy cart's footprint: the generator drew the smoke
plume into the sprite, which would have shrunk the cart as it took damage, so
the plume is cropped off and the smoke is drawn procedurally where it can move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:05:44 +02:00
wuwiandClaude Opus 5 1168e9ee5b Keep the wounded walking, and make the steward worth his size
A wound skin was a single still, so anything hurt froze on one frame and slid
across the court. Every skin has its …b twin now — the stride swapped, stepped
off the same clock as the healthy walk — for both shamblers, both racket-zombie
stages, and the steward, who had no wounded state at all.

The steward also died like a shambler at 3 hp, which made a bigger body read as
decoration. Five now, with a wound skin to show the second half of that. hpMax
is recorded on every body that has hp, so "hurt = show the wound" is one rule
instead of a per-variant constant to remember.

First pass at the wound frames was rejected on sight and deserved it: the
generator drew small figures with fine pixels on a GRADIENT background, and a
nearest-neighbour downscale to sprite height shredded the outlines. Regenerated
two at a time, drawn large, chunky pixels, flat background, hard black outline —
then fitted per channel onto each frame's own healthy twin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:57:45 +02:00
wuwiandClaude Opus 5 d62478e8b1 Put a steward on the court
A new ordinary body between the shambler and the brute: a stadium security
steward in a hi-vis vest, 2.15 m, three hp, a touch slower. Deliberately the
mildest thing in the advanced pool — no wound skins, no special rule to learn —
so he can unlock early (game 4) and thicken the ordinary crowd, which a mix of
two shamblers never did. A fifth of horde spawns come out as stewards too, so a
grown wave is not one body dying at one range.

Sheet generated against the existing shambler walk (assets/_sheet_dwalk.png),
sliced to a 4-frame cycle, so he shambles as smoothly as the crowd he walks in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:57:03 +02:00
wuwiandClaude Opus 5 9a4d921706 Pin the canvas height to what is actually visible, and three fixes
Viewport, again — the homescreen app was the case that broke it. Watching for a
signal cannot work there: launched standalone, iOS resolves the CSS viewport once
against a provisional box and never re-resolves it, so no resize, no layout
change, nothing for a ResizeObserver to observe. Only a rotation forced the issue.
So the CSS box is now a proposal: visualViewport says what is really visible and
the height gets pinned in px to match, re-read on every fit — plus a short settle
salvo after launch, since that launch announces nothing.

- The countdown skipped the transition, where overT is parked at 9999 to hold the
  point flow. It read as RESUMING IN 9999 after the first game you ever win.
- Countdown moved off the HUD line into the open, at a readable size.
- A returned shot is a glancing blow now, not a mirror. Reversing the incoming
  line sent every parry straight back up it — into the thrower — so a boss handed
  you a free wound per volley. Contact across the racket face sets the angle:
  dead centre still goes back at him, which is a placement you earn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:18:55 +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 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
wuwiandClaude Opus 5 8bc57e9358 Tighten the returns, give each chapter its own boss, and let the umpire talk
Five reports from Oliver's second playtest.

The parry was too generous — at 6 m of arm with a 0.34 s window the shot flew
into reach on its own, so any tap once it was vaguely close worked. 3.4 m /
0.2 s / 1.7 m reach now: a tap at 4.5 m is rejected and costs blood, 3.2 m and
in returns. The boss telegraphs in 0.5 s against the machine's 0.9 and throws
harder, because a boss should be worse than furniture.

His three balls were decoration: fanned ±1.9 m across space, so only the middle
one could ever reach you. They are a timed salvo now — three shots 0.42 s apart,
each aimed at where you are at that moment. hurtGrace still caps a whole volley
at one point of damage, which is what it is for.

Every chapter was the same boss. Three designs cycling now, each with its own
sheet, stride, throw pose, wrecked skin and wrecked-throw skin: the
GROUNDSKEEPER, the LINE JUDGE, the CHAIR UMPIRE with his chair strapped on. The
chapter card names them. Designs are data in BOSS_ART, so a fourth is a sheet
plus a row.

Healing announces itself with a first-aid tin and a red cross instead of a 1.4 s
flash. And 14 new umpire lines: ten sarcastic asides on a jittered timer, four
hooked to specific beats. He still has not acknowledged that the match became a
massacre, so he keeps officiating it.

Two traps found and written into the pipeline notes. Sprite loads now carry
?v=SPRV, because python http.server sends no cache headers and the browser
caches 404s — a name added before its PNG exists stays permanently missing for
that browser. And a scripted replace that silently didn't match left drawZombie
on the old hardcoded boss names while everything else moved to BOSS_ART; the
built-in fallback hid it perfectly, all four designs rendering as design 1 with
no error. Assert your rewiring edits matched.

Tennis untouched: 30 tapped points, 50 hits / 0 whiffs. 45-game soak clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:56:00 +02:00
wuwiandClaude Opus 5 295470b73c Make the parry reachable, and stop a boss dying to one flame ball
Six reports from Oliver's first playtest of the boss build. Two mattered.

A boss sprite turned up in the shambler crowd because z_B1.png and z_b1.png
are the same file on a case-insensitive filesystem — writing the boss frames
silently overwrote the 'b' shambler's first two frames, and `ls` only ever
showed the original lowercase name. Restored from HEAD~1, boss frames renamed
z_boss*, and the rule is now in the asset pipeline notes.

The parry was unreachable, which killed the entire new mechanic: P.swingT is
only set in keyPressed's `phase === 'rally'` branch, and splatter sits in
pretoss almost all the time, so every tap aimed at an incoming shot tossed the
serve ball instead of swinging. An incoming shot inside parryArm now owns the
tap in every phase, with its own generous reach and window — this is a phone,
one thumb, against something aimed at you. The three fail cases (too early,
too late, no tap) are what keep it a timing skill. The swish no longer eats
shots: it runs first in keyPressed and was swallowing the very ball the parry
was about to return.

One flame ball killed a 16 hp boss, and that was a bug rather than tuning. A
flame ball is not consumed on contact (full pierce, so it can cut a lane) but a
boss is not removed when hit, so the same ball re-wounded it every frame. Boss
bodies now take a contact cooldown and flame spends itself on them. Chapter 1
costs 4 flame balls or 16 clean smashes, and bossHp goes 10 to 16.

The boss had no wounded state while throwing — correct, the wound skins were
standing poses only. New sheet gives all four damage states their own overarm
pitch. The ball machine's four painted rects are replaced with a real sheet,
idle and firing, keeping only the muzzle glow procedural because the telegraph
has to read at 20px tall. And the player's blood no longer sits on him as a
painted layer: live drip and the screen-edge wash, nothing else.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:50:47 +02:00
wuwiandClaude Opus 5 4a066e5cc6 Add chapter bosses and a second damage channel; demote the racket zombie
Steps 4 and 5 of the late-game depth plan.

The ranged channel. A rogue ball machine gets wheeled onto the far court every
four games: it never walks, never crosses your baseline and never scores, so
it is a standing threat you choose when to deal with — the target-priority
decision the game was missing. Its shot is a real arc solved like a serve, not
a flat bolt, so it reads coming in and the swing window has something to meet.
Three outcomes: a swing inside the volley window returns it as your own ball
flying up-court (an extras clone, so "a parry is a weapon" cost nothing to
build), the panic swish bats it away, and letting it reach you costs blood.
The swish path is load-bearing, not politeness: in pretoss the player is
rooted and cannot swing at all.

Contact still kills outright — that stays a separate channel, and killPlayer()
is now the single death path for both causes so the run summary and the best-run
write cannot drift. Health is drawn on the man: procedural spatter at fixed
anchors, a live trickle, and a red wash from the screen edges. No bar. Healing
is winning a duel, which finally gives the duel a reason to be wanted.

Chapter bosses every ten games, with their own sheet — a colossal zombie
groundskeeper hauling a ball hopper, coming apart through arm-off, ribs-out and
headless-still-throwing. Chapter 1 is 10 hp and 2.7 m, chapter 4 is 34 hp and
3.67 m. It outranks the set and duel beats at a clear, because a boss walking
into a standing wave is unreadable. And it fixes the economy: a chapter-4 boss
costs 12 bombs or 100 rounds or 34 side balls against a per-type cap of 9, so
no single weapon can take one and a banked mixed arsenal is mandatory.

The racket zombie is demoted to an occasional opponent, as asked — appearing
every won game is exactly why he read as the boss.

Five silent bugs found on the way, all in HANDOFF: a blast deleted a 34 hp boss
outright (isBoss() exists to stop that class recurring), the machine's hp did
nothing because it fell through to the one-shot fallback, the boss both scored
and fired its card, the machine gated the hand-clear, and a boss died with a
shambler-sized gore burst.

Tennis untouched: 30 tapped points still 50 hits / 0 whiffs. 45-game soak clean
with all four chapters firing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:55:35 +02:00
wuwiandClaude Opus 5 ffac9b5512 Make every weapon hurt the boss, drop his HP bar, un-cap the director
Steps 1-3 of the late-game depth plan, after Oliver's 50+ game playtest.

"Sometimes the racket zombie doesn't seem harmable" was real, not a feeling.
Three of the five rack slots did nothing at all to him and said nothing about
it: TRIPLE side balls skipped him, GUN and horde pellets skipped him, fire
couldn't ignite him. BOMB meanwhile deleted him outright ignoring his hp. All
of it now routes through one wound() with a TUNE.dmg* table, so damage is
proportional to the body and a future 20 hp boss needs no new code. Fractional
damage banks on zb.chip so whole points only come off hp — the wound skins
switch on hp thresholds. His contact radius goes to 0.7 m, because part of
"unharmable" was just missing him. Melee still skips him and that's correct:
he's on the far side of the net and melee can't span it.

The HP bar is gone. It was the only floating UI on a body, it was on screen
for nearly the whole run, and nobody knew what it meant — while the wound
skins already say the same thing in the language every other multi-hit enemy
uses. A one-per-run modal explains what he is instead. Skins and bleed anchors
now read a fraction of hpMax, since his hp scales with the run.

And the plateau: every difficulty dial bottomed out by about game 12, so games
12-50 were arithmetically identical. Body cap, spawn gap, shamble speed,
variant mix, boss hp and horde size all keep climbing now, calibrated to track
the old curve through game 12 rather than replace it. One deliberate exception:
shamble speed was time-based and maxed during game 2, so the early game is
genuinely gentler now — zSpeedPerGame if that reads flabby.

HANDOFF carries the full spec for steps 4 and 5 (ranged attacks with player
damage, then chapter bosses) including the decisions we settled, so the next
session can pick it up cold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:30:27 +02:00
wuwiandClaude Opus 5 79fb43f60d Stop the dead for the changeover instead of letting the player wander
Oliver vetoed the free movement between points on sight: he never walks
freely during play, so being able to only between points reads as the game
breaking, not as a fix. Reverted to 'rally' only, and the rule is written into
the settled-feel list so it doesn't get re-invented.

The frozen changeover is answered his other way instead — the dead wait too.
`held` in updateZombies stops their advance, their steering and the eat check
for the breather; they still bleed and burn, they just stand there, because
the umpire called time and it is still a tennis match. The path where this
actually bit: losing a duel scheduled on a changeover beat spawns five fresh
shamblers and THEN starts five seconds you have no agency in.

The stuck pose was never about movement — 'run' is the one pose expirePose
doesn't expire, and runT stops advancing outside the block, so a beat that
began mid-stride welded him to a single frame. Reset it and it's fixed for
every phase.

And he could walk off the screen in ordinary play, not just during the beat I
had added: P_BOUNDS.x is 6.5 m, portrait shows about ±4 m at the baseline.
The xVis clamp already existed but only applied during auto-fire; it now
always applies, floored at the sideline so every legal ball stays catchable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:07:38 +02:00
wuwiandClaude Opus 5 e00886f57e Let the player walk between points, and fix seven retest gripes
All eight issues from Oliver's 35-game retest.

The freeze (1 and 6 — one root cause): the movement block, pose update
included, lived inside `if (G.phase === 'rally')`, so every 'over' beat and
every pretoss stranded the player mid-animation while the zombies walked on.
It now also runs whenever splatter is in any phase but 'toss' — 'toss' is the
aim, and the thumb is busy. Tennis keeps its old gating.

The rest:
- the mop boy is waved off the sudden-death duel and barred for its duration
- triple-serve clones inherit the serve's topspin gravity, so the wings stop
  floating while the centre ball dives
- "Time." belongs to the changeover's own countdown, not to whatever beat
  happened to expire while the breather ran
- the tutorial illustration is a finger pressing and pulling back now, not a
  curved arrow that read as a scroll
- aiming holds the pretoss stance; the smash plays when the shot fires
- GAME N sits on the scoreboard permanently

HANDOFF records all eight as closed with what the fix was, plus two ways a
soak rig lies to you and why the CPU-return benchmark can't be reproduced
from the number alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:33:37 +02:00
wuwiandClaude Opus 5 21b9dd53fa Thumb-scale draw meter, looser aim magnet, a bomb worth the tin
- The draw meter is a fixed bar across the bottom, ~90% of the width and 10px
  tall, with the kill notch on it. It was a 26x4 stub floating on the apron by
  the player's feet, which is nothing under a thumb at arm's length. The serve
  hint is suppressed while pulling since they share the slot. pullFull 95 -> 130:
  at 95 every pull maxed out by accident, so the meter never meant anything.
- The aim magnet took the nearest body ALONG the lane, so a close shambler a
  little off-axis stole shots clearly aimed past it. It now picks the best
  ALIGNED body, with TUNE.aimTie of slack in which a nearer one still wins — a
  straight-ahead lane still eats the nearest body first, which is the settled
  feel. aimTie: 1 restores the old behaviour exactly, for A/B.
- Bomb radius 2.5 -> 4.2 (2 kills -> 5 in a scattered ten-wave), a 120-particle
  eruption, a ring of thrown stains and a full-size pile. The blinding white
  frame is now a brief warm bloom — it was hiding the very gore it announced.
- Rack tiles 22 -> 30 tall, and the touch band is padded above and runs to the
  screen edge below. At 22 logical px they were ~30 CSS px on a phone, well under
  the ~44 a thumb expects; now ~62.

26-game soak clean with heavy bomb use, arrays stay bounded. Serve returns
136/150 = 90.7% +/-2.4%, 30 human-tapped points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:51:24 +03:00
wuwiandClaude Opus 5 55ddf5932e Fix the serve deadlock, give the mop boy a job, align net and umpire
The freeze first: G.mgT only counted down inside `G.phase === 'rally'`, so
anything that moved the phase mid-burst — a scheduled duel calling setupPoint —
stranded it above zero forever. mgT > 0 is one of keyPressed's early returns, so
no tap could serve again, and the yellow shape over the player's head was
drawMgRacket's muzzle flash still being drawn. Fixed at the root (the clock ticks
in every phase, only the pellets are gated on rally) and semantically: starting a
duel cancels any burst, because the duel is tennis.

The mop boy now does an actual round: walk to the biggest nearby stain, stand on
it and push the mop while the cycle animates in place, scrub it out, move to the
next, then leave. Gore fades ~3.5x faster while he is stopped and working than
when walking past, so the stain dies under him instead of fading as he drifts by.
boyMaxJobs caps the round so a kill streak can't employ him forever.

Applause: the clip is 4.16s and always plays in full, but every cheer() passed a
shorter beat, so the stand sat down while it was still audible. Floored at the
clip length. New, much bolder clap frame — the re-roll came back brighter and
bluer, which would have flashed the whole stand, so it is colour-matched to frame
one per channel on mean and sd before quantising.

Net and umpire were both wrong since forever: netW was the singles-stick width
(10.06) against a court drawn to the doubles lines, so the net stopped 0.46m
short each side; the chair's near legs stood 0.45m inside the sideline. Both now
sit exactly on the doubles line.

26-game soak clean, no stalls. Serve returns 134/150 = 89.3% +/-2.5% with the
wider net (the 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 18:09:18 +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 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