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.
This commit is contained in:
Brian Fertig 2026-07-07 18:09:41 -06:00
parent 8c8bce814f
commit 4b35876a34
1 changed files with 26 additions and 0 deletions

View File

@ -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);