Nothing arrives unannounced: the horde gets its breather, the boss gets a name

THEY KEEP COMING is a 2.4s 'over' beat again (warnHorde in beats.js) instead of a
flash raised on the same frame the wave lands, and the chapter boss walks on behind
an overlay — his name in red over ENTERED THE COURT — held for 3s by the new
self-dismissing modal clock (m.hold) rather than announced on a routine-set card.

startHorde returns a bool: if the court has been taken when the warning expires the
pendingHorde latch is re-armed rather than the set losing the horde it paid for. The
warning ticks on its own clock, not on the 'over' beat, so a beat that moves the
phase underneath it can't strand it (the mgT lock, again).

Three tests, 15 total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 16:38:29 +02:00
1 parent 1f636530b9
commit 5ea84f8511
8 files changed
+132 -16

No files matched your search

+27 -6
View File
@@ -94,8 +94,14 @@ export function startBossFight(chapter) {
zombieDuck(true); // the music drops out for it
voice('ump_boss_arrives', 0, 0.9); voice('sudden_death', 2.6, 0.85);
const art = bossArt(chapter);
G.card = { title: art.name, color: UI.bad, t: TUNE.setCardT };
G.flash = null;
// THE WALK-ON IS AN OVERLAY, not a band card. A card announced the biggest fight in the game in
// the same slot and size as a routine set, and it read as the boss simply being there. This is on
// the sanctioned modal list (a boss stage is guaranteed calm — the two lines above just cleared
// it), and it HOLDS itself for bossIntroT rather than waiting for a tap: the point is to prepare
// the player, and demanding a tap to start a fight he didn't ask for is the control steal the
// popup policy exists to prevent. A tap still skips it.
G.card = null; G.flash = null;
showModal({ title: art.name, color: UI.bad, hold: TUNE.bossIntroT, body: ['ENTERED THE COURT'] });
// (the four-line machine instruction modal is gone — people figure it out)
}
@@ -163,7 +169,7 @@ export function watchdog(dt) {
if ((stuckT += dt) < TUNE.stuckT) return;
logSwing({ result: 'UNSTUCK', reason: `nothing moved for ${TUNE.stuckT}s — released a dead latch`,
phase: G.phase, sig, aim: !!G.aim });
G.horde = null; G.mgT = 0; G.changeover = false; G.interlude = false;
G.horde = null; G.mgT = 0; G.changeover = false; G.interlude = false; G.hordeWarn = 0;
G.aim = null; endBurst(); setupPoint();
stuckT = 0; stuckSig = '';
}
@@ -200,9 +206,23 @@ export function resolveInterlude(side) {
}
}
// the swarm keeps coming: no serving, the racket auto-fires, the thumb drag steers you
// THE BREATHER BEFORE THE WAVE. The horde used to begin on the same frame as its own announcement,
// so the flash was already gone by the time the crowd was on you — "the horde is not announced
// anymore". It is an ordinary 'over' beat (no serving, the world keeps walking, the RESUMING clock
// counts it down under the flash), and startHorde still owns the transition itself.
export function warnHorde() {
if (G.horde || G.interlude || G.bossFight) return false; // same court-ownership contract as the rest
G.hordeWarn = TUNE.hordeWarnT;
G.flash = { text: 'THEY KEEP COMING', t: TUNE.hordeWarnT };
ball.active = false; G.phase = 'over'; G.overT = TUNE.hordeWarnT;
return true;
}
// the swarm keeps coming: no serving, the racket auto-fires, the thumb drag steers you.
// Returns false when another mode owns the court — the caller re-arms the latch rather than
// silently eating the wave the set paid for.
export function startHorde() {
if (G.horde || G.interlude || G.bossFight) return; // same contract as startDuel: one mode owns the court
if (G.horde || G.interlude || G.bossFight) return false; // same contract as startDuel: one mode owns the court
G.changeover = false; // a cheat-forced horde mid-changeover must not leave spawns locked
const grow = 1 + (G.sets[0] + G.sets[1]) * TUNE.hordeGrow; // each set banked makes the next wave worse
// the gun grows WITH the wave: spawn rate and length both scale by grow, so a flat gun fell
@@ -213,5 +233,6 @@ export function startHorde() {
P.z = HOME.P.z; // brace at the baseline — the horde is a hold-the-line stand (finger steers x only)
aside('ump_spectators', 0.4); // a horde IS the crowd on the court. (The near-identical 'remain seated'
// that used to play right before it belongs to the transition, not here.)
G.flash = { text: 'THEY KEEP COMING', t: 1.2 };
G.flash = { text: 'THEY KEEP COMING', t: 1.2 }; // re-raised so the warning's line carries into the wave
return true;
}