Story phase 4: help when stuck, the arcade opens with the story

- From the third attempt at a checkpoint, the death screen offers TRY
  AGAIN · WITH HELP, at most two tiers, said openly. In the cup the help
  is fresher legs and a longer swing; in gore, one more heart and a
  slower director and horde. Every checkpoint passed resets the offer.
- The arcade shows only what the story has opened. Training and Classic
  open after act I, the Climb after II, Splatter Gore after III, and
  hosting a friend after IV (joining stays free). Locked doors are shown
  greyed with the act that opens them.
- The non-story endings point at the story instead of the cup.
- Store switches for the free tier (act I's card points at the full
  game) and a premium arcade, both off.
- The prologue's HUD no longer overlaps the kill pips.

PROJECT.md logs phases 1-4. One new test. v382, staging only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-15 14:22:25 +02:00
1 parent ce27bbc48b
commit 789bf41a9a
10 files changed
+191 -34

No files matched your search

+21 -1
View File
@@ -90,7 +90,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { ringCue, predictLanding, solveShot, update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs, startStory, startStoryChapter } = await import('./src/beats.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs, startStory, startStoryChapter, startHorde } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick, netAgain } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
// player would, so the tests step a live court
@@ -3787,6 +3787,26 @@ test('THE STORY: death is not permadeath — it banks nothing and restarts from
assert.equal(G.hp, G.hpMax);
});
test('THE STORY: the help offer — fresher legs and a longer swing in the cup, a heart and a slower horde in gore', () => {
const H = cfg.STORY_HELP;
intoStory(1, 'cup');
const leg0 = _legLevel(), win0 = _winLevel();
G.story.help = 2;
assert.ok(Math.abs(_legLevel() / leg0 - (1 + 2 * H.run)) < 1e-9, 'legs');
assert.ok(Math.abs(_winLevel() / win0 - (1 + 2 * H.window)) < 1e-9, 'swing window');
endMatch(); G.story = null;
prof.prof.story.help = 2;
intoStory(1, 'chap');
prof.prof.story.help = 2;
G.story = null; assert.ok(startStoryChapter(false));
assert.equal(G.hpMax, TUNE.playerHp + 2 * H.heart, 'a heart per tier');
G.sets = [1, 0]; startHorde();
const slow = G.horde.rate;
G.horde = null; G.story.help = 0; startHorde();
assert.ok(Math.abs(G.horde.rate / slow - (1 + 2 * H.slow)) < 1e-9, 'the horde comes slower');
G.horde = null;
});
test('THE STORY: a barrel holds what the act has earned', () => {
intoStory(1, 'chap');
const seen = new Set();