The host names the stakes: classic tennis, or the death match

Three chips under VS A FRIEND — the host's pick rides the room and the
start message rebuilds both courts on pairing. In the death match your
opponent IS the zombie: their lost points tear its wound stages, their
won points draw your blood, and bleeding out crosses the wire as the
other phone's win. The same receiver-is-judge verdicts drive the match
machinery through resolveInterlude, so the fall, the mop beat and the
burst come along unmodified. New: gushProne — the game-winning body
bleeds out on the ground, from prone-height anchors, until it bursts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 18:02:44 +02:00
1 parent 5e80c4e69d
commit c0b59eee90
10 files changed
+241 -26

No files matched your search

+7 -2
View File
@@ -1017,6 +1017,9 @@ function onBounce() {
// below read 'X' as HIS ball — bounce out and you were handed the duel, bounce twice in-court
// and you LOST it for hitting him. Same rule as the dead-roller path in update().
if (ball.lastHit === 'X') { spentBall(); return; }
// NETPLAY (death match): same rule as the tennis block below — the receiving phone is the only
// judge of a flight; our outbound ball is picture and its verdict arrives as a message.
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'); }
@@ -1035,7 +1038,9 @@ function onBounce() {
// from before"). No more ordering proofs: the latch goes up BEFORE, the resolution in
// progress is exempted via G.matchEarly in resolveInterlude's swallow, and any path that
// re-arms play clears it in matchReady. Stale by construction impossible.
else if (G.match && !G.match.ending && !G.match.decided && ball.lastHit === 'P'
else if (G.match && !G.net && !G.match.ending && !G.match.decided && ball.lastHit === 'P'
// !G.net: the early decide predicts the CPU's legs — a human on another phone is
// not predictable, and the receiver is the only judge of this flight anyway
&& matchFallWillPlay() && matchOutOfReach()) {
G.match.decided = true; // scope: the REMAINING ball events of this rally
G.matchEarly = true; // startFall leaves the ball rolling; the swallow exempts us
@@ -1804,7 +1809,7 @@ export function update(dt) {
if (G.mode === 'splatter' && !G.horde)
// ...and not while the body is on the ground: the fall owns the sprite AND the pose for its
// whole run, and racketAI would put a collapsing corpse back on its feet to chase a stray ball.
if (!G.bench && !G.matchFall) for (const rz of zombies) if (rz.variant === "r") racketAI(rz, dt); // the bench drives its own body
if (!G.bench && !G.matchFall && !G.net) for (const rz of zombies) if (rz.variant === "r") racketAI(rz, dt); // the bench drives its own body; netplay's body is the other phone (netTick)
// --- ball physics
if (ball.active) {