Commit Graph
100 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5.5 8e4b298d92 Keep the auto-run's target where the player is allowed to stand
Oliver's v369 log ("vieeel besser") showed two misses with the auto-run
heading to x 6.5 and 8.7, far outside the frame he is held in. Since the
dip ends at the bounce, a hard cross-court topspin ball drifts sideways
long enough that its comfort point lies off the court; intercept had no
lateral bound, and its last-resort target was never checked at all
(headless: x 14.8). Candidates now respect xLimit — a kicking ball is
taken earlier, on the rise — and the final target is clamped. v370.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:46:53 +02:00
wuwiandClaude Opus 5.5 e25047ed0c End a shot's topspin dip at the bounce, so a kicking ball kicks up
A shot's extra gravity is its dive in flight, but it was carried through
the bounce: heavy topspin skidded away low and Oliver met nearly every
ball 0-25 cm off the grass. After its bounce a ball now falls under plain
gravity (tennis modes only), and every planner that rolls a bounce
forward switches at the same moment. The higher bounces pushed the
auto-run into volleys, so it now takes a kicking ball at the top of its
bounce, up to chest height. Median contact rises from 0.42 to 1.13 m at
the bottom of the ladder. v369.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:38:01 +02:00
wuwiandClaude Opus 5.5 fe090f2fba Stop back-dating flicks, and give the half-volley rescue the right deadline
Oliver's v367 log: every 62-64 ms flick landed 76-92 ms early and a 97 ms
one whiffed — v366's touch-down timing for flicks was wrong; a flick is
timed by its swipe (flickLead now 0, the age is still logged). The
early-swing half-volley rescue checked the bounce against the long volley
window and then expired on the bounce frame; it now uses the shorter
groundstroke window. The log also records where he stood when the
opponent struck and where the auto-run was taking him on a miss. v368.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:13:08 +02:00
wuwiandClaude Opus 5.5 dfdb699f9d Stop calling "passed you" while the ball is still on its way to him
Oliver's v366 log: a LOST "passed you" with no swing. The match gave the
CPU the point the moment its ball crossed 0.3 m behind the baseline — a
line from when the player stood inside it. Since step 1 he waits up to
2.2 m behind the line for deep balls, and a ball going by ~1.5 m beside
him only enters his reach just in front of him, so the point was called
before it was ever playable. In the match "past you" now means past his
reach. The swing log's hits also say whether the timing was early or
late. v367.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:03:30 +02:00
wuwiandClaude Opus 5.5 5bbde720ad Time flicks from touch-down, log the opponent's shots, save early half-volleys
From Oliver's first swing log (cup vs the Brute): 13 of 22 hits were
perfect, 9 badly early or late — mostly forward-right flicks — and the
weak balls became seven "passed you". Every hit was a flick, and flicks
still swung only at the flickMin crossing while the timed ring says "touch
now": they are now timed from touch-down too (capped at flickLead 0.1 s).

The log gains its other half: a THEM line per opponent shot (kind, pace,
where it lands relative to you, coming in) and "never swung" judged per
incoming ball instead of once a point. And an early swing on a low ball
digs it out as a capped half-volley instead of whiffing. v366.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:48:51 +02:00
wuwiandClaude Opus 5.5 d110e12321 Bring the swing log to every rally mode, and time the gold ring
Oliver wants to hand over a log of why he missed balls. It existed behind
a scoreboard tap but was gated on G.mode === 'tennis', so the Climb, the
cup and training (which run as 'splatter') logged nothing. It now writes
in every rally mode: hits with shot, tap age and any quality cap, misses
with the new let-it-bounce reason, and a WON/LOST line naming why each
point went. Tapping the open log copies all 200 entries with a header.

