The staff learn your habits, the walk-on waits for your tap, and the club opens a practice court
Four answers to one phone report: - THE READ: repeat a lane and the opponent anticipates it; break the habit and it is wrong-footed for that ball — scaled by rung, so surprise is the weapon that opens the ladder past the HEAD COACH (TUNE block THE READ). - The opponent walk-on is a real modal now (head, punchline, tap to dismiss) in arcade, tournament and training — readable without a clock. - THE TRAINING COURT: third card under ZOMBIE TENNIS, pick any rung, one game at a time, the felled body walks straight back on, nothing banks, bleeding out is a popup instead of a death. mode_train.png rolled in the banner style. - MOVEMENT READS: sprinting feet kick per-court dust and fast bodies leave a two-ghost speed streak (trailMin gates both; feel verdict pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a42dc6fc81
commit
43784c5b33
9 files changed
+360
-43
No files matched your search
+51
-22
@@ -356,9 +356,13 @@ export function watchdog(dt) {
|
||||
// `draw` is the bracket screen's settled rungs, one per CUP_ROUNDS entry (main.buildBracket). It is
|
||||
// optional and null means "roll each round as it comes", which is what the mode did before the
|
||||
// screen existed and is still what the #cup back door gets.
|
||||
export function startMatch(cup = false, draw = null) {
|
||||
// `train` (2026-08-28) is a RUNG NUMBER and turns the run into the TRAINING COURT: that one
|
||||
// opponent, one game at a time, nothing banked. The ladder is parked — d.opp never advances, a
|
||||
// felled body walks straight back on, and bleeding out costs a modal instead of the run. All of
|
||||
// it rides the ordinary match machinery behind the d.train flag; see matchGameWon / matchPoint.
|
||||
export function startMatch(cup = false, draw = null, train = 0) {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
|
||||
G.match = { opp: 1, ending: false };
|
||||
G.match = { opp: train || 1, ending: false, train: !!train };
|
||||
// THE TOURNAMENT rides the whole match machinery and differs in exactly three places: who walks
|
||||
// on (a rung DRAWN from the round's band — cupDraw sets d.opp, so every ladder dial downstream
|
||||
// scales off the drawn rung untouched), how deep the body's pool is (matchOpponent multiplies
|
||||
@@ -372,16 +376,19 @@ export function startMatch(cup = false, draw = null) {
|
||||
zombies.length = 0;
|
||||
G.boy = null;
|
||||
score.pts = [0, 0];
|
||||
matchOpponent();
|
||||
matchReady();
|
||||
zombieDuck(true);
|
||||
voice('play_resume', 0.2);
|
||||
if (cup && !G.seen.cup) {
|
||||
// The mode's rules modal fires BEFORE matchOpponent, so it lands FIRST and the opponent's own
|
||||
// walk-on modal queues behind it (showModal's modalQ keeps call order) — rules, then the body.
|
||||
if (train && !G.seen.train) {
|
||||
G.seen.train = true;
|
||||
showModal({ title: 'THE TRAINING COURT', color: UI.hot,
|
||||
body: ['ONE BODY, ONE GAME AT A TIME',
|
||||
'FELL IT — OR BLEED OUT — AND IT COMES BACK', 'NOTHING IS BANKED HERE'] });
|
||||
} else if (cup && !G.seen.cup) {
|
||||
G.seen.cup = true;
|
||||
showModal({ title: 'THE TOURNAMENT', color: UI.hot,
|
||||
body: ['FOUR ROUNDS UP THE DEAD HIERARCHY',
|
||||
'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] });
|
||||
} else if (!cup && !G.seen.match) {
|
||||
} else if (!cup && !train && !G.seen.match) {
|
||||
G.seen.match = true;
|
||||
// Oliver's copy direction: name the stake, count the blood. TUNE.matchPlayerHp is the real
|
||||
// number, so the modal can never drift from the pool it describes.
|
||||
@@ -389,6 +396,10 @@ export function startMatch(cup = false, draw = null) {
|
||||
body: ['WIN THE POINT — WOUND THE ZOMBIE',
|
||||
'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] });
|
||||
}
|
||||
matchOpponent();
|
||||
matchReady();
|
||||
zombieDuck(true);
|
||||
voice('play_resume', 0.2);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -418,6 +429,8 @@ export function matchReady() { // exported for net.js: a paired death match re-p
|
||||
ball.active = false;
|
||||
G.serveNum = 1;
|
||||
if (G.match) G.match.decided = false; // the next rally may be decided early again
|
||||
// ...and THE READ's one-ball states die with the point: a lean only lasts the flight it answered
|
||||
if (G.match) G.match.surprised = G.match.expected = false;
|
||||
setupPoint();
|
||||
const rz = zombies.find(z => z.variant === 'r');
|
||||
// THE OPPONENT STANDS WHERE THE POINT PUTS IT: on its receiving mark, or behind its own baseline
|
||||
@@ -627,13 +640,18 @@ function matchOpponent() {
|
||||
// so killZombie can file the kill under it without asking G which opponent we were on.
|
||||
zb.foe = matchFoe(G.match.opp);
|
||||
zb.fam = zb.foe.fam; // ...and the body it wears (config.ZFAMS)
|
||||
// the walk-on card wears the opponent's own head (config.HEAD_CROP — a crop, not new art).
|
||||
// 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.
|
||||
// THE WALK-ON IS A MODAL NOW, not a timed card (2026-08-28, Oliver: "als richtiges Modal, das
|
||||
// man wegtappen muss — you can actually read it without time pressure, and tap it away fast when
|
||||
// you have read it a hundred times"). The walk-on is a guaranteed-calm court (the mop beat has
|
||||
// just ended, the intro bar is armed), so the popup policy allows it; update()'s freeze holds the
|
||||
// 1.6 s intro beat until the tap, and the head wears the same z_<fam>head crop the card wore.
|
||||
// In the cup the round names itself on the second 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 };
|
||||
showModal({ title: zb.foe.name, color: UI.hot, icon: 'z_' + zb.fam + 'head', iconH: 64,
|
||||
body: [zb.foe.line].concat(cup ? [CUP_ROUNDS[cup.round].name + (CUP_ROUNDS[cup.round].need > 1
|
||||
? ' · FIRST TO ' + CUP_ROUNDS[cup.round].need + ' GAMES' : '')] : []) });
|
||||
// ...and a fresh body has read NOTHING yet: the habit tracker starts clean (see THE READ, config)
|
||||
G.match.laneRun = 0; G.match.lastLane = undefined; G.match.surprised = G.match.expected = false;
|
||||
// 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
|
||||
@@ -680,10 +698,18 @@ function matchPoint(side) {
|
||||
// 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) {
|
||||
// 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
|
||||
// THE TRAINING COURT HAS NO DEATH IN IT. Bleeding out costs a modal and a fresh pool — the
|
||||
// point still goes to the body (advancePoint below), so a game you were losing stays lost.
|
||||
if (d.train) {
|
||||
G.hp = G.hpMax;
|
||||
showModal({ title: 'IT BLED YOU OUT', color: UI.bad,
|
||||
body: ['THE TRAINER MOPS YOU UP — GO AGAIN', 'NOTHING IS BANKED HERE'] });
|
||||
} else {
|
||||
// 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
|
||||
@@ -737,7 +763,7 @@ function matchGameWon(zb, banked = false) {
|
||||
// REGICIDE arrives here without advancePoint having banked anything (the body died mid-game, not
|
||||
// on a game point) — bank the game by hand or the scoreboard drifts from the ladder.
|
||||
if (!banked) { score.games[0]++; score.pts = [0, 0]; }
|
||||
G.runGames++; // the ladder metric, same as every mode
|
||||
if (!d.train) G.runGames++; // the ladder metric — training banks nothing
|
||||
voice('game_player', 0.3); cheer(2.4, 0.6);
|
||||
// GAME is the FLASH and the next opponent's name plate is the CARD. It used to be the other way
|
||||
// round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew
|
||||
@@ -745,8 +771,9 @@ function matchGameWon(zb, banked = false) {
|
||||
G.flash = { text: 'GAME.', t: 1.4 };
|
||||
// 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++;
|
||||
// opp number drives are AI-only and dormant here anyway). Training parks the ladder the same
|
||||
// way: the rung you picked is the rung that walks back on, as many times as you want it.
|
||||
if (!G.net && !d.train) 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).
|
||||
@@ -772,7 +799,9 @@ function matchGameWon(zb, banked = false) {
|
||||
}
|
||||
// Everything past the burst waits for it — the heal card, the boy, the walk-on clock.
|
||||
d.afterFall = () => {
|
||||
matchHealBeat();
|
||||
// training resets the blood WITHOUT the WOUNDS TREATED ceremony — every game starts whole,
|
||||
// and a popup between every single rep is exactly the tap-tax the mode exists to avoid
|
||||
if (d.train) G.hp = G.hpMax; else matchHealBeat();
|
||||
// ...and the boy comes out to deal with what four wound stages left on the far court. Deliberately
|
||||
// the FAR side: that is where the body has been standing and bleeding, and it keeps him out of the
|
||||
// near court where the player is about to serve. Between games only — see sendBoy's match note.
|
||||
|
||||
Reference in new issue
Block a user