Story phase 1: the spine — acts, cups, chapters, checkpoints
The front door now continues the story. An act is its league cup, whose final is the boss's own body (acts II and IV play their band out of ladder order for that), then a ten-game gore chapter on the act's court. The chapter has no duel at all, takes the act's weapon stock into the rack, meets a mid-boss after game 5 (the checkpoint: restock and full blood) and the act boss after game 10, then the act card. Death is not permadeath: it counts the attempt, banks nothing on the boards, and TRY AGAIN restarts from the checkpoint the profile holds. Barrels hold what the act has earned. The chapter plays the arcade's games base+1..base+10 so every director curve keeps its slope. Profiles from before the story start it from the top (league, cups and champion reset; level kept). The server mirror stores the act and checkpoint and only ever moves them forward. Plain cards stand in for the screens (phase 3) and the transformation (phase 2). Six new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e1c6abba4
commit
75b450f105
10 files changed
+456
-39
No files matched your search
+19
-4
@@ -3,13 +3,13 @@
|
||||
// your baseline — one crossing it is a point for THEM.
|
||||
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
|
||||
|
||||
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, TIN_LEAGUE, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
|
||||
import { TUNE, UI, COURT, CONTACT, BOSS_ART, bossArt, WEAPONS, BARREL_WEAPONS, TIN_LEAGUE, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R, storyPool } from './config.js';
|
||||
import { G, P, O, ball, score, sendBoy, setupPoint } from './state.js';
|
||||
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh, flame, squeak, crackle, whump, gunshot, stomp, growl } from './audio.js';
|
||||
import { haptic } from './haptics.js';
|
||||
import { drillKill, drillCaught } from './drill.js';
|
||||
import { splatPoint, cheer, spentBall, aside, grantWeapon, endBurst, sliceCarom } from './gameplay.js';
|
||||
import { resolveInterlude, endBossFight } from './beats.js'; // runtime-only cycle, like the gameplay one
|
||||
import { resolveInterlude, endBossFight, storyDeath } from './beats.js'; // runtime-only cycle, like the gameplay one
|
||||
import { tally, multiKill, endRun, MODE_MATCH } from './stats.js';
|
||||
import { award, bonus, prof, recordCup } from './profile.js';
|
||||
|
||||
@@ -83,6 +83,14 @@ let lastBarrel = null;
|
||||
// run so the rocket is genuinely a late-game find, and it never hands you the same thing twice in
|
||||
// a row, which is the part a player actually notices.
|
||||
function pickBarrelWeapon() {
|
||||
// THE STORY: a barrel holds what the act has earned (config.storyPool) — the act decides the rack,
|
||||
// so a barrel in act I is a bomb, a triple or fire, never the laser the story hands out in act III
|
||||
if (G.story) {
|
||||
const own = storyPool(G.story.act).filter(id => id !== lastBarrel);
|
||||
const id = own.length ? own[Math.floor(Math.random() * own.length)] : lastBarrel || 'bomb';
|
||||
lastBarrel = id;
|
||||
return id;
|
||||
}
|
||||
const pool = [];
|
||||
for (const [id, b] of Object.entries(BARREL_WEAPONS)) {
|
||||
if (G.runGames < b.from) continue;
|
||||
@@ -132,8 +140,10 @@ export function spawnMachine() {
|
||||
// slowly and volleys at you, so it threatens BOTH channels: its shots chip your hp, and letting it
|
||||
// reach you is the ordinary instant death. Reuses the whole existing damage model — it is just a
|
||||
// body with a lot of hp (see the wound() invariant in PROJECT.md), so no weapon needs special-casing.
|
||||
export function spawnChapterBoss(chapter) {
|
||||
const art = bossArt(chapter); // which of the boss designs this chapter is (every tenth = THE CHAMPION)
|
||||
export function spawnChapterBoss(chapter, design = null) {
|
||||
// which of the boss designs this chapter is (every tenth = THE CHAMPION) — or, in THE STORY, the
|
||||
// act's own design at the act's strength (config.storyBossArt / storyBossChapter)
|
||||
const art = design || bossArt(chapter);
|
||||
// Every behaviour field on the design is OPTIONAL and falls back to the shared TUNE number, so a
|
||||
// design row that carries nothing but art behaves exactly the way a boss did before there were any.
|
||||
const hp = Math.round((TUNE.bossHp + (chapter - 1) * TUNE.bossHpPerChapter) * (art.hpMul ?? 1));
|
||||
@@ -317,6 +327,11 @@ export function killPlayer() {
|
||||
// It goes through the SAME endRun with the mode tagged and its own two headline numbers in the
|
||||
// record's two headline slots (see the shape note there), so the best is derived from the record
|
||||
// rather than stored a second time in its own key and left to drift.
|
||||
// THE STORY HAS NO PERMADEATH: the death costs the attempt, not the story (prof.story keeps the
|
||||
// checkpoint), and it banks nothing on the boards — the story is not a high-score run, and its
|
||||
// chapter games are numbered on the arcade's scale (STORY.base), which would post act V's game
|
||||
// 85 as a gore best. The summary reads G.story and offers the retry.
|
||||
if (G.story) { storyDeath(); return; }
|
||||
const m = G.match;
|
||||
// the cup's opp is a DRAWN RUNG, not a counter — bodies felled there is completed rounds. The
|
||||
// arcade counts kills on d.felled now (THE LEAGUES: opp starts at the band's bottom rung and
|
||||
|
||||
Reference in new issue
Block a user