Its first catch: the gold ring went gold at a distance, which is too
early for the slow balls steps 1-3 brought in. It is now timed off the
ball's real path to his reach (against where his run is taking him):
gold means touch now. ringTime 0 restores the old ring. v365.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:41:05 +02:00
wuwiandClaude Opus 5.5 180288ef8f Give every opponent a style, and split the soft ball into drop and lob
Step 2 of the rally rework: eight styles as dials on the opponent rows —
pusher, retriever, slicer, big hitter, baseliner, touch, net rusher,
all-court — read where the CPU chooses its shot (moonball, drop, lob,
spin), plans its approach and serves (serve and volley sprints in). The
walk-on names the style in one line so the player can plan against it.
A player lobbed at the net now backpedals and blocks, like the CPU.

Oliver on v363: the down-flick "always felt like a lob". The length is read
at contact and a thumb has usually travelled on; on the continuum even
50 px floated the ball. Below dropMax (90 px) it is now the drop, above it
the lob. Also un-flakes the machine-squeak test (seed 31337). v364.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:25:52 +02:00
wuwiandClaude Opus 5.5 72760a9d1e Make the down-flick the soft ball: a short one drops, a long one lobs
Step 3 of the rally rework, Oliver's pick: in rally modes the down-flick
is soft, always, and its length is how far — a dying drop just over the
net at one end, an attacking lob over a net man at the other. One gesture,
one meaning; not the context-armed lob that was cut.

Ballistics from the cut lob (pace derived from the apex); backspin lives
on ball.spin and every bounce planner reads it, netplay sends it. Against
the CPU it was returned ~100% until the real-tennis reasons went in: soft
balls are read late (disguise), a net man reacts to a pass and backpedals
from a lob, and a lobbed or dug-out reply is a weak block. Four new tests;
v363.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:17:25 +02:00
wuwiandClaude Opus 5.5 f9b0dd2efb Time a tap from touch-down, and give the opponents' legs a knee
Oliver after step 1: much harder, stuck at the Steward, "trotz gutem
Timing nicht mehr bekommen". A tap only proves it is a tap when the
finger lifts, so the swing started 100-150 ms after he meant it; the old
volley window hid that, step 1's groundstrokes did not. A tap made on the
gold ring fell from q 0.82 to 0.09 (and a third were missed at 150 ms),
murky balls landed short and the opponent came in. The swing clock now
starts at the touch's age (rally modes only); flicks keep their crossing.

The opponents' legs had no ceiling: 7.3 m/s at the Runner, 15.7 at the
Champion against the player's ~6.2. Past legKnee (6 m/s) only 15% of the
growth survives, monotone, so the ladder order holds; champion ~7.5. v362.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:06:08 +02:00
wuwiandClaude Opus 5.5 4e7c816a25 Give the rally its length back: deep balls, baseliners, approaches off short balls
Every match had collapsed into serve, both at the net, reflex volleys at
~0.7 s per shot. The rally ball landed on the service line by definition,
the match CPU stood on the bounce and volleyed 90-100% of everything, and
the auto-run chased the earliest contact.

