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
+14
-5
@@ -1,6 +1,6 @@
|
||||
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
||||
|
||||
import { TUNE, UI, bossArt, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS, racketStage, matchWound, STYLES, LADDER, LEAGUES, leagueOfRung, foeLevel, TIN_LEAGUE, TIN_DOUBLE_LEAGUE } from './config.js';
|
||||
import { TUNE, UI, bossArt, STORY, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS, racketStage, matchWound, STYLES, LADDER, LEAGUES, leagueOfRung, foeLevel, TIN_LEAGUE, TIN_DOUBLE_LEAGUE } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||
import { audio, pock, swish, whoosh, applause, voice, voiceSeq, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
|
||||
import { haptic } from './haptics.js';
|
||||
@@ -11,7 +11,7 @@ import { endRun, beginRun, MODE_CLASSIC } from './stats.js';
|
||||
import { award, beginRunXp, prof, bonus, levelAt } from './profile.js';
|
||||
import { recordEvent, recordFrame } from './recorder.js';
|
||||
import { updateZombies, updateParts, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam , pops, popText } from './zombies.js';
|
||||
import { proceedAfterClear, startDuel, startHorde, warnHorde, resolveInterlude, watchdog, updateBench, benchKey, tickMatchFall, placeReceiver } from './beats.js'; // runtime-only cycle: beats calls back into cheer/aside/endBurst/deliverTin/showModal/logSwing
|
||||
import { proceedAfterClear, startDuel, startHorde, warnHorde, resolveInterlude, watchdog, updateBench, benchKey, tickMatchFall, placeReceiver, tickStory } from './beats.js'; // runtime-only cycle: beats calls back into cheer/aside/endBurst/deliverTin/showModal/logSwing
|
||||
import { netSendShot, netSendToss, netTick, netJudging, netSendPoint, netSendFault, netClose } from './net.js'; // runtime-only cycle: net calls back into pointTo/fault/tossBall/showModal
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
@@ -892,13 +892,20 @@ export function splatPoint(side, quiet = false) {
|
||||
G.runGames++;
|
||||
// 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.
|
||||
if (G.runGames === TUNE.duelFirst ||
|
||||
// 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.
|
||||
const chap = G.story && G.story.part === 'chap';
|
||||
if (chap) {
|
||||
const rel = G.runGames - G.story.base;
|
||||
if (rel >= STORY.length) G.storyBoss = 'end';
|
||||
else if (rel === STORY.midAt && !G.story.midDone) G.storyBoss = 'mid';
|
||||
} else if (G.runGames === TUNE.duelFirst ||
|
||||
(G.runGames > TUNE.duelFirst && (G.runGames - TUNE.duelFirst) % TUNE.duelEvery === 0))
|
||||
G.duelDue = true;
|
||||
// ...and every bossEvery games the schedule calls a CHAPTER BOSS. Like the duel it fires from
|
||||
// proceedAfterClear(), the one beat where the court is guaranteed empty, and it outranks
|
||||
// everything else there — a boss must not walk on into a standing wave.
|
||||
if (G.runGames % TUNE.bossEvery === 0)
|
||||
if (!chap && G.runGames % TUNE.bossEvery === 0)
|
||||
G.bossDue = Math.max(G.bossDue, Math.floor(G.runGames / TUNE.bossEvery)); // never backwards; a
|
||||
// long stretch with no clear skips a chapter rather than queueing one, which is intended
|
||||
|
||||
@@ -2532,7 +2539,8 @@ export function update(dt) {
|
||||
// The schedule is a FLOOR, not a ceiling: this lucky-clear trigger stays, and a bonus duel is
|
||||
// pure upside for weapon supply.
|
||||
if (G.mode === 'splatter' && !G.interlude && !G.horde && !G.changeover && !(G.mgT > 0) && !G.bossFight
|
||||
&& G.phase === 'pretoss' && zombies.length === 1 && zombies[0].variant === 'r' && !zombies[0].dueled)
|
||||
&& G.phase === 'pretoss' && zombies.length === 1 && zombies[0].variant === 'r' && !zombies[0].dueled
|
||||
&& !G.story) // the story has no duel at all
|
||||
startDuel();
|
||||
if (G.interlude && G.interlude.introT > 0) G.interlude.introT -= dt;
|
||||
// the duel is played under tennis lights: lerp the palette home while it runs, back after
|
||||
@@ -2753,6 +2761,7 @@ export function update(dt) {
|
||||
// ...and the match's FALL runs on its own clock for exactly the same reason, above the 'over'
|
||||
// block so it can hand the point back before that block puts the player in a serve stance.
|
||||
tickMatchFall(dt);
|
||||
tickStory(dt);
|
||||
if (G.phase === 'over') {
|
||||
G.overT -= dt;
|
||||
if (G.overT <= 0) {
|
||||
|
||||
Reference in new issue
Block a user