Story phase 1: the spine — acts, cups, chapters, checkpoints

The front door now continues the story. An act is its league cup, whose
final is the boss's own body (acts II and IV play their band out of
ladder order for that), then a ten-game gore chapter on the act's court.
The chapter has no duel at all, takes the act's weapon stock into the
rack, meets a mid-boss after game 5 (the checkpoint: restock and full
blood) and the act boss after game 10, then the act card. Death is not
permadeath: it counts the attempt, banks nothing on the boards, and
TRY AGAIN restarts from the checkpoint the profile holds. Barrels hold
what the act has earned. The chapter plays the arcade's games
base+1..base+10 so every director curve keeps its slope.

Profiles from before the story start it from the top (league, cups and
champion reset; level kept). The server mirror stores the act and
checkpoint and only ever moves them forward.

Plain cards stand in for the screens (phase 3) and the transformation
(phase 2). Six new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-15 14:10:09 +02:00
1 parent 5e1c6abba4
commit 75b450f105
10 files changed
+456 -39

No files matched your search

+15 -6
View File
@@ -3,10 +3,10 @@
// 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, cupFixture, cupRoundName, 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 , storyFixture, storyAct } 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 { startDuel, resolveInterlude, startHorde, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs, endNetVs , startStory } from './beats.js';
import { netConnect, netHooks, netAgain } from './net.js';
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook, endHit, logGeom } from './render.js';
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
@@ -371,6 +371,7 @@ function leavePlayer(saved) {
if (back === 'card') { buildCard(); go('card'); }
else if (back === 'setup') go('setup');
else if (back === 'cup' && saved) { pickMode('cup'); start('cup'); }
else if (back === 'story' && saved) start('story');
else go('splash');
}
document.getElementById('pBack').addEventListener('click', () => leavePlayer(false));
@@ -633,7 +634,8 @@ function refreshSplash() {
document.getElementById('plFill').style.width = Math.round(pr.frac * 100) + '%';
// THE FRONT DOOR (THE SEASON): the fixture, in the same words the summary and the promotion use
// (config.cupFixture) — minus the round count, which lives in the tournament's own modal now.
const fx = cupFixture(profile.prof.lg, profile.prof.ch);
// THE STORY IS THE MAIN PATH now (2026-10-15): the front door names where it stands
const fx = storyFixture(profile.prof.story);
document.getElementById('msTitle').textContent = fx.title;
document.getElementById('msLine').textContent = fx.line;
document.getElementById('mCourt').textContent = 'COURT · ' +
@@ -651,7 +653,8 @@ function start(modeId = pickedMode) {
selectKit(pickedKit); selectSkin(pickedSkin);
// THE LEAGUES: the CUP is the league fixture and plays the league's own court; every other
// mode — the climb included, it is an exhibition — plays your pick (among the unlocked).
selectCourt(modeId === 'cup' ? leagueOf(profile.prof.lg).court : picked);
selectCourt(modeId === 'story' ? leagueOf(storyAct(profile.prof.story.act).lg).court
: modeId === 'cup' ? leagueOf(profile.prof.lg).court : picked);
menuDuck(true); // the theme belongs to the menus — hand the court over to the ambience
intro.hidden = true; ambient(); voice('quiet_please'); // low atmosphere bed starts with the first gesture
// ...and the mode the setup screen picked. THREE LINES, because that is genuinely all the modes
@@ -669,6 +672,8 @@ function start(modeId = pickedMode) {
if (modeId === 'cup') { intoSplatter(); startMatch(true); } // cupDraw shuffles per round
// training: the picked rung rides the match machinery with the ladder parked (beats.startMatch)
if (modeId === 'train') { intoSplatter(); startMatch(false, null, trainRung); }
// THE STORY: the act's court, the splatter world under it, and the checkpoint the profile holds
if (modeId === 'story') { intoSplatter(); startStory(); }
}
// Career totals and detailed records live one level beneath the profile.
@@ -725,8 +730,8 @@ document.addEventListener('visibilitychange', () => {
addEventListener('pagehide', () => recorder.saveRecording());
const goMode = () => { buildTops(); go('mode'); };
document.getElementById('mStart').addEventListener('click', () => {
if (!profile.prof.setup) { playerReturn = 'cup'; buildPlayer(); go('player'); return; }
pickMode('cup'); start('cup');
if (!profile.prof.setup) { playerReturn = 'story'; buildPlayer(); go('player'); return; }
start('story');
});
document.getElementById('mOther').addEventListener('click', goMode);
document.getElementById('mVs').addEventListener('click', () => vsOpen());
@@ -1023,6 +1028,7 @@ const maybeRestart = e => {
// resets both phones itself and the menu never comes back. It refuses (and falls through to the
// clubhouse) if the friend has already hung up.
if (door === 'rematch' && netAgain()) return;
const story = !!G.story; // restartRun wipes G — the story's place is in the profile
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
@@ -1035,6 +1041,8 @@ const maybeRestart = e => {
// prompt it may also have earned (prGo hands the run on), then the title.
const promo = profile.takePromo();
if (promo) { postCup(promo); buildPromo(promo, run); go('promo'); return; }
// THE STORY: TRY AGAIN / CONTINUE re-enter at the checkpoint; anywhere else is the club
if (story) { if (door === 'story') start('story'); else go('splash'); 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
// ...on a board that still EXISTS: classic tennis left the world board with the stats streamline
@@ -1159,5 +1167,6 @@ requestAnimationFrame(frame);
window.T = { TUNE, ball, P, O, score, G, zombies, keyPressed, SPR, laserScene, laserFire, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, dragServe, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch,
stats, step: dt => update(dt), render, press: keyPressed, FALL, benchFall,
start, intoSplatter, startHorde, sendBoy, // the two run doors + the wave: the promo rig (tools/) stages every clip through them
profile, startStory, // THE STORY: read and set its place from the console (prof.story)
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };