Commit Graph
100 Commits
Author SHA1 Message Date
wuwiandClaude Fable 5 dbfd085493 The draw screen retires: the league cup rolls inside the run, ENTER THE DRAW moves onto the setup button
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:46:34 +02:00
wuwiandClaude Fable 5 812bea3234 Plaque polish: the goal line finds its gold (--warm never existed in CSS), the chevron centres, the name grows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:41:52 +02:00
wuwiandClaude Fable 5 3aaf2b9baf The plaque becomes a letterhead: crest left, the level line writ large beside it, the bar full-width
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:39:42 +02:00
wuwiandClaude Fable 5 1a39c9fdcb The plaque floats mid-court on glass, the blood pool breathes, and the 94vw card stops scrolling sideways
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:36:48 +02:00
wuwiandClaude Fable 5 689550d2d5 The climb: the arcade runs the whole earned ladder, the tempo follows the territory, the court is yours
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:24:43 +02:00
wuwiandClaude Fable 5 041827c4dc The league has its court: the tournament setup shows the home venue, not a choice
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:12:20 +02:00
wuwiandClaude Fable 5 d4780de76d The setup grows a league face: badge, felled tally, the band's roster — and a bigger ribbon
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:10:31 +02:00
wuwiandClaude Fable 5 5c34632513 Drop the stale duplicate ribbon rule the last edit left behind
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:05:50 +02:00
wuwiandClaude Fable 5 54e5db8ea2 The ribbon keeps its shadow (drop-shadow outlives the mask) and fades later
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:04:42 +02:00
wuwiandClaude Fable 5 77e6418e8e The way-up ribbon, final form: horizontal over the border, dissolving through a Bayer pixel fade
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:03:26 +02:00
wuwiandClaude Fable 5 3ba3d9dac0 The way-up ribbon, third cut: the wrap bug — one measured line, snug band, honest overhang
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:00:51 +02:00
wuwiandClaude Fable 5 6ea06ee42b The way-up ribbon hangs over the border — sized to stay out of the neighbouring poster
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 15:57:10 +02:00
wuwiandClaude Fable 5 fa6db7f77b The leagues, the polish round: the way-up ribbon, upright chevrons, the breathing badges, the card counts up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 15:51:59 +02:00
wuwiandClaude Fable 5 87ac3294bc The leagues, the plaque round: the meta speaks — the player plaque, the goal line, the way-up tag, the centred ceremony
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 15:30:24 +02:00
wuwiandClaude Fable 5 787877bce6 The leagues, phase 4: the duel learns, tin tiers by league, level buffs, the board wears the badge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 12:53:12 +02:00
wuwiandClaude Fable 5 76d88030e5 The leagues, phase 3: the character card, the honours collection, the skill trail
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 12:48:34 +02:00
wuwiandClaude Fable 5 16bf5b9fed The leagues, phase 2: five bands, pace tiers, locked courts, the members' ground, the promotion ceremony
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 12:43:23 +02:00
wuwiandClaude Fable 5 9133c0d351 The leagues, phase 1: the profile clock ticks — xp, levels, the identity screen, the server mirror
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 12:22:33 +02:00
wuwiandClaude Fable 5 bc10216d0b The leagues: the meta-progression plan of record — five leagues, XP by play, tin-tier gore coupling
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 11:55:27 +02:00
wuwiandClaude Fable 5 98b01fea5a The trail reads as motion, never as residue: the ghosts fade with the sprint
The lingering doubles were a real bug, not a taste problem: racketAI's
!incoming return kept the last chase's vx/vz on the body, so the speed
ghosts trailed a zombie that had already stopped. The velocity dies with
the chase now (tested). The look is halved with it: ghost alphas
0.22/0.10 -> 0.11/0.05, trailK 0.05 -> 0.035 — a hint of speed in the
corner of the eye, never a second body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 16:36:19 +02:00
wuwiandClaude Fable 5 4ea9af5442 The surprise is an opening, not a free point: THE READ turned down a notch
Oliver, same day: 'die Gegner sind jetzt etwas zuu einfach.' The habit takes
four same-lane shots to learn (was three) and the wrong-foot costs about a
third less on every dial: surpRead 0.85->0.6, surpSlow 0.45->0.3, surpFlub
0.35->0.22. The whiff-window test pin rides down with it (0.25->0.15); the
slower-than-the-coach pin still holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 16:34:38 +02:00
wuwiandClaude Fable 5 43784c5b33 The staff learn your habits, the walk-on waits for your tap, and the club opens a practice court
Four answers to one phone report:
- THE READ: repeat a lane and the opponent anticipates it; break the habit
  and it is wrong-footed for that ball — scaled by rung, so surprise is the
  weapon that opens the ladder past the HEAD COACH (TUNE block THE READ).
- The opponent walk-on is a real modal now (head, punchline, tap to dismiss)
  in arcade, tournament and training — readable without a clock.
- THE TRAINING COURT: third card under ZOMBIE TENNIS, pick any rung, one
  game at a time, the felled body walks straight back on, nothing banks,
  bleeding out is a popup instead of a death. mode_train.png rolled in the
  banner style.
- MOVEMENT READS: sprinting feet kick per-court dust and fast bodies leave
  a two-ghost speed streak (trailMin gates both; feel verdict pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 16:08:18 +02:00
wuwiandClaude Opus 5 a42dc6fc81 The lob is out: a gesture the opponent's position redefines is unpredictable
Oliver, on the build: "all good but the lob. please remove it - this is
totally unpredictable."

Reverted in full — oppAtNet, farBody, TUNE.lobArm, TUNE.lobApex,
SHOT_DEPTH.lob, the branch in doHit and its two tests. The forward flick is
the smash it always was. Everything else from that commit stays: the pace
feedback (streak, crack, camera kick) and the death match's blood alignment.

The reason is worth more than the code, and PROJECT.md carries it as a rule:
a gesture whose MEANING depends on where the opponent is standing is
unpredictable however well the shot is tuned. It is the same objection
input.js already states about the slingshot — the direction you moved is the
thing you asked for, no invisible modes — and the written comparison let
option (c) past it on the argument that context cannot mis-fire. It can: you
do not track where the far body is standing while a ball is coming at you, so
one thumb movement produced two shots for reasons you were not watching. The
LOB word at contact explained the surprise; it did not prevent it.

If it comes back it needs a gesture of its own. The ballistics are solved and
are in the history at 4f4534e.

111 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:13:56 +02:00
wuwiandClaude Opus 5 4f4534e272 The lob is the flick you already have, and a game stops being a death
Three from Oliver.

THE LOB. He picked option (c) from the written comparison — "no additional
gesture to learn" — and corrected the premise: the zombie does come to the
net (racketAI chases to the far service line, the classic CPU to z 9.5). So
the forward flick keeps its one meaning, "put it deep behind him", and the
game picks the trajectory that actually does that: a smash while he is back,
over his head once he is in. oppAtNet() is the whole switch, and it reads the
BODY, not O — in the match O is the invisible tennis body and only moves
between points, so it would have answered "on his mark" all rally.

lobArm 7.0 m is measured, not taste: a lob peaking at lobApex passes a man 7 m
from the net at almost exactly CONTACT.maxY, so the shot arms exactly as far
out as it can still clear him. The pace is derived from the height, so the same
arc comes off the baseline and off mid-court, and SHOT_DEPTH.lob is deeper than
deep — at the ordinary deep target the ball passed 2.05 m over him at the edge
of the band, which is inside his reach.

PACE, SEEN FROM THE STRIKER. "the opponent sees the ball really fast - the
player barely sees the speed increase." Measured: the ball is the same ball on
both phones, the camera is not — it compresses your own ball into the distance
and expands theirs as it arrives. Pace untouched; three channels added on the
striker's side: the ghost streak now reads pace (3-8 ghosts, was 3 at every
speed), the crack is pitched by it, and a fast ball kicks the camera. The
serve's own ladder is 49.2 plain / 50.8-52.4 hard, because serveArc's loft
margin takes 10% of the +9 back — widening that is his call, and PROJECT.md
records the knob and the risk.

THE DEATH MATCH. "each player has 8 blood points but the player explodes for
the winner already at 0:40." Two clocks: a game is four points, a life is
eight. A won game is only a game now (flash, serve, heal); the burst moved to
the death itself, on the dead message, where both phones agree. The summary
waits 1.4 s for the gore, because update() freezes the world on G.done and
would have hung a still frame of an explosion behind the trophy screen. And
bleedO tracks THEIR blood off the pos stream instead of our points-this-game,
which reset every game and ignored the heal.

4 new tests, 113 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:58:48 +02:00
wuwiandClaude Opus 5 6e07dc74bb The receiver waits behind his line, and a missed serve buys a second one
Oliver, three notes; two of them are here.

"when the opponent serves the player does not stand at the baseline - but
that's how it should be." setupPoint's receiving marks sat over a metre
INSIDE the court on both sides (22.5 near, 1.2 far) — where you stand to
attack a second serve, not where you wait for a first one. They mirror the
server's depth now, in the one line every serving mode shares.

"when serving results in an out there should be a second service... the
umpire should announce 2nd service and the player must get another chance."
True in tennis mode since forever and absent everywhere else: the duel, the
match and the death match had no service rule at all, so a serve long or
into the net was judged as an ordinary ground stroke — a point, and in the
match a point costs blood. The out branch of onBounce's interlude block
routes a serve to fault() instead, so the second serve, the HUD line, the
umpire and the double fault all come for free.

Three things it dragged out with it:
 - fault()'s double-fault branch called pointTo — TENNIS scoring — which in
   an interlude banks a number and skips the mode that owns the court. It
   goes through resolveInterlude there now. Nothing had ever reached that
   line from a duel or a match, because those modes could not fault.
 - startDuel never reset G.serveNum: harmless while a duel could not fault,
   the next duel's death loop the moment it can.
 - the match puts the body back on its receiving mark for the second serve
   (placeReceiver, split out of matchReady, which resets the serve number).

"Fault. Second service." is one voiceSeq — a second voice() call would be
dropped, one man one microphone. New recording via the usual say/afconvert.

The service BOX is still not enforced in the match, deliberately: this is
strictly more forgiving than what shipped. The lob gesture is OPEN 14.

4 new tests, 110 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 09:40:48 +02:00
wuwiandClaude Opus 5 3d7e66449a A new run rewinds the clock; the camera flashes did not rewind with it
Oliver: "I start a new zombie tennis match. when playing and scoring no
fotograph flashes appear, but at a random place in the crowd some frozen
midflash circle is visible. after a few points the flashes unfreeze."

Both halves are the same cause. resetState() puts G.idleT back to 0 for every
new run, while the flash list and its "next at" live in render.js's own closure
and survive it. So the new run inherited a flashNext from the old one — no
photographs at all until the clock climbed back past it — and any flash still in
flight was left with a t0 in its own future, which makes its fade
1 - (now - t0)/life permanently greater than 1: a shutter stuck at full
brightness in the stand until something else cleared it.

tickFlashes now notices the clock moving backwards and clears and re-arms. One
guard, in the only place that owns the state, so it covers any future mode that
rewinds the clock as well. Verified on the canvas: after a rewind there is no
permanently-bright pixel in the stand, and the photographers resume within
seconds instead of staying dark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:04:33 +02:00
wuwiandClaude Opus 5 ca3e72d0a9 The white flashing was the top fade: the live raisers were not repainting it
Oliver: "what is this white flashing while applause in the upper right corner of
the crowd". With the photographers switched off entirely it was still there, so
it was never them — it was mine, from this morning. The top fade is baked into
the cached stand, and drawRaisers was painting a spectator's box straight over
it with only the dim applied. Every time that spectator stood up his box lost
the fade and lit up; every time he sat down it came back. Measured on the canvas:
a 146-luminance jump, twice a second, in exactly the two places raisers sit near
the top of the picture.

Both stand treatments now come from one function each and are applied in both
places. The gradient is built in user space, so filling a raiser's sub-rect with
it picks up the right alpha for free. Probed again after the fix: zero bright
pops during applause and during the wave, against the four hot spots before.

While chasing it, two real detector faults turned up and are fixed too: a blob
may not be a pale block (a re-rolled cream shirt is "new material" by the
stand-turned-person test but nothing rises), and it may not cover something
already bright (a genuine arm drawn over a white shirt deletes a white block,
which flashes just as hard). 94 raisers survive both rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:55:13 +02:00
wuwiandClaude Opus 5 93c4a21be5 The applause moves down out from under the scoreboard
Oliver: "in the upper parts of the crowd there seems to be a lot more action
going on, but I can barely see it because the hud is covering it." Measured, 31
of 134 raisers were being drawn under the board or the blood panel — a quarter
of every cheer spent where nobody can see it.

The cause is structural: a raise is only detectable where there is empty stand
above somebody's head, and the sheet only offers that above the top row of each
seating block. One of those rows is at the very top of the picture, which is
where the scoreboard lives.

So both ends are fixed. The two arms-up rolls now ask for the standers to be in
the lower two thirds only, and the pass drops any raiser whose box centre falls
under the board or the blood panel. 92 of 101 raisers now sit below the board's
bottom edge, against 103 of 134 before — and because "how many join in" is a
fraction of the table, pruning the invisible ones makes every cheer denser where
it counts. Measured churn during a won game: 2-5.5% of the mid and lower stand
changing every 90 ms, against 0.1-1% under the HUD.

The re-roll also came back with 41 spectators owning both raised poses, up from
28, so more of them clap rather than just going up and down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:40:22 +02:00
wuwiandClaude Opus 5 b34a9159a7 The HUD lifts off the crowd, and the frame is dark at both ends
A drop shadow under the scoreboard, the game/sets panel, the BLOOD panel and
every rack tile — and the same numbers in CSS on the quit chip, because half the
overlay lifting off the backdrop while the other half sits flat on it is worse
than neither. Canvas shadow offsets are device pixels and are not scaled by the
transform, so hudShadow multiplies by RES; without that a 3px shadow is three
quarters of a logical pixel on a 4x phone.

And the top of the frame now fades out of black the way the bottom already fades
into it (TUNE.standTop) — Oliver: "the bottom of the screen falls off into a
black tone.. please make the top come out of a black tone". The stand comes up
out of the dark instead of being cut off at the top edge, which also gives the
scoreboard something to sit on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:21:12 +02:00
wuwiandClaude Opus 5 d0c9ea4f0d PROJECT: the two stand dims do not stack, and centre court is graded off a lifted target
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:12:33 +02:00
wuwiandClaude Opus 5 eab0a4e837 The two dims stopped stacking, and centre court's crowd is graded off a lifted target
Oliver: "it is too dim to see anything on the centre court and the dark court."
Both were the same mistake — standBack was ADDED to a court's own dim instead of
being the default it falls back to. Centre court's crowd sheet is a night-lit
navy that is dark to begin with, and the night court dims the hard-court sheet
on top of that, so the pair of them went to mud.

standDim is now court.dim ?? TUNE.standBack, plus the rot. Centre court carries
a 0.06 of its own, the night court comes down 0.62 -> 0.5 (it no longer has the
base added to it), and clay and hard keep the 0.22 that was approved.

Centre court's sheet is also graded off a LIFTED target now — the old crowd it
matches sat at a mean luminance of 39 against clay's 96, dark enough to throw
the new roll's detail away before any dim ran. A 0.72 gamma on the target brings
the sheet to 60 and the faces come back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:12:07 +02:00
wuwiandClaude Opus 5 0250e9bd20 The crowd sheet becomes an animation sheet: three frames, and the stand sits back
Oliver: "when clapping I barely see any movement at all now.. also all of the
art could be colored a little more dim as it is perspective wise farther in the
background and should not steal too much attention" — and then, "can't we make
an animation sprite sheet of the new crowd?", which is the answer to the first
half of the first one.

A third roll (_crowdroll.py mid) takes the arms-up frame as its reference and
asks for the same standing people with their hands brought together in front of
their faces. It drifts by 53% of its pixels and that does not matter: the same
filter keeps only what was stand and became person. 100 arm blobs in pose 1, 74
in pose 2, 134 spectators once merged, 28 of them owning BOTH poses — those 28
cycle between two raised poses, which is a clap; the rest alternate with the
seated frame, which is arms going up and down.

The point is the cycle, not the pose. A raiser is ~8x6 logical px on a phone and
one that appears and then holds for a second is not something the eye catches;
arms moving at 3.5 Hz is. Measured: 2-3% of the stand band now changes every
90 ms during a won game, against near zero before.

The sheets are one file per court now — three frames stacked, graded as a single
image so they cannot drift apart in colour — and x_crowd2/_c2/_h2 are deleted.

