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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>