Commit Graph
186 Commits
Author SHA1 Message Date
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
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 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 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 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 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 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 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 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 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 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 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 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 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 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