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:
1 parent
5e1c6abba4
commit
75b450f105
10 files changed
+456
-39
No files matched your search
@@ -1018,6 +1018,54 @@ export const cupFixture = (lg, champion = 0) => {
|
||||
sub: 'LEAGUE ' + lg + ' · ' + rounds.length + ' ROUNDS' };
|
||||
};
|
||||
|
||||
// ---- THE STORY (2026-10-15, PROJECT.md "THE STORY"): five acts, one per league and court. An act is
|
||||
// the league cup (zombie tennis), the transformation, and one ten-game gore chapter with a MID-BOSS
|
||||
// at game 5 (the checkpoint) and the act's BOSS at game 10. Pure data; beats.js runs it.
|
||||
// rounds the cup's rungs, in order. The FINAL is always the boss's own body (it grows into the
|
||||
// boss), so acts II and IV play their band out of ladder order — cupRoundsFor is untouched.
|
||||
// mid BOSS_ART index of the game-5 boss (the five designs one chapter per act leaves over)
|
||||
// boss BOSS_ART index of the game-10 boss (the cup finalist, grown)
|
||||
// pool weapons this act ADDS to the rack; the stock is every pool up to and including the act
|
||||
// double the act's stock and restock pay two tins of each (league 5's double tins)
|
||||
// The chapter plays the arcade's games base+1..base+10 (base = (act-1)*20), so every director curve
|
||||
// that reads G.runGames — spawn ramps, barrel gaps, machines, racket hp — keeps its tuned slope.
|
||||
export const STORY = {
|
||||
acts: [
|
||||
{ lg: 1, rounds: [1, 2, 3, 4], mid: 8, boss: 0, pool: ['bomb', 'triple', 'fire'] },
|
||||
{ lg: 2, rounds: [5, 6, 8, 7], mid: 6, boss: 5, pool: ['gun', 'beacon'] },
|
||||
{ lg: 3, rounds: [9, 10, 11, 12], mid: 1, boss: 2, pool: ['laser'] },
|
||||
{ lg: 4, rounds: [13, 15, 14], mid: 4, boss: 3, pool: ['flamer', 'rocket'] },
|
||||
{ lg: 5, rounds: [16, 17, 18], mid: 7, boss: 9, pool: [], double: true },
|
||||
],
|
||||
midAt: 5, // the mid-boss walks on after this many chapter games (and is the checkpoint)
|
||||
length: 10, // games in a chapter; the act's boss walks on after the last
|
||||
base: act => (act - 1) * 20,
|
||||
};
|
||||
export const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V'];
|
||||
export const storyAct = act => STORY.acts[Math.max(1, Math.min(5, act)) - 1];
|
||||
// the story cup: the league's own round names and needs, on the act's rung order
|
||||
export const storyRounds = act => {
|
||||
const A = storyAct(act);
|
||||
return cupRoundsFor(A.lg).map((r, i) => ({ ...r, band: [A.rounds[i]] }));
|
||||
};
|
||||
// every weapon the act has earned so far, and how many tins of each the rack starts the chapter with
|
||||
export const storyPool = act => STORY.acts.slice(0, Math.max(1, Math.min(5, act))).flatMap(a => a.pool);
|
||||
export const storyStock = act => storyPool(act).map(id => ({ id, n: WEAPONS[id].tin * (storyAct(act).double ? 2 : 1) }));
|
||||
export const storyBossArt = (act, kind) => BOSS_ART[storyAct(act)[kind === 'mid' ? 'mid' : 'boss']];
|
||||
// strength: the act's two bosses stand where they would in the arcade's ten chapters (mid 2a-1, boss 2a)
|
||||
export const storyBossChapter = (act, kind) => 2 * act - (kind === 'mid' ? 1 : 0);
|
||||
// the front door's words for where the story stands (refreshSplash, the summaries)
|
||||
export const storyFixture = s => {
|
||||
const act = Math.max(1, Math.min(5, s.act)), lg = leagueOf(storyAct(act).lg);
|
||||
const court = (COURTS.find(c => c.id === lg.court) || COURTS[0]).name;
|
||||
if (s.cp === 'done') return { title: 'THE STORY IS TOLD', line: 'THE DEAD CLUB IS QUIET · PLAY IT AGAIN', court };
|
||||
const where = s.cp === 'pro' ? 'PROLOGUE · A MATCH LIKE ANY OTHER'
|
||||
: s.cp === 'chap' ? 'CHAPTER ' + ROMAN[act] + ' · FROM GAME 1'
|
||||
: s.cp === 'mid' ? 'CHAPTER ' + ROMAN[act] + ' · FROM GAME ' + (STORY.midAt + 1)
|
||||
: 'THE CUP · ' + storyRounds(act)[0].name + ': ' + matchFoe(storyAct(act).rounds[0]).name;
|
||||
return { title: s.cp === 'pro' ? 'THE STORY' : 'ACT ' + ROMAN[act] + ' · ' + court, line: where, court };
|
||||
};
|
||||
|
||||
// How far a cup record reads, in the rounds' own words: 3 of 4 rounds reached is THE SEMI-FINAL.
|
||||
export const cupRoundName = (lg, reached) => {
|
||||
const rounds = cupRoundsFor(lg);
|
||||
|
||||
Reference in new issue
Block a user