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:
1 parent
c0b59eee90
commit
4c47295a73
6 files changed
+95
-68
No files matched your search
+29
-13
@@ -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;
|
||||
|
||||
+8
-22
@@ -79,10 +79,6 @@ export function netApplyShot(m) {
|
||||
G.phase = 'rally'; G.swung = false; G.rallyNear = null;
|
||||
O.x = s.x; O.z = s.z; // the strike happened where the ball says it did
|
||||
O.pose = m.serve ? 'serve' : 'swing'; O.poseT = 0;
|
||||
if (G.net.death) { // ...and in a death match the PICTURE is the zombie, not O
|
||||
const rz = zombies.find(z => z.variant === 'r');
|
||||
if (rz) { rz.x = s.x; rz.z = s.z; if (m.serve) rz.serveHold = 0.4; }
|
||||
}
|
||||
}
|
||||
|
||||
function netApplyPoint(m) {
|
||||
@@ -117,7 +113,14 @@ function netStart(side, death) {
|
||||
G.classic = false;
|
||||
startMatch();
|
||||
G.net = n;
|
||||
G.modal = null; G.modalQ.length = 0; // the DEATH MATCH explainer must not eat the first serve
|
||||
// THE OPPONENT IS THE FRIEND, NOT A ROSTER BODY: startMatch walked THE SHAMBLER on (spawn +
|
||||
// name card) because it could not see the swap — take both back. The living O is the whole
|
||||
// picture (render draws him whenever hideO is false, splatter included — the bench proved it),
|
||||
// and the score reads on him as blood (bleedO), not as wound skins he does not have.
|
||||
zombies.length = 0;
|
||||
G.hideO = false;
|
||||
G.card = null;
|
||||
G.modal = null; G.modalQ.length = 0; // ...and the DEATH MATCH explainer must not eat the first serve
|
||||
}
|
||||
score.server = side === 'A' ? 'P' : 'O'; // the host serves first, on both scoreboards
|
||||
score.pts = [0, 0];
|
||||
@@ -169,23 +172,6 @@ export function netTick(dt) {
|
||||
O.x += (tx - O.x) * k; O.z += (tz - O.z) * k;
|
||||
if (O.pose !== 'swing' && O.pose !== 'serve') O.pose = o.m ? 'run' : 'idle';
|
||||
if (o.m) O.runT += dt;
|
||||
// DEATH MATCH: the body on screen is the racket zombie; O is only the serve-machinery ghost it
|
||||
// always is in a match. The zombie shadows O's driven position — stride bookkeeping like the
|
||||
// bench's walk, so the frames step off ground covered — except while the fall or a named serve
|
||||
// frame owns the picture. vx is recorded so a fall inherits the run, clamped against snaps.
|
||||
if (n.death && !G.matchFall) {
|
||||
const rz = zombies.find(z => z.variant === 'r');
|
||||
if (rz && !rz.showFrame) {
|
||||
const dx = O.x - rz.x, step = Math.hypot(dx, O.z - rz.z);
|
||||
if (step > 0.03) {
|
||||
rz.dist = (rz.dist || 0) + step;
|
||||
rz.vx = Math.max(-3, Math.min(3, dx / Math.max(dt, 1e-3)));
|
||||
if (Math.abs(dx) > 0.02) rz.face = dx > 0 ? 1 : -1;
|
||||
rz.pose = rz.pose === 'swing' ? 'swing' : 'run';
|
||||
} else { rz.pose = rz.pose === 'swing' ? 'swing' : 'idle'; rz.vx = 0; }
|
||||
rz.x = O.x; rz.z = O.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-1
@@ -4,7 +4,7 @@
|
||||
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
|
||||
|
||||
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
|
||||
import { G, P, ball, score, sendBoy, setupPoint } from './state.js';
|
||||
import { G, P, O, ball, score, sendBoy, setupPoint } from './state.js';
|
||||
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame } from './audio.js';
|
||||
import { drillKill, drillCaught } from './drill.js';
|
||||
import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js';
|
||||
@@ -921,6 +921,24 @@ function gushProne(zb) {
|
||||
}
|
||||
}
|
||||
|
||||
// NETPLAY DEATH MATCH: the opponent is the LIVING O — a human sprite with no wound skins to climb
|
||||
// (Oliver: "the normal opponent does not have wound states — this was the reason I said increased
|
||||
// dynamic bleeding"). So the score reads on him as BLOOD: each point taken off him this game opens
|
||||
// another anchor and thickens the flow, love → game point, and the burst (matchGameWon) resets it
|
||||
// with the body. Same thin-drop discipline as bleed() — more EMISSIONS, never heavier drops.
|
||||
const O_BLEED = [[0.12, 1.55, 0.9], [-0.22, 1.2, 0.8], [0.16, 0.9, 0.7]];
|
||||
function bleedO() {
|
||||
const k = Math.min(3, score.pts[0]); // points off them THIS game
|
||||
if (!k || G.hideO || parts.length > 120) return;
|
||||
for (const w of O_BLEED.slice(0, k)) {
|
||||
if (Math.random() > 0.14 * k) continue; // ramps: one light nick → gushing at game point
|
||||
parts.push({
|
||||
x: O.x + w[0] + (Math.random() * 2 - 1) * 0.04, y: w[1] + (Math.random() * 2 - 1) * 0.04, z: O.z + (Math.random() * 2 - 1) * 0.04,
|
||||
vx: (Math.random() * 2 - 1) * 0.35, vy: w[2] * (0.6 + Math.random() * 0.5), vz: (Math.random() * 2 - 1) * 0.3,
|
||||
t: 0.3 + Math.random() * 0.3, dark: Math.random() < 0.55 });
|
||||
}
|
||||
}
|
||||
|
||||
function bleed(zb) {
|
||||
if (parts.length > 90) return;
|
||||
let wounds;
|
||||
@@ -962,6 +980,7 @@ export function updateParts(dt) {
|
||||
export function updateZombies(dt) {
|
||||
moanT -= dt;
|
||||
if (moanT <= 0) { moan(); moanT = 2.5 + Math.random() * 4; }
|
||||
if (G.net && G.net.death && G.match) bleedO(); // the friend's damage, read as blood (see bleedO)
|
||||
|
||||
if (G.mode === 'splatter') { // transition spawns are scripted, not timed
|
||||
G.splatT += dt;
|
||||
|
||||
Reference in new issue
Block a user