Commit Graph
245 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 0620e97d6c Zombie tennis: one game per opponent, blood only
Triage failed the only test that matters — "i think the normal player does
not grasp this" — and the lesson across four drill designs is that every
invented rule needs teaching, while the rules the scene already implies need
none. So the new mode is the game's own oldest rule: tennis.

MATCH TEST in the back room (#match). One game per opponent, real 15/30/40
scoring. Built on top of the duel machinery: G.match runs a permanent
interlude, which is already what makes serving tennis-like, hides the rack,
quiets the spawn director and wakes racketAI. The one change is in
resolveInterlude — a settled point feeds matchPoint instead of ending
anything. About 90 lines for a whole mode, because the deepest system in the
repo was already built.

The score is the body: a quarter of its pool per point won, floored at 1, so
a love game walks the racket zombie through every wound stage and the game
point fells it. Its points draw your blood — hp--, eaten at zero, matchHeal 2
per game won is how you last. Regicide is inherited: kill the body mid-game
and the game banks by hand.

The timing is drawn, per Oliver's ask: an approach ring around the incoming
ball that kisses a fixed inner ring exactly at the sweet contact distance
(gold inside the band), and an apex band beside the toss spanning the same
0.12m keyPressed scores as perfect, with a tick riding the live ball. Both
cues use the mechanics' own numbers, so the picture cannot lie.

Verified by autopilot: 7 games in 90 simulated seconds, opponents 1..8, pools
4/6/8/10, wounds tracking points exactly.

36 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:19:37 +02:00
wuwiandClaude Opus 5 be191dba82 The marker stops lying, and the rule announces itself
"the marker flips all the time even during a ball being midair.. i am not
sure that a player would get the mechanic intuitively let alone without the
marker".

The flip had a specific cause and it was a design bug, not jitter. With lanes
held and speeds constant, arrival differences never change while bodies walk
— the queue only reorders on a kill or an arrival. The marker flipped because
it showed only q[0] while the rule accepts any near-tie within the slack:
every kill made it jump between tied bodies, so the marker was stricter than
the rule it was explaining. drillLegal() marks the whole slack set now,
bright head and dim ties, which is stable by construction. Measured: zero
head-changes across 12s of walking with no kills.

Learnability, two answers. The rule gets one modal, once per run, on the
mode's own calm court — CLEARANCE DRILL / DROP WHOEVER WOULD REACH YOU FIRST
/ FAST BEATS CLOSE / STREAKS PAY — because "most urgent first" is not
guessable from a chevron alone. And the markers are training wheels that take
themselves off: hidden at streak >= 8 (the streak is the evidence you can
read the court), back on any fault. The teacher steps in exactly when you
demonstrably need it.

31 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:44:58 +02:00
wuwiandClaude Opus 5 6a16e77480 Triage: always kill the body that would reach you first
The sequence drill tested nothing — the strip was always visible (no memory),
the batch was slow (no urgency), the aim was easy (no execution). A checklist
with gore, and Oliver said so: "not interesting enough to keep me playing".

Settled in discussion, he picked triage + streak. One rule: always kill the
body that would reach you first. Nothing is called, nothing shown — the court
is the puzzle. arrival = distance / speed down a held lane, and the roster's
speed spread means the answer is not the closest body: a runner at the far
baseline beats a roller at the service line. Court sense — and it trains the
exact target-priority read the gore mode rewards.

Wounds are free: only the kill is judged, so pre-softening a heavy while it
is third in line is legal and eventually necessary. The slack (0.9s) makes
near-ties legal both ways, or the rule is a coin flip. The streak is both
punishment and payout: x1..x5, a misread shows OUT OF TURN and resets it, and
nothing else happens — the queue keeps walking. Caught is the real setback.
Best score banks to localStorage; the world board can carry it if it ships.

drillMark draws a chevron over the queue head for playtest calibration. It
answers the exact question the mode asks, so it must be off in anything
shipped.

Deleted: the alphabet, the batch, the strip, the eaters. Shamblers are
first-class stream members with pts 20; a roller is 100.

30 tests, rewritten around arithmetic riggings (known speeds and distances)
instead of luck; 20 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:32:17 +02:00
wuwiandClaude Opus 5 57e96d629e The finishing shot is spent, and the wound skins come back
"one-shot is bad - way too easy - also we're losing all the nice gore
animation sprites" and "I noticed the last kill of a game can carry to the
spawn of a new zombie of the next round".

drillOneShot is off. The second half of that note is the part worth
remembering: a 1 hp body never passes through a wound stage, so every skin in
ZWOUND — arm off, ribs out, still walking — is skipped and the roster's best
art never renders at all. Kept as a switch, never a default.

The carry-over was a real unfairness, not a curiosity. A ball keeps pace
through a kill and travels UP-COURT, which is exactly where the next batch is
born — so the shot that finished your round flew on into bodies that did not
exist when you took it, and faulted you on a round you had not been shown
yet. The shot that ends a round is spent now, and drillCaught does the same
since it also refills the court up-court. Pierce within a batch is untouched:
that is honest occlusion you could have read, this was not.

Verified with real aimed serves through a whole round: round banks, ball goes
inactive, the next batch walks on with 0 faults and a clean sequence, and the
gang carries roster HP again (brute 5, ball boy 3).

33 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:43:26 +02:00
wuwiandClaude Opus 5 0f524a08c9 Two testing switches: one-shot bodies, and the eaters off
"please make the zombie killable with a single hit for testing" and "remove
the fake zombies for now".

drillOneShot drops every body on a drill court to 1 hp, applied at spawn to
the batch and the eaters alike. Worth knowing what it removes: the roster's
HP is what makes the called ORDER matter — a roller is normally seven serves,
so being told to take it third means starting on it first. At one hit that
planning layer is gone and the sequence is pure recall. Flagged in PROJECT.md
so it gets decided on feel rather than left on by drift.

drillEaterMax 3 -> 0 switches the plain shamblers off. The mechanism is
untouched — spawnEater, the wide entry away from the gang, the never-a-fault
rule — so one number brings them back.

The eater test now asserts whichever contract the knob describes (at 0:
nothing but the puzzle ever appears; above 0: they arrive, capped, always
plain shamblers) instead of being deleted and rotting. And the never-a-fault
rule gets its own test with a hand-placed body, so the rule the eaters depend
on stays covered while they are switched off.

31 tests, 15 seeds clean. Verified live: brute/steward/roller all at 1 hp
where the roster says 5/5/7, zero strays over 40s, one aimed serve puts down
the roller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:37:01 +02:00
wuwiandClaude Opus 5 5b2f1d55ce The roster is the alphabet: the sequence is the zombies themselves
Oliver's insight, and it retires a whole subsystem: "we don't need colors at
all! we already have different zombie types.. so the sequence is the zombies
themselves".

The bib and then the ground disc were both inventions bolted on to give a
body an identity it already had. A brute does not look like a runner, the
game is built on reading those silhouettes at speed, and the strip can show
the body's own walk frame instead of a swatch — no legend, no mapping, no new
art ever. It also deletes the accessibility problem outright rather than
deferring it, because a silhouette is not a hue. DRILL_COLS, drawColourRing,
drawBib, PIP_FACES, blip(), drillRingR and drillTones are all gone.

Two rules keep a type unambiguous: the plain shambler is never in the
alphabet (it is what the eaters are, so "not the puzzle" needs no marking),
and one of each type is on court at a time.

The new failure mode this created is instructive. Types move at different
speeds and drill bodies hold their lane, so one in a lane you are not in
walks the whole court and off the back untouched — stranding the sequence
asking for a body that no longer existed, i.e. the un-winnable state the mode
exists to prevent, reintroduced by a change that looked cosmetic. A batch
body crossing your baseline counts as caught now ("GOT PAST YOU"), which is
also just better design, and the speed spread is narrowed to 1.35..0.70 so a
batch stays coherent. Their real difference is HP and that is untouched: a
runner is one shot, a roller is seven.

Verified: 14400 frames of simulation, zero frames where the sequence named a
body that was not on court.

29 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:24:58 +02:00
wuwiandClaude Opus 5 3379e83efc Clear the court of everything that is neither the puzzle nor the threat
"remove the mob boy - make the normal zombies rarer and not so close to the
color gang".

sendBoy refuses during a drill, guarded at the function rather than at the
call sites — same shape as its existing G.interlude guard, so every present
and future caller inherits it. He is one more body crossing the lanes you are
reading colours down, and unlike everything else out there he is neither the
puzzle nor the threat. Measured: 0 boy-frames in 90 seconds. Gore no longer
gets mopped in this mode; the existing 40/30 caps are what bound it.

The eaters are rarer — every 12s falling to 5s, capped at 3 on court. At the
old rate they were a second crowd rather than an interruption, and a threat
you break off to deal with only works if breaking off is occasional. 8
arrivals in 90s where the old numbers gave ~13.

And they enter wide of the colour gang: best-of-9 lane sampling against the
bibbed bodies, the same trick the batch uses. Narrowest clearance measured
across 8 spawns was 0.91 m. They still converge on you afterwards — that is
what they are for — this only decides where they walk in, so they stop
screening the bodies you are trying to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:08:58 +02:00
wuwiandClaude Opus 5 d775ac40ec The colour goes on the court, and the dead go off like everyone else
"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>
2026-08-11 19:57:56 +02:00
wuwiandClaude Opus 5 abd6d4f789 Every colour you are asked for is already on the far side
"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>
2026-08-11 19:29:04 +02:00
wuwiandClaude Opus 5 586e8d4e4b Drop the dots, and stop handing back the dodge
"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>
2026-08-11 19:17:20 +02:00
wuwiandClaude Opus 5 1d49c4343b The rounds stay, the seams go: one continuous stream
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>
2026-08-11 19:06:48 +02:00
wuwiandClaude Opus 5 435a470ec7 The umpire is one man with one microphone, and voiceSeq forgot it
"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>
2026-08-11 18:29:11 +02:00
wuwiandClaude Opus 5 ef8a413031 You aim it yourself now, and no two rounds are dealt alike
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>
2026-08-11 18:16:25 +02:00
wuwiandClaude Opus 5 22df96cc1d The umpire reads a list, and the bibs come out to meet it
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>
2026-08-11 18:01:33 +02:00
wuwiandClaude Opus 5 c659b6882f A burst fires for its whole window, and the countdown is the tile itself
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>
2026-08-11 16:49:02 +02:00
wuwiandClaude Opus 5 5ea84f8511 Nothing arrives unannounced: the horde gets its breather, the boss gets a name
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>
2026-08-11 16:38:29 +02:00
wuwiandClaude Fable 5 1f636530b9 The code moves into src/: index, server and the test harness keep the root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:45:23 +02:00
wuwiandClaude Fable 5 c122745390 One file owns the court: beats.js, PROJECT.md (né HANDOFF), and tests gate the build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:23:27 +02:00
wuwiandClaude Fable 5 05930f0877 One mode owns the court: the duel can no longer start inside a horde, and the game has tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:55:37 +02:00
wuwiandClaude Fable 5 4569981365 Dust settles on every court: tennis ticks particles too, EPIC! cheers, the machine explains itself
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:30:54 +02:00
wuwiandClaude Fable 5 4e80bd8cb2 Cards say one thing: trimmed copy, and a title that fits the band
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:14:32 +02:00
wuwiandClaude Fable 5 9fab26eec9 The court keeps no score: the landing ring is gone
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:05:32 +02:00
wuwiandClaude Fable 5 8307a05f79 The wind-up is a drawn bow, not a crouch: her pull re-rolled off his arch
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:55:15 +02:00
wuwiandClaude Fable 5 70642a0a3d Her serve is his serve: the women's pull and smash redrawn off the men's poses
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:51:44 +02:00
wuwiandClaude Opus 5 d57433ed2b A charge that passes, two strides that actually stride, a door in the corner
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>
2026-08-09 16:56:05 +02:00
wuwiandClaude Opus 5 1bb528f7e0 The back room: five taps on the logo, not three on the scoreboard
The weapon-test cheat sat on the SCOREBOARD, where a single tap is the miss-log
overlay — so the thing you tap to read a log was also the thing that opened a
sandbox mid-run, three taps in, by accident. That tap does one job again.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:08:57 +02:00
wuwiandClaude Opus 5 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 8658a93dc7 White countdown; centred plain cards, ragged-left only beside art
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:41:47 +02:00
wuwiandClaude Fable 5 dc35b802dc Tuned live, together: the card breathes, the countdown floats the net
- 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>
2026-08-08 21:32:33 +02:00
wuwiandClaude Fable 5 f69fc12742 The card reads as one thing: padded icon, ragged-left type
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:26:36 +02:00
wuwiandClaude Fable 5 54d5d0949d Low power by default — the phone judged it invisible
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:19:43 +02:00
wuwiandClaude Fable 5 3b6a82c97d The battery pass: paint less, wake less
- 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>
2026-08-08 21:16:15 +02:00
wuwiandClaude Fable 5 911035af00 The card lies flat, and a popup owns its screen
- icon LEFT of the text: every card is 62px at a fixed top, fully over
  the crowd — the stacked layout hung its lower third over live court,
  and the art draws bigger this way, not smaller
- nothing draws or ticks under a modal: a card raised in the same beat
  used to burn out dimmed beneath it — it holds and plays after

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 20:50:46 +02:00
wuwiandClaude Fable 5 9476f354c2 A real hand for the ghost, and the countdown hugs its frame
- 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>
2026-08-08 20:28:24 +02:00
wuwiandClaude Fable 5 aaae452338 Every message knows its place now: over the crowd
- 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>
2026-08-08 20:10:31 +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 Opus 5 c5a9746bc1 Load the game once, not on every launch
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>
2026-08-05 07:47:27 +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 3291246425 Put a real world board behind the RANKINGS button
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>
2026-08-04 17:03:17 +02:00
wuwiandClaude Opus 5 1fccc11b1d Name the opponent, and stop making phones squint
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>
2026-08-04 16:39:51 +02:00
wuwiandClaude Opus 5 a5a5661534 Make the menu buttons opaque
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>
2026-08-04 16:30:13 +02:00
wuwiandClaude Opus 5 c5edc94b73 Redraw the backdrop and cool the menus down
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>
2026-08-04 16:26:04 +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 0ca43bf68f Let the ball follow the mirror, and stop the serve shrinking her
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>
2026-08-04 12:37:59 +02:00
wuwiandClaude Opus 5 14a1e5d261 Let the tour be a choice, and give her a body of her own
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>
2026-08-04 12:22:21 +02:00
wuwiandClaude Opus 5 c75bd5b9ba Pay out every won duel, and stop the popup eating the announcement
Two separate faults, both of which read as "the duel gave me nothing".

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:15:14 +02:00