diff --git a/src/games/advancewars/AdvanceWarsGame.js b/src/games/advancewars/AdvanceWarsGame.js index 141c0ed..e76df71 100644 --- a/src/games/advancewars/AdvanceWarsGame.js +++ b/src/games/advancewars/AdvanceWarsGame.js @@ -553,6 +553,15 @@ export default class AdvanceWarsGame extends Phaser.Scene { const v = run.view.unitViews.get(e.unitId); if (v) { v.c.destroy(); run.view.unitViews.delete(e.unitId); } } + if (e.type === 'loaded' || e.type === 'joined') { + // unit merged into a transport / another unit — remove its now + // defunct board sprite once its move-in has been animated (no + // explosion). Reaped here rather than by a state-diff sync so it + // vanishes at this exact moment, not early (state is already the + // whole turn's final result — see syncUnits' prune note). + const v = run.view.unitViews.get(e.unitId); + if (v) { v.c.destroy(); run.view.unitViews.delete(e.unitId); } + } if (e.type === 'powerFired') { await new Promise((resolve) => Screens.powerCutIn(this, this.rules, this.oppById, run.state.armies[e.army].co, e.army, resolve)); run.hud.refresh(run.state, 0); @@ -570,9 +579,16 @@ export default class AdvanceWarsGame extends Phaser.Scene { // full-screen reveal, so those still do a full sync. const wideReveal = events.some((e) => ['powerFired', 'healedAll', 'refreshed', 'tsunami', 'meteor', 'armyEliminated'].includes(e.type)); + // A wide effect snaps every unit to its (final) state, but only an + // actual army elimination may *prune* dead views here — otherwise a + // mid-turn CO power would reap units merely doomed to die later in this + // same pre-resolved turn, yanking them off the board before their own + // death animation plays. Non-elimination deaths are reaped by the + // 'destroyed'/'crashed' handlers above, at the moment they animate. + const eliminated = events.some((e) => e.type === 'armyEliminated'); const touchedIds = wideReveal ? null : new Set(events.flatMap((e) => [e.unitId, e.attackerId, e.defenderId, e.transportId, e.intoId, e.byId].filter((id) => id != null))); - run.view.syncUnits(touchedIds); + run.view.syncUnits(touchedIds, { prune: eliminated }); run.view.refreshFog(0); run.hud.refresh(run.state, 0); if (enemy && (moved || battles.length || capturing)) await this.wait(500); diff --git a/src/games/advancewars/AdvanceWarsMapView.js b/src/games/advancewars/AdvanceWarsMapView.js index 83b1025..d8a0eac 100644 --- a/src/games/advancewars/AdvanceWarsMapView.js +++ b/src/games/advancewars/AdvanceWarsMapView.js @@ -442,7 +442,7 @@ export class AdvanceWarsMapView { // Used by replayEvents to reveal a turn's units one action at a time // instead of snapping everyone straight to the turn's final result. Pass // nothing (or null) for a full sync — creates/updates/prunes everyone. - syncUnits(onlyIds = null) { + syncUnits(onlyIds = null, { prune = !onlyIds } = {}) { const { state, rules, scene } = this; const present = new Set(); for (const u of state.units) { @@ -473,11 +473,12 @@ export class AdvanceWarsMapView { } if (justCreated || !onlyIds || onlyIds.has(u.id)) this.placeUnit(u, v); } - // Stale-view cleanup only runs on a full sync — a scoped pass can't - // tell a genuinely-dead unit from one whose own (not yet replayed) - // death is later in this same turn's log; the 'destroyed'/'crashed' + // Stale-view cleanup only runs when explicitly pruning (a full sync, by + // default) — a mid-replay pass can't tell a genuinely-dead unit from one + // whose own (not yet replayed) death/load/join is later in this same + // pre-resolved turn's log; the 'destroyed'/'crashed'/'loaded'/'joined' // event handlers already remove views at the right moment for that. - if (!onlyIds) { + if (prune) { for (const [id, v] of [...this.unitViews]) { if (!present.has(id)) { v.c.destroy(); this.unitViews.delete(id); } } @@ -507,7 +508,15 @@ export class AdvanceWarsMapView { } setUnitVisibility(visibleIds) { + // Views whose unit is already gone from state are mid-replay: the enemy + // turn is resolved up front, so a unit that dies (or loads/joins) later + // this turn vanishes from state while its board sprite is deliberately + // held until its own event is replayed (see syncUnits / replayEvents). + // Never force-hide those here — leave them exactly as last shown, so a + // doomed unit stays put until its death animation actually plays. + const alive = new Set(this.state.units.map((u) => u.id)); for (const [id, v] of this.unitViews) { + if (!alive.has(id)) continue; v.c.setVisible(visibleIds.has(id)); } }