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
+32
@@ -636,6 +636,38 @@ Each phase ships to staging and gets a phone verdict, and tests ride along with
|
|||||||
5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members'
|
5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members'
|
||||||
Night (option C).
|
Night (option C).
|
||||||
|
|
||||||
|
## Session log: THE STORY, Oliver's first test (2026-10-15, v384, STAGING ONLY)
|
||||||
|
Oliver: *"after my first test it all made a good impression"*, plus five asks.
|
||||||
|
- **The metal track** ("Dystopian Survival", 3:36). `assets/audio/metal-loop.mp3` is 0.5-214 s with the
|
||||||
|
last 10 s cross-faded (equal power, `acrossfade qsin`) into the first 10. The seam sits inside the
|
||||||
|
fade and the loop is exactly 203.5 s.
|
||||||
|
- Built with a static ffmpeg in the scratchpad: decode to WAV first, then three `-ss/-t` inputs. Two
|
||||||
|
`atrim`s of ONE input read out of order yield an empty crossfade.
|
||||||
|
- It was AAC first; MP3 at Oliver's call. The LAME tag carries the encoder delay and padding, and the
|
||||||
|
decoder strips them: 8974350 frames measured in Chromium, signal at both ends. `GORE_BED.loopEnd`
|
||||||
|
pins it anyway.
|
||||||
|
- `lowcut` is 40 Hz (the old 800 Hz high-pass would gut metal) and `vol` is 0.5: it is mastered at
|
||||||
|
-14 LUFS against the old bed's -8.
|
||||||
|
- The open-source Chromium used for tests cannot decode AAC at all, the old beds included. Phones
|
||||||
|
can; that was never a file defect.
|
||||||
|
- **The prologue goes straight on** (*"momentan wechselt man kurz in den Gore-Mode, das ist
|
||||||
|
verwirrend"*). In the story the old transformation stops after the turn: no mob, no "remain
|
||||||
|
seated", no gore beat. At 5.2 s, `enterSplatter(quiet)` and act I's card and cup take over.
|
||||||
|
- Because the quiet entry no longer loads the gore bed, the chapter start and the transformation's
|
||||||
|
run-off call `zombieMusic()` (load if needed, then un-duck) instead of `zombieDuck(false)`.
|
||||||
|
- **RESET STORY** (test only, `STORE.testReset`) under the splash's build stamp. Two taps within 3 s.
|
||||||
|
- It resets the story, league, cups and champion flag; the level stays.
|
||||||
|
- It pushes at once, or `pull()` (forward-only) would hand the old act back.
|
||||||
|
- Gotcha: `armT` started at 0, which counts as armed in a page younger than 3 s.
|
||||||
|
- **Screen sounds:**
|
||||||
|
- Act card: a whoosh and a boom when the title lands.
|
||||||
|
- Ceremony: applause, and the rack's own `weapon-click2` as each tin lands, at 70% of its drop.
|
||||||
|
- Act card at the end: applause, a boom for the badge, a rising pock per arcade door.
|
||||||
|
- Timers are cleared when a screen is tapped away.
|
||||||
|
- **Chapters are 2×3** (*"statt 2x5 lieber 2x3"*): `STORY.midAt` 3, `length` 6, and `hordeAt` 4. A
|
||||||
|
chapter's "set" is four games: at the arcade's six the set would land on the act boss, and the horde
|
||||||
|
would vanish behind him. The test pins one horde after game four.
|
||||||
|
|
||||||
## Session log: THE STORY, PHASES 1-4 — built on staging (2026-10-15, branch `story`, v380-v382, STAGING ONLY)
|
## Session log: THE STORY, PHASES 1-4 — built on staging (2026-10-15, branch `story`, v380-v382, STAGING ONLY)
|
||||||
|
|
||||||
Oliver: *"mache alle, ich vertraue. alles erst mal auf staging"*. The plan is in "THE STORY" below.
|
Oliver: *"mache alle, ich vertraue. alles erst mal auf staging"*. The plan is in "THE STORY" below.
|
||||||
|
|||||||
Binary file not shown.
+7
-1
@@ -250,6 +250,10 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#intro .slogan b { color: #c9564a; }
|
#intro .slogan b { color: #c9564a; }
|
||||||
/* which build is served — filled in by main.js, empty (and invisible) if the header is missing */
|
/* which build is served — filled in by main.js, empty (and invisible) if the header is missing */
|
||||||
#build { margin: 14px 0 0; font-size: 9px; color: #4a5a52; letter-spacing: 0.5px; }
|
#build { margin: 14px 0 0; font-size: 9px; color: #4a5a52; letter-spacing: 0.5px; }
|
||||||
|
#mReset { display: block; margin: 6px auto 0; padding: 6px 10px; background: none; border: 1px solid #2c3a33;
|
||||||
|
color: #6b7a72; font: 400 9px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer; }
|
||||||
|
#mReset.arm { color: #d9705f; border-color: #d9705f; }
|
||||||
|
#splash.busy #mReset { display: none; }
|
||||||
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
|
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
|
||||||
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
|
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
|
||||||
court through between menus. */
|
court through between menus. */
|
||||||
@@ -973,6 +977,8 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
<button class="btn sec" id="mRanks">WORLD BOARD</button>
|
<button class="btn sec" id="mRanks">WORLD BOARD</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="build"></p>
|
<p id="build"></p>
|
||||||
|
<!-- TEST ONLY (Oliver, 2026-10-15): start the story over. config.STORE.testReset hides it. -->
|
||||||
|
<button id="mReset" type="button">RESET STORY</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Additional solo modes; multiplayer has a separate splash entrance. -->
|
<!-- Additional solo modes; multiplayer has a separate splash entrance. -->
|
||||||
@@ -1195,6 +1201,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=383"></script>
|
<script type="module" src="src/main.js?v=384"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+1
-1
@@ -211,7 +211,7 @@ export const ambient = () => {
|
|||||||
// ...and the splatter bed, same treatment: 5.9 MB of 24-bit WAV down to 287 KB. Its master is
|
// ...and the splatter bed, same treatment: 5.9 MB of 24-bit WAV down to 287 KB. Its master is
|
||||||
// 982597 frames at 48 kHz, which is the un-round 20.47077 s below — an AAC loop needs the SOURCE
|
// 982597 frames at 48 kHz, which is the un-round 20.47077 s below — an AAC loop needs the SOURCE
|
||||||
// length as loopEnd, not the decoder's padded one. See the note on menuMusic.
|
// length as loopEnd, not the decoder's padded one. See the note on menuMusic.
|
||||||
export const zombieMusic = () => { loopBed(GORE_BED.file, 800, GORE_BED.vol, GORE_BED.loopEnd); zombieDuck(false); };
|
export const zombieMusic = () => { loopBed(GORE_BED.file, GORE_BED.lowcut ?? 800, GORE_BED.vol, GORE_BED.loopEnd); zombieDuck(false); };
|
||||||
// Menu theme: every menu screen and the death summary. Barely highpassed — this one is MUSIC, not
|
// Menu theme: every menu screen and the death summary. Barely highpassed — this one is MUSIC, not
|
||||||
// an atmosphere bed, so the low end is the point. Started by the FIRST TAP (main.js `go`), never at
|
// an atmosphere bed, so the low end is the point. Started by the FIRST TAP (main.js `go`), never at
|
||||||
// boot: a title screen that starts playing at you before you have touched it is rude, and on a phone
|
// boot: a title screen that starts playing at you before you have touched it is rude, and on a phone
|
||||||
|
|||||||
+7
-8
@@ -11,7 +11,7 @@ import { TUNE, UI, bossArt, matchFoe, STYLES, BENCH, ZFAMS, COURT, HOME, racketS
|
|||||||
import { endRun, MODE_MATCH } from './stats.js';
|
import { endRun, MODE_MATCH } from './stats.js';
|
||||||
import { award, prof, save as profSave, push as profPush, setPromo, levelAt, recordCup, recordCupWin } from './profile.js';
|
import { award, prof, save as profSave, push as profPush, setPromo, levelAt, recordCup, recordCupWin } from './profile.js';
|
||||||
import { G, P, O, ball, score, setupPoint, sendBoy, other } from './state.js';
|
import { G, P, O, ball, score, setupPoint, sendBoy, other } from './state.js';
|
||||||
import { voice, zombieDuck, aww, applause, shriek, boom, splat } from './audio.js';
|
import { voice, zombieDuck, zombieMusic, aww, applause, shriek, boom, splat } from './audio.js';
|
||||||
import { haptic } from './haptics.js';
|
import { haptic } from './haptics.js';
|
||||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||||
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, logPoint, advancePoint, scoreKey, grantWeapon } from './gameplay.js';
|
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, logPoint, advancePoint, scoreKey, grantWeapon } from './gameplay.js';
|
||||||
@@ -30,7 +30,7 @@ export function proceedAfterClear() {
|
|||||||
// The chapter boss outranks everything here, including a set: a boss walking on into a standing
|
// The chapter boss outranks everything here, including a set: a boss walking on into a standing
|
||||||
// wave (or into a horde) would be unreadable. Like the duel it parks the interrupted beat.
|
// wave (or into a horde) would be unreadable. Like the duel it parks the interrupted beat.
|
||||||
// THE STORY's bosses ride the same slot, ahead of everything: the mid-boss at game 5, the act's
|
// THE STORY's bosses ride the same slot, ahead of everything: the mid-boss at game 5, the act's
|
||||||
// boss at game 10 (splatPoint arms G.storyBoss; the arcade's G.bossDue is never set in a chapter)
|
// boss at the chapter's last game (splatPoint arms G.storyBoss; the arcade's G.bossDue is never set in a chapter)
|
||||||
if (G.storyBoss) { const k = G.storyBoss; G.storyBoss = null; G.afterDuel = mode || 'game'; startStoryBoss(k); return; }
|
if (G.storyBoss) { const k = G.storyBoss; G.storyBoss = null; G.afterDuel = mode || 'game'; startStoryBoss(k); return; }
|
||||||
if (G.bossDue) { const c = G.bossDue; G.bossDue = 0; G.afterDuel = mode || 'game'; startBossFight(c); return; }
|
if (G.bossDue) { const c = G.bossDue; G.bossDue = 0; G.afterDuel = mode || 'game'; startBossFight(c); return; }
|
||||||
// The set/horde beat outranks the duel; the duel just waits for the next clear.
|
// The set/horde beat outranks the duel; the duel just waits for the next clear.
|
||||||
@@ -1129,7 +1129,7 @@ export function startHorde() {
|
|||||||
|
|
||||||
// ---- THE STORY (2026-10-15, PROJECT.md "THE STORY") ---------------------------------------------
|
// ---- THE STORY (2026-10-15, PROJECT.md "THE STORY") ---------------------------------------------
|
||||||
// Five acts, one per league and court: the league cup, the transformation, one ten-game gore chapter
|
// Five acts, one per league and court: the league cup, the transformation, one ten-game gore chapter
|
||||||
// with a mid-boss at game 5 (the checkpoint) and the act's boss at game 10, then the act card. Same
|
// with a mid-boss at STORY.midAt (the checkpoint) and the act's boss at its last game, then the act card. Same
|
||||||
// court-ownership contract as every mode in this file. The PLACE in the story lives in the profile
|
// court-ownership contract as every mode in this file. The PLACE in the story lives in the profile
|
||||||
// (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
|
||||||
@@ -1182,7 +1182,7 @@ export function startStoryChapter(fromMid = false, keepCrowd = false) {
|
|||||||
s.cp = fromMid ? 'mid' : 'chap';
|
s.cp = fromMid ? 'mid' : 'chap';
|
||||||
if (!fromMid) s.rack = stock.map(e => ({ ...e }));
|
if (!fromMid) s.rack = stock.map(e => ({ ...e }));
|
||||||
profSave(); profPush();
|
profSave(); profPush();
|
||||||
zombieDuck(false);
|
zombieMusic(); // the gore bed — loaded here if no door loaded it yet
|
||||||
ball.active = false; setupPoint();
|
ball.active = false; setupPoint();
|
||||||
if (!keepCrowd) G.card = { title: 'CHAPTER ' + ROMAN[act], color: UI.hot, t: 3.2,
|
if (!keepCrowd) G.card = { title: 'CHAPTER ' + ROMAN[act], color: UI.hot, t: 3.2,
|
||||||
sub1: fromMid ? 'FROM GAME ' + (STORY.midAt + 1) + ' · ' + storyBossArt(act, 'end').name + ' WAITS AT ' + STORY.length
|
sub1: fromMid ? 'FROM GAME ' + (STORY.midAt + 1) + ' · ' + storyBossArt(act, 'end').name + ' WAITS AT ' + STORY.length
|
||||||
@@ -1197,7 +1197,7 @@ export function startStoryChapter(fromMid = false, keepCrowd = false) {
|
|||||||
// 0.0 the ceremony modal (the world waits for its tap)
|
// 0.0 the ceremony modal (the world waits for its tap)
|
||||||
// 1.2 the applause is cut, "quiet, please", the camera closes in on him
|
// 1.2 the applause is cut, "quiet, please", the camera closes in on him
|
||||||
// 2.8 THE TURN: hitstop, flash, he bursts — and the act's boss stands where he stood, growing
|
// 2.8 THE TURN: hitstop, flash, he bursts — and the act's boss stands where he stood, growing
|
||||||
// 3.3 his name and "GAME 10"
|
// 3.3 his name and "GAME <length>"
|
||||||
// 4.6 he runs out of the frame; the stands empty onto the court; the metal comes in
|
// 4.6 he runs out of the frame; the stands empty onto the court; the metal comes in
|
||||||
// 6.4 game 1 of the chapter, without a cut (the crowd that came down is its first wave)
|
// 6.4 game 1 of the chapter, without a cut (the crowd that came down is its first wave)
|
||||||
// The spawn director, the boss's AI and serving all stay held by the interlude until the chapter.
|
// The spawn director, the boss's AI and serving all stay held by the interlude until the chapter.
|
||||||
@@ -1249,7 +1249,7 @@ function tickMorph(dt) {
|
|||||||
// the stands empty onto the court: the chapter's first wave walks on from the far side
|
// the stands empty onto the court: the chapter's first wave walks on from the far side
|
||||||
for (const [x, z, v] of [[-3.5, -1.6, 'a'], [2.5, -2.1, 'b'], [-1.2, -2.6, 'a'], [3.8, -1.2, 'b']]) spawnZombie(x, z, v);
|
for (const [x, z, v] of [[-3.5, -1.6, 'a'], [2.5, -2.1, 'b'], [-1.2, -2.6, 'a'], [3.8, -1.2, 'b']]) spawnZombie(x, z, v);
|
||||||
G.shake = { t: 0.5, mag: 6 }; voice('remain_seated', 0, 0.95);
|
G.shake = { t: 0.5, mag: 6 }; voice('remain_seated', 0, 0.95);
|
||||||
zombieDuck(false); // the chapter's music comes in here (metal: phase 5)
|
zombieMusic(); // THE METAL comes in here (config.GORE_BED)
|
||||||
}
|
}
|
||||||
if (m.boss && t >= MORPH.run) { // ...and he runs out of the picture, left
|
if (m.boss && t >= MORPH.run) { // ...and he runs out of the picture, left
|
||||||
const step = 8 * dt;
|
const step = 8 * dt;
|
||||||
@@ -1311,7 +1311,6 @@ function storyActCleared() {
|
|||||||
export function tickStory(dt) {
|
export function tickStory(dt) {
|
||||||
if (!G.story) return;
|
if (!G.story) return;
|
||||||
if (G.story.morph) return tickMorph(dt);
|
if (G.story.morph) return tickMorph(dt);
|
||||||
if (G.story.proT > 0 && (G.story.proT -= dt) <= 0) return storyPrologueDone();
|
|
||||||
if (G.story.winT > 0 && (G.story.winT -= dt) <= 0) { G.story.winT = 0; storyActCleared(); }
|
if (G.story.winT > 0 && (G.story.winT -= dt) <= 0) { G.story.winT = 0; storyActCleared(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,7 +1320,7 @@ export function tickStory(dt) {
|
|||||||
// the dead take their seats for the first act's cup.
|
// the dead take their seats for the first act's cup.
|
||||||
export function storyPrologueGo() {
|
export function storyPrologueGo() {
|
||||||
if (!G.story || G.story.part !== 'pro') return;
|
if (!G.story || G.story.part !== 'pro') return;
|
||||||
G.story.proT = 2.4; // the mob is seen walking on before the act opens
|
storyPrologueDone(); // straight on: the act's card, then its cup
|
||||||
}
|
}
|
||||||
function storyPrologueDone() {
|
function storyPrologueDone() {
|
||||||
prof.story.cp = 'cup'; profSave(); profPush();
|
prof.story.cp = 'cup'; profSave(); profPush();
|
||||||
|
|||||||
+18
-9
@@ -1020,7 +1020,7 @@ export const cupFixture = (lg, champion = 0) => {
|
|||||||
|
|
||||||
// ---- THE STORY (2026-10-15, PROJECT.md "THE STORY"): five acts, one per league and court. An act is
|
// ---- THE STORY (2026-10-15, PROJECT.md "THE STORY"): five acts, one per league and court. An act is
|
||||||
// the league cup (zombie tennis), the transformation, and one ten-game gore chapter with a MID-BOSS
|
// the league cup (zombie tennis), the transformation, and one ten-game gore chapter with a MID-BOSS
|
||||||
// at game 5 (the checkpoint) and the act's BOSS at game 10. Pure data; beats.js runs it.
|
// (STORY.midAt, the checkpoint) and the act's BOSS at its last game. Pure data; beats.js runs it.
|
||||||
// rounds the cup's rungs, in order. The FINAL is always the boss's own body (it grows into the
|
// rounds the cup's rungs, in order. The FINAL is always the boss's own body (it grows into the
|
||||||
// boss), so acts II and IV play their band out of ladder order — cupRoundsFor is untouched.
|
// boss), so acts II and IV play their band out of ladder order — cupRoundsFor is untouched.
|
||||||
// mid BOSS_ART index of the game-5 boss (the five designs one chapter per act leaves over)
|
// mid BOSS_ART index of the game-5 boss (the five designs one chapter per act leaves over)
|
||||||
@@ -1028,7 +1028,7 @@ export const cupFixture = (lg, champion = 0) => {
|
|||||||
// 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)
|
// 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+length (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: [
|
||||||
@@ -1038,8 +1038,12 @@ export const STORY = {
|
|||||||
{ lg: 4, rounds: [13, 15, 14], mid: 4, boss: 3, pool: ['flamer', 'rocket'], arcade: ['vs'] },
|
{ 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, arcade: [] },
|
{ 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)
|
// Oliver on v383: "Gore scheint mir etwas zu lang — statt 2x5 lieber 2x3 Spiele". Six games, the
|
||||||
length: 10, // games in a chapter; the act's boss walks on after the last
|
// mid-boss after the third, the act's boss after the sixth — and the horde after the fourth, between
|
||||||
|
// them (a chapter's 'set' is hordeAt games: at the arcade's six it would land on the boss and vanish).
|
||||||
|
midAt: 3, // the mid-boss walks on after this many chapter games (and is the checkpoint)
|
||||||
|
length: 6, // games in a chapter; the act's boss walks on after the last
|
||||||
|
hordeAt: 4, // the chapter's set: the horde comes after this many games
|
||||||
base: act => (act - 1) * 20,
|
base: act => (act - 1) * 20,
|
||||||
};
|
};
|
||||||
export const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V'];
|
export const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V'];
|
||||||
@@ -1049,13 +1053,18 @@ export const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V'];
|
|||||||
export const STORY_HELP = { offerAt: 2, max: 2, run: 0.08, window: 0.12, heart: 1, slow: 0.25 };
|
export const STORY_HELP = { offerAt: 2, max: 2, run: 0.08, window: 0.12, heart: 1, slow: 0.25 };
|
||||||
// THE STORE's switches (PROJECT.md "LATER — shipping it"). Both OFF until the unlock IAP exists: the
|
// THE STORE's switches (PROJECT.md "LATER — shipping it"). Both OFF until the unlock IAP exists: the
|
||||||
// free tier ends on act I's card, and the arcade may become a premium door. premium() is the stub.
|
// free tier ends on act I's card, and the arcade may become a premium door. premium() is the stub.
|
||||||
export const STORE = { freeTier: false, arcadePremium: false };
|
export const STORE = { freeTier: false, arcadePremium: false, testReset: true }; // testReset: the splash's RESET STORY button
|
||||||
// THE GORE BED — the one track that plays while the dead are being killed (gore, every story chapter,
|
// THE GORE BED — the one track that plays while the dead are being killed (gore, every story chapter,
|
||||||
// from the transformation's run-off on), ducked everywhere else. Oliver wants METAL here (2026-10-15)
|
// from the transformation's run-off on), ducked everywhere else. Oliver wants METAL here (2026-10-15)
|
||||||
// and is finding the track: drop it into assets/audio and change these three values. `loopEnd` is the
|
// sent "Dystopian Survival" (2026-10-15). metal-loop.mp3 is that track made loopable: 0.5-214 s with
|
||||||
// SOURCE length in seconds for an AAC file (its padded last packet clicks at the seam otherwise), 0
|
// its last ten seconds cross-faded (equal power) into its first ten, so the seam is inside the fade
|
||||||
// for a WAV; `vol` is the bed's level under the umpire.
|
// and the loop is 203.5 s exactly. MP3 at Oliver's call; the encoder's delay and padding are carried
|
||||||
export const GORE_BED = { file: 'zombie-mode.m4a', loopEnd: 20.47077, vol: 0.2 };
|
// in the LAME tag and the decoder strips them (measured in Chromium: 8974350 frames, no silence at
|
||||||
|
// either end), and `loopEnd` pins the source length anyway. `vol` sets it near the menu theme's level
|
||||||
|
// (it is mastered at -14 LUFS, the old bed at -8); `lowcut` 40 Hz keeps the guitars' body, where the
|
||||||
|
// old bed's 800 Hz high-pass would have thinned metal to a radio. The old bed: zombie-mode.m4a,
|
||||||
|
// 20.47077 s, 0.2, 800.
|
||||||
|
export const GORE_BED = { file: 'metal-loop.mp3', loopEnd: 203.5, vol: 0.5, lowcut: 40 };
|
||||||
export const storyAct = act => STORY.acts[Math.max(1, Math.min(5, act)) - 1];
|
export const storyAct = act => STORY.acts[Math.max(1, Math.min(5, act)) - 1];
|
||||||
// the story cup: the league's own round names and needs, on the act's rung order
|
// the story cup: the league's own round names and needs, on the act's rung order
|
||||||
export const storyRounds = act => {
|
export const storyRounds = act => {
|
||||||
|
|||||||
+9
-5
@@ -893,7 +893,7 @@ export function splatPoint(side, quiet = false) {
|
|||||||
// the schedule calls a DUEL, not a tin: it comes round on its own so there's always a chance
|
// the schedule calls a DUEL, not a tin: it comes round on its own so there's always a chance
|
||||||
// at a weapon, but you still have to win the point to be handed one. Fires at the next clear.
|
// at a weapon, but you still have to win the point to be handed one. Fires at the next clear.
|
||||||
// THE STORY's chapter has no duel, and its two bosses are its own: the mid-boss after game 5
|
// THE STORY's chapter has no duel, and its two bosses are its own: the mid-boss after game 5
|
||||||
// (the checkpoint) and the act's boss after game 10. The arcade schedule below never runs there.
|
// (the checkpoint) and the act's boss after the last (config.STORY). The arcade schedule below never runs there.
|
||||||
const chap = G.story && G.story.part === 'chap';
|
const chap = G.story && G.story.part === 'chap';
|
||||||
if (chap) {
|
if (chap) {
|
||||||
const rel = G.runGames - G.story.base;
|
const rel = G.runGames - G.story.base;
|
||||||
@@ -913,7 +913,7 @@ export function splatPoint(side, quiet = false) {
|
|||||||
if (side === 'O') aside('ump_scoreline', 1.4); // they took a game: he would like that noted
|
if (side === 'O') aside('ump_scoreline', 1.4); // they took a game: he would like that noted
|
||||||
if (side === 'P') cheer(2.2, 0.5); else aww(0.85); // your game = applause, theirs = a groan
|
if (side === 'P') cheer(2.2, 0.5); else aww(0.85); // your game = applause, theirs = a groan
|
||||||
const i = side === 'P' ? 0 : 1;
|
const i = side === 'P' ? 0 : 1;
|
||||||
if (score.games[i] >= 6) { G.sets[i]++; score.games = [0, 0]; G.afterClear = 'set'; } // set banked; a horde follows the card
|
if (score.games[i] >= (chap ? STORY.hordeAt : 6)) { G.sets[i]++; score.games = [0, 0]; G.afterClear = 'set'; } // set banked; a horde follows the card
|
||||||
else if ((score.games[0] + score.games[1]) % 2 === 1) G.afterClear = 'changeover'; // real rule: after odd games
|
else if ((score.games[0] + score.games[1]) % 2 === 1) G.afterClear = 'changeover'; // real rule: after odd games
|
||||||
else G.afterClear = 'game'; // even game: a beat, then the next wave
|
else G.afterClear = 'game'; // even game: a beat, then the next wave
|
||||||
G.clearWinner = side;
|
G.clearWinner = side;
|
||||||
@@ -1188,13 +1188,17 @@ function transition(dt) {
|
|||||||
gore.push({ x: O.x, z: O.z, r: 1.1, a: 0.9 });
|
gore.push({ x: O.x, z: O.z, r: 1.1, a: 0.9 });
|
||||||
spawnZombie(O.x, O.z, 'c'); G.hideO = true; // the opponent... turns
|
spawnZombie(O.x, O.z, 'c'); G.hideO = true; // the opponent... turns
|
||||||
});
|
});
|
||||||
|
// THE STORY's prologue stops at the turn (Oliver, 2026-10-15: "direkt nach der Verwandlung das Tennis-
|
||||||
|
// Spiel fortfahren — momentan wechselt man kurz in den Gore-Mode, das ist verwirrend"): no mob, no gore
|
||||||
|
// beat — the dead take their seats and act I's cup carries the match on.
|
||||||
|
const pro = G.story && G.story.part === 'pro';
|
||||||
|
if (pro) { once('story', 5.2, () => { enterSplatter(true); storyPrologueGo(); }); return; }
|
||||||
once('mob', 4.8, () => { spawnZombie(-3.5, -1.6, 'a'); spawnZombie(2.5, -2.1, 'b'); G.shake = { t: 0.5, mag: 6 }; });
|
once('mob', 4.8, () => { spawnZombie(-3.5, -1.6, 'a'); spawnZombie(2.5, -2.1, 'b'); G.shake = { t: 0.5, mag: 6 }; });
|
||||||
once('seated', 5.4, () => voice('remain_seated', 0, 0.95));
|
once('seated', 5.4, () => voice('remain_seated', 0, 0.95));
|
||||||
once('go', 8.2, () => {
|
once('go', 8.2, () => {
|
||||||
enterSplatter();
|
enterSplatter();
|
||||||
G.shake = { t: 0.7, mag: 9 }; // the floor goes out from under the match
|
G.shake = { t: 0.7, mag: 9 }; // the floor goes out from under the match
|
||||||
G.flash = { text: 'THE MATCH HAS CHANGED', t: 2.0 };
|
G.flash = { text: 'THE MATCH HAS CHANGED', t: 2.0 };
|
||||||
storyPrologueGo(); // THE STORY's prologue: the first act's cup takes over from here
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,10 +1207,10 @@ function transition(dt) {
|
|||||||
// NO modal here — nobody read it (Oliver's testteam, verbatim). The ghost hand teaches the pull
|
// NO modal here — nobody read it (Oliver's testteam, verbatim). The ghost hand teaches the pull
|
||||||
// instead: the idle clock is primed so the apron demo starts miming IMMEDIATELY rather than after
|
// instead: the idle clock is primed so the apron demo starts miming IMMEDIATELY rather than after
|
||||||
// the usual beat of inaction, and it keeps coming back whenever the player stalls.
|
// the usual beat of inaction, and it keeps coming back whenever the player stalls.
|
||||||
function enterSplatter() {
|
function enterSplatter(quiet = false) {
|
||||||
G.mode = 'splatter'; G.everTransitioned = true;
|
G.mode = 'splatter'; G.everTransitioned = true;
|
||||||
score.server = 'P'; setupPoint();
|
score.server = 'P'; setupPoint();
|
||||||
voice('play', 0.4, 0.9); zombieMusic();
|
if (!quiet) { voice('play', 0.4, 0.9); zombieMusic(); } // (the story's prologue goes straight on to a cup)
|
||||||
G.idleHintT = TUNE.pullHintAfter;
|
G.idleHintT = TUNE.pullHintAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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 { 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, 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 { 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 { cv } from './dom.js';
|
||||||
import { haptic } from './haptics.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 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 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
|
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) {
|
function storyScreen(kind, d) {
|
||||||
const act = d.act, { lg, court } = courtOf(act);
|
const act = d.act, { lg, court } = courtOf(act);
|
||||||
let html = '', glow = '#3a1d16', auto = 0;
|
let html = '', glow = '#3a1d16', auto = 0;
|
||||||
|
styTimers.forEach(clearTimeout); styTimers = [];
|
||||||
if (kind === 'act') {
|
if (kind === 'act') {
|
||||||
auto = 2.8;
|
auto = 2.8;
|
||||||
glow = '#1d2233';
|
glow = '#1d2233';
|
||||||
@@ -776,6 +783,7 @@ function storyScreen(kind, d) {
|
|||||||
<img class="badge r" ${delay(6)} src="${art('badge_l' + lg.id)}" alt="">
|
<img class="badge r" ${delay(6)} src="${art('badge_l' + lg.id)}" alt="">
|
||||||
<div class="lab tap">TAP</div>
|
<div class="lab tap">TAP</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
whoosh(0.3); at(0.3, () => boom(0.45)); // the court flies in, ACT n lands
|
||||||
} else if (kind === 'loot') {
|
} else if (kind === 'loot') {
|
||||||
const xd = profile.runDelta(), mid = storyBossArt(act, 'mid').name;
|
const xd = profile.runDelta(), mid = storyBossArt(act, 'mid').name;
|
||||||
html = `<div class="col">
|
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>
|
<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>
|
<button class="btn big r" ${delay(9)} data-door="go">TO THE COURT</button>
|
||||||
</div>`;
|
</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') {
|
} else if (kind === 'clear') {
|
||||||
// THE FREE TIER ends here, on a win (STORE.freeTier, off until the unlock exists): act I's card
|
// 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
|
// 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>
|
<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>`}
|
${done || wall ? '' : `<button class="btn r" ${delay(10)} data-door="club">THE CLUBHOUSE</button>`}
|
||||||
</div>`;
|
</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.style.setProperty('--sty-glow', glow);
|
||||||
sty.innerHTML = html; sty.hidden = false; paused = true;
|
sty.innerHTML = html; sty.hidden = false; paused = true;
|
||||||
@@ -826,7 +839,9 @@ function storyScreen(kind, d) {
|
|||||||
return new Promise(res => {
|
return new Promise(res => {
|
||||||
styClose = door => {
|
styClose = door => {
|
||||||
if (performance.now() - t0 < 700) return; // a tap still on its way from the court cannot skip it
|
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);
|
if (auto) setTimeout(() => styClose && styClose('auto'), auto * 1000);
|
||||||
});
|
});
|
||||||
@@ -850,6 +865,25 @@ document.getElementById('mStart').addEventListener('click', () => {
|
|||||||
start('story');
|
start('story');
|
||||||
});
|
});
|
||||||
document.getElementById('mOther').addEventListener('click', goMode);
|
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('mVs').addEventListener('click', () => vsOpen());
|
||||||
document.getElementById('mGore').addEventListener('click', () => {
|
document.getElementById('mGore').addEventListener('click', () => {
|
||||||
if (!arcadeOpen('gore')) return;
|
if (!arcadeOpen('gore')) return;
|
||||||
|
|||||||
@@ -2080,7 +2080,7 @@ test('the menus own the theme, and hand the court back its silence', () => {
|
|||||||
// never bring it BACK from the duck start() put it under — and nothing ducked the court's own
|
// never bring it BACK from the duck start() put it under — and nothing ducked the court's own
|
||||||
// beds at all. The menu was silent and noisy at once, permanently, from the first run onward.
|
// beds at all. The menu was silent and noisy at once, permanently, from the first run onward.
|
||||||
const d = audio.bedTest.ducked, THEME = 'background-music.m4a',
|
const d = audio.bedTest.ducked, THEME = 'background-music.m4a',
|
||||||
AMB = 'tennis-ambient-loop.wav', ZOM = 'zombie-mode.m4a';
|
AMB = 'tennis-ambient-loop.wav', ZOM = cfg.GORE_BED.file; // the gore bed (metal since 2026-10-15)
|
||||||
audio.menuMusic(); // on the menu
|
audio.menuMusic(); // on the menu
|
||||||
assert.ok(!d.has(THEME), 'the theme plays on the menu');
|
assert.ok(!d.has(THEME), 'the theme plays on the menu');
|
||||||
audio.menuDuck(true); audio.ambient(); audio.zombieMusic(); // ...a run starts
|
audio.menuDuck(true); audio.ambient(); audio.zombieMusic(); // ...a run starts
|
||||||
@@ -3737,7 +3737,7 @@ test('THE STORY: the prologue is a living match that turns, and then the first a
|
|||||||
assert.ok(G.match && G.match.cup && G.story && G.story.part === 'cup', 'act I\'s cup takes the court');
|
assert.ok(G.match && G.match.cup && G.story && G.story.part === 'cup', 'act I\'s cup takes the court');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('THE STORY: a chapter has no duel, a mid-boss checkpoint at five and the act boss at ten', () => {
|
test('THE STORY: a chapter has no duel, a mid-boss checkpoint, a horde between the bosses, and the act boss last', () => {
|
||||||
prof.prof.lg = 1;
|
prof.prof.lg = 1;
|
||||||
intoStory(2, 'chap');
|
intoStory(2, 'chap');
|
||||||
assert.equal(G.runGames, cfg.STORY.base(2));
|
assert.equal(G.runGames, cfg.STORY.base(2));
|
||||||
@@ -3756,7 +3756,12 @@ test('THE STORY: a chapter has no duel, a mid-boss checkpoint at five and the ac
|
|||||||
update(DT);
|
update(DT);
|
||||||
assert.ok(!G.interlude, 'the racket zombie is an ordinary body in the story');
|
assert.ok(!G.interlude, 'the racket zombie is an ordinary body in the story');
|
||||||
zombies.length = 0;
|
zombies.length = 0;
|
||||||
for (let g = 0; g < cfg.STORY.length - cfg.STORY.midAt; g++) { storyGame(); G.modal = null; }
|
let horde = false;
|
||||||
|
for (let g = 0; g < cfg.STORY.length - cfg.STORY.midAt; g++) {
|
||||||
|
storyGame(); G.modal = null;
|
||||||
|
if (G.pendingHorde) { horde = true; assert.equal(G.runGames - G.story.base, cfg.STORY.hordeAt, 'the horde comes after game ' + cfg.STORY.hordeAt); G.pendingHorde = false; }
|
||||||
|
}
|
||||||
|
assert.ok(horde, 'one horde, between the two bosses');
|
||||||
const boss = zombies.find(z => z.variant === 'B');
|
const boss = zombies.find(z => z.variant === 'B');
|
||||||
assert.ok(G.bossFight && boss, 'the act boss walks on after game ten');
|
assert.ok(G.bossFight && boss, 'the act boss walks on after game ten');
|
||||||
assert.equal(boss.art.name, cfg.storyBossArt(2, 'end').name, 'act II\'s boss is the sponsor mascot');
|
assert.equal(boss.art.name, cfg.storyBossArt(2, 'end').name, 'act II\'s boss is the sponsor mascot');
|
||||||
@@ -3825,7 +3830,7 @@ test('THE STORY: a profile from before the story starts it from the top, keeping
|
|||||||
const t = prof.cleanStory({ act: 9, cp: 'nonsense', rack: [{ id: 'bomb', n: 500 }, { n: 2 }], tries: -3 });
|
const t = prof.cleanStory({ act: 9, cp: 'nonsense', rack: [{ id: 'bomb', n: 500 }, { n: 2 }], tries: -3 });
|
||||||
assert.deepEqual([t.act, t.cp, t.tries], [5, 'cup', 0]);
|
assert.deepEqual([t.act, t.cp, t.tries], [5, 'cup', 0]);
|
||||||
assert.deepEqual(t.rack, [{ id: 'bomb', n: 99 }]);
|
assert.deepEqual(t.rack, [{ id: 'bomb', n: 99 }]);
|
||||||
assert.equal(cfg.storyFixture({ act: 1, cp: 'mid' }).line, 'CHAPTER I · FROM GAME 6');
|
assert.equal(cfg.storyFixture({ act: 1, cp: 'mid' }).line, 'CHAPTER I · FROM GAME ' + (cfg.STORY.midAt + 1));
|
||||||
assert.equal(cfg.storyFixture({ act: 5, cp: 'done' }).title, 'THE STORY IS TOLD');
|
assert.equal(cfg.storyFixture({ act: 5, cp: 'done' }).title, 'THE STORY IS TOLD');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user