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:
1 parent
ce27bbc48b
commit
789bf41a9a
10 files changed
+191
-34
No files matched your search
+40
-8
@@ -3,7 +3,7 @@
|
||||
// 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 , storyFixture, storyAct , STORY, ROMAN, storyBossArt } 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 , STORY, ROMAN, storyBossArt , STORY_HELP, STORE } 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 , startStory , storyUI } from './beats.js';
|
||||
@@ -116,14 +116,27 @@ let pickedDiff = remembered('diff') || 'medium';
|
||||
const modeOf = id => MODES.find(x => x.id === id);
|
||||
const modeList = document.getElementById('modeList');
|
||||
const pickMode = id => { pickedMode = id; try { localStorage.setItem('mode', id); } catch {} };
|
||||
// THE ARCADE OPENS WITH THE STORY (Oliver, 2026-10-15: "nur die bereits über die Hauptstory
|
||||
// freigeschalteten Spielmodi spielbar machen"): a door is open once the act that unlocks it is cleared
|
||||
// (config.STORY acts[].arcade), and everything is open once the story is told. A locked door is shown,
|
||||
// greyed, with the act that opens it — pointed at, never hidden. STORE.arcadePremium is the stub for
|
||||
// making the arcade a premium door; premium() answers true until the unlock IAP exists.
|
||||
const premium = () => true;
|
||||
const arcadeAct = id => STORY.acts.findIndex(a => a.arcade.includes(id)) + 1; // 0 = never gated
|
||||
const arcadeOpen = id => {
|
||||
const n = arcadeAct(id), s = profile.prof.story;
|
||||
return (!n || s.cp === 'done' || s.act > n) && (!STORE.arcadePremium || premium());
|
||||
};
|
||||
const lockLine = id => 'CLEAR ACT ' + ROMAN[arcadeAct(id)] + ' TO OPEN';
|
||||
function buildTops() {
|
||||
modeList.innerHTML = '';
|
||||
for (const { mode } of MENU) {
|
||||
const card = modeOf(mode), b = document.createElement('button');
|
||||
b.className = 'mode';
|
||||
const card = modeOf(mode), b = document.createElement('button'), open = arcadeOpen(mode);
|
||||
b.className = 'mode' + (open ? '' : ' locked');
|
||||
b.style.backgroundImage = `url(assets/${card.art}.png?v=${SPRV})`;
|
||||
b.innerHTML = `<span class="cb"><b>${card.name}</b></span>`;
|
||||
b.innerHTML = `<span class="cb"><b>${card.name}</b>${open ? '' : `<i>${lockLine(mode)}</i>`}</span>`;
|
||||
b.addEventListener('click', () => {
|
||||
if (!arcadeOpen(mode)) { b.classList.remove('nope'); void b.offsetWidth; b.classList.add('nope'); return; }
|
||||
pickMode(mode);
|
||||
go(mode === 'train' ? 'train' : 'setup');
|
||||
});
|
||||
@@ -161,6 +174,10 @@ const vsName = document.getElementById('vsName'), vsCode = document.getElementBy
|
||||
const vsSay = (id, text) => { document.getElementById(id).textContent = text; };
|
||||
|
||||
function vsOpen(code = '') {
|
||||
// hosting is the arcade door act IV opens; joining a friend's room is always free
|
||||
const host = arcadeOpen('vs');
|
||||
for (const id of ['vsHostC', 'vsHostD']) document.getElementById(id).disabled = !host;
|
||||
document.getElementById('vsHostLock').textContent = host ? '' : lockLine('vs');
|
||||
vsName.value = stats.savedName();
|
||||
vsCode.value = code;
|
||||
vsPick.hidden = false; vsWait.hidden = true;
|
||||
@@ -637,6 +654,11 @@ function refreshSplash() {
|
||||
// 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;
|
||||
// the arcade's gore door says what opens it while it is shut
|
||||
const gOpen = arcadeOpen('gore'), mg = document.getElementById('mGore');
|
||||
mg.classList.toggle('locked', !gOpen);
|
||||
mg.querySelector('i').textContent = gOpen ? 'NO CUP. JUST CARNAGE.' : lockLine('gore');
|
||||
document.getElementById('mCourt').style.display = gOpen ? '' : 'none'; // (its CSS display beats [hidden])
|
||||
document.getElementById('msLine').textContent = fx.line;
|
||||
document.getElementById('mCourt').textContent = 'COURT · ' +
|
||||
(COURTS.find(c => c.id === picked) || COURTS[0]).name;
|
||||
@@ -773,6 +795,9 @@ function storyScreen(kind, d) {
|
||||
<button class="btn big r" ${delay(9)} data-door="go">TO THE COURT</button>
|
||||
</div>`;
|
||||
} else if (kind === 'clear') {
|
||||
// THE FREE TIER ends here, on a win (STORE.freeTier, off until the unlock exists): act I's card
|
||||
// names the next act and points at the full game instead of continuing into it
|
||||
const wall = STORE.freeTier && !premium() && act === 1;
|
||||
const done = act >= 5, next = done ? null : courtOf(act + 1);
|
||||
const opens = storyAct(act).arcade.map(id => ARCADE_NAMES[id]).filter(Boolean);
|
||||
glow = '#3b3216';
|
||||
@@ -789,8 +814,9 @@ function storyScreen(kind, d) {
|
||||
${next ? `<div class="next r" ${delay(7, `background-image:linear-gradient(#0006,#000e),url(${art(next.court.tile)})`)}>
|
||||
<div><div class="lab gold">NEXT · ACT ${ROMAN[act + 1]}</div><div class="mid">${next.court.name}</div></div></div>`
|
||||
: `<div class="mid gold r" ${delay(7)}>THE STORY IS TOLD</div>`}
|
||||
<button class="btn big r" ${delay(9)} data-door="next">${done ? 'THE CLUBHOUSE' : 'CONTINUE'}</button>
|
||||
${done ? '' : `<button class="btn r" ${delay(10)} data-door="club">THE CLUBHOUSE</button>`}
|
||||
${wall ? `<div class="lab gold r" ${delay(8)}>THE STORY CONTINUES IN THE FULL GAME</div>` : ''}
|
||||
<button class="btn big r" ${delay(9)} data-door="${wall ? 'club' : 'next'}">${done || wall ? 'THE CLUBHOUSE' : 'CONTINUE'}</button>
|
||||
${done || wall ? '' : `<button class="btn r" ${delay(10)} data-door="club">THE CLUBHOUSE</button>`}
|
||||
</div>`;
|
||||
}
|
||||
sty.style.setProperty('--sty-glow', glow);
|
||||
@@ -826,9 +852,11 @@ document.getElementById('mStart').addEventListener('click', () => {
|
||||
document.getElementById('mOther').addEventListener('click', goMode);
|
||||
document.getElementById('mVs').addEventListener('click', () => vsOpen());
|
||||
document.getElementById('mGore').addEventListener('click', () => {
|
||||
if (!arcadeOpen('gore')) return;
|
||||
pickMode('gore'); start('gore');
|
||||
});
|
||||
document.getElementById('mCourt').addEventListener('click', () => {
|
||||
if (!arcadeOpen('gore')) return;
|
||||
pickMode('gore'); go('setup');
|
||||
});
|
||||
document.getElementById('cdStats').addEventListener('click', openStats);
|
||||
@@ -1132,7 +1160,11 @@ const maybeRestart = e => {
|
||||
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; }
|
||||
if (story) {
|
||||
if (door === 'help') { profile.prof.story.help = Math.min(STORY_HELP.max, profile.prof.story.help + 1); profile.save(); }
|
||||
if (door === 'story' || door === 'help') 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
|
||||
@@ -1140,7 +1172,7 @@ const maybeRestart = e => {
|
||||
if (run && run.newKillBest && run.score > 0 && run.mode !== stats.MODE_CLASSIC) { askName(run); return; }
|
||||
// the fixture button restarts the canonical path without a detour through the menus; PLAY AGAIN
|
||||
// re-deals the mode that just ended (pickedMode is what started it, cup included)
|
||||
if (door === 'next') { pickMode('cup'); start('cup'); return; }
|
||||
if (door === 'next') { start('story'); return; } // THE STORY is the fixture now
|
||||
if (door === 'again') { start(pickedMode); return; }
|
||||
go('splash');
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user