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:
1 parent
5e80c4e69d
commit
c0b59eee90
10 files changed
+241
-26
No files matched your search
+42
-7
@@ -14,7 +14,7 @@ import { voice, zombieDuck, aww } from './audio.js';
|
||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, advancePoint, scoreKey } from './gameplay.js';
|
||||
import { startDrillStream } from './drill.js';
|
||||
import { netClose } from './net.js';
|
||||
import { netClose, netJudging, netSendPoint, netSendDead } from './net.js'; // runtime-only cycle: net calls back into startMatch/matchReady/netMatchWon
|
||||
|
||||
// the court is mopped up (or was already clear): play advances to whatever the won game earned —
|
||||
// a changeover breather, a plain even-game beat, or (on a set) the centered SET card then a horde.
|
||||
@@ -394,7 +394,7 @@ function cupDraw() {
|
||||
// ...and G.serveNum resets HERE, because the match bypasses tennis's pointTo (the only other place
|
||||
// that resets it). Without this, the first fault of the match armed 'second serve' PERMANENTLY:
|
||||
// every missed serve after it was a double fault, a point, and blood — the death loop Oliver hit.
|
||||
function matchReady() {
|
||||
export function matchReady() { // exported for net.js: a paired death match re-places both bodies
|
||||
ball.active = false;
|
||||
G.serveNum = 1;
|
||||
if (G.match) G.match.decided = false; // the next rally may be decided early again
|
||||
@@ -587,9 +587,15 @@ 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;
|
||||
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 };
|
||||
// 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 };
|
||||
}
|
||||
// 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
|
||||
@@ -635,7 +641,12 @@ function matchPoint(side) {
|
||||
// is still talking (one man, one microphone) — and matchPoint calls scoreKey() at 0.4 s with a
|
||||
// clip over a second long, so every one of these was being swallowed silently.
|
||||
aside(G.hp <= 2 ? 'ump_low_blood' : 'ump_wound_you2', 2.2);
|
||||
if (G.hp <= 0) return killPlayer(); // eaten — the run summary, as ever
|
||||
if (G.hp <= 0) {
|
||||
// netplay: my blood is bookkept only on MY phone, so my death has to be said out loud —
|
||||
// the other phone turns it into its win screen (netMatchWon)
|
||||
if (G.net && G.net.ready) netSendDead();
|
||||
return killPlayer(); // eaten — the run summary, as ever
|
||||
}
|
||||
}
|
||||
const over = advancePoint(side); // banks the game and resets the points itself
|
||||
if (over) return side === 'P' ? matchGameWon(zb, true) : matchGameLost();
|
||||
@@ -689,7 +700,10 @@ function matchGameWon(zb, banked = false) {
|
||||
// round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew
|
||||
// once — and the plate is the better card anyway: it announces the win by replacing the loser.
|
||||
G.flash = { text: 'GAME.', t: 1.4 };
|
||||
d.opp++;
|
||||
// netplay: the SAME opponent walks back on — the body across the net is one particular friend,
|
||||
// and handing them the roster's next name and face would break the fiction (the ladder dials the
|
||||
// opp number drives are AI-only and dormant here anyway)
|
||||
if (!G.net) d.opp++;
|
||||
// ALTERNATING SERVICE, as the sport has it. The match calls advancePoint directly, which banks
|
||||
// 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).
|
||||
@@ -776,6 +790,23 @@ function cupWon() {
|
||||
G.done = { won: true, t: 0, cup: true };
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function netMatchWon() {
|
||||
if (!G.match) return;
|
||||
const d = G.match;
|
||||
d.ending = false; d.mopT = 0;
|
||||
G.interlude = false; G.matchFall = null;
|
||||
ball.active = false; G.flash = null; G.modal = null; G.card = null;
|
||||
cheer(4, 1); voice('unbelievable', 0.4);
|
||||
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;
|
||||
d.felled = score.games[0]; d.games = games; d.best = b.games;
|
||||
G.done = { won: true, t: 0 };
|
||||
}
|
||||
|
||||
function matchGameLost() {
|
||||
// Reachable only through deuce (four straight points is exactly your hp) — the body keeps its
|
||||
// wounds, the scoreboard banks their game, and the match simply plays on.
|
||||
@@ -795,6 +826,10 @@ export function resolveInterlude(side) {
|
||||
// for the picture still reaches its ordinary bounce-2 / rolled-out calls, and they land here.
|
||||
// G.matchEarly exempts the deciding resolution ITSELF — the latch goes up before it runs.
|
||||
if (G.match.decided && !G.matchEarly) { ball.active = false; return; }
|
||||
// netplay (death match): this phone judged the live flight — the verdict crosses the wire
|
||||
// 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);
|
||||
return matchPoint(side);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user