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
+38
-5
@@ -34,17 +34,37 @@ const cleanCups = (c, cap = 9) => {
|
||||
return out;
|
||||
};
|
||||
|
||||
// THE STORY (2026-10-15, PROJECT.md "THE STORY"): where the player stands in it. `act` 1..5, `cp` the
|
||||
// checkpoint to restart from ('pro' the prologue, 'cup' the act's cup, 'chap' the chapter's first
|
||||
// game, 'mid' after the mid-boss, 'done' the story is told), `rack` the weapons that checkpoint
|
||||
// hands back, `tries` failed attempts since the checkpoint (the help offer reads it). An ACT is the
|
||||
// unit that is saved; the checkpoints live inside it.
|
||||
const CPS = ['pro', 'cup', 'chap', 'mid', 'done'];
|
||||
export const freshStory = () => ({ act: 1, cp: 'pro', rack: [], tries: 0 });
|
||||
export const cleanStory = s => {
|
||||
if (!s || typeof s !== 'object') return freshStory();
|
||||
const rack = Array.isArray(s.rack) ? s.rack.filter(e => e && typeof e.id === 'string' && e.n > 0).slice(0, 9)
|
||||
.map(e => ({ id: e.id.slice(0, 12), n: Math.min(99, e.n | 0) })) : [];
|
||||
return { act: Math.min(5, Math.max(1, s.act | 0 || 1)), cp: CPS.includes(s.cp) ? s.cp : 'cup',
|
||||
rack, tries: Math.max(0, Math.min(99, s.tries | 0)) };
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const p = JSON.parse(localStorage.getItem(KEY) || 'null');
|
||||
if (p && typeof p === 'object') {
|
||||
return { v: 1, xp: Math.max(0, p.xp | 0), lg: Math.min(5, Math.max(1, p.lg | 0 || 1)),
|
||||
ch: p.ch ? 1 : 0, setup: p.setup ? 1 : 0,
|
||||
cups: cleanCups(p.cups), at: cleanCups(p.at, 50),
|
||||
out: Math.max(0, Math.min(99, p.out | 0)) };
|
||||
// A PROFILE FROM BEFORE THE STORY starts the story from the top (Oliver: the testers play the
|
||||
// whole thing). Its level stays — xp is the server mirror's one-way ratchet and cannot go
|
||||
// down — but the league, the cups and the champion flag were earned on the old main path,
|
||||
// and the story is the main path now: it hands them out again, act by act.
|
||||
const pre = !p.story;
|
||||
return { v: 2, xp: Math.max(0, p.xp | 0), lg: pre ? 1 : Math.min(5, Math.max(1, p.lg | 0 || 1)),
|
||||
ch: !pre && p.ch ? 1 : 0, setup: p.setup ? 1 : 0,
|
||||
cups: pre ? {} : cleanCups(p.cups), at: pre ? {} : cleanCups(p.at, 50),
|
||||
out: pre ? 0 : Math.max(0, Math.min(99, p.out | 0)), story: cleanStory(p.story) };
|
||||
}
|
||||
} catch {}
|
||||
return { v: 1, xp: 0, lg: 1, ch: 0, setup: 0, cups: {}, at: {}, out: 0 };
|
||||
return { v: 2, xp: 0, lg: 1, ch: 0, setup: 0, cups: {}, at: {}, out: 0, story: freshStory() };
|
||||
}
|
||||
|
||||
export const prof = load();
|
||||
@@ -185,8 +205,21 @@ export async function pull() {
|
||||
if (!r.ok) return;
|
||||
const p = (await r.json()).p;
|
||||
if (!p) return;
|
||||
// the story's place comes back FORWARD only, and only from a mirror that has one: a mirror
|
||||
// written before the story carries the old main path's league and cups, which the story hands
|
||||
// out again — adopting them would skip acts the tester is meant to play
|
||||
const story = p.story && typeof p.story === 'object';
|
||||
if (story) {
|
||||
const t = cleanStory(p.story), here = prof.story;
|
||||
const rank = s => s.cp === 'done' ? 99 : s.act * 10 + ['pro', 'cup', 'chap', 'mid'].indexOf(s.cp);
|
||||
if (rank(t) > rank(here)) { prof.story = { ...t, rack: [], tries: 0, cp: t.cp === 'mid' ? 'chap' : t.cp }; save(); }
|
||||
}
|
||||
if ((p.xp | 0) > prof.xp) {
|
||||
prof.xp = p.xp | 0;
|
||||
prof.setup = prof.setup || (p.setup ? 1 : 0); // the identity is done whichever path it was done on
|
||||
if (!story) { runFrom = levelAt(prof.xp); save(); }
|
||||
}
|
||||
if (story && (p.xp | 0) >= prof.xp) {
|
||||
prof.lg = Math.min(5, Math.max(prof.lg, p.lg | 0 || 1));
|
||||
prof.ch = prof.ch || (p.ch ? 1 : 0);
|
||||
prof.setup = prof.setup || (p.setup ? 1 : 0);
|
||||
|
||||
Reference in new issue
Block a user