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
+22
-9
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.js';
|
||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, TINT, SHOT_DEPTH, matchFoe, cupFixture, cupRoundName } from './config.js';
|
||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, TINT, SHOT_DEPTH, matchFoe, cupFixture, cupRoundName , STORY, ROMAN, storyFixture, storyBossArt } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour, kit, skin } from './state.js';
|
||||
import { RAISERS, FRAMES } from './crowd.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY, rallyFeel, swayOf } from './gameplay.js';
|
||||
@@ -3026,6 +3026,9 @@ export function render() {
|
||||
// the arcade names the RUNG and counts d.felled (THE LEAGUES: opp is a band position now)
|
||||
: G.match ? `${matchFoe(G.match.opp).short} · ${G.match.felled || 0} FELLED`
|
||||
: G.classic ? `SET ${G.sets[0] + G.sets[1] + 1} · FIRST TO ${TUNE.classicSets}`
|
||||
// THE STORY's chapter counts its own ten (its games run on the arcade's scale underneath)
|
||||
: G.story && G.story.part === 'chap'
|
||||
? `ACT ${ROMAN[G.story.act]} · GAME ${Math.min(STORY.length, G.runGames - G.story.base + 1)} / ${STORY.length}`
|
||||
: `GAME ${G.runGames + 1} · ${G.sets[0]} SET${G.sets[0] === 1 ? '' : 'S'}`, bW - 3, bH + 8);
|
||||
}
|
||||
// ...and NOT during a match: nothing on that court is killed for points, so the pips were both a
|
||||
@@ -3072,12 +3075,21 @@ export function render() {
|
||||
// "OUT IN SEMI-FINAL" reads like a telegram: the round names carry their own article when
|
||||
// they have one (THE FINAL, ROUND ONE) and want one when they do not.
|
||||
const theRound = n => /^(THE |ROUND )/.test(n) ? n : 'THE ' + n;
|
||||
const title = dead
|
||||
// THE STORY's endings: a death names the checkpoint it goes back to, a cleared act names the
|
||||
// next one. Nothing here is a run summary — the story keeps its place in the profile.
|
||||
const st = G.story;
|
||||
const sf = st ? storyFixture(prof.story) : null;
|
||||
const title = st ? (dead ? (cup ? 'OUT IN ' + theRound(cup.rounds[Math.min(cup.round, cup.rounds.length - 1)].name) + '.' : 'EATEN.')
|
||||
: G.done.story >= 5 ? 'THE DEAD CLUB IS QUIET.' : 'ACT ' + ROMAN[G.done.story] + ' CLEARED.')
|
||||
: dead
|
||||
? (cup ? 'OUT IN ' + theRound(cupRoundName(G.league, cup.round + 1)) + '.'
|
||||
: m ? 'BLED OUT.' : 'GAME, SET, MASSACRE.')
|
||||
: G.done.cup ? (G.league >= 5 ? 'CHAMPION OF THE DEAD CLUB.' : 'THE CUP IS YOURS.')
|
||||
: won ? 'GAME, SET, MATCH.' : 'BEATEN.';
|
||||
const line = dead
|
||||
const line = st ? (dead ? (cup ? 'ACT ' + ROMAN[st.act] + ' · BACK TO ROUND ONE'
|
||||
: 'GAME ' + Math.min(STORY.length, G.runGames - st.base + 1) + ' OF ' + STORY.length + ' · ATTEMPT ' + prof.story.tries)
|
||||
: storyBossArt(G.done.story, 'end').name + ' FELL')
|
||||
: dead
|
||||
? (cup ? (cup.round + 1 >= cupBest(G.league) ? 'THE BOARD NOTES IT'
|
||||
: 'YOUR BEST STANDS: ' + theRound(cupRoundName(G.league, cupBest(G.league))))
|
||||
: m ? 'FELLED ' + m.felled + ' OPPONENT' + (m.felled === 1 ? '' : 'S') + ' · ' + m.games + ' GAMES'
|
||||
@@ -3102,7 +3114,7 @@ export function render() {
|
||||
const lineLines = wrapText(line, F(9), inner);
|
||||
// the cup keeps ITS OWN mark (the round reached, in the headline pair above) — the board's
|
||||
// felled count beside it was two bests answering different questions in the same box
|
||||
const mark = G.newBest || cup ? null
|
||||
const mark = G.newBest || cup || st ? null
|
||||
: G.best && G.best.games > 0
|
||||
? (m ? 'BEST ' + G.best.games + ' FELLED'
|
||||
: G.classic ? 'LONGEST MATCH ' + G.best.games + ' GAMES'
|
||||
@@ -3112,7 +3124,7 @@ export function render() {
|
||||
// 2026-09-11). Netplay used to end on THE CLUBHOUSE alone, which meant a fresh room code and
|
||||
// a second trip through the lobby to play the same person again.
|
||||
const online = !promo && !!(G.net && G.net.ready && !G.net.dead);
|
||||
const fxLines = ready ? wrapText(promo ? 'THE CLUB WANTS A WORD'
|
||||
const fxLines = ready ? wrapText(st ? sf.line : promo ? 'THE CLUB WANTS A WORD'
|
||||
: online ? 'YOUR FRIEND IS STILL ON THE LINE'
|
||||
: G.net ? 'THE CLUB IS WAITING' : fx.line, F(8, 0), inner) : [];
|
||||
let h = pad + headLines.length * 24 + lineLines.length * 13 + 6;
|
||||
@@ -3177,7 +3189,7 @@ export function render() {
|
||||
ctx.font = F(7, 0);
|
||||
shadowText('NEXT', inX, y + 6, UI.dim);
|
||||
ctx.font = F(9, 700);
|
||||
shadowText(promo ? 'THE PROMOTION' : online ? 'ANOTHER DUEL'
|
||||
shadowText(st ? sf.title : promo ? 'THE PROMOTION' : online ? 'ANOTHER DUEL'
|
||||
: G.net ? 'BACK TO THE CLUBHOUSE' : fx.title,
|
||||
inX + 34, y + 6, UI.warm);
|
||||
y += 16;
|
||||
@@ -3196,16 +3208,17 @@ export function render() {
|
||||
const againLabel = G.classic ? 'CLASSIC AGAIN'
|
||||
: G.match && G.match.train ? 'TRAIN AGAIN'
|
||||
: G.match ? 'CLIMB AGAIN' : 'GORE AGAIN';
|
||||
const second = solo ? (cup || (G.done && G.done.cup) ? ['club', 'THE CLUBHOUSE']
|
||||
const second = st ? ['club', 'THE CLUBHOUSE']
|
||||
: solo ? (cup || (G.done && G.done.cup) ? ['club', 'THE CLUBHOUSE']
|
||||
: ['again', againLabel])
|
||||
: online ? ['club', 'THE CLUBHOUSE'] : null;
|
||||
const bw = second && !stack ? Math.round((inner - 8) / 2) : inner;
|
||||
// "THE DRAW" was tennis for the field of entrants — and nothing is drawn any more since the
|
||||
// cup went deterministic (2026-08-29). The button names what it opens instead.
|
||||
const label = promo ? 'TAKE THE FLOOR' : solo ? 'ENTER THE CUP'
|
||||
const label = st ? (dead ? 'TRY AGAIN' : 'CONTINUE') : promo ? 'TAKE THE FLOOR' : solo ? 'ENTER THE CUP'
|
||||
: online ? 'REMATCH' : 'THE CLUBHOUSE';
|
||||
drawButton(inX, y, bw, MODAL_BTN, label, UI.pick, true);
|
||||
endBtns.push({ id: solo ? 'next' : online ? 'rematch' : 'club', x: inX, y, w: bw, h: MODAL_BTN });
|
||||
endBtns.push({ id: st ? 'story' : solo ? 'next' : online ? 'rematch' : 'club', x: inX, y, w: bw, h: MODAL_BTN });
|
||||
if (second) {
|
||||
const sx = stack ? inX : inX + bw + 8, sy = stack ? y + MODAL_BTN + 8 : y;
|
||||
drawButton(sx, sy, bw, MODAL_BTN, second[1], UI.warm, false);
|
||||
|
||||
Reference in new issue
Block a user