Score-is-the-body was a picture: the opponent lost an arm and swung exactly as
far, lost its head and missed exactly as often. Two dials now read the same
racketStage the SKINS are picked with, so what you watch it lose is what it
loses — deliberately small (6% reach, 17.5% whiff per stage), texture under the
ladder rather than a second difficulty curve, with a test on that ceiling. An
unhurt body still returns exactly 1 and 1, and the whole thing is gated on
G.match: the gore duel's racket zombie is untouched.
And the placement flick, the mode's one act of skill, was taught nowhere on
touch — the placement ring is a receipt, not a lesson, and you only get one if
you already knew to swipe. The ghost hand teaches it now, in the same register
as PULL DOWN and TAP AS IT ARRIVES, riding the failure it answers: a run of
returns that left with no aim on them. The tap lesson still wins when both
qualify — placement is the wrong thing to teach someone who cannot reach the
ball.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FLICK, again. "i can barely reach the extremes of the court." It could
reach 3.3 m against a 4.115 m line — safe, and too safe. TUNE.flickWidth is
3.95 now (96% of the way to the corner, 17 cm to spare) with flickReach 0.42,
which keeps the guarantee intact: reach x timing + scatter stays inside the
line at EVERY quality, worst case 4.06. 20% more reach, still impossible to
send out by aiming.
A FORWARD FLICK IS A SMASH. Up-screen is the far court, so flicking into it
asked for depth and nothing else — a direction with no consequence behind it.
It buys pace now (smashBoost, scaled by timing). Deliberately NOT distance:
solveShot still lands it on the same spot, so the shot arrives flatter and
sooner rather than longer. A smash you can hit out on a whim is just the old
bug wearing a hat.
THE FALL HOLDS. "the opponent should fall and then lie there for a quick
moment, the player stands up too quick." fall3 goes 0.30 -> 0.62. A corpse
that touches the ground and is instantly upright reads as a stumble.
THE CARD. Ten opponents, six of them NEW bodies off the one-sheet pipeline
(shambler, ball boy, steward, runner, groundskeeper, line judge), with the
club pro's own body and the brute woven in as 7 and 8. It is a ladder of
STAFF: the further you get, the further up the club the thing across the net
used to be. Narrative and difficulty are the same list read twice, and a test
walks all ten — whiff must fall every rung, names must fit the board's 8-char
slot, speed may zigzag by character but never regress.
Steeper per rung (speedPer .08->.11, pacePer .035->.05, flubKeep .82->.76):
"in general this is too easy now". With aimed returns no longer sailing out,
the player stopped handing over free points and the ladder had to earn them
back. The opening got SOFTER at the same time — 0.72 where the old first
opponent was 0.85.
THE UMPIRE WAS MUTED IN THIS MODE and nobody noticed. aside() refused during
any interlude; the match runs on a permanent one. So the single mode that is
literally a tennis match he is sitting over was the one mode he never spoke
in. Same bug shape as sendBoy, same fix. Six dry lines via say -v Daniel: two
on taking a limb, two on your blood, one when there is nearly none left, one
on felling the thing. asideEvery still gates him.
...and sendBoy had the identical bug, so the mop boy now comes out between
games to the FAR court, where one body has been bleeding through four wound
stages. Between games only, never mid-point.
The line judge took two rolls: the first cut to 6 figures in its last row and
_zsheet.py refused it rather than mis-slicing. That refusal is the feature.
59 tests green. Verified in the browser: SHAMBLER on court in its own body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FLICK. "when returning a ball with a spin it seems too sensitive, 8/10
times the ball lands out." Arithmetic, not feel. The singles line is 4.115 m;
an aimed shot targeted a flat ax*3.3 and THEN added err() on top — up to
+-2.4 m at quality 0 — so a flicked return landed as wide as 5.7 m, and the
only clamp in the path sat at +-5, outside the court. An UNAIMED return
targets 0 + the same scatter and is therefore always in: aiming was the thing
losing the point, which is backwards for the mode's one act of skill.
TUNE.flickReach scales the aim width by the same quality the scatter shrinks
with, so the two can never sum past the line — worst case 1.49+2.4 = 3.89, and
a perfectly timed flick still gets the full 3.3 with nothing left to scatter.
The test asserts that sum across the whole quality range rather than sampling
rallies. Not named `aimReach`: that key already exists further down TUNE (the
slingshot's 22 m aim ray) and the duplicate silently won until a test caught it.
The placement ring carries `q` now and is the flick's receipt — tight and hot
when your timing earned the full width, wide and dim when you barely got a
racket on it. Without it the rule is invisible: the shot just goes less wide.
THE FALL (handoff item 4). A point that takes a limb knocks the body down and
it drags itself up short of it. The three collapse frames wear the stage it
STILL WAS and the rise wears the stage it has BECOME — that order is the whole
beat, and it is what turns a popping wound stage into something the point did.
Own clock beside the horde's warning, for the reason this file keeps relearning
(a beat that moves the phase under a latch strands it); the 'over' beat it sets
outlives the fall so it cannot expire into setupPoint() mid-collapse. Only
fires when the stage actually moved — a quarter off a full pool is 0.75, so
point one has no limb to explain and a second of collapse for nothing is a beat
the player waits out.
THE OPPONENT SERVES (handoff item 7), and it was mostly switched off rather
than missing. setupPoint already placed a non-player server behind its own
baseline and armed aiServeT; update() already tossed and struck for O. Both
were gated on `G.mode === 'tennis'` and the match runs in splatter under a
permanent interlude — one condition. Genuinely new: service alternates at each
game boundary (the match calls advancePoint directly and the flip lives in
tennis's pointTo, which it bypasses — the same shape as the G.serveNum bug),
the zombie takes O's mark so picture and ballistics agree on where it stands,
and the serve poses are named onto it because O is invisible here.
racketStage() moves to config: drawZombie and the fall must not answer the
same question twice, which is how the boss's skins and its blood drifted apart.
Docs: PROJECT.md gains THE ZOMBIE FAMILY PIPELINE — the measured API contract
(4K is a parameter and the default is 1K; n is 1 so "return both" is not a
thing; aspect is an enum so pad to it), why magenta stays despite green being
the documented key, and the two things _zsheet.py must do globally.
Verified in the browser: brute in the match, z_brfall2 -> z_brw1rise on a
wounding point, and bounce -> toss -> serve off the opponent's own baseline.
58 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Left over from the rename: the handoff still described opponent 2 as "rung N"
and pointed at matchRung, and three comments in gameplay.js still called the
opponent curve a ladder. One word per thing, everywhere now — OPPONENTS, WOUND
STAGES, a sprite FAMILY — with a note in the handoff saying which word replaced
what, so the next reader is not left translating.
(The barrel weapons keep their own "rungs of the same ladder" line: that one is
about the WEAPON payout order and was never part of the confusion.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A day went into sprite consistency and most of it was spent solving the problem
in the wrong place. Written down so the next session starts from the answer
instead of rediscovering it.
The boundary everything else follows:
the generator, GIVEN REFERENCE IMAGES -> identity, pose, composition
the pipeline, DETERMINISTICALLY -> exact colour
the gates -> layout accidents only
With the proof, because it is the part that is hard to believe until measured:
the brute's serve sheet came back with his build, his garments and his DARK
TONES pixel-identical to the reference and his big pale shirt two shades
brighter. The model was looking straight at the character. Every call
re-renders and re-rendering re-lights. No prompt fixes that.
And the rule underneath all of it — A REFERENCE BEATS A DESCRIPTION, at every
level. The racket zombie's sheet was made from the character as an IMAGE plus
the poses as IMAGES and landed first time; the brute's three sheets each
re-derived the character from the same paragraph and produced three different
brutes. Corollary worth having: every corrective sentence in a prompt is
compensating for a reference you did not supply. When a prompt grows a third
"do NOT" clause, add an image instead.
Also written down: correct-then-check, calibrate-before-you-gate (with the
numbers every threshold was calibrated on), the five gates and what each one
actually measures, six traps that each shipped something silently before they
were caught, and three things still open — including that `changed` passed a
headless brute that still had its head, because it knows something moved and
not what.
The brute is PARKED: out of ZFAMS, 31 frames on disk, nine batches in the JSON,
one line away from live. Its healthy eleven are good; its wound stages are not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Slices 1, 2 and 5 struck through with what they actually became; 3, 4, 6 and 7
left standing, with a note on 3 that the wound-stage art now exists for it and
on 4 that the racket_* batches are its template. The parts of slice 2 that are
still open (shots[] and the PARRY — the cheapest depth in the mode) are called
out rather than buried under a DONE.
Added: the record shape table (which of g/s/dead each mode uses and why), the
classic-tennis mode, the second ending, and four new traps — the stats banked
flag that no-kill modes never clear, the magenta key that ate racket strings,
the read-only PIL image that made the fix look like a no-op, and the detail
bar that ratchets against what it is replacing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Temporary file, written because a session ran out of context mid-slice — fold
into PROJECT.md and delete when the work lands. Not in the Dockerfile COPY;
docs are not served.
Records the decision (A: blood-only, measure becomes opponents felled) and
the finding behind it: Oliver believed losing a match killed him, when he was
bleeding out during deuce wars. That misreading is itself the bug — model A
only works if the player can see they are bleeding, not losing.
Also carries the build order (summary+measure, named ladder with
personalities, wound-driven play, the fall animation, menu, flick teaching,
opponent serve games), the flight-recorder recipe that ended four rounds of
guesswork, the traps this mode already sprung, and the do-nots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>