The leagues, phase 2: five bands, pace tiers, locked courts, the members' ground, the promotion ceremony
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9133c0d351
commit
16bf5b9fed
12 files changed
+406
-71
No files matched your search
+47
-17
@@ -7,9 +7,9 @@
|
||||
// (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, CUP_ROUNDS } from './config.js';
|
||||
import { TUNE, UI, bossArt, matchFoe, BENCH, ZFAMS, COURT, HOME, racketStage, leagueOf, cupRoundsFor } from './config.js';
|
||||
import { endRun, MODE_MATCH } from './stats.js';
|
||||
import { award } from './profile.js';
|
||||
import { award, prof, save as profSave, push as profPush, setPromo } from './profile.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';
|
||||
@@ -354,7 +354,7 @@ 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).
|
||||
// `draw` is the bracket screen's settled rungs, one per CUP_ROUNDS entry (main.buildBracket). It is
|
||||
// `draw` is the bracket screen's settled rungs, one per cup round (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.
|
||||
// `train` (2026-08-28) is a RUNG NUMBER and turns the run into the TRAINING COURT: that one
|
||||
@@ -363,12 +363,20 @@ export function watchdog(dt) {
|
||||
// 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: train || 1, ending: false, train: !!train };
|
||||
// THE LEAGUES: the profile's league is the world (band, pace tier, court — see PROJECT.md).
|
||||
// Stamped onto G so gameplay's leaguePace() and the dials below never read the profile module.
|
||||
// Netplay and training stay league-1 worlds: a human opponent and a practice rep both play the
|
||||
// shipped game (train still picks any met rung, but at the shipped pace).
|
||||
G.league = G.net || train ? 1 : prof.lg;
|
||||
const band = leagueOf(G.league).band;
|
||||
// the arcade ladder opens at the league's own bottom rung and never leaves the band: past its
|
||||
// top the league simply has nobody stronger, so the top body walks on again (see matchGameWon)
|
||||
G.match = { opp: train || (G.net ? 1 : band[0]), ending: false, train: !!train, felled: 0 };
|
||||
// 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, draw: draw || null }; cupDraw(); }
|
||||
// by the round's `need`), and where a won game goes (cupGameWon). See config.cupRoundsFor.
|
||||
if (cup) { G.match.cup = { round: 0, g: 0, draw: draw || null, rounds: cupRoundsFor(G.league) }; 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
|
||||
@@ -412,9 +420,17 @@ export function startMatch(cup = false, draw = null, train = 0) {
|
||||
// Guarded on the band all the same: a saved or hand-passed draw that names a rung this round does
|
||||
// not contain falls back to an honest roll rather than dealing the champion in round one.
|
||||
function cupDraw() {
|
||||
const cup = G.match.cup, R = CUP_ROUNDS[cup.round];
|
||||
const cup = G.match.cup, R = cup.rounds[cup.round];
|
||||
const drawn = cup.draw && cup.draw[cup.round];
|
||||
cup.rung = R.band.includes(drawn) ? drawn : R.band[Math.floor(Math.random() * R.band.length)];
|
||||
// WITHOUT REPLACEMENT: a league band is 3-4 rungs and every non-final round carries the whole
|
||||
// sub-band, so the run remembers who has already fallen (cup.played) and never deals a body
|
||||
// twice — a repeat would read as the bracket screen lying. The final's band is one rung and
|
||||
// sails through untouched.
|
||||
cup.played = cup.played || [];
|
||||
const pool = R.band.filter(r => !cup.played.includes(r));
|
||||
cup.rung = R.band.includes(drawn) && !cup.played.includes(drawn) ? drawn
|
||||
: (pool.length ? pool : R.band)[Math.floor(Math.random() * (pool.length ? pool : R.band).length)];
|
||||
cup.played.push(cup.rung);
|
||||
G.match.opp = cup.rung;
|
||||
}
|
||||
|
||||
@@ -649,8 +665,8 @@ function matchOpponent() {
|
||||
// In the cup the round names itself on the second line.
|
||||
const cup = G.match.cup;
|
||||
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' : '')] : []) });
|
||||
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
|
||||
@@ -658,7 +674,7 @@ function matchOpponent() {
|
||||
// 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);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -683,7 +699,7 @@ 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);
|
||||
const div = 4 * ((d.cup && CUP_ROUNDS[d.cup.round].need) || 1); // the cup spreads the pool over the round
|
||||
const div = 4 * ((d.cup && d.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.
|
||||
@@ -774,7 +790,12 @@ function matchGameWon(zb, banked = false) {
|
||||
// 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). 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++;
|
||||
if (!G.net && !d.train) {
|
||||
d.felled = (d.felled || 0) + 1; // bodies actually felled (the board's number)
|
||||
// ...and the ladder climbs only inside the league's band: past its top rung the league has
|
||||
// nobody stronger, so the top body walks back on (THE LEAGUES — the cup is the way up)
|
||||
d.opp = Math.min(d.opp + 1, leagueOf(G.league).band[1]);
|
||||
}
|
||||
// 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).
|
||||
@@ -830,7 +851,7 @@ function matchGameWon(zb, banked = false) {
|
||||
// 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];
|
||||
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++;
|
||||
@@ -853,7 +874,7 @@ function cupGameWon(zb, banked = false) {
|
||||
d.afterFall = () => {
|
||||
matchHealBeat();
|
||||
cup.round++; cup.g = 0;
|
||||
if (cup.round >= CUP_ROUNDS.length) return cupWon();
|
||||
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);
|
||||
@@ -877,10 +898,19 @@ function cupWon() {
|
||||
ball.active = false; G.flash = null; G.modal = null; G.card = null;
|
||||
cheer(4, 1); voice('unbelievable', 0.4);
|
||||
award('cup');
|
||||
// THE PROMOTION (the leagues): the cup of your CURRENT league is the gate up. Winning it moves
|
||||
// the profile, unlocks the next league's court for every mode, and parks a ceremony that
|
||||
// main.maybeRestart plays once the summary is tapped away. Winning league 5 crowns the profile
|
||||
// instead (prof.ch) — the summary below already says CHAMPION OF THE DEAD CLUB.
|
||||
if (!d.train && G.league === prof.lg) {
|
||||
if (prof.lg < 5) { prof.lg++; setPromo({ to: prof.lg }); }
|
||||
else if (!prof.ch) { prof.ch = 1; setPromo({ champion: true }); }
|
||||
profSave(); profPush();
|
||||
}
|
||||
const games = score.games[0] + score.games[1];
|
||||
const b = endRun(CUP_ROUNDS.length, games, G.splatT, false, MODE_MATCH);
|
||||
const b = endRun(d.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;
|
||||
d.felled = d.cup.rounds.length; d.games = games; d.best = b.games;
|
||||
G.done = { won: true, t: 0, cup: true };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user