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
+49
-1
@@ -122,6 +122,51 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#qAsk { position: fixed; inset: 0; z-index: 6; background: rgba(0,0,0,0.55);
|
#qAsk { position: fixed; inset: 0; z-index: 6; background: rgba(0,0,0,0.55);
|
||||||
display: flex; align-items: center; justify-content: center; }
|
display: flex; align-items: center; justify-content: center; }
|
||||||
#quit[hidden], #qAsk[hidden] { display: none; }
|
#quit[hidden], #qAsk[hidden] { display: none; }
|
||||||
|
/* THE STORY's screens — the storyboard's look: a dark club at night, Bangers shouting, Silkscreen
|
||||||
|
labelling, the weapons the only pink. One column, phone first; the world is paused beneath. */
|
||||||
|
#sty { position: fixed; inset: 0; z-index: 7; display: grid; place-items: center; overflow: hidden;
|
||||||
|
background: radial-gradient(circle at 50% 28%, var(--sty-glow, #3a1d16), #0b0a0c 72%);
|
||||||
|
color: #e8e8dc; font: 400 14px Silkscreen, monospace; -webkit-tap-highlight-color: transparent;
|
||||||
|
padding: env(safe-area-inset-top, 0px) max(16px, env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px) max(16px, env(safe-area-inset-left, 0px)); }
|
||||||
|
#sty[hidden] { display: none; }
|
||||||
|
#sty .col { width: min(420px, 100%); display: grid; gap: 14px; justify-items: center; text-align: center; }
|
||||||
|
#sty .bgc { position: absolute; inset: 0; background-size: cover; background-position: center 30%;
|
||||||
|
image-rendering: pixelated; opacity: .55; }
|
||||||
|
#sty .bgc::after { content: ''; position: absolute; inset: 0; background: linear-gradient(#0009, #000d 80%); }
|
||||||
|
#sty .col { position: relative; }
|
||||||
|
#sty .lab { font: 400 10px Silkscreen, monospace; letter-spacing: 2px; color: #8fa0b8; text-transform: uppercase; }
|
||||||
|
#sty .gold { color: #e0cc72; } #sty .hot { color: #d9705f; } #sty .pink { color: #ff5cf0; }
|
||||||
|
#sty .huge { font: 400 92px/0.9 Bangers, Impact, sans-serif; text-shadow: 4px 4px 0 #000; margin: 0; }
|
||||||
|
#sty .big { font: 400 34px/1 Bangers, Impact, sans-serif; text-shadow: 3px 3px 0 #000; margin: 0; }
|
||||||
|
#sty .mid { font: 400 24px/1 Bangers, Impact, sans-serif; text-shadow: 2px 2px 0 #000; }
|
||||||
|
#sty img { image-rendering: pixelated; }
|
||||||
|
#sty .badge { width: 46px; }
|
||||||
|
#sty .stats { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; text-align: left;
|
||||||
|
font: 400 18px 'Jersey 15', sans-serif; }
|
||||||
|
#sty .stats b { font-weight: 400; font-variant-numeric: tabular-nums; }
|
||||||
|
#sty .rack { width: 100%; display: flex; justify-content: space-around; align-items: end; gap: 6px; min-height: 92px;
|
||||||
|
padding: 12px 6px; border-block: 3px solid #6b4a2a; background: #0006; flex-wrap: wrap; }
|
||||||
|
#sty .rack div { display: grid; justify-items: center; gap: 4px; }
|
||||||
|
#sty .rack img { height: 40px; filter: drop-shadow(0 0 6px #ff5cf0); }
|
||||||
|
#sty .pips { width: 100%; display: grid; grid-template-columns: repeat(10, 1fr); gap: 3px; }
|
||||||
|
#sty .pips i { height: 10px; border-radius: 2px; background: #ffffff30; }
|
||||||
|
#sty .pips i.m { background: #e0cc72; } #sty .pips i.b { background: #d9705f; }
|
||||||
|
#sty .row { width: 100%; display: flex; justify-content: space-between; }
|
||||||
|
#sty .chips { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
|
||||||
|
#sty .chip { font: 400 9px Silkscreen, monospace; letter-spacing: 1px; padding: 6px 8px; border: 1px solid #ffffff40; border-radius: 3px; }
|
||||||
|
#sty .next { width: 100%; height: 92px; border-radius: 4px; background-size: cover; background-position: center;
|
||||||
|
image-rendering: pixelated; display: flex; align-items: flex-end; padding: 8px; text-align: left; box-sizing: border-box; }
|
||||||
|
#sty .next .lab { text-shadow: 1px 1px 0 #000, 0 0 6px #000; }
|
||||||
|
#sty .btn { max-width: 320px; }
|
||||||
|
#sty .tap { animation: styBlink 1.2s steps(2) infinite; }
|
||||||
|
#sty .r { animation: styRise .5s ease-out both; } #sty .s { animation: stySlam .55s cubic-bezier(.2,.9,.3,1.2) both; }
|
||||||
|
#sty .d { animation: styDrop .6s cubic-bezier(.2,.8,.3,1.1) both; } #sty .p { animation: styPan 2.4s ease-out both; }
|
||||||
|
@keyframes styRise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
|
||||||
|
@keyframes stySlam { 0% { opacity: 0; transform: scale(2.2); } 60% { opacity: 1; transform: scale(.94); } 100% { transform: none; } }
|
||||||
|
@keyframes styDrop { 0% { opacity: 0; transform: translateY(-110px) rotate(-12deg); } 70% { opacity: 1; transform: translateY(6px) rotate(3deg); } 100% { transform: none; } }
|
||||||
|
@keyframes styPan { from { background-position: center 0%; } to { background-position: center 55%; } }
|
||||||
|
@keyframes styBlink { 50% { opacity: .3; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { #sty * { animation: none !important; } }
|
||||||
.qBox { background: var(--solid); border: 2px solid var(--line); padding: 18px 16px;
|
.qBox { background: var(--solid); border: 2px solid var(--line); padding: 18px 16px;
|
||||||
width: min(320px, 84vw); display: flex; flex-direction: column; gap: 10px; }
|
width: min(320px, 84vw); display: flex; flex-direction: column; gap: 10px; }
|
||||||
/* it is a QUESTION, and a question calls — the one Bangers line on this overlay */
|
/* it is a QUESTION, and a question calls — the one Bangers line on this overlay */
|
||||||
@@ -871,6 +916,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
<button class="btn sec" id="qYes">LEAVE MATCH</button>
|
<button class="btn sec" id="qYes">LEAVE MATCH</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- THE STORY's screens (2026-10-15, PROJECT.md "THE STORY", phase 3): the act title card, the cup's
|
||||||
|
ceremony with its loot, and the act card. Built by main.storyScreen; the world is paused under it. -->
|
||||||
|
<div id="sty" hidden></div>
|
||||||
<div id="intro" data-stage="splash">
|
<div id="intro" data-stage="splash">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<!-- The title screen IS the menu. A separate menu stage behind it was the same logo over the
|
<!-- The title screen IS the menu. A separate menu stage behind it was the same logo over the
|
||||||
@@ -1140,6 +1188,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="recLamp" hidden>REC</span>
|
<span id="recLamp" hidden>REC</span>
|
||||||
<script type="module" src="src/main.js?v=380"></script>
|
<script type="module" src="src/main.js?v=381"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+14
-5
@@ -418,8 +418,9 @@ export function startMatch(cup = false, rounds = null, train = 0) {
|
|||||||
body: ['ONE BODY, ONE GAME AT A TIME',
|
body: ['ONE BODY, ONE GAME AT A TIME',
|
||||||
'FELL IT — OR BLEED OUT — AND IT COMES BACK', 'NO RUN IS BANKED — REPS PAY QUARTER XP'],
|
'FELL IT — OR BLEED OUT — AND IT COMES BACK', 'NO RUN IS BANKED — REPS PAY QUARTER XP'],
|
||||||
rules: ['FELL IT OR BLEED OUT — IT COMES BACK', 'NOTHING IS BANKED · REPS PAY ¼ XP'] });
|
rules: ['FELL IT OR BLEED OUT — IT COMES BACK', 'NOTHING IS BANKED · REPS PAY ¼ XP'] });
|
||||||
|
} else if (cup && G.story && storyUI.show) {
|
||||||
|
storyUI.show('act', { act: G.story.act }); // the act title card (it pauses the world itself)
|
||||||
} else if (cup && G.story) {
|
} else if (cup && G.story) {
|
||||||
// THE STORY's act opens on its own card (phase 3 gives it the animated title card)
|
|
||||||
const A = storyAct(G.story.act), R = G.match.cup.rounds;
|
const A = storyAct(G.story.act), R = G.match.cup.rounds;
|
||||||
showModal({ title: 'ACT ' + ROMAN[G.story.act], color: UI.warm, strap: leagueOf(A.lg).name,
|
showModal({ title: 'ACT ' + ROMAN[G.story.act], color: UI.warm, strap: leagueOf(A.lg).name,
|
||||||
body: ['WIN THE CUP — ' + R.length + ' ROUNDS', R[R.length - 1].name + ': ' + matchFoe(A.rounds[R.length - 1]).name],
|
body: ['WIN THE CUP — ' + R.length + ' ROUNDS', R[R.length - 1].name + ': ' + matchFoe(A.rounds[R.length - 1]).name],
|
||||||
@@ -1132,6 +1133,10 @@ export function startHorde() {
|
|||||||
// (prof.story: act, checkpoint, the rack it hands back) because restartRun wipes G; G.story is only
|
// (prof.story: act, checkpoint, the rack it hands back) because restartRun wipes G; G.story is only
|
||||||
// what this run is doing with it. Death is not permadeath here: it costs the attempt and restarts
|
// what this run is doing with it. Death is not permadeath here: it costs the attempt and restarts
|
||||||
// from the checkpoint (storyDeath, then main.maybeRestart re-enters through startStory).
|
// from the checkpoint (storyDeath, then main.maybeRestart re-enters through startStory).
|
||||||
|
// THE STORY's screens live in the page (main.storyScreen, phase 3). The hook is filled by main; headless
|
||||||
|
// (the suite) it stays null and every screen falls back to the plain card it replaced.
|
||||||
|
export const storyUI = { show: null };
|
||||||
|
|
||||||
export function startStory() {
|
export function startStory() {
|
||||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net || G.story) return false;
|
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net || G.story) return false;
|
||||||
const s = prof.story;
|
const s = prof.story;
|
||||||
@@ -1211,8 +1216,9 @@ function storyMorph(zb) {
|
|||||||
G.story.part = 'morph';
|
G.story.part = 'morph';
|
||||||
G.story.morph = { t: 0, x: zb ? zb.x : 0, z: zb ? zb.z : -1, body: zb, boss: null, stock };
|
G.story.morph = { t: 0, x: zb ? zb.x : 0, z: zb ? zb.z : -1, body: zb, boss: null, stock };
|
||||||
prof.story.tries = 0;
|
prof.story.tries = 0;
|
||||||
// the ceremony: phase 3 makes this the animated loot screen; the timeline starts when it is gone
|
// the ceremony: the loot screen pauses the world, and the timeline starts when it is gone
|
||||||
showModal({ title: 'THE CUP IS YOURS', color: UI.pick, story: { act, stock },
|
if (storyUI.show) storyUI.show('loot', { act, stock, rounds: d.cup.rounds.length });
|
||||||
|
else showModal({ title: 'THE CUP IS YOURS', color: UI.pick,
|
||||||
body: ['…AND THESE FOR THE HORDES TO COME', stock.map(e => WEAPONS[e.id].label).join(' · ')] });
|
body: ['…AND THESE FOR THE HORDES TO COME', stock.map(e => WEAPONS[e.id].label).join(' · ')] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1290,11 +1296,14 @@ function storyActCleared() {
|
|||||||
award('round');
|
award('round');
|
||||||
if (act < 5) { s.act = act + 1; s.cp = 'cup'; prof.lg = Math.max(prof.lg, storyAct(act + 1).lg); }
|
if (act < 5) { s.act = act + 1; s.cp = 'cup'; prof.lg = Math.max(prof.lg, storyAct(act + 1).lg); }
|
||||||
else { s.cp = 'done'; prof.ch = 1; }
|
else { s.cp = 'done'; prof.ch = 1; }
|
||||||
s.rack = []; s.tries = 0;
|
const deaths = s.deaths;
|
||||||
|
s.rack = []; s.tries = 0; s.deaths = 0;
|
||||||
profSave(); profPush();
|
profSave(); profPush();
|
||||||
G.story.cleared = act;
|
G.story.cleared = act;
|
||||||
ball.active = false; G.flash = null; G.modal = null; G.card = null;
|
ball.active = false; G.flash = null; G.modal = null; G.card = null;
|
||||||
G.done = { won: true, t: 0, story: act };
|
G.done = { won: true, t: 0, story: act };
|
||||||
|
// the act card (main.storyScreen) owns the handover; headless, the canvas summary does
|
||||||
|
if (storyUI.show) storyUI.show('clear', { act, deaths });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ticked from update beside tickMatchFall: the act boss's death beat before the act card.
|
// Ticked from update beside tickMatchFall: the act boss's death beat before the act card.
|
||||||
@@ -1324,7 +1333,7 @@ function storyPrologueDone() {
|
|||||||
// (the help offer reads it) and the checkpoint stands.
|
// (the help offer reads it) and the checkpoint stands.
|
||||||
export function storyDeath() {
|
export function storyDeath() {
|
||||||
G.card = null; // the world freezes in 'eaten': a card would hang there
|
G.card = null; // the world freezes in 'eaten': a card would hang there
|
||||||
prof.story.tries++;
|
prof.story.tries++; prof.story.deaths++;
|
||||||
profSave(); profPush();
|
profSave(); profPush();
|
||||||
G.story.dead = true;
|
G.story.dead = true;
|
||||||
}
|
}
|
||||||
+6
-5
@@ -1027,15 +1027,16 @@ export const cupFixture = (lg, champion = 0) => {
|
|||||||
// boss BOSS_ART index of the game-10 boss (the cup finalist, grown)
|
// boss BOSS_ART index of the game-10 boss (the cup finalist, grown)
|
||||||
// pool weapons this act ADDS to the rack; the stock is every pool up to and including the act
|
// pool weapons this act ADDS to the rack; the stock is every pool up to and including the act
|
||||||
// double the act's stock and restock pay two tins of each (league 5's double tins)
|
// double the act's stock and restock pay two tins of each (league 5's double tins)
|
||||||
|
// arcade the arcade doors clearing this act opens (MODES ids, plus 'vs' for hosting a friend)
|
||||||
// The chapter plays the arcade's games base+1..base+10 (base = (act-1)*20), so every director curve
|
// The chapter plays the arcade's games base+1..base+10 (base = (act-1)*20), so every director curve
|
||||||
// that reads G.runGames — spawn ramps, barrel gaps, machines, racket hp — keeps its tuned slope.
|
// that reads G.runGames — spawn ramps, barrel gaps, machines, racket hp — keeps its tuned slope.
|
||||||
export const STORY = {
|
export const STORY = {
|
||||||
acts: [
|
acts: [
|
||||||
{ lg: 1, rounds: [1, 2, 3, 4], mid: 8, boss: 0, pool: ['bomb', 'triple', 'fire'] },
|
{ lg: 1, rounds: [1, 2, 3, 4], mid: 8, boss: 0, pool: ['bomb', 'triple', 'fire'], arcade: ['train', 'classic'] },
|
||||||
{ lg: 2, rounds: [5, 6, 8, 7], mid: 6, boss: 5, pool: ['gun', 'beacon'] },
|
{ lg: 2, rounds: [5, 6, 8, 7], mid: 6, boss: 5, pool: ['gun', 'beacon'], arcade: ['zombie'] },
|
||||||
{ lg: 3, rounds: [9, 10, 11, 12], mid: 1, boss: 2, pool: ['laser'] },
|
{ lg: 3, rounds: [9, 10, 11, 12], mid: 1, boss: 2, pool: ['laser'], arcade: ['gore'] },
|
||||||
{ lg: 4, rounds: [13, 15, 14], mid: 4, boss: 3, pool: ['flamer', 'rocket'] },
|
{ lg: 4, rounds: [13, 15, 14], mid: 4, boss: 3, pool: ['flamer', 'rocket'], arcade: ['vs'] },
|
||||||
{ lg: 5, rounds: [16, 17, 18], mid: 7, boss: 9, pool: [], double: true },
|
{ lg: 5, rounds: [16, 17, 18], mid: 7, boss: 9, pool: [], double: true, arcade: [] },
|
||||||
],
|
],
|
||||||
midAt: 5, // the mid-boss walks on after this many chapter games (and is the checkpoint)
|
midAt: 5, // the mid-boss walks on after this many chapter games (and is the checkpoint)
|
||||||
length: 10, // games in a chapter; the act's boss walks on after the last
|
length: 10, // games in a chapter; the act's boss walks on after the last
|
||||||
|
|||||||
+91
-2
@@ -3,10 +3,10 @@
|
|||||||
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
|
// 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)
|
// 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 { 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 { 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 { netConnect, netHooks, netAgain } from './net.js';
|
||||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook, endHit, logGeom } from './render.js';
|
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook, endHit, logGeom } from './render.js';
|
||||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||||
@@ -729,6 +729,95 @@ document.addEventListener('visibilitychange', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
addEventListener('pagehide', () => recorder.saveRecording());
|
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'); };
|
const goMode = () => { buildTops(); go('mode'); };
|
||||||
document.getElementById('mStart').addEventListener('click', () => {
|
document.getElementById('mStart').addEventListener('click', () => {
|
||||||
if (!profile.prof.setup) { playerReturn = 'story'; buildPlayer(); go('player'); return; }
|
if (!profile.prof.setup) { playerReturn = 'story'; buildPlayer(); go('player'); return; }
|
||||||
|
|||||||
+2
-2
@@ -40,13 +40,13 @@ const cleanCups = (c, cap = 9) => {
|
|||||||
// hands back, `tries` failed attempts since the checkpoint (the help offer reads it). An ACT is the
|
// 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.
|
// unit that is saved; the checkpoints live inside it.
|
||||||
const CPS = ['pro', 'cup', 'chap', 'mid', 'done'];
|
const CPS = ['pro', 'cup', 'chap', 'mid', 'done'];
|
||||||
export const freshStory = () => ({ act: 1, cp: 'pro', rack: [], tries: 0 });
|
export const freshStory = () => ({ act: 1, cp: 'pro', rack: [], tries: 0, deaths: 0 });
|
||||||
export const cleanStory = s => {
|
export const cleanStory = s => {
|
||||||
if (!s || typeof s !== 'object') return freshStory();
|
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)
|
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) })) : [];
|
.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',
|
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)) };
|
rack, tries: Math.max(0, Math.min(99, s.tries | 0)), deaths: Math.max(0, Math.min(999, s.deaths | 0)) };
|
||||||
};
|
};
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function tally(variant, weapon) {
|
|||||||
// A shot that took two or more at once — counted ONCE per shot, on the transition to 2, so a
|
// A shot that took two or more at once — counted ONCE per shot, on the transition to 2, so a
|
||||||
// massacre of six is one multi-kill and not five.
|
// massacre of six is one multi-kill and not five.
|
||||||
export function multiKill() { run.mk++; }
|
export function multiKill() { run.mk++; }
|
||||||
|
export const runKills = () => run.k; // THE STORY's act card counts the bodies of the run in hand
|
||||||
|
|
||||||
// The run is over: fold it into the record and hand back the best run so far. `dead` separates a
|
// The run is over: fold it into the record and hand back the best run so far. `dead` separates a
|
||||||
// death from a tab closed mid-match — both are worth recording, only one is a death.
|
// death from a tab closed mid-match — both are worth recording, only one is a death.
|
||||||
|
|||||||
Reference in new issue
Block a user