Story phase 3: the act card, the ceremony with its loot, the act's end
Three animated screens in the storyboard's look, built in the page over
a paused world:
- The act title card: the act's court pans in behind ACT n, its league,
its court and its goal; it closes itself after three seconds or on a
tap.
- The cup's ceremony: YOU CLEARED THE COURT, the rounds and the run's
xp, then the act's weapons drop into a rack one by one ("…and gained
these for the hordes to come"), and the chapter preview with the
mid-boss named and game 10 still "???" until the transformation.
- The act card: what the act cost (bodies, bosses, deaths), what it
opened in the arcade, and the next act's court; CONTINUE goes straight
on, THE CLUBHOUSE goes home.
The checkpoint keeps its in-game banner. Headless, every screen falls
back to the plain card it replaced. v381, staging only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff63c832cc
commit
ce27bbc48b
6 files changed
+163
-15
No files matched your search
+91
-2
@@ -3,10 +3,10 @@
|
||||
// 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 } 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 } 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 } from './beats.js';
|
||||
import { startDuel, resolveInterlude, startHorde, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs, endNetVs , startStory , storyUI } from './beats.js';
|
||||
import { netConnect, netHooks, netAgain } from './net.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook, endHit, logGeom } from './render.js';
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
@@ -729,6 +729,95 @@ document.addEventListener('visibilitychange', () => {
|
||||
}
|
||||
});
|
||||
addEventListener('pagehide', () => recorder.saveRecording());
|
||||
// ---- THE STORY's screens (2026-10-15, PROJECT.md "THE STORY", phase 3) ------------------------------
|
||||
// In-game, animated, in the storyboard's look: the act title card, the cup's ceremony with its loot,
|
||||
// and the act card. Each pauses the world while it is up (the same `paused` the quit chip uses) and
|
||||
// resolves with the door that closed it. beats asks for them through storyUI.show.
|
||||
const sty = document.getElementById('sty');
|
||||
let styClose = null, styDoors = false;
|
||||
const art = n => `assets/${n}.png?v=${SPRV}`;
|
||||
const courtOf = act => { const lg = leagueOf(storyAct(act).lg); return { lg, court: COURTS.find(c => c.id === lg.court) || COURTS[0] }; };
|
||||
const ARCADE_NAMES = { train: 'TRAINING', classic: 'CLASSIC TENNIS', zombie: 'THE CLIMB', gore: 'SPLATTER GORE', vs: 'VS A FRIEND' };
|
||||
const delay = (k, more = '') => `style="animation-delay:${(k * 0.15).toFixed(2)}s;${more}"`; // one style attribute per element
|
||||
function storyScreen(kind, d) {
|
||||
const act = d.act, { lg, court } = courtOf(act);
|
||||
let html = '', glow = '#3a1d16', auto = 0;
|
||||
if (kind === 'act') {
|
||||
auto = 2.8;
|
||||
glow = '#1d2233';
|
||||
html = `<div class="bgc p" style="background-image:url(${art(court.tile)})"></div>
|
||||
<div class="col">
|
||||
<div class="lab gold r">${lg.name}</div>
|
||||
<h1 class="huge s" ${delay(1)}>ACT ${ROMAN[act]}</h1>
|
||||
<div class="big gold r" ${delay(3)}>${court.name}</div>
|
||||
<div class="lab r" ${delay(5)}>WIN THE CUP · CLEAR THE COURT</div>
|
||||
<img class="badge r" ${delay(6)} src="${art('badge_l' + lg.id)}" alt="">
|
||||
<div class="lab tap">TAP</div>
|
||||
</div>`;
|
||||
} else if (kind === 'loot') {
|
||||
const xd = profile.runDelta(), mid = storyBossArt(act, 'mid').name;
|
||||
html = `<div class="col">
|
||||
<div class="lab gold r">CUP WON · ${court.name}</div>
|
||||
<div class="big r" ${delay(1)}>YOU CLEARED THE COURT</div>
|
||||
<div class="stats r" ${delay(2)}>
|
||||
<span class="lab">ROUNDS</span><b>${d.rounds} / ${d.rounds}</b>
|
||||
<span class="lab">XP THIS RUN</span><b>+${xd.xp}</b>
|
||||
<span class="lab">LEVEL</span><b>${xd.to}</b></div>
|
||||
<div class="lab r" ${delay(3, 'color:#e8e8dc')}>…AND GAINED THESE FOR THE HORDES TO COME</div>
|
||||
<div class="rack">${d.stock.map((e, k) => `<div class="d" style="animation-delay:${(0.7 + k * 0.22).toFixed(2)}s">
|
||||
<img src="${art(WEAPONS[e.id].icon)}" alt=""><span class="lab pink">${WEAPONS[e.id].label} ×${e.n}</span></div>`).join('')}</div>
|
||||
<div class="r" style="width:100%;display:grid;gap:6px;animation-delay:${(0.9 + d.stock.length * 0.22).toFixed(2)}s">
|
||||
<div class="lab hot">CHAPTER ${ROMAN[act]} · ${STORY.length} GAMES</div>
|
||||
<div class="pips">${Array.from({ length: STORY.length }, (_, k) => `<i class="${k === STORY.midAt - 1 ? 'm' : k === STORY.length - 1 ? 'b' : ''}"></i>`).join('')}</div>
|
||||
<div class="row"><span class="lab gold">${STORY.midAt} · ${mid}</span><span class="lab hot">${STORY.length} · ???</span></div></div>
|
||||
<button class="btn big r" ${delay(9)} data-door="go">TO THE COURT</button>
|
||||
</div>`;
|
||||
} else if (kind === 'clear') {
|
||||
const done = act >= 5, next = done ? null : courtOf(act + 1);
|
||||
const opens = storyAct(act).arcade.map(id => ARCADE_NAMES[id]).filter(Boolean);
|
||||
glow = '#3b3216';
|
||||
html = `<div class="col">
|
||||
<div class="lab gold r">ACT ${ROMAN[act]} CLEARED</div>
|
||||
<div class="big s" ${delay(1)}>${done ? 'THE DEAD CLUB IS QUIET' : court.name + ' IS QUIET'}</div>
|
||||
<img class="badge s" ${delay(2)} src="${art(done ? 'badge_champ' : 'badge_l' + lg.id)}" alt="">
|
||||
<div class="stats r" ${delay(3)}>
|
||||
<span class="lab">BODIES</span><b>${stats.runKills ? stats.runKills() : 0}</b>
|
||||
<span class="lab">BOSSES</span><b>2 / 2</b>
|
||||
<span class="lab">DEATHS THIS ACT</span><b>${d.deaths}</b></div>
|
||||
${opens.length ? `<div class="r" ${delay(5, 'display:grid;gap:6px')}><div class="lab">NEW IN ARCADE</div>
|
||||
<div class="chips">${opens.map(n => `<span class="chip">${n}</span>`).join('')}</div></div>` : ''}
|
||||
${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>`}
|
||||
</div>`;
|
||||
}
|
||||
sty.style.setProperty('--sty-glow', glow);
|
||||
sty.innerHTML = html; sty.hidden = false; paused = true;
|
||||
styDoors = /data-door/.test(html);
|
||||
const t0 = performance.now();
|
||||
return new Promise(res => {
|
||||
styClose = door => {
|
||||
if (performance.now() - t0 < 700) return; // a tap still on its way from the court cannot skip it
|
||||
sty.hidden = true; sty.innerHTML = ''; paused = false; styClose = null; res(door);
|
||||
};
|
||||
if (auto) setTimeout(() => styClose && styClose('auto'), auto * 1000);
|
||||
});
|
||||
}
|
||||
sty.addEventListener('click', e => {
|
||||
if (!styClose) return;
|
||||
const b = e.target.closest('[data-door]');
|
||||
if (b) styClose(b.dataset.door); else if (!styDoors) styClose('tap');
|
||||
});
|
||||
storyUI.show = (kind, d) => storyScreen(kind, d).then(door => {
|
||||
if (kind !== 'clear') return;
|
||||
// the act card IS the act's ending: the run closes here, and the story picks up where it now stands
|
||||
restartRun(); profile.push(); refreshSplash();
|
||||
intro.hidden = false; quitChip.hidden = true;
|
||||
if (door === 'next' && profile.prof.story.cp !== 'done') start('story'); else go('splash');
|
||||
});
|
||||
|
||||
const goMode = () => { buildTops(); go('mode'); };
|
||||
document.getElementById('mStart').addEventListener('click', () => {
|
||||
if (!profile.prof.setup) { playerReturn = 'story'; buildPlayer(); go('player'); return; }
|
||||
|
||||
Reference in new issue
Block a user