TUNE.standBack 0.22 dims the crowd and the boards on every court before the
night court's own dim and the splatter rot, in the one function both the cached
backdrop and the live raisers already read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:59:02 +02:00
wuwiandClaude Opus 5 1ad2898530 Track the two crowd rolls as _sheet_crowd3/3b — _refcheck is scratch and they are the only copy of the art
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:31:29 +02:00
wuwiandClaude Opus 5 5f53a686c8 The crowd is drawn again, in detail — and the stand now moves one spectator at a time
Oliver on the first pass: "the new lighter areas read as rather sloppy artwork.
please regenerate the crowd art and make it more detailed. also the animation
also looks pretty artificial. the flashes from the fotographs are too rare and
the whole thing somehow does not translate well to the other courts. also when
the opponent makes a point there is an aww-sample (which has an applaus trail)
so the crowd should still clap but more subtle."

The paint pass is deleted — recolouring blobs of an old drawing is not drawing,
and the reason it looked necessary was a wrong conclusion: the roll scripts read
OPENROUTER_API_KEY from the repo's .env, so the art could be rolled all along.

_crowdroll.py rolls the crowd itself: individually drawn spectators with hats,
sunglasses, drinks, cameras, flags and banners, empty seats, stairways, and
three zombies sitting in the middle of it behaving like furniture. ONE roll now
serves all three courts through a per-channel histogram match onto each court's
old palette, so anything the art gains shows up on clay and at night too.

_crowdpass.py keeps only what was empty stand in the seated roll and became a
person in the clap roll — the raised arms, 1.2% of the sheet — so the clap frame
cannot drift by construction, and writes a box around each pair of arms into
src/crowd.js. render.js raises ONE SPECTATOR AT A TIME out of that table, each
on their own period: the 16-column strip swap moved a seventeenth of the crowd
as a block, which is what read as machinery. crowd.pop is now a head count, so
the same code covers a dozen people and the whole stand, and past 0.75 the wave
travels person by person.

aww() sets a small crowd.pop of its own — the sample is a groan that settles
into applause, and the stand has to match what you can hear. flashEvery 5.0 ->
1.9, because at a five-second mean over a stand this wide nobody ever saw one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:30:34 +02:00
wuwiandClaude Opus 5 74afd5988c The stand stops wobbling: a real clap frame, a wave, and three spectators who have already turned
The wobble was never the animation. x_crowd2 was a RE-ROLL of x_crowd, not a
pose of it — 13-16% of its pixels differed, over faces, torsos, seat backs and
the railing — so swapping 16 columns at 4.5 Hz re-hashed a strip of crowd twice
a second. assets/_crowdpass.py rebuilds the pair: the clap frame is now the
seated frame plus 36 genuine raises, each contained in ONE of render's columns,
so any column swap is seamless by construction and an empty column is invisible
instead of being a wobble. The three courts are one composition in three
palettes, so a raise found in one court's roll is transplanted into the other
two through a colour map learnt from the aligned seated pair.

The same tool paints the crowd, because at ~8 logical px a spectator detail can
only mean chroma and silhouette: 55 shirts and 44 heads of hair in colours the
court does not own, 28 caps, 4 flags above the heads, 8 emptied seats, 8 people
permanently standing — and 3 who are already turned, grey-green, mid-row, who
are also the only three who never applaud.

Movement, one draw path and three behaviours: applause scatters per column, a
WAVE crosses the stand for a moment big enough to deserve one (crowd.pop >=
0.75), and with nobody cheering ONE column stands for a third of every
TUNE.fidgetEvery, so the stand between points is no longer a photograph.

Paparazzi now scale with the moment: cheer() records the size of it (vol was
already exactly that at every call site) and queues shutters proportional to it,
drained at a fifth of the normal gap. pop is held at the loudest while an
applause is live, so a won rally landing inside a match-point ovation cannot
demote the stand to a polite clap — that rule has a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:58:26 +02:00
wuwiandClaude Opus 5 76ea1d0d79 Sound was dead in the app because status 0 is not a failure; and back is a swipe to the RIGHT
Oliver on the first shell build: *"I don't hear any of the games sounds.. daniel the umpire is
synthesized which is horrible and no music is playing on start"* — two independent causes.

- **Opaque fetch.** Capacitor serves the bundle through a custom WKURLSchemeHandler, and `fetch`
  reports those responses as `status: 0, ok: false` with the whole body present and decoding
  perfectly. All three loads in `audio.js` guarded with `if (!r.ok) throw`, so inside the app every
  recorded sound failed at the door: the umpire fell through to the synthesized `say()`, no bed
  started, no sample played. The synthesized sfx kept working, which made it read as a mixing bug
  rather than a loader one. Fixed once, in the shared `bytes()` helper every audio fetch now routes
  through — a status of 0 is the ABSENCE of an HTTP status, so trust it only when there is one and
  a real 404 still falls back. Any future `!r.ok` on a bundle-relative fetch is this same bug.
- **The ring switch.** WKWebView's WebAudio runs in the SoloAmbient session, which the hardware
  silent switch mutes outright — a silent game on a phone that lives on silent, with nothing on
  screen to explain it. AppDelegate now sets AVAudioSession `.playback`, the games category.

Confirmed in the shell: the bed reaches PLAYING, the context is `running` with no gesture at all
(autoplay really is allowed in WKWebView, as LATER predicted), and `say()` never fires. On the web,
a genuinely missing file still falls back — checked both.

And the swipe flips: **right goes back**, the way iOS's own pop gesture goes (*"that's the iOS
way"*). Left now does nothing. Noted in PROJECT.md that on a platform gesture the platform's
convention beats the literal wording of the brief, and that is worth saying up front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 17:18:47 +02:00
wuwiandClaude Opus 5 d71a26831a The game gets a shell: it buzzes in your hand, never turns sideways, and a swipe walks you back
A Capacitor iOS project (`npm run ios`), and the first three bullets of LATER item 2 built. The web
build is untouched and stays primary: every line here degrades to a no-op in a browser.

- HAPTICS ride the sound map, as the plan said they would: six call sites, all of them places an
  sfx already fires — your racket contact, the serve strike, `killZombie` (the one funnel every
  weapon routes through), `explode`, `hurtPlayer`, `killPlayer`, the weapon swap. The restraint
  rule lives in `haptics.js`, not at the call sites: a minimum gap per kind, so a bomb that kills
  twelve bodies is felt twice and the machine gun is a metronome rather than one long rumble.
  Pinned by a test. Untested on hardware — the simulator has no Taptic Engine.
- The plugin is reached through the INJECTED bridge, `Capacitor.nativePromise('Haptics', ...)`.
  `Capacitor.Plugins.Haptics` reads like the zero-dependency answer and is silently empty without
  @capacitor/core's registerPlugin — it would have fallen through to the web path inside the app
  and looked exactly like a phone with haptics switched off. No package JS ships.
- PORTRAIT is enforced where it can be: one orientation in Info.plist, so the app never rotates.
  That retires landscape as a surface rather than fixing it (OPEN 6). The web half is best-effort —
  `orientation` in both manifests and one guarded `screen.orientation.lock()`.
- SWIPE LEFT GOES BACK. The gesture finds the screen's one visible chevron and clicks it, so the
  shape of the wizard stays in the handlers that already own it. Not passive on purpose: a swipe
  inside a poster card also fires that card's click, and preventDefault is what stops the screen
  answering forward in the frame the chevron walked it back.
- The app has no server behind it, so `config.API` and one branch in `net.js` point the shell at the
  live host by name. Consequence: the app always talks to PROD, never staging.

`node test.js` green (105) over four seeds; builds, launches and reaches the native plugin on the
iOS 26.5 simulator; swipe-back walks the whole wizard in the browser pane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 17:00:36 +02:00
wuwiandClaude Opus 5 c5eb6846e3 The tour rides the wire: your friend plays in the body they picked
A tour is which body you play in, so it belongs in the look message next to the
kit, the skin and the name. Without it both phones drew the friend in whatever
tour THIS phone had picked, and a men's/women's pairing showed each player
someone their friend was not using.

`oTour()` is the one accessor — yours everywhere, theirs in netplay — and it
feeds the three things that must agree about a body: the sprite suffix, how the
art takes a colour (her whites wash, his blue dyes) and the palm the idle ball
bounces under. An older build sends no tour and falls back to ours, which is
what it did before.

