THE TOURNAMENT, the pace-lit trail, and brighter opponents
The fourth mode: four rounds up the dead hierarchy (CUP_ROUNDS), each round drawing a rung from its band, the champion always the final. It rides the match machinery whole — cupDraw feeds the drawn rung through d.opp so every ladder dial scales untouched; the body's pool spans the round (need-scaled pool and divisor, so wounds carry between games); cupGameWon holds the same body mid-round, kills it on the round, and past the final cupWon banks a NOT-dead match run under CHAMPION OF THE DEAD CLUB. Blood carries; round one is a single game; deathmatch stays as the arcade run. Full-run state test. Sidequests: the ghost trail is pace-qualified (TUNE.trailSpeed) so both tennis modes streak — pow>0.8 was only ever reached by the gore slingshot; opponent sprites draw ~18% brighter via a load-time baked copy (render.brighten, TUNE.oppBright) with no PNG touched; the cup scoreboard line names the round instead of lying with deathmatch arithmetic; the rack and the apron demo no longer draw under a trophy screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
098aefeb63
commit
f9b26e5380
8 files changed
+202
-22
No files matched your search
+92
-10
@@ -7,7 +7,8 @@
|
||||
// (beats <-> gameplay and beats <-> zombies are runtime-only import cycles, same pattern as
|
||||
// input <-> gameplay: the imported bindings are only touched inside functions, never at load.)
|
||||
|
||||
import { TUNE, UI, bossArt, matchFoe, BENCH, ZFAMS, COURT, HOME, racketStage } from './config.js';
|
||||
import { TUNE, UI, bossArt, matchFoe, BENCH, ZFAMS, COURT, HOME, racketStage, CUP_ROUNDS } from './config.js';
|
||||
import { endRun, MODE_MATCH } from './stats.js';
|
||||
import { G, P, O, ball, score, setupPoint, sendBoy, other } from './state.js';
|
||||
import { voice, zombieDuck, aww } from './audio.js';
|
||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||
@@ -320,9 +321,14 @@ export function watchdog(dt) {
|
||||
// the match mode ends. Regicide is inherited for free: smashing the ball into the body still wounds
|
||||
// it, and killing it outright wins the game early (killZombie fires resolveInterlude('P'), which
|
||||
// lands in matchGameWon via the empty-court check).
|
||||
export function startMatch() {
|
||||
export function startMatch(cup = false) {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return false;
|
||||
G.match = { opp: 1, ending: false };
|
||||
// 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
|
||||
// by the round's `need`), and where a won game goes (cupGameWon). See config.CUP_ROUNDS.
|
||||
if (cup) { G.match.cup = { round: 0, g: 0 }; cupDraw(); }
|
||||
G.matchFall = null;
|
||||
G.hp = G.hpMax = TUNE.matchPlayerHp; // a real match is longer than four mistakes — the gore
|
||||
// mode's 4 was "too weak" here, felt not calculated
|
||||
@@ -335,7 +341,12 @@ export function startMatch() {
|
||||
matchReady();
|
||||
zombieDuck(true);
|
||||
voice('play_resume', 0.2);
|
||||
if (!G.seen.match) {
|
||||
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) {
|
||||
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.
|
||||
@@ -346,6 +357,14 @@ export function startMatch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The round's rung, drawn from its band. d.opp carries it so matchFoe, the ladder speed dials and
|
||||
// the felled arithmetic all read the drawn rung with no cup-awareness of their own.
|
||||
function cupDraw() {
|
||||
const cup = G.match.cup, R = CUP_ROUNDS[cup.round];
|
||||
cup.rung = R.band[Math.floor(Math.random() * R.band.length)];
|
||||
G.match.opp = cup.rung;
|
||||
}
|
||||
|
||||
// Every point starts like a real one: the server behind his line (setupPoint's tennis branch) and
|
||||
// THE RECEIVER ON ITS MARK — the zombie is dropped onto the receiver position the tennis branch
|
||||
// just computed for O, instead of standing wherever the last rally abandoned it. Oliver: "it spawns
|
||||
@@ -530,12 +549,17 @@ function matchOpponent() {
|
||||
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")
|
||||
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 4.0, head: zb.fam };
|
||||
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
||||
// scoreboard (a quarter per point), so a deeper pool would
|
||||
// change nothing — the ladder escalates in racketAI instead
|
||||
// (matchSpeedPer / matchFlubKeep)
|
||||
// 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 };
|
||||
// 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
|
||||
// matchPoint divides the same way, so the wound stages spread across the whole round instead of
|
||||
// being spent in game one — the body enters game two already missing what game one cost it.
|
||||
zb.hp = zb.hpMax = TUNE.matchOppHp * (cup ? CUP_ROUNDS[cup.round].need : 1);
|
||||
G.interlude = { introT: 1.6 }; // the duel machinery IS the match machinery
|
||||
}
|
||||
|
||||
@@ -560,7 +584,8 @@ function matchPoint(side) {
|
||||
// and only the game-winning point actually fells it, so the body and the scoreboard agree all
|
||||
// the way to the end. (Ball hits wound it on top of this; that is the regicide path, kept.)
|
||||
const was = racketStage(zb);
|
||||
zb.hp = Math.max(1, zb.hp - Math.ceil(zb.hpMax / 4));
|
||||
const div = 4 * ((d.cup && CUP_ROUNDS[d.cup.round].need) || 1); // the cup spreads the pool over the round
|
||||
zb.hp = Math.max(1, zb.hp - Math.ceil(zb.hpMax / div));
|
||||
// ONLY when the stage actually moved. A point that chips the pool without crossing a threshold
|
||||
// has no limb to explain, and a second of collapse for nothing is a beat the player waits out.
|
||||
if (racketStage(zb) !== was) fellFrom = was;
|
||||
@@ -592,6 +617,7 @@ function matchPoint(side) {
|
||||
|
||||
function matchGameWon(zb, banked = false) {
|
||||
const d = G.match;
|
||||
if (d.cup) return cupGameWon(zb, banked);
|
||||
d.ending = true; // killZombie below re-enters resolveInterlude
|
||||
if (zb && zombies.includes(zb)) explode(zb.x, zb.z); // the game-winning point IS the kill
|
||||
// REGICIDE arrives here without advancePoint having banked anything (the body died mid-game, not
|
||||
@@ -638,6 +664,62 @@ function matchGameWon(zb, banked = false) {
|
||||
G.phase = 'over'; G.overT = d.mopT + 2.2; // ticks down through the beat; reset at the walk-on
|
||||
}
|
||||
|
||||
// THE CUP'S GAME. Mid-round: the same body plays on, wearing what the games have cost it. Round
|
||||
// won: the kill, the mop beat, and the next round's draw — or, past the final, the trophy.
|
||||
function cupGameWon(zb, banked = false) {
|
||||
const d = G.match, cup = d.cup, R = CUP_ROUNDS[cup.round];
|
||||
d.ending = true;
|
||||
if (!banked) { score.games[0]++; score.pts = [0, 0]; }
|
||||
G.runGames++;
|
||||
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'] });
|
||||
voice('game_player', 0.3); cheer(2.4, 0.6);
|
||||
cup.g++;
|
||||
if (cup.g < R.need) {
|
||||
// the round is still alive: no kill, no mop beat — the body keeps its wounds and its feet
|
||||
G.flash = { text: 'GAME.', t: 1.4 };
|
||||
G.card = { title: R.name, sub1: 'YOU LEAD ' + cup.g + ' OF ' + R.need + ' — SAME BODY, FEWER PIECES',
|
||||
color: UI.pick, t: 3.0 };
|
||||
score.server = other(score.server);
|
||||
d.ending = false;
|
||||
matchReady();
|
||||
G.phase = 'over'; G.overT = 2.2;
|
||||
return;
|
||||
}
|
||||
// the round is won: the corpse comes apart
|
||||
if (zb && zombies.includes(zb)) explode(zb.x, zb.z);
|
||||
cup.round++; cup.g = 0;
|
||||
if (cup.round >= CUP_ROUNDS.length) return cupWon();
|
||||
G.flash = { text: R.name + ' — YOURS.', t: 1.8 };
|
||||
// the mop beat, exactly as the deathmatch runs it: the boy owns the court, then the next draw
|
||||
sendBoy(0, 4.5, true);
|
||||
if (G.boy && G.boy.mop) { G.boy.jobs = TUNE.boyMaxJobs; d.mopT = TUNE.matchMopT; }
|
||||
else d.mopT = 0.6;
|
||||
G.interlude = { introT: d.mopT + 1.6 };
|
||||
cupDraw(); // the next round's rung -> d.opp
|
||||
score.server = other(score.server);
|
||||
G.phase = 'over'; G.overT = d.mopT + 2.2; // d.ending stays true until the walk-on tick
|
||||
}
|
||||
|
||||
// CHAMPION OF THE DEAD CLUB — the run's other ending, on the classic win's whole shape (a held
|
||||
// summary over a still court, then the gated restart). Banked through the same endRun as every
|
||||
// match run, not dead: the stats screen's deaths column keeps meaning what it means.
|
||||
function cupWon() {
|
||||
const d = G.match;
|
||||
d.ending = false; d.mopT = 0;
|
||||
G.interlude = false;
|
||||
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(CUP_ROUNDS.length, games, G.splatT, false, MODE_MATCH);
|
||||
G.best = b; G.newBest = b.newBest;
|
||||
d.felled = CUP_ROUNDS.length; d.games = games; d.best = b.games;
|
||||
G.done = { won: true, t: 0, cup: true };
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Reference in new issue
Block a user