One mode owns the court: the duel can no longer start inside a horde, and the game has tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-11 12:55:37 +02:00
1 parent 4569981365
commit 05930f0877
4 files changed
+210 -5

No files matched your search

+25 -3
View File
@@ -470,6 +470,10 @@ function pointTo(side, reason) {
const blocksClear = z => !'rmo'.includes(z.variant);
export function splatPoint(side, quiet = false) {
// The duel is one clean tennis point and resolveInterlude is its ONLY exit. A machine or leftover
// killed mid-duel used to score here, and a game banked mid-duel re-entered proceedAfterClear —
// which can start a boss, a card, or a horde while the duel still owns the court.
if (G.interlude) return;
if (side === 'P' && ++G.killCount < TUNE.killsPerPoint) return; // K kills bank one point; the rest just splatter
if (side === 'P') G.killCount = 0;
const gameOver = advancePoint(side);
@@ -708,7 +712,9 @@ function tickTestMode(dt) {
// Tennis rules apply for exactly one point — the absurdity peak: the umpire never stopped watching.
// This is now the ONLY source of weapons in the game: win the point or leave empty-handed.
export function startDuel() {
if (G.interlude) return;
// Hard mutual exclusion, HERE, where every caller routes through: a duel must never begin while
// another control mode owns the court. Each of these has ended a run when they overlapped.
if (G.interlude || G.horde || G.bossFight) return;
if (!zombies.some(z => z.variant === 'r')) spawnRacketZombie(); // the schedule guarantees an opponent
for (const z of zombies) if (z.variant === 'r') z.dueled = true; // a veteran can be challenged again — this marks the fresh duel
G.weapon = 0; // tennis rules for the duel: plain balls, no tins
@@ -786,6 +792,19 @@ export function endBossFight() {
let stuckT = 0, stuckSig = '';
function watchdog(dt) {
if (G.mode !== 'splatter' || G.modal) { stuckT = 0; return; } // a popup waits on the PLAYER: legitimate
// INVARIANT NET, before the clock-based test — because the clock-based test is blind to exactly
// this: a horde's own timer keeps ticking, so the sig always "moves" while the player is welded
// in place. Two states the guards above should make impossible; if a path still reaches them,
// repair on the spot and leave evidence in the miss log.
if (G.horde && G.interlude) { // a duel inside a horde: no steering, no serving, spawns keep coming
logSwing({ result: 'INVARIANT', reason: 'duel active during horde — interlude dropped, duel re-scheduled' });
G.interlude = false; G.duelDue = true; G.duelLerp = 0;
}
if (G.horde && G.phase !== 'rally') { // pretoss under a horde = the finger steers nothing
logSwing({ result: 'INVARIANT', reason: `phase '${G.phase}' during horde — forced back to rally`, phase: G.phase });
G.phase = 'rally'; ball.active = false;
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
}
const blocked = G.horde || G.mgT > 0 || G.changeover || G.phase === 'over'
|| (G.interlude && G.interlude.introT > 0)
|| (G.phase === 'toss' && ball.y < 2.0); // a toss on the floor: only a held aim keeps it there
@@ -867,7 +886,7 @@ const xLimit = () => Math.max(xVisible(), COURT.W / 2);
// the swarm keeps coming: no serving, the racket auto-fires, the thumb drag steers you
function startHorde() {
if (G.horde) return;
if (G.horde || G.interlude || G.bossFight) return; // 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
@@ -1499,7 +1518,10 @@ export function update(dt) {
if (G.meleeT > 0) G.meleeT -= dt;
if (G.rackGlow && (G.rackGlow.t -= dt) <= 0) G.rackGlow = null; // the fresh-pickup pulse fades out
watchdog(dt);
if (G.clearing && !zombies.some(blocksClear)) proceedAfterClear(); // the won-game wave is mopped up — advance
// !G.horde: a horde mows the court down to an EMPTY FRAME between spawns, and a clear latched
// before the wave began (a game won during the set card) then advanced the beat mid-horde —
// which is how a sudden-death duel once started INSIDE a horde. The clear waits for endHorde.
if (G.clearing && !G.horde && !zombies.some(blocksClear)) proceedAfterClear(); // the won-game wave is mopped up — advance
if (G.mode === 'transition') transition(dt);
// --- interlude trigger: the court is clear, an opponent holds the far court, you're about