The palm knobs were re-checked against the new front sheets rather than assumed
— the poses were copied, so the ball still lands under the drawn hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:11:13 +02:00
wuwiandClaude Opus 5 5f9170b7d7 The friend across the net wears your kit, drawn from the front
His colour landed on the shirt while yours lands on the shorts, because he was
drawn on the roster opponent's red sheets and dyed on an arc of his own. Red is
the one hue that art cannot hang a kit on: the men's shorts are neutral grey at
saturation 0.05, so no rotation ever reached them, and the women's red top sits
at her skin's and her hair's hue — a dark pick was painting her arms and her
ponytail, live, on the shipped build.

So he gets his own sheets. `assets/_frontroll.py` is `_famroll`'s trick pointed
at the tennis poses — image 1 is the ten `o_*` frames composed from disk, image 2
is the player from behind — and two calls drew all twenty. He rides the PLAYER's
arcs and the tour's own mode now: his shorts dye, her whites wash. `TINT.oKit` is
gone. The roster CPU keeps the red art; it is not a person.

The setup screen shows that front too — picking a face off the back of a head was
only ever what the art allowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:04:10 +02:00
wuwiandClaude Fable 5 4052ab1404 The relative lane is vetoed; an aimed ball never lands on the wrong side of you
'No that's way too difficult now - I am dying constantly of out. revert
it.' aimAnchor is gone — the lane is absolute again, every out invariant
back exactly as tuned. What survives is the answer to his follow-up: the
ball sometimes flew a LITTLE RIGHT out of a left flick, because a
mistimed left flick's target sits only slightly left of CENTRE — which
is right of a player standing in the left service box. The absolute
lane's blind spot, stated the other way round.

The aimed target is now clamped to the player's own x on the flicked
side, bounded at flickWidth so a body parked in the alley clamps to the
corner rather than conjuring an out. From the left box a left flick goes
at least down your own line, from centre it can no longer cross centre,
a perfect corner flick and the crosscourt sweep are untouched, and the
outward scatter tail keeps the whole tuned out-risk. Verified in his
scene: ten rushed weak left flicks land -2.0..-2.7 where the same case
used to target -0.7..-1.3 (a visibly rightward flight); the clean full
flick still lands -3.92.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 14:39:10 +02:00
wuwiandClaude Fable 5 7e5c3ab399 The flick aims relative to the player: a full sweep from where you stand, not from the court's centre
Third round of the flick report, and this was the core all along: 'when
the opponent and the player are both standing in the left service box it
is virtually impossible to flick the ball into the left out. no matter
how hard/good I flick.' The lane was absolute — a full flick asked for
flickWidth off the court's CENTRE — so the gesture saturated near a
sideline: from x -2 a perfect left flick moved the ball under 2 m and
could never pass the line, and the better the timing the firmer the cap,
because scatter was the only thing that could cross it. 'Left is
impossible, right works' was never left-vs-right: it was
toward-the-near-sideline vs across-the-court.

TUNE.aimAnchor: the anchor slides from the court centre to the player's
own x as the swipe commits (ch.x * |sway| * aimAnchor), so a flat tap is
exactly what it always was and a full flick is a full sweep from where
you stand — including past the line. Out-by-aim is a choice now, per
Oliver's explicit direction; the placement ring already turns red for
it. 0 restores the absolute lane, blendable live in the panel. Player
rally lanes only; the opponent's lanes and the centre-stated pOut
invariants are untouched. Verified in his exact scene: full left flick
from the left box lands -5 (the alley), half swipe -3.07, full right
+1.93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 14:25:24 +02:00
wuwiandClaude Fable 5 60127436a0 An aimed shot keeps its direction: the scatter can push it wider, never back past centre
Second round on the service-line flick report. The crossing-fired swing
alone did not move the feel, and Oliver's sharpened observation — the
flick only lands left when the opponent already stands left — pointed at
a magnet that does not exist: measured with the body teleported at
contact, the aim never tracks it. The real mechanism: at rushed quality
the symmetric scatter (aimError x (1-q)) outweighs the quality-shrunk
aim, so a mistimed left flick landed anywhere in a ~4 m blob around
'slightly left' — usually centre — and the eye anchors that on the
opponent. Its position was a proxy for preparation time all along.

TUNE.aimTrust: an outward scatter roll is untouched — the committed
swipe's chance of sailing keeps its exact tuned shape, flickReach stays
the only out dial — but an inward roll keeps only 0.3 of itself. Bad
timing costs width, never direction. Player lane shots only; the
opponent's symmetric scatter is its width mechanic. Verified at
worst-case q~0.08: 14/14 rushed left flicks landed left, 9/9 right
flicks right, where before it was a coin toss. The suite pins the
direction guarantee and the aimTrust/aimError/flickReach relationship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 14:03:57 +02:00
wuwiandClaude Fable 5 6f9dea4ffc The flick fires the moment it is a flick: the swing launches at the crossing, and the aim rides the thumb to contact
Oliver's service-line report: flicking the incoming ball LEFT was near
impossible mid-court at the service line while RIGHT worked, easing toward
the baseline. The lane math was symmetric to the centimetre — the defect
was latency. The swing waited for the RELEASE, so the whole gesture ran as
dead time on top of the windup: ~120 ms of lateness drops contact quality
1.0 -> 0.08, where flickReach halves the width and aimError drowns the
rest. A left swipe is a right thumb's slowest direction, the service line
leaves ~700-870 ms of budget (measured), and any swipe over FLICK_MS was
silently dead. Left + service line was the slowest gesture meeting the
shortest budget.

Now, in rally modes: crossing flickMin fires the swing (once per gesture,
rally phase only) and the vector stays LIVE — trySwing reads G.swipe at
contact, with the released flickAim carrying the aim only when the ball
arrives after the finger lifts. The FLICK_MS deadline goes with it where
the crossing can fire; it survives where it cannot (gore free-aim, the
serve strike). Serve gesture, duel, gore and keys untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 13:26:30 +02:00
wuwiandClaude Opus 5 532dd3b83e The shot list documents itself: all eight tins, five beats, ten bosses
The header block in tools/scenes.json is now the whole schema — every key with
its range and default, what each of the EIGHT weapons does (fire, laser and gun
were missing from both the docs and the validator), the five beats, the crowd
roster diversity walks in order, the bosses by chapter, and the traps worth
knowing: a serve tin is spent in about four seconds of autopilot fire, a burst
tin owns its own window, a horde locks the rack, a boss cannot start inside a
match, and `movement` fights the match's own auto-run legs.

Two scenes added from the campaign's own wish list: `epidemic` (the fire ball
lighting a whole wave off the body it hit) and `champion` (chapter ten, the
night court). Both verified on film.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:34:17 +02:00
wuwiandClaude Opus 5 472c86b36e The shot list becomes a config file, and the camera holds a phone
tools/scenes.json says WHAT to shoot; tools/clips.js only knows how. Every
knob Oliver asked for is a key: seconds, setting (gore/zombie), beat, court,
boss, weapons, movement, density, diversity, gore, games, screen — documented
in the file's own "_" block, validated in capture.py so a typo names itself
instead of coming back as fifteen seconds of empty court. Nine scenes ship in
the default list, across all four courts.

The frame is a PHONE now (393x852 -> 1080x2340), not 9:16: the portrait branch
derives its logical height from the canvas box, so a 9:16 take was never the
framing a player sees. The player has legs — held arrow keys through the real
axisX, so horde and flamer are no longer a statue with a flamethrower. The
crowd tops itself up where the game's own spawner puts it, diversity walks the
roster, and `gore` paints in the run's history a fifteen-second clip has no
time to earn. The pilot is picked by what the court is doing rather than by
config: tennis mode taps to swing, splatter draws the slingshot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:25:59 +02:00
wuwiandClaude Opus 5 491f3b97f1 The highlights shoot themselves: one command, seven vertical clips
`python3 tools/capture.py` fills promo/clips/ with 1080x1920 h264/aac takes of
the moments the campaign plan (item 4) already named — transition, horde,
flamer, mopboy, death on the gore side, boss and match on the zombie-tennis
side. capture.py serves the repo with tools/clips.js injected, opens a
throwaway-profile Chrome and converts each POSTed webm with ffmpeg; clips.js
stages every scene through the real doors on window.T and drives it with the
reachability soak's own mediocre autopilot. Nothing is faked for the camera.

