Compare commits

..

No commits in common. "1753f43f2e7f70aded60fdfcfd12c37745aae740" and "4b35876a345e9cdeb7b5446bcef30c9c4c693805" have entirely different histories.

1 changed files with 17 additions and 113 deletions

View File

@ -295,7 +295,6 @@ 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.
@ -1812,11 +1811,6 @@ 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;
@ -2538,13 +2532,8 @@ 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);
@ -2578,47 +2567,12 @@ export default class DungeonBossGame extends Phaser.Scene {
this.tweens.add({ targets: root, alpha: 1, duration: 220 });
}
// 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).
// 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.
syncHeroRow(events) {
const ab = this._advBattle;
const newUids = [];
@ -2632,7 +2586,7 @@ export default class DungeonBossGame extends Phaser.Scene {
}
}
if (!newUids.length) return;
const n = ab.heroRowN || Math.max(1, ab.heroOrder.length);
const n = 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));
@ -3079,13 +3033,6 @@ 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);
@ -3381,23 +3328,16 @@ 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; }
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(); } });
});
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(); } });
}
// Rooms only get dimmed to mark "already passed through THIS hero's run" —
@ -3450,11 +3390,8 @@ export default class DungeonBossGame extends Phaser.Scene {
onUpdate: () => heroRec.cont.hpText.setText(`${Math.round(hpObj.v)}`),
});
}
if (step.heroDies) {
this.time.delayedCall(480, () => this.removeHeroCard(uid, () => {
this.showAdventureImpactText(`${this.advHeroName(uid)}'s Soul Captured`, '#39ff6a', onDone);
}));
} else this.time.delayedCall(480, onDone);
if (step.heroDies) this.time.delayedCall(480, () => this.removeHeroCard(uid, onDone));
else this.time.delayedCall(480, onDone);
});
});
}
@ -3478,9 +3415,7 @@ export default class DungeonBossGame extends Phaser.Scene {
ab.stage.add(flash);
this.tweens.add({ targets: flash, alpha: 0, duration: 260, onComplete: () => flash.destroy() });
}
this.time.delayedCall(300, () => this.removeHeroCard(uid, () => {
this.showAdventureImpactText(`${this.advHeroName(uid)}'s Soul Captured`, '#39ff6a', onDone);
}));
this.time.delayedCall(300, () => this.removeHeroCard(uid, onDone));
});
}
@ -3506,39 +3441,8 @@ export default class DungeonBossGame extends Phaser.Scene {
ab.heroCards.delete(uid);
const idx = ab.heroOrder.indexOf(uid);
if (idx >= 0) ab.heroOrder.splice(idx, 1);
this.showAdventureImpactText(`${this.seatName(ab.seat)} was wounded`, '#ff3b3b', () => {
if (step.eliminated) this.showAdventureEliminatedBanner(this.seatName(ab.seat), onDone);
else onDone();
});
},
});
},
});
}
// Big punchy center-screen text for a boss wound or a hero's death — pops
// in, shakes/pulses for about a second, then shrinks away to nothing.
// onDone only fires once the whole sequence completes, so callers can
// chain it as a blocking beat before the walk proceeds.
showAdventureImpactText(text, color, onDone) {
const ab = this._advBattle;
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
const t = this.add.text(cx, cy, text, {
fontFamily: 'Righteous', fontSize: '60px', color, align: 'center',
stroke: '#120e16', strokeThickness: 8,
}).setOrigin(0.5).setScale(0);
ab.root.add(t);
this.tweens.add({
targets: t, scale: 1, duration: 220, ease: 'Back.easeOut',
onComplete: () => {
this.tweens.add({ targets: t, x: cx - 5, duration: 55, ease: 'Sine.easeInOut', yoyo: true, repeat: 9 });
this.tweens.add({
targets: t, scale: 1.08, duration: 150, ease: 'Sine.easeInOut', yoyo: true, repeat: 2,
onComplete: () => {
this.tweens.add({
targets: t, scale: 0, alpha: 0, duration: 260, ease: 'Cubic.easeIn',
onComplete: () => { t.destroy(); onDone(); },
});
if (step.eliminated) this.showAdventureEliminatedBanner(this.seatName(ab.seat), onDone);
else onDone();
},
});
},