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>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-22 12:58:48 +02:00
1 parent 6e07dc74bb
commit 4f4534e272
9 files changed
+355 -38

No files matched your search

+35 -3
View File
@@ -499,6 +499,7 @@ export function tickMatchFall(dt) {
// 'over' beat (see the section note above). When the boy's time is up the next opponent walks
// on with its own intro card, exactly as it used to on the kill frame.
const d = G.match;
if (d && d.winT > 0 && (d.winT -= dt) <= 0) { d.winT = 0; return netMatchDone(); }
if (d && d.mopT > 0 && (d.mopT -= dt) <= 0) {
// Time is up — but THE COURT IS HIS UNTIL HE LEAVES IT (Oliver: "no playing as long as the
// mopboy is on court"). He is ordered off (jobs zeroed, the half-pushed stain dropped) and
@@ -753,7 +754,22 @@ function matchGameWon(zb, banked = false) {
// NETPLAY: the human body has no fall art to play, so the game-winning point bursts him where
// he stands (procedural gore is the one sanctioned fake) and the mop beat runs over the gap;
// matchOpponent's net branch un-hides him when the beat hands the court back.
if (G.net) { explode(O.x, O.z); G.hideO = true; }
// NETPLAY DEATH MATCH: A GAME IS NOT A DEATH, AND ONLY BLOOD IS (Oliver, 2026-08-22: "each player
// has 8 blood points but the player explodes for the winner already at 0:40 — the winner sees the
// zombie explode but the player on the other side is not dead yet. this needs to be aligned").
// Two clocks were running: a GAME is four points, a life is eight, so the body burst on this
// phone while the friend was still standing on theirs with half a pool left. The burst moves to
// the only event both phones agree is death — their blood running out, said out loud over the
// wire (netSendDead -> netMatchWon). A won game keeps everything it is worth: the flash, the
// serve, and the heal it pays. Solo is untouched — there the body IS the scoreboard.
if (G.net) {
matchHealBeat();
d.ending = false; // nothing is walking on: play resumes here
matchReady();
G.interlude = { introT: 1.2 }; // a breath before the next serve, as ever
G.phase = 'over'; G.overT = 2.2;
return;
}
// Everything past the burst waits for it — the heal card, the boy, the walk-on clock.
d.afterFall = () => {
matchHealBeat();
@@ -839,13 +855,29 @@ function cupWon() {
// NETPLAY'S WIN ENDING: the other phone said 'dead' — their player bled out, so this side is the
// last one standing. The classic win's whole shape (held summary, gated tap), banked NOT-dead
// through the same endRun as every match run; felled = the games that actually burst the body.
// THE FRIEND'S BLOOD RAN OUT — the one event that ends a death match, and now the only one that
// bursts him. It arrives as a message from their phone (they bookkeep their own hp), which is why
// this is the honest place for the burst: both sides are agreeing about the same death.
export function netMatchWon() {
if (!G.match) return;
if (!G.match || G.match.winT > 0) return; // already ending
const d = G.match;
d.ending = false; d.mopT = 0;
G.interlude = false; G.matchFall = null;
G.matchFall = null;
ball.active = false; G.flash = null; G.modal = null; G.card = null;
explode(O.x, O.z); G.hideO = true;
cheer(4, 1); voice('unbelievable', 0.4);
// ...AND THE SUMMARY WAITS FOR THE GORE. update() freezes the whole world on G.done — no
// particles, no parts — so setting it on the same frame as the burst would hang a still frame of
// an explosion behind the trophy screen. The beat is ticked in tickMatchFall, like every other
// beat in this file that has to outlive the frame that started it.
d.winT = TUNE.netWinBurst;
G.interlude = { introT: TUNE.netWinBurst + 1 }; // nothing gets served over a body coming apart
G.phase = 'over'; G.overT = TUNE.netWinBurst + 1;
}
function netMatchDone() {
const d = G.match;
G.interlude = false;
const games = score.games[0] + score.games[1];
const b = endRun(score.games[0], games, G.splatT, false, MODE_MATCH);
G.best = b; G.newBest = b.newBest;