The game gains four names on window.T (start, intoSplatter, startHorde,
sendBoy) and nothing else: the frame is pinned by the canvas box rather than
the window, RES 4 comes from lowPower 0 plus device-scale-factor 3, and the
audio is tapped by making BaseAudioContext's `destination` a gain node — so
audio.js needs no master bus for a tool that ships to nobody. tools/ and
promo/clips/ are ignored by git and docker alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:07:21 +02:00
wuwiandClaude Fable 5 296d34d8ca The shell's gifts get written down: portrait, haptics, and a thumb the OS stops stealing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 11:52:03 +02:00
wuwiandClaude Fable 5 7f85920de8 The rating note learns the new tier names
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 11:44:13 +02:00
wuwiandClaude Fable 5 7b395fcba4 The price tag comes off the door and moves inside: free download, one 2.99 unlock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 11:43:41 +02:00
wuwiandClaude Fable 5 f8201ce7a2 The poster holds still, names its price, and points at the stores
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 10:15:21 +02:00
wuwiandClaude Fable 5 26c6298aed Merge the promo page into main
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 10:09:58 +02:00
wuwiandClaude Fable 5 95b867a0fc The club gets a poster: a marketing page at /promo, drawn from the game's own sprites
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 10:09:24 +02:00
wuwiandClaude Opus 5 440db9dcc1 Merge the flick's strength, the freed legs and the quieter out into main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 07:59:09 +02:00
wuwiandClaude Opus 5 7ff8671a22 The aim cloud goes quiet, and a mistimed flick falls short of the line
Hidden behind TUNE.aimCloud rather than deleted: the reading it gives — how
much width the swipe is buying, before the ball leaves — is the only teacher
the gesture has, and the panel can flip it back live for the next look at it.

And "a little harder to hit the out" is a number: flickReach 0.62 -> 0.52,
which halves the worst-case rate from about 15% to about 7%, with the test
tightened so it cannot drift back. The shape is untouched — a perfect flick
still never sails, a badly timed full swipe still can — but a mistimed one now
lands nearer the middle than the line, which is the only half of that trade a
player who cannot see his own timing can do anything about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 07:58:47 +02:00
wuwiandClaude Opus 5 1b16b422df The aim is a mark on the grass, not a line drawn over the game
The live flick readout was a ring on a ray from the player's feet — a HUD
element laid across the court, and read as one: "pretty bare bones... the line
must go away, it is distracting". It is a soft cloud on the ground now, in the
register the dust and the blood already use: it sits where the ball is being
asked to land, grows with the length of the swipe, and warms from pale lime
through gold to red as that ask approaches the width no timing can keep in. The
threshold stopped being a flip, so too hard is something you watch coming.

Three things it took to make it read, kept in the comments so the next pass
does not re-learn them: it is sized in METRES off the projection, or it is an
eight-pixel smudge at the far corner where it actually lives; it is a stain and
not an additive glow, which turned the red end yellow over green grass; and the
ramp starts pale rather than green, because green on green showed nothing at
all across the whole safe half of the range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 07:53:09 +02:00
wuwiandClaude Opus 5 d5a069a90c The auto-run owns the legs in a rally, so the drag lets go of them
On touch, a drag in zombie tennis and classic still walked the player — and
that was not a harmless second option. A manual axis suppresses the assist, so
holding a drag traded a full-speed run to the intercept point for a 0.35x
shuffle aimed wherever the thumb went instead of where the ball will be.
Walking was strictly worse than standing still, which is most of "i tend to not
be able to reach the balls".

touchAxis is no longer written under rallyFeel. The drag keeps its one rally
meaning — released quickly it is the aimed swing — keys stay a deliberate
desktop override, and gore keeps its drag-walk, where the assist recovers depth
only and holding your own lane is the entire point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 07:09:19 +02:00
wuwiandClaude Opus 5 f3d6f71144 The length of the flick is the strength of the aim, and you can see it leave
The rally flick was read as three lanes: sideways past 0.38 of its own length
took the whole corner, a hair under it took dead centre, and nothing lived in
between — "sometimes applied very strong, sometimes it barely does anything",
with no way to tell which one you had asked for. swayOf reads the swipe's LENGTH
as a continuous strength, exactly as the serve already did, and doHit spends
that instead of the lane vote. Keys keep the full lane; the opponent's aim has
no length and is untouched.

That is also what hands the OUT back: going out was never a choice, because
every aimed shot took the full width and then the scatter on top. Below about
0.7 of a full swipe — the line minus aimError, over the width timing can earn —
the worst mistiming in the game cannot sail it now, and above it the old risk
curve is exactly as tested.

And the aim is drawn while the thumb is still moving: the placement ring is a
receipt that arrives with the ball already gone, so the same ring is shown live
with a ray from the player's feet, cream while the ask is safe and hot once the
sway crosses that derived threshold. The receipt goes red when the shot really
is out, so the two rings together are the lesson.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 06:56:44 +02:00
wuwiandClaude Opus 5 99b2dec3bf The match handoff folds into the only planning document, and goes
HANDOFF-MATCH.md was always temporary — written when a session ran out of
context mid-slice, and carrying its own instruction to fold into PROJECT.md and
be deleted once the slice was done. The slice is done, so it is.

What crossed over is the contract, not the diary: what the mode is, the
blood-only scoring and the misreading that forced the blood readout, the four
ladder dials, the card of ten, the wound dials, the fall's frame contract, the
flick arithmetic and its lesson, the umpire and the boy, the opponent's serve
games, classic as its own mode, the record shape, the traps, and the do-nots
(now including the scratched body-shot thrower). The reasoning behind the
numbers already lived in the drill/match session logs and was not copied twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:54:38 +02:00
wuwiandClaude Opus 5 553b765846 A gesture is read in a pause, not in a point
The flick lesson rode the incoming ball, over the player's shoulder — the one
moment he has no attention to spare for reading anything. It moves into the
room this game already keeps for teaching gestures: the serve stance, standing
still, mimed on the apron by the very demo the slingshot lesson plays. Same
clock, same hand, same words slot — one teacher, three lessons, and nothing new
drawn over a live rally.

The words carry both halves now, SWIPE AS YOU HIT TO AIM: WHEN alone teaches
nothing, and WHAT FOR alone gets tried at the wrong moment and read as a
gesture that does not work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:45:23 +02:00
wuwiandClaude Opus 5 67cb0b74d7 The flick lesson belongs to the rally modes, and gore's opener is not one
The gate said `G.mode === 'tennis' || G.match`, and mode 'tennis' is two
different things: classic, and the single prologue game a gore run plays before
the court turns. Placement is not the skill that prologue is asking for.
rallyFeel() already draws exactly the line wanted — classic and zombie tennis,
the two modes that ARE rallies — so the hint rides that instead of a mode
string. Verified both ways on a phone-sized preview: the lesson beside the
player in zombie tennis, and gone under gore-opener conditions with everything
else identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:39:17 +02:00
wuwiandClaude Opus 5 c613b6ceba The body plays the wounds it wears, and the flick finally has a teacher
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>
2026-08-17 08:21:18 +02:00
wuwiandClaude Opus 5 0c90337f2d Merge the drill lane fix and the rate-gate sample size into staging
index.html conflicted only on the cache-buster: staging was at 276 and the
branch at 275, so the merge takes 277 — a merge that reuses either number
leaves phones on the old build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:16:47 +02:00
wuwiandClaude Opus 5 131c0fa114 A rate gate needs its sample size derived from the rate
The slid-serve test asked for 6 of 10 sliced serves returned. Measured over 200
trials a seed across eight seeds, the receiver's real rate is 0.85..0.92 — so
the assertion was sound and the SAMPLE was too small: at p ~= 0.88, 10 trials
land at or below 5 with probability ~0.004, one seed in 230, and SEED=80 was it.

n is now 40 at the same 60% bar, which puts the threshold five standard
deviations below the true rate while still failing instantly if the receiver
regresses toward the 0-in-24 the test was born from. It is free: a returned
serve breaks the frame loop early, and the suite still runs in ~0.11 s.

Green on seeds 1..400.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:15:39 +02:00
wuwiandClaude Opus 5 8cef111277 The groundskeeper stops wearing a hat nobody drew him
His family sheet was rolled against a description that disagreed with every picture
of him — "stained work overalls and a shapeless hat" against a green club blazer and
braces — so the 31 frames averaged the two and came out a third character. The card
portrait and the boss on court have been different men ever since.

He now anchors on his own throw frame redrawn at the grain the other bosses ship at
(density 0.34 against the legacy 0.21, tracked as _ref_gk_hi so the roll stays
reproducible), and the description names what the art actually shows. The hopper
basket is called out as gone: a prop at head height is the one thing a 31-figure
sheet cannot hold.

