The friend across the net is no shambler: he just bleeds, and then he bursts

Two retest findings, one root: the death match borrowed the roster's
body. Gone — nothing spawns, no walk-on card, the living opponent is the
whole picture. His damage reads as blood (bleedO: an anchor per point
taken this game, love to game point, thin drops as ever), the game
point bursts him where he stands, and IT RISES AGAIN brings him back
clean. The heal beat became a card in netplay — a modal freezes this
world while the other phone's keeps moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 18:10:50 +02:00
1 parent c0b59eee90
commit 4c47295a73
6 files changed
+95 -68

No files matched your search

+29 -13
View File
@@ -575,6 +575,17 @@ function tickMatchServe(dt) {
}
function matchOpponent() {
// NETPLAY: THE OPPONENT IS THE FRIEND, NOT A ROSTER BODY (Oliver: "it should be the normal
// opponent"). Nothing spawns, nobody is renamed, no walk-on card — the living O carries the
// whole picture and the score reads on it as BLOOD (bleedO in zombies.js), because the human
// sprite has no wound skins to climb. This beat only returns the body after a burst and
// re-arms the intro bar.
if (G.net) {
G.hideO = false; // the burst took him; the walk-on returns him
G.flash = { text: 'IT RISES AGAIN.', t: 1.8 };
G.interlude = { introT: 1.6 };
return;
}
spawnRacketZombie();
const zb = zombies.find(z => z.variant === 'r');
zb.dueled = true; // wakes racketAI, same as a fresh duel
@@ -587,15 +598,9 @@ function matchOpponent() {
// 4.0 s: at 2.4 the head + name + punchline were gone before they were read ("the intro card
// of the player should last longer"). In the cup the round names itself on the third line.
const cup = G.match.cup;
// netplay: the same body keeps coming back, and the identical name card twice running reads as
// a stuck HUD (the praise-word lesson). The first walk-on introduces; every return is a flash.
if (G.net && G.match.introduced) G.flash = { text: 'IT RISES AGAIN.', t: 1.8 };
else {
G.match.introduced = true;
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 4.0, head: zb.fam,
sub2: cup ? CUP_ROUNDS[cup.round].name + (CUP_ROUNDS[cup.round].need > 1
? ' · FIRST TO ' + CUP_ROUNDS[cup.round].need + ' GAMES' : '') : undefined };
}
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 4.0, head: zb.fam,
sub2: cup ? CUP_ROUNDS[cup.round].name + (CUP_ROUNDS[cup.round].need > 1
? ' · FIRST TO ' + CUP_ROUNDS[cup.round].need + ' GAMES' : '') : undefined };
// a FLAT pool per body: with regicide gone the body is pure scoreboard (matchPoint takes a
// game's-quarter per point), so a deeper pool would change nothing — the ladder escalates in
// racketAI instead (matchSpeedPer / matchFlubKeep). THE CUP MULTIPLIES BY THE ROUND'S NEED and
@@ -668,9 +673,14 @@ function matchPoint(side) {
function matchHealBeat() {
const healed = Math.min(G.hpMax, G.hp + TUNE.matchHeal) - G.hp;
G.hp += healed;
if (healed > 0)
showModal({ title: 'WOUNDS TREATED', icon: 'x_cross', iconH: 52, color: UI.pick,
body: ['THE TRAINER PATCHED YOU UP', G.hp + ' OF ' + G.hpMax + ' BACK'] });
if (healed > 0) {
// NETPLAY: a CARD, never the modal — a modal freezes THIS world while the other phone's keeps
// moving, and an unread popup would hold the opponent's next serve hostage (the popup policy's
// live-court rule, applied to a court that is live on someone else's screen).
if (G.net) G.card = { title: 'WOUNDS TREATED', sub1: G.hp + ' OF ' + G.hpMax + ' BACK', color: UI.pick, t: 3.0 };
else showModal({ title: 'WOUNDS TREATED', icon: 'x_cross', iconH: 52, color: UI.pick,
body: ['THE TRAINER PATCHED YOU UP', G.hp + ' OF ' + G.hpMax + ' BACK'] });
}
}
// The game-winning point fells the body but no longer bursts it on the same frame: the KILL FALL
@@ -708,6 +718,10 @@ function matchGameWon(zb, banked = false) {
// the game but never touches the server — that flip lives in tennis's own pointTo, which the
// match bypasses (the same shape as the G.serveNum bug this file already carries a note about).
score.server = other(score.server);
// 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; }
// Everything past the burst waits for it — the heal card, the boy, the walk-on clock.
d.afterFall = () => {
matchHealBeat();
@@ -830,7 +844,9 @@ export function resolveInterlude(side) {
// before it is applied, exactly as pointTo does for classic. netJudging() is false while a
// remote verdict is being applied, which is what stops the echo.
if (netJudging()) netSendPoint(side, null);
if (!zombies.some(z => z.variant === 'r') && !G.match.ending) return matchGameWon(null);
// the empty-court check reads "no body = regicide" — meaningless in netplay, where the court
// NEVER carries a body (the friend is the living O) and every point would read as a game
if (!G.net && !zombies.some(z => z.variant === 'r') && !G.match.ending) return matchGameWon(null);
return matchPoint(side);
}
G.interlude = false;