THE SEASON, phases 1-4: the front door is the fixture, and the club remembers
The tournament is the canonical path, so the game finally says so. The splash's primary button names the next cup fixture and enters the draw; the poster wall moved one tap down and deals three named rooms (the season, the training, out of competition) with the cup at the top. Two profile fields remember every attempt — rounds ever reached per league, and the body that put you out — so a lost cup is a line on the board instead of a null event, and the training court offers that body first, marked. The promotion names what waits in the new league. Overlays consolidated in the same pass: three modal drawers became one box with a masthead, hairline-separated rows and a real footer button (any tap still dismisses), and the run-end summary wears the same box — headline, xp, mark, and the fixture with ENTER THE DRAW beside PLAY AGAIN. The cup's own exit is a ceremony now: OUT IN THE QUARTER-FINAL, your best standing beside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
063b01d2a5
commit
5c7521f867
10 files changed
+603
-204
No files matched your search
+73
-14
@@ -3,12 +3,12 @@
|
||||
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
|
||||
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
|
||||
|
||||
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, LEAGUES, leagueOf, cupRoundsFor, matchFoe, LADDER } from './config.js';
|
||||
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, LEAGUES, leagueOf, cupRoundsFor, cupFixture, cupRoundName, matchFoe, LADDER } from './config.js';
|
||||
import { P, O, ball, score, G, setupPoint, sendBoy, selectCourt, selectTour, selectKit, selectSkin } from './state.js';
|
||||
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, startGore, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
|
||||
import { startDuel, resolveInterlude, startHorde, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs, endNetVs } from './beats.js';
|
||||
import { netConnect, netHooks } from './net.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook } from './render.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook, endHit } from './render.js';
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
|
||||
import { cv } from './dom.js';
|
||||
@@ -146,6 +146,14 @@ function buildSub(top) {
|
||||
function buildTops() {
|
||||
modeList.innerHTML = '';
|
||||
for (const t of MENU) {
|
||||
// a GROUP row is the heading over the posters under it — what that shelf is for (config MENU)
|
||||
if (t.group) {
|
||||
const h = document.createElement('div');
|
||||
h.className = 'mgroup';
|
||||
h.innerHTML = `<b>${t.group}</b><i>${t.sub}</i>`;
|
||||
modeList.appendChild(h);
|
||||
continue;
|
||||
}
|
||||
const b = banner(t.mode ? modeOf(t.mode) : t, 'mode', topOf(pickedMode) === t);
|
||||
b.addEventListener('click', () => {
|
||||
// the netplay poster opens THE LOBBY and never touches pickedMode — it is not a run mode
|
||||
@@ -342,9 +350,15 @@ function buildLeaguePanel() {
|
||||
foes.appendChild(d);
|
||||
}
|
||||
const size = lg.band[1] - lg.band[0] + 1;
|
||||
document.getElementById('lgSub').textContent = felled >= size
|
||||
? 'ALL ' + size + ' FELLED — TAKE THE CUP'
|
||||
: felled + ' OF ' + size + ' FELLED · THE CUP IS THE WAY UP';
|
||||
// ...and THE RECORD (THE SEASON): how far this league's cup has ever let you get. It is the one
|
||||
// line that makes a lost attempt count for something, so it takes the second half of the sub
|
||||
// whenever there is one to show.
|
||||
const best = profile.cupBest(lg.id);
|
||||
document.getElementById('lgSub').innerHTML = (felled >= size
|
||||
? 'ALL ' + size + ' FELLED'
|
||||
: felled + ' OF ' + size + ' FELLED')
|
||||
+ (best ? '<br>BEST IN THE CUP: ' + cupRoundName(lg.id, best)
|
||||
: '<br>THE CUP IS THE WAY UP — NEVER ENTERED');
|
||||
}
|
||||
|
||||
// The setup screen's rows, brought up to date with picks made elsewhere (the identity screen).
|
||||
@@ -490,9 +504,19 @@ const trainList = document.getElementById('trainList');
|
||||
function buildTrain() {
|
||||
trainList.innerHTML = '';
|
||||
const top = Math.min(LADDER, leagueOf(profile.prof.lg).band[1]);
|
||||
for (let n = 1; n <= top; n++) {
|
||||
// THE REVANCHE (THE SEASON): the body that ended your last cup attempt is marked and sits FIRST
|
||||
// in its own right — a practice list where every rung looks the same is a shelf, and this one
|
||||
// room in the game has an obvious reason to exist the moment you have just been put out.
|
||||
const out = profile.prof.out;
|
||||
const rungs = [];
|
||||
for (let n = 1; n <= top; n++) rungs.push(n);
|
||||
if (out >= 1 && out <= top) rungs.splice(rungs.indexOf(out), 1), rungs.unshift(out);
|
||||
for (const n of rungs) {
|
||||
const f = matchFoe(n), b = document.createElement('button');
|
||||
b.innerHTML = `<img src="${foeArt(f)}" alt=""><b>${f.name}</b><i>No. ${n}</i>`;
|
||||
const put = n === out;
|
||||
if (put) b.className = 'put';
|
||||
b.innerHTML = `<img src="${foeArt(f)}" alt=""><b>${f.name}</b>`
|
||||
+ `<i>${put ? 'PUT YOU OUT OF THE CUP' : 'No. ' + n}</i>`;
|
||||
b.addEventListener('click', () => { trainRung = n; start('train'); });
|
||||
trainList.appendChild(b);
|
||||
}
|
||||
@@ -605,6 +629,12 @@ function buildPromo(pr, run) {
|
||||
? 'THE HONOURS BOARD REMEMBERS YOUR NAME'
|
||||
: `NEW COURT UNLOCKED — ${c.name} · OPEN IN EVERY MODE`;
|
||||
document.getElementById('prGo').textContent = champion ? 'TAKE A BOW' : 'WALK ON';
|
||||
// ...and what the new league has you down for. The champion has nothing above him, so his beat
|
||||
// is the title he now has to hold instead of a fixture he has to win.
|
||||
const fx = cupFixture(profile.prof.lg, profile.prof.ch);
|
||||
document.getElementById('prNext').innerHTML = champion
|
||||
? `THE TITLE IS YOURS TO HOLD<u>EVERY CUP FROM HERE IS A DEFENCE</u>`
|
||||
: `NEXT: ${fx.title}<u>${fx.line}</u>`;
|
||||
}
|
||||
document.getElementById('prGo').addEventListener('click', () => {
|
||||
const run = promoRun; promoRun = null;
|
||||
@@ -657,11 +687,21 @@ function refreshSplash() {
|
||||
document.getElementById('plFrom').textContent = pr.level;
|
||||
document.getElementById('plTo').textContent = pr.need ? pr.level + 1 : 'MAX';
|
||||
document.getElementById('plFill').style.width = Math.round(pr.frac * 100) + '%';
|
||||
// ...and the goal line carries THE RECORD now (THE SEASON): the fixture button below already
|
||||
// says what to do, so repeating "win the cup to advance" over it was the same sentence twice.
|
||||
// What the plaque can say that nothing else does is how far you have ever got.
|
||||
const bestR = profile.cupBest(profile.prof.lg);
|
||||
document.getElementById('plGoal').textContent = profile.prof.ch
|
||||
? 'CHAMPION OF THE DEAD CLUB — DEFEND THE NAME'
|
||||
: profile.prof.lg >= LEAGUES.length
|
||||
? 'WIN THE ' + lg.name.replace('THE ', '') + ' CUP — BECOME CHAMPION'
|
||||
: 'WIN THE ' + lg.name.replace('THE ', '') + ' CUP TO ADVANCE';
|
||||
: bestR
|
||||
? 'THIS CUP · BEST SO FAR: ' + cupRoundName(profile.prof.lg, bestR)
|
||||
: 'THE CUP IS THE WAY UP — NEVER ENTERED';
|
||||
// THE FRONT DOOR (THE SEASON): the fixture, in the same words the summary and the promotion use
|
||||
// (config.cupFixture). The record rides the sub line when there is one — a lost cup is the only
|
||||
// thing the club has to say about your last attempt, and it belongs where the next one starts.
|
||||
const fx = cupFixture(profile.prof.lg, profile.prof.ch);
|
||||
document.getElementById('msTitle').textContent = fx.title;
|
||||
document.getElementById('msLine').textContent = fx.line;
|
||||
summary();
|
||||
}
|
||||
|
||||
@@ -710,7 +750,14 @@ const goMode = () => {
|
||||
if (!profile.prof.setup) { buildPlayer(); go('player'); return; }
|
||||
buildTops(); go('mode');
|
||||
};
|
||||
document.getElementById('mStart').addEventListener('click', goMode);
|
||||
// the fixture button IS the tournament — it skips the setup screen on purpose: the cup has no
|
||||
// court to pick (it plays the league's own) and no opponent to choose, so that screen was a
|
||||
// confirmation page with nothing on it to confirm
|
||||
document.getElementById('mStart').addEventListener('click', () => {
|
||||
if (!profile.prof.setup) { buildPlayer(); go('player'); return; } // the club wants a name first
|
||||
pickMode('cup'); summary(); start('cup');
|
||||
});
|
||||
document.getElementById('mOther').addEventListener('click', goMode);
|
||||
document.getElementById('mStats').addEventListener('click', openStats);
|
||||
// the world board opens on the mode you last PICKED, like the stats screen does — that is the one
|
||||
// whose names you came to look at
|
||||
@@ -997,10 +1044,18 @@ addEventListener('pagehide', () => {
|
||||
// NOT a page reload any more: that reset everything honestly but also tore down the AudioContext, so
|
||||
// the theme restarted from silence. restartRun puts every bag back from a pristine copy, and the
|
||||
// bed that has been playing under the death summary simply carries on into the title screen.
|
||||
const maybeRestart = () => {
|
||||
const maybeRestart = e => {
|
||||
// ...and the classic ending taps out the same way, on its own shorter clock (G.done, no corpse)
|
||||
const ready = G.done ? G.done.t > DONE_RESTART : G.mode === 'eaten' && G.eatenT > EATEN_RESTART;
|
||||
if (G.modal || !ready) return;
|
||||
// WHICH DOOR (THE SEASON: "das ende reicht weiter"). The summary lays its own buttons out as it
|
||||
// draws — it measures its box, so only it knows where they landed — and hands the rects back
|
||||
// through endHit. A tap anywhere ELSE is the old behaviour exactly: back to the club.
|
||||
let door = null;
|
||||
if (e && e.clientX !== undefined) {
|
||||
const r = cv.getBoundingClientRect();
|
||||
door = endHit((e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height);
|
||||
}
|
||||
restartRun();
|
||||
profile.push(); // the run's xp is banked — mirror it while the moment is quiet
|
||||
refreshSplash(); // the run just ended: the splash count has moved
|
||||
@@ -1015,8 +1070,12 @@ const maybeRestart = () => {
|
||||
if (promo) { buildPromo(promo, run); go('promo'); return; }
|
||||
// ...on the BOARD's number, not on kills: a classic match has none, so the gore-shaped test here
|
||||
// silently barred two of the three modes from ever reaching the prompt
|
||||
if (run && run.newKillBest && run.score > 0) askName(run);
|
||||
else go('splash');
|
||||
if (run && run.newKillBest && run.score > 0) { askName(run); return; }
|
||||
// the fixture button restarts the canonical path without a detour through the menus; PLAY AGAIN
|
||||
// re-deals the mode that just ended (pickedMode is what started it, cup included)
|
||||
if (door === 'next') { pickMode('cup'); summary(); start('cup'); return; }
|
||||
if (door === 'again') { start(pickedMode); return; }
|
||||
go('splash');
|
||||
};
|
||||
cv.addEventListener('pointerdown', maybeRestart);
|
||||
addEventListener('keydown', maybeRestart);
|
||||
|
||||
Reference in new issue
Block a user