Story after Oliver's first test: metal, a straight prologue, 2x3 chapters
- The gore bed is "Dystopian Survival", made loopable: the last ten seconds cross-faded into the first ten, 203.5 s, as MP3 at 128k (the decoder strips the encoder's delay, so the seam has no gap). High-pass at 40 Hz instead of 800, level matched to the menu theme. - The prologue no longer drops into gore for a moment: after the turn, act I's card and cup take over directly. - RESET STORY under the build stamp (two taps; test only). It resets the story, league, cups and title, keeps the level, and pushes at once so the mirror cannot hand the old act back. - The story screens have sound: a whoosh and a boom for the act card, applause and the rack's own click as each weapon lands, applause and a pock per arcade door on the act's end. - Chapters are six games: mid-boss after three, the horde after four, the act boss after six. v384, staging only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e6d566c08
commit
16d0532f97
9 files changed
+119
-30
No files matched your search
+36
-2
@@ -9,7 +9,7 @@ import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZo
|
||||
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';
|
||||
import { voice, ambient, menuMusic, menuDuck, sfx, boom, whoosh, applause, pock } from './audio.js';
|
||||
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
|
||||
import { cv } from './dom.js';
|
||||
import { haptic } from './haptics.js';
|
||||
@@ -761,9 +761,16 @@ 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
|
||||
// THE SOUND OF THE SCREENS (Oliver: "kleine Soundeffekte — die Waffen könnten das Klacken haben, das
|
||||
// auch beim Waffenwechsel im Gore-Mode spielt"). Each cue is timed to the animation it belongs to and
|
||||
// is dropped if the screen is tapped away first.
|
||||
let styTimers = [];
|
||||
const at = (secs, fn) => styTimers.push(setTimeout(fn, secs * 1000));
|
||||
const CLICK = () => sfx('weapon-click2.mp3', 0.7); // the rack's own click (gameplay.selectWeapon)
|
||||
function storyScreen(kind, d) {
|
||||
const act = d.act, { lg, court } = courtOf(act);
|
||||
let html = '', glow = '#3a1d16', auto = 0;
|
||||
styTimers.forEach(clearTimeout); styTimers = [];
|
||||
if (kind === 'act') {
|
||||
auto = 2.8;
|
||||
glow = '#1d2233';
|
||||
@@ -776,6 +783,7 @@ function storyScreen(kind, d) {
|
||||
<img class="badge r" ${delay(6)} src="${art('badge_l' + lg.id)}" alt="">
|
||||
<div class="lab tap">TAP</div>
|
||||
</div>`;
|
||||
whoosh(0.3); at(0.3, () => boom(0.45)); // the court flies in, ACT n lands
|
||||
} else if (kind === 'loot') {
|
||||
const xd = profile.runDelta(), mid = storyBossArt(act, 'mid').name;
|
||||
html = `<div class="col">
|
||||
@@ -794,6 +802,9 @@ function storyScreen(kind, d) {
|
||||
<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>`;
|
||||
applause(2.4, 0.45); at(0.3, () => pock(0.5, 700));
|
||||
// each tin lands at 70% of its 0.6 s drop — that is where it clicks into the rack
|
||||
d.stock.forEach((e, k) => at(0.7 + k * 0.22 + 0.42, CLICK));
|
||||
} 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
|
||||
@@ -818,6 +829,8 @@ function storyScreen(kind, d) {
|
||||
<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>`;
|
||||
applause(3.2, 0.55); at(0.4, () => boom(0.4)); // the act is down; the badge lands
|
||||
opens.forEach((_, k) => at(0.85 + k * 0.12, () => pock(0.55, 1100 + k * 180)));
|
||||
}
|
||||
sty.style.setProperty('--sty-glow', glow);
|
||||
sty.innerHTML = html; sty.hidden = false; paused = true;
|
||||
@@ -826,7 +839,9 @@ function storyScreen(kind, d) {
|
||||
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);
|
||||
sty.hidden = true; sty.innerHTML = ''; paused = false; styClose = null;
|
||||
styTimers.forEach(clearTimeout); styTimers = [];
|
||||
res(door);
|
||||
};
|
||||
if (auto) setTimeout(() => styClose && styClose('auto'), auto * 1000);
|
||||
});
|
||||
@@ -850,6 +865,25 @@ document.getElementById('mStart').addEventListener('click', () => {
|
||||
start('story');
|
||||
});
|
||||
document.getElementById('mOther').addEventListener('click', goMode);
|
||||
// RESET STORY (Oliver, 2026-10-15: "ich brauche zumindest während des Tests einen Button, der meinen
|
||||
// Spielstand wieder resettet"). Two taps, so a stray one costs nothing. It puts the story back at the
|
||||
// prologue and takes back what the story hands out — league, cups, the champion's flag — and keeps the
|
||||
// level (xp is the server mirror's one-way ratchet). The push makes the mirror agree at once: pull()
|
||||
// moves the story only forward, and a mirror still holding act III would hand it straight back.
|
||||
{
|
||||
const b = document.getElementById('mReset');
|
||||
b.hidden = !STORE.testReset;
|
||||
let armT = -Infinity; // (0 would count as armed in a page younger than 3 s)
|
||||
b.addEventListener('click', () => {
|
||||
if (performance.now() - armT > 3000) { armT = performance.now(); b.textContent = 'TAP AGAIN TO RESET'; b.classList.add('arm'); return; }
|
||||
const p = profile.prof;
|
||||
p.story = profile.freshStory(); p.lg = 1; p.ch = 0; p.cups = {}; p.at = {}; p.out = 0;
|
||||
profile.save(); profile.push();
|
||||
armT = -Infinity; b.textContent = 'STORY RESET'; b.classList.remove('arm');
|
||||
refreshSplash(); buildTops();
|
||||
setTimeout(() => { b.textContent = 'RESET STORY'; }, 1600);
|
||||
});
|
||||
}
|
||||
document.getElementById('mVs').addEventListener('click', () => vsOpen());
|
||||
document.getElementById('mGore').addEventListener('click', () => {
|
||||
if (!arcadeOpen('gore')) return;
|
||||
|
||||
Reference in new issue
Block a user