fix: show complete hero row upfront in adventure modal
Store pre-walk entrance snapshot and seed the entire hero row when the adventure modal opens, rather than waiting for heroEnters events to stream in. This fixes the issue where only a subset of heroes were visible initially, since the engine may have already shifted heroes off the entrance queue before events reach the UI. - Add _advEntranceSnapshot to capture pre-mutation hero queue state - Add seedHeroRowFromEntrance() to populate all heroes at modal open - Fix hero row pitch (heroRowN) so it never reflows mid-session - Refresh snapshots in react/window decision path (bypasses applyDecision) - Use moveToStage() in playEnterStep for consistent hero positioning
This commit is contained in:
parent
4b35876a34
commit
3e9ee8e47b
|
|
@ -295,6 +295,7 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
this._hoverSuppressCont = null; // card container currently mid-drag — its own hover popup stays disabled until dropped
|
||||
this._advBattle = null; // adventure battle modal session state (see openAdventureModal) while open
|
||||
this._advDungeonSnapshot = null; // per-seat pre-mutation dungeon snapshot, refreshed every applyDecision (see there)
|
||||
this._advEntranceSnapshot = null; // per-seat pre-mutation entrance snapshot, refreshed alongside it (see seedHeroRowFromEntrance)
|
||||
// Bait-walk overlay (town -> entrance), refreshed every playEvents() batch
|
||||
// that contains heroWalks events — see renderTown/renderHumanBoard/
|
||||
// renderOpponents and eventFx's 'heroWalks' case.
|
||||
|
|
@ -1811,6 +1812,11 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
this._advDungeonSnapshot = this.gs.players.map((p) => p.dungeon.map((s) => ({
|
||||
room: { id: s.room.id, uid: s.room.uid }, deactivated: s.deactivated, armed: s.armed,
|
||||
})));
|
||||
// Same reasoning, for the hero queue: seedHeroRowFromEntrance needs the
|
||||
// FULL pre-walk entrance roster, but stepAdventure shifts heroes off it
|
||||
// one at a time as it computes events, so live entrance may already be
|
||||
// partially (or fully) drained by the time the modal opens.
|
||||
this._advEntranceSnapshot = this.gs.players.map((p) => p.entrance.map((h) => ({ uid: h.uid, id: h.id })));
|
||||
try {
|
||||
switch (d.kind) {
|
||||
case 'setupDiscard': actSetupDiscard(this.gs, d.seat, choice); break;
|
||||
|
|
@ -2532,8 +2538,13 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
// can appear the instant the step's own damage animation ends, rather
|
||||
// than after a further (separate) return-home tween finishes too.
|
||||
pendingSettleStep: null,
|
||||
// Fixed hero-row pitch count, set once by seedHeroRowFromEntrance so
|
||||
// later additions (syncHeroRow's rare safety-net case) don't reflow
|
||||
// everyone already laid out.
|
||||
heroRowN: 0,
|
||||
};
|
||||
this.buildAdventureRoomRow(seat);
|
||||
this.seedHeroRowFromEntrance(seat);
|
||||
this.syncHeroRow(firstBatch);
|
||||
const ab = this._advBattle;
|
||||
this.buildAdventureAttackedPortrait(seat);
|
||||
|
|
@ -2567,12 +2578,47 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
this.tweens.add({ targets: root, alpha: 1, duration: 220 });
|
||||
}
|
||||
|
||||
// Adds any hero from `events` not already in the row (every heroEnters the
|
||||
// first time; only genuinely new ones on a later, resumed batch), sized by
|
||||
// the pitch formula for the CURRENT total queue length — mirrors the
|
||||
// existing townPos pitch pattern. Existing cards are left at their original
|
||||
// slot/size on a later batch rather than reflowed, a deliberate simplification
|
||||
// since that only matters for the rare mid-session-decision resume case.
|
||||
// Populates the ENTIRE hero row up front, from the pre-walk entrance
|
||||
// snapshot (see applyDecision/applyAdventureWindowChoice), so every hero
|
||||
// queued for this seat's dungeon this turn is visible in ADV_HERO_ROW the
|
||||
// moment the modal opens — not just the ones whose heroEnters event has
|
||||
// streamed in so far. Reading gs.players[seat].entrance directly wouldn't
|
||||
// work: by the time any of these events reach the UI, the engine may have
|
||||
// already shifted several heroes off entrance in the same advance() call
|
||||
// (see the CRITICAL CORRECTNESS NOTE above splitAdventureWalk) — the
|
||||
// snapshot is the only reliable source for the true starting roster.
|
||||
// Fixes ab.heroRowN so the row's pitch never changes later, whether a hero
|
||||
// leaves (starts walking, or dies) or a rare mid-session addition shows up
|
||||
// via syncHeroRow's safety net.
|
||||
seedHeroRowFromEntrance(seat) {
|
||||
const ab = this._advBattle;
|
||||
const roster = (this._advEntranceSnapshot && this._advEntranceSnapshot[seat]) || [];
|
||||
if (!roster.length) return;
|
||||
const n = roster.length;
|
||||
ab.heroRowN = n;
|
||||
const { x0, x1, y, maxW } = ADV_HERO_ROW;
|
||||
const pitch = Math.min(maxW + 14, (x1 - x0) / n);
|
||||
const w = Math.max(46, Math.min(maxW, pitch - 14));
|
||||
const h = w / CARD_ASPECT.hero;
|
||||
roster.forEach((entry, i) => {
|
||||
if (ab.heroMeta.has(entry.uid)) return;
|
||||
const def = heroDef({ id: entry.id });
|
||||
ab.heroMeta.set(entry.uid, { id: entry.id, def });
|
||||
ab.heroHp.set(entry.uid, def.hp);
|
||||
ab.heroOrder.push(entry.uid);
|
||||
const x = x0 + pitch / 2 + i * pitch;
|
||||
const cont = this.makeHeroCard(x, y, { uid: entry.uid, id: entry.id, hp: def.hp }, w, h,
|
||||
{ parent: ab.root, hover: false, noHoverPreview: true });
|
||||
ab.heroCards.set(entry.uid, { cont, x, y, w, h, id: entry.id, def });
|
||||
});
|
||||
}
|
||||
|
||||
// Safety net for any hero heroEnters reveals that ISN'T already in the row
|
||||
// (seedHeroRowFromEntrance normally covers the whole session up front) —
|
||||
// e.g. a hero added to this seat's entrance mid-adventure-phase by some
|
||||
// other seat's room ability. Reuses ab.heroRowN (fixed at seed time) for
|
||||
// the pitch so it never reflows heroes already laid out; falls back to the
|
||||
// current count only if seeding never ran (e.g. no snapshot available).
|
||||
syncHeroRow(events) {
|
||||
const ab = this._advBattle;
|
||||
const newUids = [];
|
||||
|
|
@ -2586,7 +2632,7 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
if (!newUids.length) return;
|
||||
const n = Math.max(1, ab.heroOrder.length);
|
||||
const n = ab.heroRowN || Math.max(1, ab.heroOrder.length);
|
||||
const { x0, x1, y, maxW } = ADV_HERO_ROW;
|
||||
const pitch = Math.min(maxW + 14, (x1 - x0) / n);
|
||||
const w = Math.max(46, Math.min(maxW, pitch - 14));
|
||||
|
|
@ -3033,6 +3079,13 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
ab.pendingWindowDecision = null;
|
||||
ab.continueBtn.setVisible(false);
|
||||
ab.spellsBtn.setVisible(false);
|
||||
// See applyDecision — this bypasses it, so the pre-mutation snapshots it
|
||||
// normally refreshes need refreshing here too (a mid-walk cast/pass can
|
||||
// itself cascade straight into a brand-new seat's walk beginning).
|
||||
this._advDungeonSnapshot = this.gs.players.map((p) => p.dungeon.map((s) => ({
|
||||
room: { id: s.room.id, uid: s.room.uid }, deactivated: s.deactivated, armed: s.armed,
|
||||
})));
|
||||
this._advEntranceSnapshot = this.gs.players.map((p) => p.entrance.map((h) => ({ uid: h.uid, id: h.id })));
|
||||
try {
|
||||
if (d.kind === 'react') actReact(this.gs, d.seat, choice);
|
||||
else actWindow(this.gs, d.seat, choice);
|
||||
|
|
@ -3328,16 +3381,23 @@ export default class DungeonBossGame extends Phaser.Scene {
|
|||
return C.ink;
|
||||
}
|
||||
|
||||
// A fresh hero's turn: bring it down from the (still-full) upper hero row
|
||||
// into the battle zone, where it stays for its whole run (playRoomHitStep's
|
||||
// own moveToStage calls become harmless no-ops once it's already there) —
|
||||
// every other queued-but-not-yet-walked hero stays put up top, undisturbed.
|
||||
playEnterStep(step, onDone) {
|
||||
const ab = this._advBattle;
|
||||
const rec = ab.heroCards.get(step.uid);
|
||||
this.resetRoomDimming(); // fresh hero, fresh run — every room but a truly destroyed one is fair game again
|
||||
if (!rec) { onDone(); return; }
|
||||
const hl = this.add.graphics();
|
||||
hl.lineStyle(3, C.gold, 1);
|
||||
hl.strokeRoundedRect(rec.x - rec.w / 2 - 5, rec.y - rec.h / 2 - 5, rec.w + 10, rec.h + 10, 8);
|
||||
ab.root.add(hl);
|
||||
this.tweens.add({ targets: hl, alpha: 0, duration: 500, ease: 'Cubic.easeOut', onComplete: () => { hl.destroy(); onDone(); } });
|
||||
this.moveToStage(step.uid, null, { duration: 280 }, () => {
|
||||
const w = ADV_STAGE_HERO_W, h = w / CARD_ASPECT.hero;
|
||||
const hl = this.add.graphics();
|
||||
hl.lineStyle(3, C.gold, 1);
|
||||
hl.strokeRoundedRect(ADV_STAGE_HERO.x - w / 2 - 5, ADV_STAGE_HERO.y - h / 2 - 5, w + 10, h + 10, 8);
|
||||
ab.root.add(hl);
|
||||
this.tweens.add({ targets: hl, alpha: 0, duration: 500, ease: 'Cubic.easeOut', onComplete: () => { hl.destroy(); onDone(); } });
|
||||
});
|
||||
}
|
||||
|
||||
// Rooms only get dimmed to mark "already passed through THIS hero's run" —
|
||||
|
|
|
|||
Loading…
Reference in New Issue