"the colored zombies should also explode like they did before and make the
normal splash sound on destruction.. no bleeps please.. can we make the color
as a color circle the walk on? the sign they're holding is not nice. also
remove the ball slicing in this mode.. this leads to unwanted hits".
The silent deaths were my bug: the hook in killZombie did drillKill(zb) and
then RETURNED, and everything below that return is the death — decal, pile,
36-particle burst, splat, headshot crack. It resolves after the splice and
without returning now. Both halves are load-bearing and each was a bug in
turn: splice first or a freshly drawn combination names the corpse; don't
return, or the body simply vanishes.
The colour is a disc on the court the body walks on, drawn before the sprite.
The bib was a flat quad pinned to a walking sprite — UI stuck onto a
character, covering the art it sat on. On the ground it takes the court's own
perspective, measures 12.4px across at the spawn line against a chest patch's
few, and hides none of the sprite. A hard dark rim plus a bright inner line
is what keeps it from reading as another gore decal.
No slicing in the drill: a cut ball leaves the corpse and strikes whatever it
finds next, which in a mode where the wrong body is the failure is the game
choosing your target for you, badly.
And the mode has no sound of its own now, deliberately. The umpire reading
the list was vetoed, the arpeggio that replaced him was vetoed. blip() is
deleted. A kill bursts and splats like every other kill in this game; the
strip carries the list.
28 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"make the game that it is actually winable meaning that all required colors
are visible on the far side before when a color combination is shown.. for
now no colors that are not in the current combination but occasional zombies
which don't have a color and are converging to the player trying to eat him".
Two rules define the file now. Every colour on court is IN the current
combination — no coloured decoys, so the strip can never name a body that is
not there. And a combination is only shown once its bodies are all on the far
side, which is free because the batch is BORN there: it is a property of the
construction rather than a check that could be wrong.
The pressure moved to the eaters: plain unbibbed shamblers on their own
clock, deliberately not flagged `drill` so they keep the gore mode's homing
and converge on you while the bibbed batch walks its lanes. Killing one is
never a fault; letting one reach you is the ordinary caught. The puzzle plays
fair and the danger arrives separately, which is a better trade than a puzzle
that cheats.
The batch is the round: kill the last of it and the next walks in on the same
frame with its combination already called.
One real bug, caught by the new test: killZombie resolved the drill BEFORE
splicing the body, so a combination drawn on the spot could name the corpse —
a round asking for a colour that is not on court, i.e. exactly the unwinnable
state this pass exists to prevent. Splice first, resolve second.
28 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"what are the dice dots for?" -> "drop them", and "the dodge swipe should not
reset to the middle when firing".
The pips were colourblind redundancy so the mode did not rest on hue alone,
but they were noise to everyone who can separate the colours and they shrank
to nothing on a body at distance — exactly where the channel had to work.
Gone from the bib and the chip. Recorded in PROJECT.md as a known gap rather
than pretended away: if the mode ships, the answer is bibs that differ by
shape at sprite scale, not dots.
The dodge one was never a drill bug. The auto-assist recovered toward HOME.P
including its x, so firing walked you back to the centre line: measured, a
step out to 1.94 m was dragged to 0.77 m over one ball's flight — most of a
dodge, handed back for free, which is the welded-in-place complaint the
sidestep exists to answer arriving slowly instead of all at once. Splatter
recovers in DEPTH ONLY now and holds whatever lane you stepped to. Tennis
keeps the full recovery: recovering to centre is the real sport's footwork
and there is no sidestep there to protect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Oliver: "daniel announcing is maximum annoying" / "don't let the rush in
position just let them coming from the back in steady speed.. try to space
them not too close" / "when one combination is done just let the new one in..
in a continuous stream that increases in speed and difficulty" / "the rounds
can stay, they should be continuous".
The round survives as the unit. Every seam around it is deleted: no walk-on
to a mark, no announcement beat, no between-rounds pause, no deal. Bodies
walk in from the far baseline forever and a round is drawn from whoever is on
court — finish one and the next is drawn in the same frame, off the same
rolling population. The bodies you were ignoring a second ago are the ones
being called now.
- Daniel is gone from this mode. Each colour has a pitch; a round announces
itself as a four-note figure in half a second (blip()). drillTones 0 mutes
it. The recordings stay in assets.
- PLACEMENTS is deleted with the marks — nothing rushes anywhere and nothing
stops, so there are no shapes to place.
- A DRILL BODY HOLDS ITS LANE. updateZombies homes bodies onto the player's x
past the net, so they arrived spread and bunched into a knot right in front
of you; spawn spacing cannot survive that. Their lane is the puzzle here.
- One ramp on TIME SURVIVED drives speed, spawn gap and round length. Speed
is stamped per body at spawn, so the ramp reads as newcomers outpacing the
ones already walking, not the court accelerating under you.
- The empty-court watchdog is gone and nothing replaced it: with a stream
there is no state to be stuck in. One line covers every case.
27 tests, rewritten around the stream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"the drill mode immediately goes into some horrible audio loop" — on a phone,
never on desktop, and that is the tell. Three of the four defects can only
happen on a phone, and every one of them is a rule voice() had followed for
months that my new function quietly broke.
- A SUSPENDED AudioContext HAS A FROZEN CLOCK. Scheduling four clips at
currentTime +0/+0.9/+1.7/+2.4 while the context is suspended puts all of
them in the past by the time it starts, so they fire on one instant through
a 1.3s reverb tail. Resume first, re-read the clock after the decode.
- A second call layered instead of replacing. voiceSeq SET voiceUntil and
never checked it. It stops the outstanding list now.
- ...and cancelling a list has to give back the time it booked, or the
replacement queues behind a list that will never play. Measured at 6.6s of
silence before the call the player is waiting on.
- The amplifier was in drill.js: the empty-court net redealt on the spot,
making the repair rate the frame rate — and every deal calls the umpire. It
goes through the ordinary between-rounds beat now.
Verified by tagging each decoded buffer with its source file and counting
start() calls: one drill_call, three colours, spaced, zero overlapping voice
lines. The earlier "clean" reading counted fetches, which proves nothing —
VOICE[] caches, so a repeat play never fetches again.
27 tests; the new one empties the court every frame for six seconds and
asserts at most one umpire call per beat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Oliver played it: "the auto aim just mean no skills involved at all. this
should just go away.. there is no penalty at the moment.. make some
variations of the position on the court and the color".
The first two notes are one finding. With the shot locked to the called body
it was unmissable, so the fault could never fire — there was no penalty
because there was no way to earn one. Both snaps are deleted. What replaced
them is one line: aimTie is 0 during a drill. That is not auto-aim, it is
removing the guess — the ordinary magnet lets a nearer body inside the same
cone steal a shot aimed past it, which is right for a crowd you are mowing
down and exactly wrong where hitting the wrong body IS the failure. An
unaimed tap now lands on grass.
And the variation:
- six colours (purple, white) with the court dealt three to five of them, so
which colours turn up and how many decoys there are are live questions. At
four out of four every round dealt every colour and only the order moved.
- PLACEMENTS: fan, cluster, column, stagger, wing — weighted, mirrored per
round, lanes shuffled against the bib order. column stacks bodies in one
lane, and the ball hits the first thing it touches, so feet matter.
- pips are dice faces now, shared by bib and HUD chip. A row of five dots has
to be counted; a die is recognised.
Measured over 30 deals: all five shapes, counts even, all six colours, and
the first target screened by another body in ~10% of rounds. Verified end to
end in the browser that FAULT and DOUBLE FAULT both fire.
26 tests, including a named regression for the auto-aim staying gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The late game is repetitive because the optimal move never changes: kill the
nearest thing, now. This is the first swing at a mode where it does — a
coaching drill where the umpire CALLS a colour sequence aloud while bibbed
bodies take their marks, and you must put them down in that order while they
walk at you. Uncalled colours are decoys you have to ignore. Two faults is a
double fault and the round is theirs.
Back room only (DRILL TEST, or #drill): this is a feel-test for one question —
tense, or just noisy? — so it keeps its own round count and touches neither
tennis scoring nor the run.
- voiceSeq: voice() drops a line that would start while the umpire is still
talking, which is fatal for a list. Decode first, queue at accumulated
offsets, and the anti-overlap rule is satisfied instead of fought.
- Four colours with PIPS as well as hue — red/green alone makes the mode
unplayable for ~8% of men.
- drawBib is a ponytail-marked overlay that knowingly breaks the sprite rule;
it is the thing to delete if the mode survives.
- BOTH target pickers snap (flickTarget and zombieTarget). Snapping only the
slingshot left fifteen tap-serves in a row scoring nothing.
- Contact costs the point, not the run — permadeath stays in the gore mode.
- startDrill disarms the ordinary schedule; updateDrill redeals and logs an
INVARIANT if anything empties its court.
Eight new tests (24 total) pin the ownership contract both ways, the call
beat, the win, the double fault, the un-winnable-round guard and the death.
PROJECT.md carries the four open questions for the playtest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gun, beacon, flamer and rocket fell silent the moment they cleared the court:
firing and steering were both gated on phase 'rally', and every beat raised by the
kill that empties the court (a point, a won game, the mop-up) parks the phase in
'over'. A burst is its own control mode for its whole len, like a horde — its own
clock is the only thing that ends it. Not fixed by forcing the phase back to rally:
a parked 'over' beat that never expires is how pendingHorde gets stranded.
The countdown moved off the tile's bottom edge (the rounded frame, and the bottom of
the screen) into the tile: the armed slot's well drains as a gold wash, clipped to
its own rounded path, under the icon and labels.
16 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THEY KEEP COMING is a 2.4s 'over' beat again (warnHorde in beats.js) instead of a
flash raised on the same frame the wave lands, and the chapter boss walks on behind
an overlay — his name in red over ENTERED THE COURT — held for 3s by the new
self-dismissing modal clock (m.hold) rather than announced on a routine-set card.
startHorde returns a bool: if the court has been taken when the warning expires the
pendingHorde latch is re-armed rather than the set losing the horde it paid for. The
warning ticks on its own clock, not on the 'over' beat, so a beat that moves the
phase underneath it can't strand it (the mgT lock, again).
Three tests, 15 total.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE LEGS. Measured as lower-third silhouette difference between the two stride
frames: the groundskeeper, whose walk has always been right, is 0.80. The seller
was 0.017 — the same legs twice, which is why he hopped along on one of them.
Both sheets regenerated with the stride as the entire subject (one foot planted
ahead, the other trailing, and they SWAP): seller 0.42, champion 0.67.
Three things the champion's re-rolls proved, all of them the same lesson —
THE REFERENCE IS WHAT IT COPIES. Anchored on his own one-legged frame it drew
that pose again. Given a working walk from another boss alongside him "for the
legs only" it blended the two and returned a slim side-on walker, then a
pot-bellied man in cream. One character in the reference, one character out —
and pick the keeper by EYE, because the metric happily crowned the side view.
THE CHARGE. It rushed, then withdrew a fraction of what it covered, so it ended
the fight parked on top of you: dodge or die every two seconds with no window to
hit it, and nothing but a hitscan laser quick enough to use the window there
was — Oliver, "they come too close, they seem unbeatable". A rush now RETREATS
to its line afterwards, and to keep it from being toothless the other way (from
the band it ran out of clock 8 m short) the rush is TIMED rather than paced: it
covers whatever ground the retreat left in bossChargeT, so every charge arrives
and every charge has the same readable run-up. Measured over 30 s: five rushes,
each one reaching you, 13% of the fight in your face instead of all of it.
And the back room moved off the whole splash onto an invisible box in the TOP-
LEFT CORNER. Five taps in the middle of the screen — which is where your thumb
goes on the way to PLAY MATCH — now do nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
- card: padding 20, smaller icon (46x44 box), band up to 88
- RESUMING IN: Bangers over the net tape, no panel — the countdown
belongs to the court the pause is holding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- opaque canvas: the compositor stops alpha-blending 2.5Mpx at 60Hz
- settled modals and the death summary idle at 20-24 fps; play untouched
- TUNE.lowPower caps RES at 3 (-44% pixels/frame), flippable live
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
- 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>
- x_hand.png generated to Oliver's reference pose (index up, curled
knuckles, thumb) — drawGhostHand draws the art, vector stays as fallback
- fingertip anchor measured off the PNG, not guessed
- RESUMING panel top no longer floats above the glyphs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- one definite band for cards, flash and the countdown — never on the court
- the card heading down a size; RESUMING IN finally on a readable panel
- the gesture-teaching strings are gone, the ghost hand is the teacher
- ghost hand v2: bigger, bigger type, and knuckles that make it a hand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
The cold-start complaints — late fonts, zombies missing from the stats
screen — were one header. server.py sent no-cache, must-revalidate for
everything, so a fresh launch revalidated ~230 sprites and three fonts
before the game had a full set of frames. Assets under /assets now cache
for a year when the URL carries ?v= (changing the file changes the URL,
which is what SPRV is for) and a day when it does not; the fonts get a
?v= too. HTML and JS stay no-cache, so a deploy is still visible.
Two client-side halves of the same report: a dropped sprite request used
to be permanent for the session — the sheet stayed undefined and the
zombie drew as some other design — so each one gets one retry. And the
splash now holds its buttons back behind a loading bar until the last
sheet has landed and both faces are ready, instead of letting you open
the stats screen while half of it is still in flight.
The bar is driven by setInterval, not rAF: rAF does not fire in a hidden
tab, and an app that loads while backgrounded would come back to a frozen
bar and a menu with no buttons on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Ranked on KILLS IN A SINGLE RUN. Games survived is the same information at a
tenth the resolution — most players land between 5 and 20, so that board is a
wall of ties broken by who got there first. Games ride along as the tiebreak
and as a column.
The image is python:3.12-alpine running server.py now, not nginx. The board
needs one process that can write, and everything the nginx conf did here — the
no-cache headers and the manifest MIME type — is a few lines of stdlib. /data
must be a persistent volume or every deploy resets the ladder to empty.
A name belongs to whoever took it first: the client mints a random id once and
the row remembers it, so a stranger cannot overwrite your score. The id never
leaves the server. That is the whole security model and it is enough.
The prompt to sign the board appears on exactly one occasion — a run that beat
your own best kill count, on the tap that leaves the death screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The transition turns your opponent into a zombie with variant 'c', and the
roster table had no row for it — so the stats screen fell back to the raw key
and the shambler sprite, and the list appeared to hold two shamblers. He is
THE OPPONENT now, and he is not the racket zombie, who keeps coming back.
Type across the menus was sized on a desktop and read tiny on a real phone:
tabs, opponent names, counts and labels all go up a step, and the roster
sprites go from 34x46 to 48x62.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The translucent panel fill was invisible on the list screens, which sit on
flat ink, and wrong on the splash: the court's white lines ran straight
through STATS and RANKINGS and the buttons stopped reading as buttons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The court art had three faults: a hard-edged pale panel sitting on the near
half, mown stripes that stopped and restarted where the court began, and the
blood pool stranded behind the baseline where a portrait phone cropped it off.
Redrawn with the stripes running unbroken the full depth and the stain out on
the court where it can be seen.
The picture also read as zoomed in, which was not the art but the crop: at 4:6
`cover` was throwing away a third of the width on a phone. The sky is extended
to 1:2 instead, so the whole arena survives the crop.
Menu palette rotated off olive towards a cool desaturated blue-green with much
quieter borders, and the boxed BEST RUN / WORLD RANK bar is gone from the
splash — it sat on the best part of the art to say what the two buttons under
it already said.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Four things Oliver caught on the phone.
The screen title is the ANNOUNCEMENT face now. A menu heading is a thing that
shouts, the same register the canvas gives SET and CHAPTER — it was wearing the
HUD font, which is the voice for scores and key hints. Bangers carries its own
weight, so it takes size and air instead of Silkscreen's letter-spacing, plus
the padding under it he asked for.
Her toss and smash were shrinking the whole woman. The slice anchors frame 0 to
224 px, and frame 0 was the toss — whose arm is straight up, so the BODY got
squeezed to make room for it. This is the trap already written down for the
raised racket; it just moved to a different sheet. Re-sliced the retained sheet
at the men's toss height (250) instead, so toss/smash/pull come out 250/293/191
against his 250/320/213 and her body matches her own idle again. No re-roll.
The pre-serve ball did not follow the MIRROR. drawChar flips the whole sprite
when a server faces left, so dodging during the bounce swapped his hands over
and left the ball hanging out by the racket — one `* sv.facing` on the offset.
It was wrong in the men's world the whole time, it just took a second body
standing next to it before anyone saw it.
And she reaches further out and higher than he does, so BOUNCE_IDLE's pair is
overridable per tour now (0.46/0.78 against his 0.36/0.68). Measured off the
drawn hand by eye, which is the only way those two numbers have ever been set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The court screen picks WHO as well as WHERE now: two tiles above the court
rows, men's or women's singles, remembered like the court is. No new stage —
three rows and a pair of buttons already fill a phone, so the tiles go side by
side and the tap count from splash to play does not change.
Everything downstream is one line: drawChar already built every frame name off
a single prefix, so SK() appends '_f' when the women's tour is picked and falls
straight through to the men's frame for anything without a twin. The horde, the
officials and the ball boys are shared — they are staff, not the draw — but the
turned CPU is not: a male corpse opposite a female player reads as a bug.
Her frames came back as a different woman in every batch — blonde in one,
auburn in the next, greyscale line art in a third — because each was anchored on
the MAN whose pose it was. "newart" repaints the character and "state" is welded
to the corpse palette, so neither could say "the same living woman, a new pose".
That mode now exists, and every batch anchors on her own settled frame.
The death is drawn to frame 3 and recoloured after it. The generator will not
draw a dead woman in any wording worth using; frames 4 and 5 survive that
because by then she is flat and then a heap of cloth, so they are the men's
drawing with the blue shorts painted into her white (see assets/_die_f.py). The
collapse still falls 224/159/117/103/43 straight through the handoff.
Her first bounce frames reached to the shoulder, which is a catching pose, not
patting a ball — redrawn low rather than bending BOUNCE_IDLE to fit a wrong
drawing, so both tours share one apex and the ball meets her palm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
The cone is fully opaque now: solid colour, deep blue at the edges and near-white
down the core, dimming as it runs away. Depth is carried by BRIGHTNESS instead of
by alpha, so it still reads as receding without anything showing through.
That settles an argument the translucent version could not win. Its opacity and
how well you could tell above-the-plane from below-the-plane were the same dial
pulling opposite ways: readable enough to show the split and it veiled the whole
cone, subtle enough to look right and there was nothing to see. Solid, a body
above the plane is drawn on top of it and a body below is hidden behind it. The
trade is that you cannot see the crowd through it, which is the thing to judge on
the phone.
The `alpha` parameter went with it rather than being left in the signature lying
about what it does.
laserScene is gone. T.pause(true) stays -- it freezes the world but keeps drawing,
which is worth having whenever a still is what you need to look at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T.laserScene() (or #laser on the URL) drops into splatter with six bodies of
different types at known depths, the laser armed, a pull held, and the world
paused.
T.pause(true) freezes the WORLD but keeps drawing, which is the whole point: a
value changed in devtools shows on the next frame while the scene stands still.
The old recipe halted rAF, which freezes the picture too and is no use for
judging how something looks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>