From 4b35876a345e9cdeb7b5446bcef30c9c4c693805 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Tue, 7 Jul 2026 18:09:41 -0600 Subject: [PATCH] fix: handle round/game end during mid-walk adventure resolution Add ADV_SESSION_END_MARKERS to detect when a round or game ends as a direct continuation of resolving a mid-walk window choice. Previously, neither event carried a `.seat` nor was a walk-resumption type, so ADV_RESUME_MARKERS alone couldn't detect this boundary. Now, when events contain 'roundEnd' or 'gameOver', split them so the modal closes before playing anything from the new round/game-over screen, preventing narration/step queue folding into the current session. --- src/games/dungeonboss/DungeonBossGame.js | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/games/dungeonboss/DungeonBossGame.js b/src/games/dungeonboss/DungeonBossGame.js index df75fb4..be07338 100644 --- a/src/games/dungeonboss/DungeonBossGame.js +++ b/src/games/dungeonboss/DungeonBossGame.js @@ -78,6 +78,16 @@ const ADV_WALK_TYPES = new Set([ // to completion, synchronously, before advance() gets a chance to resume the // walk — so the first occurrence of one of these always marks that boundary. const ADV_RESUME_MARKERS = ['roomHits', 'adventurePause', 'heroEnters', 'bossWounded', 'eliminated']; +// A round (or the whole game) can end as a direct, uninterrupted continuation +// of resolving a mid-walk window choice — e.g. the last hero on the board +// dies and there's nothing left anywhere to react to, so stepAdventure runs +// straight through endRound()/startRound() (or endGame()) in the same +// takeEvents() batch. Neither event carries a `.seat`, and neither is a +// walk-resumption type, so ADV_RESUME_MARKERS alone can't see this boundary — +// see applyAdventureWindowChoice, which must close this modal BEFORE playing +// anything from this point on (a new round's own heroRevealed reveal, or the +// game-over screen), not fold it into this session's own narration/step queue. +const ADV_SESSION_END_MARKERS = ['roundEnd', 'gameOver']; const ADV_HERO_ROW = { x0: 40, x1: 860, y: 200, maxW: 120 }; const ADV_ROOM_ROW = { x0: 900, x1: 1880, y: 200, maxW: 150 }; const ADV_STAGE_HERO = { x: 380, y: 680 }; @@ -3033,6 +3043,22 @@ export default class DungeonBossGame extends Phaser.Scene { return; } const events = takeEvents(this.gs); + const endIdx = events.findIndex((e) => ADV_SESSION_END_MARKERS.includes(e.type)); + if (endIdx !== -1) { + // See ADV_SESSION_END_MARKERS — everything from here on belongs to a + // new round/phase (or the game-over screen), not this seat's walk. + // Close this modal first, then hand the rest to the normal pipeline — + // exactly like onSessionDrained does for the ordinary drain path, just + // using the fresh tail of THIS takeEvents() call rather than whatever + // (by now almost certainly empty) batch this session originally opened + // with. + const narration = events.slice(0, endIdx); + const rest = events.slice(endIdx); + this.playAdventureNarration(narration, () => { + this.closeAdventureModal(() => { this.busy = false; this.playEvents(rest); }); + }); + return; + } const boundary = events.findIndex((e) => ADV_RESUME_MARKERS.includes(e.type)); const narration = boundary === -1 ? events : events.slice(0, boundary); const resumed = boundary === -1 ? [] : events.slice(boundary);