Rally modes only (classic + every match; gore and the duel untouched):
rally balls land deep and mistiming only shortens them (no new long outs);
each shot latches the receiver's plan as it leaves the racket — from the
back he lets it bounce and meets it falling, up to 2.2 m behind the line,
a short ball pulls him in and he follows it to the net, nobody approaches
a man already there, and a volley below the tape is a weak block. Measured:
contacts 10.5-13.7 m from the net, back-court volleys 0-8%, 0.8-1.0 s per
baseline shot. Three new tests pin the shape; v361.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:55:05 +02:00
wuwiandClaude Opus 5.5 f33d46366a Diagnose why every rally ends in a net duel
Oliver: every match converges on serve, both at the net, reflex volleys,
won by a wrong-foot. Measured headless: rally balls land on the service
line by definition, the match CPU stands on the bounce point and volleys
90-100% of everything from opponent 6 up, and the auto-run always picks
the earliest contact. Diagnosis, research and a four-step plan go into
PROJECT.md (awaiting his go); tools/rally-sim.mjs is the gauge. Fallback
tag rally-baseline-v360 marks the shipped feel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:32:36 +02:00
wuwiandClaude Opus 5.5 806a739c6f Add MEMBERSHIP, the club's own 1980s VHS advert
cut.py --cut ad: a plummy announcer (one gpt-audio read per line, ad.sh)
sells St. Mordred's over the creepiest takes with yellow 80s supers, a
PLAY/STOP VCR read-out, two testimonials from the real lines and the small
print read at speed, until the game's umpire says "Quiet." and the jingle
dies. A VHS grade (chroma smear, scanlines, tape wobble) is the cut's own.
gen.py learns a voice command; one new silent Doris take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:46:31 +02:00
wuwiandClaude Opus 5.5 da09b55525 Add FIFTEEN–LOVE, a 19-second umpire-scored spot
cut.py --cut score: no interview audio, only the game's own recorded
umpire. "Spectators are reminded to remain seated, especially the
deceased", then a love game called against the viewer, one member's stare
per point under a broadcast score bug, "Game. Set. Massacre." on black,
and "This match is still in progress" over the logo. cut.py now holds
named cuts, each with its own build folder and output file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:28:28 +02:00
wuwiandClaude Opus 5.5 2de45510a3 Add a 36-second cutdown of the trailer
cut.py --short builds a social-length version from the same takes: one
line per witness cut inside their takes, then the challengers beat,
"Quiet, please", "Play", the splat and Reg's stinger. Each cut keeps its
own build folder and output file, so cut 2, cut 3 and the short all stay.
words.py gives local Whisper word timestamps for cutting inside lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:09:05 +02:00
wuwiandClaude Opus 5.5 5fba837ead Tighten the trailer to 1:30 and fix its two audio faults
Cut 3 of LIFETIME MEMBERS: the intro music no longer runs into Reg's first
line, Ambrose finishes "...still waiting for a result" (line ends are now
measured off each take instead of the QA transcript), every shot is
tighter, and the black title cards are set larger. Cut 2 stays on disk
beside it as the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:56:50 +02:00
wuwiandClaude Opus 5.5 d388004eac Add the LIFETIME MEMBERS trailer pipeline
A vertical live-action mockumentary trailer: the dead staff of St. Mordred's
(the game's bosses) each tell a different origin story, and the film ends on
"Quiet, please." / "Play." / splat. tools/trailer/ holds the whole pipeline:
stills and takes through the OpenRouter key (gen.py, stills.sh, clips.sh),
the frame graphics in HTML/CSS (frame.html), a QA listener (qa.py), the
music sectioner (segment.py) and the edit (cut.py). Media lands in the
gitignored promo/trailer/. SCRIPT.md is the screenplay and the cut notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:27:13 +02:00
wuwiandClaude Opus 5.5 74aa209753 Give the machine, fire, gun and boss their missing sounds
All four synthesized in audio.js like every other weapon: a squeaking
castor, a court-wide crackle clock plus a throttled catch, a real gun
crack, and one shared boss voice (stomp per stride, wind-up breath,
bark, pain, death roar) pitched by body size. Closes OPEN 13 pending
the phone verdict. v355.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:16:27 +02:00
wuwiandClaude Opus 5 d05b49909e Correct the stale #netd note in PROJECT.md
Every #net* hash opens the lobby since THE NETPLAY LOBBY; it does not host.
Cost a verification round this session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:29:07 +02:00
wuwiandClaude Opus 5 78946015b4 Give the friend's racket a sound and the duel a rematch
Two netplay-only finds from Oliver's two-phone session.

THE FRIEND'S STRIKE was silent: every other opponent is simulated locally
and cracks through gameplay's shared return path, but the friend's swing
runs on their phone alone and arrives as a 'shot' message that wrote the
ball and nothing else. netApplyShot now plays the same sample at the same
volume, pitched off the pace that was actually sent (strikeRate, read after
the ball is written). Both netplay modes, not just the death match.

THE REMATCH: the summary's only netplay door was THE CLUBHOUSE, so playing
the same friend again meant two trips through the lobby and a fresh code.
The relay cannot re-pair, so the rematch is the socket that is still open —
one 'again' frame and both phones reset the run under the decisions the room
already carries (same side, same mode, same court, host serves). netAgain
keeps G.net across restartRun(true) and replays netFresh, netStart's own
tail split out so pairing and rematch cannot drift into two courts. Applied,
never echoed; a 2 s window swallows the second frame when both tap at once;
a dead line refuses and falls through to the clubhouse.

Also: the dev server runs with SPLAT_AUTH= (server.py always said local dev
should), which the shared password had been 401-ing. audio.sfxTest is a new
test seam in netTest's shape. 146 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:28:31 +02:00
wuwiandClaude Opus 5 b53fc07dd1 Put staging and prod behind one shared password
The game is not public yet. parse_request is the one place every method passes
through, so the gate sits there rather than in each do_*. /api and /ws stay open
because the iOS shell talks to prod from capacitor://localhost and has no
credentials to send. SPLAT_AUTH overrides it; empty opens the site again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:50:28 +02:00
wuwi a09856f8c9 Streamline mobile menus and refresh splash logo 2026-09-07 13:26:17 +02:00
wuwi a399e4929c Fix match AI short-ball contact and recovery 2026-09-07 10:53:14 +02:00
wuwi fe64242090 Add opt-in phone gameplay recording and JSON export 2026-09-07 10:19:38 +02:00
wuwi ede1f48152 Sharpen league and Climb difficulty and give Gore a front door 2026-09-07 09:53:49 +02:00
wuwi e3faeb0a3a Strengthen final-league placement and returns 2026-09-04 09:11:12 +02:00
wuwi bfc70d7388 Make the opponent level matter in the cup 2026-09-03 16:41:02 +02:00
wuwiandClaude Fable 5 058636fae3 The cup board asked for itself and got the gore board
The GET filter spelled its modes out as a literal `[^gzc]` character class, so
`?m=u` sanitised to nothing, fell back to 'g', and the server answered the cup
board's request with the gore board — which the client then printed in the cup
board's words: 7121 kills as LEAGUE 7121. The filter is derived from MODES now,
where the mode list has lived all along.

And the client checks the answer it got: the response names the board it is,
so a server that does not know a board (an old one, mid-deploy) makes the
screen say THIS BOARD IS NOT UP YET instead of mislabelling another board's
rows. Nothing beats something wrong on a leaderboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:19:51 +02:00
wuwiandClaude Fable 5 115c6089be BODY COUNT: dashes for never, names on the rack, and one seam too many
A record never set reads as a dash — "BEST GORE RUN 0" looks like a bad run
when what it means is that there has not been one. The weapon tiles name their
weapon: six unlabelled silhouettes was a guessing game about which one is the
flamer.

The doubled border was the row rule firing inside the grid: `.fig + .fig` gave
cell three a left border hard against the container's own. It is scoped to
single-row figure strips now, and the two-up grid draws its own seams. Less
padding inside the box, air above it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:06:15 +02:00
wuwiandClaude Fable 5 84b1234012 The last halo goes, and its keyframes with it
The league panel's crest wore the same breathing glow; with the honours row's
gone it was the only one left, so it takes the plain drop shadow the card's
crest took. @keyframes badgeGlow has no users any more.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:03:22 +02:00
wuwiandClaude Fable 5 25ac21b852 Unearned shields go dead, and the halo leaves the honours row
Two bugs in one rule. The grey sat on the IMAGE while the breathing halo was an
animation on the same image — and a filter in a keyframe outranks a static one,
so the glow was painting the greyscale away. Worse, the selector that handed out
that halo still read `img:not(.dark)` from when the class hung on the image; the
class moved to the cell in the inscription pass, so every shield on the row was
matching it, lit or not. The grey lives on the cell now, where nothing animates.

And the halo is gone from the row entirely (Oliver): the glints do that job, and
a permanent breathing glow under an occasional flash was two effects saying the
same thing. The league panel's crest keeps its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 16:00:49 +02:00
wuwiandClaude Fable 5 708ecbb858 Bump the build: main.js changed twice under v=343
The glint rework and the ember fix before it both shipped under the same query,
so any phone that had loaded the page in between kept the stale module — which
is exactly what "ich sehe kein aufblitzen" was looking at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:57:26 +02:00
wuwiandClaude Fable 5 57650160a6 The shields glint instead of burning
A chimney of rising embers on a trophy read as a fire. Three points ON the
shield now, dark for most of a long cycle and flaring for a fraction of it,
with delays and durations that share no common multiple — so a glint lands
somewhere on the row every couple of seconds and never in the same rhythm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:56:08 +02:00
wuwiandClaude Fable 5 87833db095 The embers actually show: off the shield's top edge, not across it
They were running the whole time (0.85 opacity, measured) — they were just
flying ACROSS the artwork, where a 3px gold speck disappears into a pixel
shield. They lift off the top edge into the dark above it now, seven per badge
on a shorter delay window: spread over 2.6s the shield stood empty half the
time, which is what "ich sehe kein glimmen" was looking at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:54:31 +02:00
wuwiandClaude Fable 5 80eb60d181 The cup board: the league ranks, the level breaks the tie
Oliver wanted to rank the level a league was taken at. The challenge held: xp
comes only from playing, so a board that rewards a low level rewards not
playing — it would pay you to avoid the three rooms the season leans on. It
also inverts every other number in the game, freezes after five entries, and
makes the player who played more look worse.

So the league ranks and the level decides between everyone who got equally far,
where "lower is better" reads naturally. BOARDS.u carries subLow; server
rank_key is the other half, and the same key decides whether a submission beats
the row already standing. The level is written once at the promotion — before
the league moves, because it is the level that WON the cup — mirrored like the
rest of the profile, and inscribed under the shield it belongs to. Earned
shields smoulder while they are on screen: five CSS embers each, no loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:51:38 +02:00
wuwiandClaude Fable 5 e96b4374d8 BODY COUNT loses its nine states, the board loses a mode
Three mode tabs times three period tabs, and the same three boxes meant
something different in every one — Oliver had no idea what he was looking at,
which is the correct reaction to a screen with nine states. It is one screen
now: bodies felled, best gore run, deepest climb, runs. Never anything else.

MULTI-KILLS retired (a vanity number on no board) and DEATHS with it (every run
ends in one, so it was RUNS again). The period filter went too: LAST RUN is the
summary you just tapped away, and 7 DAYS is a fitness metric in a permadeath
game. The card stops repeating the ledger and keeps the CAREER instead —
league, cups won, best cup.

The world board drops CLASSIC: that mode is out of competition by its own
room's rule now, so ranking it contradicted the wall. A classic best no longer
opens the name prompt either — it would have had nowhere to go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:39:27 +02:00
wuwiandClaude Fable 5 5927664905 The difficulty screen drops its LAST too, and the class with it
It was the last place still breaking the wizard's own rule — a question screen
carries no memory of its own answer. Three names, three pip counts, nothing
else. `.last` has no users left, so the rule is gone from the stylesheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:23:40 +02:00
wuwiandClaude Fable 5 e09ce992e2 The wall drops its corner chips
LAST remembered what you played last time, which a gallery of six posters does
not need to do for you, and 18 RUNGS was a number about a mode on the screen
for choosing between modes. Both gone. The difficulty screen keeps its LAST —
there it is the only thing saying what you last set.

The dead sub-poster branch in buildSub went with them: nothing has fanned out
into sub-posters since the cup left the ZOMBIE TENNIS pocket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:17:19 +02:00
wuwiandClaude Fable 5 cb3074f99d PROJECT.md records the text reduction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:42:18 +02:00
wuwiandClaude Fable 5 3184752512 The handoff stays on disk, not in the repo
The prototype ships a copy of thirty repo assets and the zip is a second copy
again — a megabyte of duplicate art in the history for a document whose
decisions now live in PROJECT.md and in the code. The folder is ignored, not
deleted: it is still there to read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:41:50 +02:00
wuwiandClaude Fable 5 a331a1ae37 The menu text reduction: the design handoff, built
Sixty words on the wall became a dozen. The handoff's own summary of the rule:
Jersey 15 NAMES (screen titles, rooms, cards), Bangers CALLS exactly once per
screen (the fixture, the primary button, a modal headline), Silkscreen INFORMS
(numbers, labels, chips) — and a sentence survives only where it carries a
decision. Everything else moved into a bar, a pip or the modal that opens with
the mode.

  · Jersey 15 bundled subset (9 KB) and Bungee retired after one afternoon
  · SPLASH: the plaque is one row, the goal line is gone (the fixture button
    below said it already), the two doors carry a number each
  · PICK A GAME: rooms are one word, posters are their names, the fixture is a
    hero with the only second line — the whole wall fits one screen now
  · MATCH SETUP: unnumbered blocks, the meters' legend once instead of a
    description per court, no summary footer
  · DIFFICULTY: three pips instead of three sentences
  · TRAINING: the revanche is a full-width row, rungs are numbers, the list
    ends on the league that opens the rest of the ladder
  · VS: three paragraphs became YOU / HOST / JOIN
  · MODALS: one rule per row (the sub was the lead again in other words), the
    blood pool as pips on that same line, and the walk-on's two levels in ONE
    face at ONE size — a comparison drawn at two sizes is a headline with a
    footnote (Oliver)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:41:25 +02:00
wuwiandClaude Fable 5 4c0ede33db The screen titles go upright, and Bungee ships subset
10 KB instead of 119 — the face carries only the characters this game sets, and
the command to redo it is in PROJECT.md. The header strip's titles take it at
23px: a strip at the top of a screen is signage, not an announcement, so it
stops leaning. Bangers keeps what it was always for — the primary button, the
ceremonies, and everything the canvas shouts. The poster wall's rooms go back
to acid beside it, with the scale carrying the hierarchy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:48:48 +02:00
wuwiandClaude Fable 5 313f6d82bf A third face for the shelf labels: Bungee, upright
Bangers has no upright cut — the slant is in the glyphs, which is also why the
chevrons are Silkscreen — so a heading that wanted to shout without leaning had
to come from somewhere. Bungee is an arcade/signage face, upper case by design,
and it stays in-world where the platform's UI sans did not. Bundled beside the
other two with its OFL licence; the Dockerfile already copies assets wholesale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:44:10 +02:00
wuwiandClaude Fable 5 62c6509683 The shelf labels go cream, the season with them
Colour stops carrying the hierarchy on this screen — the acid belongs to the
screen title, so the rooms are cream and the canonical one is marked by its
poster's ribbon rather than by its heading's tint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:41:16 +02:00
wuwiandClaude Fable 5 579b45973a The shelf labels take the announcement face after all
Three tries: Bangers, then the HUD face, then the platform's UI face, and back
to Bangers. It works now because the header lost its rule and its left band and
gained air — the size carries the hierarchy (20 against the screen title's 26)
instead of the typeface having to. The strap goes back to Silkscreen under it:
a shouted line over a system-sans line was two foreign voices in one header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:38:31 +02:00
wuwiandClaude Fable 5 924838c9c1 The veil loses the blur, the shelf labels find a third voice
On nearest-neighbour art the backdrop blur read as a smear rather than depth,
so the caption sits on a plain gradient rising from the bottom edge instead —
solid where the words are, gone before it reaches the art.

And the section headings stop borrowing: the game bundles two faces and both
were already spoken for on this screen (Bangers is the screen title, Silkscreen
is the posters' own captions), so a shelf label in either sounded like
something else. They take the platform's interface face now — heavy, tracked,
upper case: chrome, which is what a shelf label is.

Scoped to the section HEADER while doing it. The posters live inside .mgroup
now, so the old bare descendant selector had been restyling every caption on
the wall — invisible only because it still named Silkscreen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:35:27 +02:00
wuwiandClaude Fable 5 49fc75a849 The veil takes Oliver's measures: 5px, half the card
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:31:54 +02:00
wuwiandClaude Fable 5 61652a9114 The poster captions sit on a blurred veil that comes out of nothing
The scrim used to be a hard linear-gradient riding in the button's own
background stack. It is a masked backdrop-blur now: nothing at all at its top
edge, a surface by the time it reaches the words. The art stays art where the
art is, the caption gets something to sit on, and no box edge appears anywhere.
Measured on this machine it costs 0.2ms a frame — the backdrop is static while
the menu is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:30:17 +02:00
wuwiandClaude Fable 5 3324fe9cda Every screen deals itself in, every time
The entrance is no longer a one-off on the boot: dealStage re-arms the class at
the funnel every stage change already passes, so the title screen flies its
plaque and buttons in whenever you arrive at it. The sub-menus join in — their
header, content and footer arrive one after the other instead of the panel
dropping in as one slab — and the poster wall deals its three rooms rather than
its list, since a moving child inside a moving parent starts at twice the
offset and reads as a stutter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:25:26 +02:00
wuwiandClaude Fable 5 aab83dacdc The menu deals itself in
The splash's elements fly in from the left on the boot, 70ms apart, on the same
easeOutBack overshoot the canvas modals have always used. It hangs off
:not(.busy), so taking the loading state off IS the trigger and it never plays
again — returning from a run should not make you wait for a flourish you have
seen. Reduced motion turns it off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:22:16 +02:00
wuwiandClaude Fable 5 380f98f3a3 The boot shows nothing, and every screen opens at its top
Two things the phone found. The splash's loading state was applied by main.js —
but this is a module script, so the browser paints the markup, placeholders and
all, before it runs: LEVEL 1 stood there and then snapped to the real values.
The busy class ships in index.html now and main.js only takes it off; the
keyboard hints hide with it.

And #intro is the one scroller the menus share, so a scroll earned deep in the
stats screen outlived it: going back put the splash up scrolled, with its
buttons off screen. go() resets it, at the funnel every stage change passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:19:26 +02:00
wuwiandClaude Fable 5 3d9400cd25 The season log records the button labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:14:15 +02:00
wuwiandClaude Fable 5 d3f8823987 The two doors say which is which: ENTER THE CUP, and AGAIN names its mode
"The draw" was tennis for the field of entrants, and nothing has been drawn
since the cup rounds went deterministic — Oliver had to ask what the button
meant, which settled it. Beside it, an unqualified PLAY AGAIN was an
instruction without a subject: both buttons read as "start something" and only
someone who remembered how the evening began could tell them apart. It carries
the mode now — GORE AGAIN, CLIMB AGAIN, CLASSIC AGAIN, TRAIN AGAIN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:13:08 +02:00
wuwiandClaude Fable 5 c1365cb467 The season log records the stacked doors
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:09:00 +02:00
wuwiandClaude Fable 5 da2f4cc7da The run-end's buttons stop being chips: stacked where there is height
A 100px label in a 120px half-button left ten pixels of air, which reads as a
squeezed chip. A tall screen has the room, so the two doors stack full width —
filled primary over ghost secondary, the same shape the modal footer and the
splash already use. A short landscape cannot stack, so there the box itself
grows to 344 and the halves come out comfortable instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:07:55 +02:00
wuwiandClaude Fable 5 0f2ea2b05e The season log records the measures pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:19:17 +02:00
wuwiandClaude Fable 5 e5a0d1a5f9 The overlays get their measures: narrower boxes, full-width buttons, air
Oliver's second pass over the new box language. The modal is narrower and the
button spans it; every row got real spacing and the footer its own margin, so
the end screens stop reading as cramped. The xp bar is taller and its ramp now
rides the FILL — spanning the gradient across the whole track meant an early
level only ever showed one flat green inch of it. An illustrated announcement
puts its two lines beside the picture, under the headline, instead of in a
boxed row below its own icon.

The poster wall's section headings leave the announcement face to the posters
(it read as a second card headline), drop the left band and the rule, and let
air do the grouping. On the splash, OTHER GAMES gets its gap to the stats row,
and the boot shows nothing but the court, the title and the loading bar — the
plaque skeleton was still a plaque telling a level-25 player he was level 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:15:23 +02:00
wuwiandClaude Fable 5 e2cb13e46d The season log records the sections
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:46:30 +02:00
wuwiandClaude Fable 5 25c10b46d7 The poster wall gets rooms, not labels: three real sections
Oliver: the group headings were "sehr klein". Each group is a section now with
its posters inside it — the announcement face one size under the screen title,
a rule closing the header, and an inset band down the left. The canonical room
wears the primary's colour, because it is the one that moves your career.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:40:57 +02:00
wuwiandClaude Fable 5 5c7521f867 THE SEASON, phases 1-4: the front door is the fixture, and the club remembers
The tournament is the canonical path, so the game finally says so. The splash's
primary button names the next cup fixture and enters the draw; the poster wall
moved one tap down and deals three named rooms (the season, the training, out of
competition) with the cup at the top. Two profile fields remember every attempt
— rounds ever reached per league, and the body that put you out — so a lost cup
is a line on the board instead of a null event, and the training court offers
that body first, marked. The promotion names what waits in the new league.

Overlays consolidated in the same pass: three modal drawers became one box with
a masthead, hairline-separated rows and a real footer button (any tap still
dismisses), and the run-end summary wears the same box — headline, xp, mark, and
the fixture with ENTER THE DRAW beside PLAY AGAIN. The cup's own exit is a
ceremony now: OUT IN THE QUARTER-FINAL, your best standing beside it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:14:25 +02:00
wuwiandClaude Fable 5 063b01d2a5 The season: the tournament becomes the story — the plan of record
Oliver's read: the modes feel decoupled, the menus and end screens enforce no
story-line. Settled in discussion: the tournament is the canonical path and
everything else is Beiwerk, a cup attempt is one evening (no resume, no
seeding), classic stays a side room. The spine — the splash names the next
fixture, the club remembers your attempts, the side modes are described in cup
terms, every ending hands over, and the season opens inside the promotion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 10:50:59 +02:00
wuwiandClaude Fable 5 41f1b6815f The copy audit: the climb card stops calling itself a death match, the machine gets named, the drill decrypts, the duel names both stakes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 17:25:41 +02:00
wuwiandClaude Fable 5 7ece8312f3 The tournament card speaks plainly: your league round by round, win it all, move up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 17:22:37 +02:00
wuwiandClaude Fable 5 bfb3631ec3 The rules cards get zones: boxed rows, threat colour only for the threat, the blood pool as pips
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:54:05 +02:00
wuwiandClaude Fable 5 00f121bec7 The walk-on gets its own face: mugshot masthead, the measure, the round strip — no more wall of text
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:22:20 +02:00
wuwiandClaude Fable 5 7fb595ca22 The look stops lying: the fallback never reports done, and the women's frame gets room to stand tall
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:16:54 +02:00
wuwiandClaude Fable 5 a38c804069 Training pays a quarter, and the level bar wears its endpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:58:34 +02:00
wuwiandClaude Fable 5 7d39032907 The weapon licence, the peer line, and the steward gets his vest back
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:52:51 +02:00
wuwiandClaude Fable 5 7e2649c888 The level is you, in the league: quicker legs, a wider window, and a curve that pays on the way up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 13:28:24 +02:00
wuwiandClaude Fable 5 7dc72009eb The leagues are cleared for main — the verdict is in
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:55:55 +02:00
wuwiandClaude Fable 5 7a9b61fc6b The boot gate learns about the profile: skeleton plaque, locked splash, a 4s cap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:55:00 +02:00
wuwiandClaude Fable 5 04ef49ef3b The climb's sub line breaks where it means to
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:51:34 +02:00
wuwiandClaude Fable 5 a5a681f9f2 The cup climbs: easy to hard, deterministic — the shuffle retires with its screen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:49:36 +02:00
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