Rolled through _famroll, cut with _zsheet. Density 0.26 -> 0.36 across the family.
First roll came back with a lilac panel over the top rows and was thrown away — the
cutter keys the corner, so that panel would have shipped as opaque background.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:42:40 +02:00
wuwiandClaude Opus 5 932673ff5d The card's punchline wraps, and the bulk answers to the name the stats gave him
Six head icons recut by hand; the new cuts are square, and a 1:1 portrait beside a
long punchline pushed the head against the left edge while the line itself ran off
both. The sub lines now wrap to the same room the title already shrank to fit, and
the plate grows with them — one line each still lands on 30/44, so every card that
fit is pixel-identical.

THE BULK is THE BRUTE: the gore mode's stats screen has called that body THE BRUTE
since it dragged a pole, and one character may not carry two names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:29:13 +02:00
wuwiandClaude Opus 5 bdc3f23f92 A lane is free when its body is gone, not when it is far away
The drill spawner measured its best-of-9 lane spacing only against bodies still
in the back half (drillNearZ 9 m). On a court where lanes are HELD for life and
speed is stamped at spawn, a body 12 m down still owns its lane — handing it to
a faster arrival books an overtake for ten seconds later, mid-court, as two
sprites walking through each other. That is what the seed-sensitive test failure
(SEED=1234) was reporting: the assertion was right, the stream was not.

Spacing now measures against every live drill body; drillNearZ is deleted (no
other reader). Still best-of-9, no retry loop, so the stream cannot stall.

60 seeds x 150 s of drill: before, 7 seeds overlapped with narrowest clearance
0.00 m; after, zero overlap frames, narrowest 0.46 m (the test asks > 0.3).
Suite green on 20260811/1234/777/99/424242/8 and on 299 of seeds 1..300; the
straggler (SEED=80, the slid-serve return rate) is unrelated and logged as open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:45:31 +02:00
wuwiandClaude Opus 5 86b8275f5d The receiver stands where the serve can actually go
The trick: hard serve, slight slide, and the first ten opponents died where
they stood. Measured headless — that serve was returned 0 times out of 24 at
EVERY rung. Three things were wrong and all three were the same thing: he
waited on the tennis mark 1.3 m off the centre line (covering the T, conceding
the whole corner), he took no first step, and he chased the ball's LIVE
position, which during a serve is still up by the server's baseline.

Now: he waits on the bisector of the serves he has to cover, takes a
returner's first step (serve-scoped — the rally is untouched), and runs at the
bounce instead of at the ball. The slid serve comes back from rung one; a
flush serve into the very corner of the box still aces the bottom of the
ladder, which is the reward placement is supposed to earn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:29:17 +02:00
wuwiandClaude Opus 5 ece27827e5 The door is exactly as tall as the blood beside it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:03:44 +02:00
wuwiandClaude Opus 5 134ce702b2 The door hangs on the HUD's own line, not the browser's
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:02:36 +02:00
wuwiandClaude Opus 5 d3510423b7 The door out takes the corner, and the blood steps aside for it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:59:49 +02:00
wuwiandClaude Opus 5 1b7fc2cdcf Both players run from a video, and the middle chip finally plays you
Six sessions of asking an image model for a row of running figures, and it
never once drew a gait: three phases repeated with the same leg leading,
"ropeskipping, not running". A frame-by-frame LEFT/RIGHT skeleton spec, a
closing checklist, a loop clause, six figures instead of eight — all lost.
One 5-second image-to-video clip has no such problem, because it moves a
body instead of drawing poses. ~$0.42 a character through the key already in
.env, pose pinned at BOTH ends (pinned only at the front, she rotated into
profile halfway), stride period MEASURED off the frames rather than sampled
by eye — hers repeats in 6 clip frames, his in 23 — and the phases cut and
palette-snapped through _stride.cut. assets/_vidroll.py films and cuts it;
her first frame came from one still roll that took his approved pose as a
picture instead of a paragraph.

Wired: runN probes the sheet through SK() so the women's tour counts HER
frames, the player is no longer clamped to a two-frame pair, runHz 2.4 ->
1.9 ("this looks like roadrunner"), and the bench grew two PLAYER stops —
a cycle only ever seen mid-rally is a cycle nobody has watched.

Also in: the difficulty retune (medium is a real step above the shipped CPU
and is where `smart` first appears; hard stepped back off superhuman; easy
closed its gap) with MEDIUM's dial-for-dial pin to TUNE removed from the
test, and Oliver's own transparency fixes on the male frames.

SPRV 138 — the frames were right on disk and wrong on screen for an hour
because assets are cached for a year unless the ?v= moves. main.js v269.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:53:58 +02:00
wuwiandClaude Opus 5 2b7eb04e19 The invitation is four letters, not a link
A URL only opens the installed app on Android; on an iPhone it opened a second
copy of the game in Safari, with its own storage and none of the name picked in
the app. So the share sheet now sends "JOIN ME IN SPLATTER TENNIS — THE CODE IS
ABCD" and nothing else, and the button says SEND THE CODE.

The #net-ABCD door stays — it is exactly what a deep link will carry the day
this is a real app with universal links. Nothing sends it in the meantime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:56:25 +02:00
wuwiandClaude Opus 5 89b2488d3c The shared message carries the code, because an iPhone cannot open the app
An Android phone opens the link in the installed app (the manifest's scope
covers the site, so its WebAPK claims these URLs). An iOS home-screen app can
claim nothing, so the link always opens in Safari — which keeps its own storage
and knows nothing about the name picked inside the app. Four letters in the same
message is the whole fix: a friend who has it installed types them instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:46:08 +02:00
wuwiandClaude Opus 5 425c18dcb6 A room you can share by link, a code you can type, and a name to be called
Multiplayer setup moves out of a canvas modal and a prompt() dialog into one
menu screen: your name, the two ways to host, and the friend's code. The socket
opens from the menu and the game stays parked behind the overlay until the room
fills — netHooks.paired takes the court one line before netStart writes to it —
so the room code can carry a real SHARE button and a wrong code is a typo you
fix rather than a CONNECTION LOST card over a match that never began.

The name is the world board's name (stats.saveName), crosses on the existing
look message, and the scoreboard finally names the person instead of CPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:20:39 +02:00
wuwiandClaude Opus 5 ad258ace97 The setup screen stops drawing the man twice, and her whites take a real colour
Oliver, on the picker: the gender tiles "themselves have the player but front
facing and that does not react to the kit and skin — so that's double somehow
and wastes space", the screen "is pretty crowded now", and the women's wash was
"a little too subtle on the white — could be more".

THE TILES ARE GONE. They were a second drawing of the same man, facing the wrong
way and deaf to every colour on the screen, and they cost the height that pushed
the court list under the fold. Who-you-are is two words now, and the live player
— which does react — stands beside every control that changes him, so a tap and
its result are a thumb apart. Four stacked sections became one block; all four
courts and SERVE UP fit a phone without scrolling.

THE WASH goes 0.30/0.93 -> 0.55/0.86: dyed from across the court, folds intact.
Past about 0.70 it stops reading as tennis whites at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:37:35 +02:00
wuwiandClaude Opus 5 b5838b2b24 The whites take a wash, the friend brings their own colours, and the menu shows the man
Three things off the back of the picker.

THE LIVE PLAYER on the setup screen is the real sprite through the real tint, not
a mock-up of the pick — so the menu cannot promise a colour the court will not
keep. It redraws on every tap of a tour, a kit or a skin.

THE WOMEN'S TOUR IS NO LONGER SAT OUT. Her set is drawn head to toe in whites, so
there was no hue to rotate — the answer is not a dye but a WASH: the pick laid
over her whites at a low saturation with the lightness kept, which is how you dye
a white kit in life and gives rose, powder blue, mint, lilac. Her hair and skin
fall out of it on lightness alone, which is exactly what a dye could not manage.

THE FRIEND ACROSS THE NET now wears what they picked on their own phone. Their
look crosses on pairing, and since the opponent art is red rather than blue the
kit rides its own hue arc — safe there for the one reason it would not be on the
player's own sheets: the opponent sprites carry no drawn blood. An older build
simply never sends it and the opponent draws as before; the match still plays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:23:34 +02:00
wuwiandClaude Opus 5 1daa3e9d6a A bed that finishes loading wakes the context it was started for
Oliver's hunch was right: the tap unlocks audio and starts a 768 KB fetch, but
the source starts seconds later inside a promise with no gesture near it. iOS
suspends an idle context in that gap, and a source started into a suspended
context is silent for good — nothing on the splash ever asks for audio again.
Resume once more when the buffer lands, which the first tap has already
authorised, and again when the app comes back to the foreground.

