The dead club doubles its staff, and the draw is made where you can see it

Six new opponent families (head coach, stringer, strawberry seller, chair
umpire, spectator, roller man) through the famroll/zsheet route, placed by
hierarchy into an 18-rung ladder; the champion steps up behind them.

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-15 11:49:19 +02:00
1 parent 4c47295a73
commit 9cc3ac54b3
207 files changed
+632 -81

No files matched your search

+23 -8
View File
@@ -545,6 +545,12 @@ export function advancePoint(side) { // shared scoring math; returns whether a g
return false;
}
// A DECIDED FLIGHT. Everywhere but netplay the ball stops dead where it always has; over the wire
// it keeps its physics and rolls to a natural halt, because the striker's phone is a whole court
// and a latency away from the call and freezing its ball mid-roll read as the game loop stopping
// (Oliver, 2026-08-15). `ball.scored` is what switches off every scoring consequence downstream.
const endFlight = () => { if (G.net) ball.scored = true; else ball.active = false; };
export function pointTo(side, reason) { // exported for net.js: a remote verdict lands here
// netplay: this phone judged the live flight — the verdict goes over the wire before it is
// applied, so both scoreboards move on the same call. netJudging() is false while a REMOTE
@@ -1005,6 +1011,7 @@ function matchOutOfReachFlight(rz, pred) {
function onBounce() {
ball.bounces++;
pock(0.35, 500);
if (ball.scored) return; // a called ball still bounces and still sounds; it just cannot score twice
// a deferred praise word that met no body lands with the ball — the DRAW was still perfect
if (ball.praise) { popText(ball.x, 0.3, ball.z, ball.praise); ball.praise = null; }
if (G.mode === 'splatter' && ball.bomb && ball.bounces === 1) { // a bomb that missed everyone still blows up
@@ -1022,7 +1029,7 @@ function onBounce() {
if (G.net && ball.lastHit === 'P') return;
if (ball.bounces === 1) {
const okSide = ball.lastHit === 'P' ? ball.z < COURT.netZ : ball.z > COURT.netZ;
if (!inCourt(ball.x, ball.z) || !okSide) { ball.active = false; resolveInterlude(other(ball.lastHit)); voice('out'); }
if (!inCourt(ball.x, ball.z) || !okSide) { endFlight(); resolveInterlude(other(ball.lastHit)); voice('out'); }
// THE FALL BEATS THE BALL (Oliver: "the zombie falls after the ball is lost although we know
// this earlier — it could play out as ball lost BECAUSE of the falling"). A winner that just
// bounced in and is provably beyond the body's reach has already decided the point, so it
@@ -1047,7 +1054,7 @@ function onBounce() {
resolveInterlude('P');
G.matchEarly = false;
}
} else if (ball.bounces === 2) { ball.active = false; resolveInterlude(ball.lastHit); }
} else if (ball.bounces === 2) { endFlight(); resolveInterlude(ball.lastHit); }
return;
}
if (G.mode !== 'tennis') return; // no lines, no faults — the umpire has other concerns
@@ -1059,16 +1066,16 @@ function onBounce() {
if (ball.bounces === 1) {
const wasServe = G.wasServe; G.wasServe = false;
if (wasServe) {
if (!inServeBox(ball.x, ball.z)) { ball.active = false; fault(); }
if (!inServeBox(ball.x, ball.z)) { endFlight(); fault(); }
return;
}
// must land on the side opposite the hitter
const okSide = ball.lastHit === 'P' ? ball.z < COURT.netZ : ball.z > COURT.netZ;
if (!inCourt(ball.x, ball.z) || !okSide) {
ball.active = false; pointTo(other(ball.lastHit), 'out');
endFlight(); pointTo(other(ball.lastHit), 'out');
}
} else if (ball.bounces === 2) {
ball.active = false; pointTo(ball.lastHit, null);
endFlight(); pointTo(ball.lastHit, null);
}
}
@@ -1845,7 +1852,14 @@ export function update(dt) {
// (predictLanding is exact), so a ball that will bounce in and is provably beyond the body's
// reach resolves MID-FLIGHT — the collapse starts while the ball is still in the air. The
// bounce-1 hook in onBounce stays as the fallback for anything too close to call up here.
if (G.match && !G.match.ending && !G.match.decided && G.phase === 'rally'
// !G.net for the same reason its bounce-1 sibling carries it, and MISSING IT WAS THE DEATHMATCH
// DESYNC ("both players stand on the same side and nobody can serve", cellular): this predicts
// the CPU's legs, and a human on another phone is not predictable. Worse, it made the STRIKER a
// judge of its own outbound flight — resolveInterlude sends the verdict — so on a slow link both
// phones scored the same rally, the two scoreboards drifted a point apart, and setupPoint then
// placed each P on its own deuce/ad side while each waited for the other to serve. The one rule
// is that the receiving phone judges; this was the one place still breaking it.
if (G.match && !G.net && !G.match.ending && !G.match.decided && G.phase === 'rally'
&& ball.active && ball.lastHit === 'P' && ball.vz < 0 && ball.z < COURT.netZ - 0.3
&& matchFallWillPlay()) {
const arz = zombies.find(z => z.variant === 'r');
@@ -1875,7 +1889,8 @@ export function update(dt) {
onBounce();
if (ball.active && Math.abs(ball.vy) < 0.8) { // rolled out — dead ball
ball.active = false;
if (G.mode === 'splatter' && G.interlude) { // a roller nobody returned: point to the hitter
if (ball.scored) { /* already called: this IS the natural halt it was left rolling for */ }
else if (G.mode === 'splatter' && G.interlude) { // a roller nobody returned: point to the hitter
if (ball.lastHit === 'X') spentBall(); // spent on the boss's body — no point, re-serve the duel
else resolveInterlude(ball.lastHit);
} else if (G.mode === 'splatter') { // hand the server a fresh one, fast — they're coming
@@ -1885,7 +1900,7 @@ export function update(dt) {
}
}
// a racket zombie's return that gets past you (crosses your baseline) is a point for THEM
if (G.mode === 'splatter' && ball.active && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
if (G.mode === 'splatter' && ball.active && !ball.scored && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
ball.active = false;
if (G.interlude) resolveInterlude('O'); // sudden death: its passing shot ends the duel
else { if (!G.horde && !G.clearing) splatPoint('O'); spentBall(); } // clearing = mop-up, no scoring either way