Intermittent because it only bites when the decode outlasts the browser's
patience: a phone, a cold cache, a slow radio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:16:41 +02:00
wuwiandClaude Opus 5 3b639e743b You pick your own kit and your own skin, and the blood stays red
Testers asked for some control over how their player looks. From behind — the
only way you ever see him — there are two things worth controlling, and neither
needs a single new asset: the kit and the skin are one hue-bucket pixel pass over
the sheets already loaded, cached per frame and built on first draw.

The buckets are measured, not guessed, and three neighbours had to be fenced off
one at a time: the shirt's warm HIGHLIGHTS (which spattered the man with mud on
every dark pick), the drawn BLOOD on the death frames (which turned the pool
brown — the worst bug this file could ship), and the women's HAIR, which is the
same hue as his shorts and cannot be told apart from it by any rule. Her set is
played in whites and has no kit to dye, so it is flagged out and the row says so
rather than silently doing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:00:57 +02:00
wuwiandClaude Opus 5 2f0c04fad4 The splatter music stays on the splatter court
Every run starts the ambience, and the ambience was un-ducking both court beds
at once — so once you had played the gore mode, its music followed you into
classic tennis for the rest of the session. Leaving the court is one event;
arriving is not. Each bed brings itself back and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:32:30 +02:00
wuwiandClaude Opus 5 3cf3e029a4 Gore opens in the gore, the theme comes back, and every mode gets a board
Splatter gore no longer makes you win a game of tennis first — the mode you
pick is the mode you get, with the man you came to play already turned. The
transition itself is untouched and still reachable; it just isn't a toll booth.

The menu theme could only ever be started, never brought back, so the first
run of a session silenced it for good while the court's ambience followed you
into the menu. Starting a bed now un-ducks it too.

Three world boards instead of one: kills, opponents felled, and games taken
off the CPU, each with its own name prompt and its own row per player.

...and the gore economy stops paying a barrel for every game won past game 16,
while the shamblers, the mix and the ball machines keep escalating past the
point they all used to stop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 13:53:42 +02:00
wuwiandClaude Opus 5 9cc3ac54b3 The dead club doubles its staff, and the draw is made where you can see it
Six new opponent families (head coach, stringer, strawberry seller, chair
umpire, spectator, roller man) through the famroll/zsheet route, placed by
hierarchy into an 18-rung ladder; the champion steps up behind them.

THE DRAW: picking the tournament now rolls its own bracket in front of you —
only the final was ever settled — and what lands is what walks on.

Netplay, off Oliver's cellular report: the mid-flight early-decide was missing
its !G.net gate, so the striker judged its own outbound flight and the two
scoreboards drifted a point apart into a court nobody could serve on. The
host's court now rides the room, its scoreboard rides the pretoss stream, and
a called ball rolls to its own halt instead of freezing where the call caught it.

Plus the night court, and somebody in the stand is taking pictures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:49:19 +02:00
wuwiandClaude Fable 5 4c47295a73 The friend across the net is no shambler: he just bleeds, and then he bursts
Two retest findings, one root: the death match borrowed the roster's
body. Gone — nothing spawns, no walk-on card, the living opponent is the
whole picture. His damage reads as blood (bleedO: an anchor per point
taken this game, love to game point, thin drops as ever), the game
point bursts him where he stands, and IT RISES AGAIN brings him back
clean. The heal beat became a card in netplay — a modal freezes this
world while the other phone's keeps moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:10:50 +02:00
wuwiandClaude Fable 5 c0b59eee90 The host names the stakes: classic tennis, or the death match
Three chips under VS A FRIEND — the host's pick rides the room and the
start message rebuilds both courts on pairing. In the death match your
opponent IS the zombie: their lost points tear its wound stages, their
won points draw your blood, and bleeding out crosses the wire as the
other phone's win. The same receiver-is-judge verdicts drive the match
machinery through resolveInterlude, so the fall, the mop beat and the
burst come along unmodified. New: gushProne — the game-winning body
bleeds out on the ground, from prone-height anchors, until it bursts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:02:44 +02:00
wuwiandClaude Fable 5 5e80c4e69d The versus poster finds its own hour: golden, not the dead club's night
Regenerated off the classic poster instead of the zombie one — sunset
oranges into violet, warm bulbs, neon rim light on the two players. Each
mode row now owns a palette: day, night green, blood red, golden hour.
SPRV 125 so cached phones refetch it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 17:49:55 +02:00
wuwiandClaude Fable 5 8face714a6 The toss travels: the receiver watches the serve coming
One event on the toss, replayed as a real toss over O — update() already
poses the server off the phase and drops an uncaught ball back to pretoss
by plain physics, so the wind-up, the cancel and the re-toss all carry for
free. The strike's shot event overwrites whatever drifted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 17:44:00 +02:00
wuwiandClaude Fable 5 f302354bb2 Two phones, one match: the shot is the message, the receiver is the judge
VS A FRIEND on the mode screen (host a code, join a code), a ~110-line
RFC6455 relay in server.py that holds no game state, and src/net.js: each
phone plays itself near-side and the opponent's court is this one rotated
half a turn about the net. The phone a ball flies toward makes every call
on it; the striker's copy is picture. Six tests pin the rule from both
seats. No reconnect yet — a dropped radio is a card and a reload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 17:36:02 +02:00
wuwiandClaude Fable 5 108ed695eb The felled body earns its fall: the burst waits for the collapse, the card waits for the burst
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:35:46 +02:00
wuwiandClaude Fable 5 97b076c574 The 6.8 experiment leaves its tombstone: runHz2 stays 3.4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:26:18 +02:00
wuwiandClaude Fable 5 11d074f2e4 The player joins the pair: his stride is frames 5+6, hers is 1+2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 15:20:22 +02:00
wuwiandClaude Fable 5 f62c7ff0ff The bob rides the stride, and the stride finds its beat: runHz2 3.4
Two live verdicts off the bench. The 1px run bob was a fixed 18 Hz wall-clock
toggle — over the calm two-frame cycle it read as independent HOPPING — so it
now shares the exact phase counter that flips the frames: one dip per step,
always in sync at any cadence. And with bob and feet locked together the
cadence itself came up for retuning: 1.7 read as a jog, doubled to 3.4 it is
a run — "that's it! way better." One knob moves both, which is what made the
doubling a one-word change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:57:16 +02:00
wuwiandClaude Fable 5 40c74be4aa Two frames each: the run cycles land where the zombies always were
The live bench session's verdict, in Oliver's words: "this looks better than
anything before." Six and eight real phases at speed read as flicker on a
far-court body — the zombies never had this problem because their walks are
FOUR well-chosen frames at a calm rate, and the fix was to stop out-drawing
them. Each opponent now runs a TWO-frame true leg-swap pair — his frames 1+2
of the grid roll, hers 7+8 of the face-transplant cycle, both picked live on
the bench with SPR swaps — at TUNE.runHz2 = 1.7 (the old hardcoded 4.5 for
short cycles was most of a stride per display frame; that is the strobe).

The tuning rig ships too: TUNE.runN (frame-count override, 0 = probe),
T.SPR for live frame swaps, and the bench's living slots — the next cadence
question gets answered by watching, not rolling. Twelve run sprites become
four; the SPR list says why. SPRV 124, main.js v248.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:54:11 +02:00
wuwiandClaude Fable 5 90d7ac1096 The bench learns to host the living: two player slots for run-cycle tuning
Oliver: "add a normal player male and female to the hidden bench and let it
run there." The fam walk (up/down) now runs two slots past ZFAMS — PLAYER
MALE and PLAYER FEMALE. In a living slot the racket zombie steps outside the
frame, G.hideO lifts (splatter set it when the match turned), and the real O
runs the walk stop's own back-and-forth at the classic CPU's speed class, so
the cadence on the bench is the cadence the game plays. SK() reads the slot
for the _f suffix, so both tours review without touching the picked tour.
The HUD swaps the wound readout for the tuning one: runHz and aiSpeed, the
dials a live session turns through T.TUNE. T also gains a keyPressed alias.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:39:36 +02:00
wuwiandClaude Fable 5 ad624d281c Her legs finally swap: the face-transplant cycle ships, and the prompt rules are law now
The female run took the whole evening because the model would not draw her
RUNNING: roll after roll came back with the same leg forward in every frame
— amplitude wiggle, no gait ("she never changes her leg... floating while
strobing"). The set that ships is the FACE TRANSPLANT: her old cycle's poses
(which always alternated properly — swaps at positions 2 and 6, 78%/74%,
where a runner's legs actually cross) redrawn with only the friendly face
and open beds changed. The model returned that roll on a painted CHECKERBOARD
— it has no concept of transparency, the word alone summons the pattern —
and the figures were custom-keyed off it: light checker blanket, dark
checker border-flood only (the dark cell is nearly her skirt colour; the
first blanket pass shredded the pleats), verified whole over a light ground.

Oliver gated every strip by eye. The alternatives lost on their own terms:
hex-only prompts obeyed the background but drew her dainty (max 44%, no
swap); the explicit leg-swap clause got alternation but clustered it in the
back half (step-step-skip). His pick: the transplant.

The lesson is in PROJECT.md now, next to the bed flag: NEVER say
"transparent" to the model — name one flat hex — and never freehand
background prose in one-off scripts; import _regen.py's CANVAS block, which
has said it correctly all along. SPRV 123, main.js v246, all 14 run frames
verified 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:28:29 +02:00
wuwiandClaude Fable 5 0f9738f5f2 The run cycles get their camera back, and the racket finally holds still
Oliver's verdict on the friendly cycles: wrong on two axes. The new rolls
were PURE PROFILE — "what we actually need is running slightly towards or
away from the player... this was better before although the art was not
coherent" — and the racket re-imagined itself every frame: "it's now jitter
town, strobe like because of the rapidly changing rackets."

Take two runs on TWO references, which is the recipe that has worked all
day: image 1 the friendly identity (anchor frames), image 2 the OLD run row
pulled from git history as the CAMERA law — three-quarter front, chest to
the viewer, exactly the orientation the far baseline needs — with the prompt
pinning the racket ("as if copy-pasted between figures; only the LEGS run").
One-row layouts kept coming back interlocked (nine figures, one 3141px blob,
uncuttable), so the ask became a 2x4 GRID — separation the model actually
delivers, re-laid locally. His roll drew 12 clean figures; the smoothest
8-cycle was chosen by maximising the weakest adjacent stride: 72.7%, against
20-30% on every earlier roll. Hers landed exactly 8. Oliver gated both
strips by eye before install — the automated gates sat this one out on his
call. SPRV 122, main.js v245, all 16 frames verified 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:39:12 +02:00
wuwiandClaude Fable 5 c4231c2227 The bed flag joins the routing notes: magenta string beds are a decision, twice
One paragraph in the asset pipeline so the next fresh session does not
rediscover it: prompts ask for magenta beds ON PURPOSE, _fine.key's grey
repaint defeats them unless bed=True is passed, and the solid white paddles
were exactly that interaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:17:22 +02:00
wuwiandClaude Fable 5 80bf6c4cee The opponents come back to life: the friendly refresh, open string beds, and a steadier chase
Oliver's batch, delivered in dependency order. "The opponents look pretty
harsh, almost already pre-zombie" — so BOTH tennis opponents got a new
identity: warm skin, human eyes, an easy near-smile, rolled anchor-first so
every later sheet inherits the face. The male's bounce pair also ADOPTS the
female's palm-out posture (his old crouched receiver stance was the wrong
pose for bouncing a ball — her better one is now the shared convention), and
the pre-serve ball was re-measured onto the drawn palms: BOUNCE_IDLE's oDx
0.45 -> 0.24 / oApex 0.55 -> 0.72 for his close carry, with her wider reach
riding TOURS (oDx 0.48 / oApex 0.73) the same way the player's pair already
does.

"parts of the body as well as the inner of the racket are solid" — the beds
were the cutter's own doing: _fine.key repaints enclosed magenta pale grey
(protection against accidental magenta strings), which defeated the beds we
now draw magenta ON PURPOSE. key()/main() grow a bed=True switch (the
famroll convention, used by _runroll and the refresh cuts); the run cycles'
beds went from ~70% to ~44% opaque — strings, not paddles. Enclosed pockets
of a roll's off-spec background (one sheet came back on blue) are scrubbed
by corner-colour distance too.

Both RUN CYCLES re-rolled off the friendly anchors: his landed 8 clean
frames (racket +0.05..+0.30, one side); hers took four rolls — the gates
caught a white background, a crossing racket, near-dup neighbours — and the
keeper drew a 10-frame cycle, from which the best 8 were chosen by
maximising the minimum adjacent stride (29.7%, smoother than his 22.7).
o_run7/8 return to the SPR list.

AND THE CHASE STOPS DITHERING (Oliver: "switching between a left and right
movement increasingly faster the nearer the ball comes"): intercept()
re-solves every frame and the earliest-reachable point jumps around its
solution as the flight shortens, flipping his run direction each time. The
CPU now commits to its stored target unless a fresh solve moves it by more
than 0.35 m. SPRV 121, main.js v244, 66 tests, every opponent frame verified
200 in the live game.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:16:55 +02:00
wuwiandClaude Fable 5 dd8dbd04e9 The women's opponent drops the shoe she was never wearing
Her run cycle carried a DETACHED SHOE floating behind her in every frame —
a cut that kept two blobs per figure — spotted while fixing the men's
flicker. Four rolls to a keeper: roll 1 closed on a duplicate (8->1 at
4.4%, gated), roll 2 came back a grid, roll 3's grid was re-laid into one
row locally but its racket flipped sides frame to frame (-0.42..+0.31, the
exact "flashing left and right" the warning exists for), roll 4 landed:
eight frames, racket steady at +0.05..+0.17, one face, no luggage. SPRV 119.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:36:17 +02:00
wuwiandClaude Fable 5 c6404c6032 The opponent is ONE man now: serve re-posed, run re-rolled, face flicker gone
Oliver: "the serving and running of the opponent during normal tennis still
feels odd. which spritesheet was used?" Two sheets, two defects. The run
(o_run1-8, _refcheck/run_o.png via _runroll.py) FLICKERED IDENTITY — frames
alternated a tanned living face with a grey red-eyed one, because the
identity reference bakes in o_toss/o_smash and THOSE were the second defect:
the serve pair came from the legacy opponent_toss regen batch in a chibi
register (big head, heavy outline) that matched nothing around it.

Fixed in dependency order. The serve pair re-rolled pose-style (o_idle1 the
identity anchor): the batch's kit gate rejected all three tries but try 3
was right by eye — the "bright" channel it tripped on is the white-filled
string bed the run frames already carry — so it was installed on the montage
verdict, which is the pipeline's own rule. Then _pose_ref_run_o recomposed
off the cleaned frames and the run re-rolled: SIX frames, one face, one
racket side (+0.18..+0.24), real strides. The model drew 6 and a clean 6
beats a fought-for 8 (the player's cycle is already 6) — o_run7/8 deleted,
the SPR list trimmed, render's run8/run6/run4 probe does the rest. All six
verified 200 in the live game. The women's cycle (a floating shoe behind
her in every frame) is still rolling and lands separately. SPRV 118.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:34:30 +02:00
wuwiandClaude Fable 5 b8595f6c35 EASY grows a backbone, and the door out steps below the blood
Two phone verdicts. "easy is now way too easy" — its first cut was tuned
before HARD taught us which dials bite, and with reaction 0.62 and a 0.62
quality cap it had become a training dummy. EASY is a mild step below MEDIUM
now (flub 0.18, quality 0.76, reaction 0.54, speed 0.9): beatable without
being able to fall asleep on it. And the quit chip sat exactly on the BLOOD
panel's corner in the splatter modes — it rides 40px down now, below the
pips' strip, one position with one meaning in every mode. Verified live over
a zombie run: pips and door stacked, nothing touching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:12:41 +02:00
wuwiandClaude Fable 5 8044857457 HARD finally outruns the runner
Oliver benchmarked HARD against the game's own roster: "the runner in zombie
mode is way faster than the regular player in hard" — and the numbers agreed.
The RUNNER covers 6.15 m/s (racketSpeed x rung x foe.speed) with no reaction
gate; HARD's classic CPU did 5.59, only after its 0.22 s read, and ambled
back to centre at HALF speed between shots — that recovery jog was the free
points. HARD now moves 4.3 x 1.5 = 6.45 m/s, and DIFFS grows `recover`: the
between-shots factor, 0.85 on HARD where the shipped 0.5 stays pinned on
MEDIUM. Wrong-footing him still works; strolling him side to side no longer
does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:07:59 +02:00