Fix combat end seam so break-off cull fires onActiveChange

- The last escort leaving the screen (encounter over without a kill) now calls `_setActive()`, which flips `active` to false and emits `onActiveChange`.
- Previously only kills triggered the flip, so GameScene's deck/soundtrack swap (`_setCombatBar`) never ran on a break-off ending.
- Add a test asserting the `true→false` active flip when the last escort is culled off-screen.
This commit is contained in:
Brian Fertig 2026-09-22 17:52:06 -06:00
parent 2f80ddfdb1
commit fededdf590
2 changed files with 17 additions and 3 deletions

View File

@ -185,15 +185,19 @@ function harness() {
scene.physics.add.existing(ship); scene.physics.add.existing(ship);
ship.radius = 12; ship.radius = 12;
ship.stop = () => {}; ship.stop = () => {};
const combat = new CombatSystem(scene, { ship }); const activeFlips = [];
return { scene, ship, combat }; const combat = new CombatSystem(scene, {
ship,
onActiveChange: (a) => activeFlips.push(a),
});
return { scene, ship, combat, activeFlips };
} }
const ids = neutralTypeIds(); const ids = neutralTypeIds();
// ---------------------------------------------------------------- roster + escort // ---------------------------------------------------------------- roster + escort
{ {
const { ship, combat } = harness(); const { ship, combat, activeFlips } = harness();
const base = config.section('combat.neutral', {}); const base = config.section('combat.neutral', {});
check('config: the escort window is data (neutral.escortMin/escortMax = 10–15 s)', check('config: the escort window is data (neutral.escortMin/escortMax = 10–15 s)',
base.escortMin === 10 && base.escortMax === 15); base.escortMin === 10 && base.escortMax === 15);
@ -240,6 +244,11 @@ const ids = neutralTypeIds();
check('despawn: broken escorts off-screen are collected (encounter ends)', check('despawn: broken escorts off-screen are collected (encounter ends)',
combat.allies.length === 0); combat.allies.length === 0);
check('active: over when the last escort is off-screen', combat.active === false); check('active: over when the last escort is off-screen', combat.active === false);
// The seam the scene hangs the deck/soundtrack swap on (GameScene
// _setCombatBar — combat bar out, normal deck + deep-space music back)
// must fire on the break-off cull, not only on a kill.
check('seam: onActiveChange flips true→false when the last escort leaves the screen',
JSON.stringify(activeFlips) === '[true,false]');
} }
// ---------------------------------------------------------------- engage // ---------------------------------------------------------------- engage

View File

@ -650,6 +650,11 @@ export class CombatSystem {
} }
} }
this.allies = this.allies.filter((a) => !a.gone && !a._removed); this.allies = this.allies.filter((a) => !a.gone && !a._removed);
// The last escort off-screen (with the pack spent) is the encounter
// OVER — the flip back to the normal deck/soundtrack rides the same
// seam as a kill (killEnemy/killAlly already fire it; the break-off
// cull is the ending that was missing it).
this._setActive();
} }
// 2) AUTO-FIRE: the nearest VISIBLE hostile in range (game.autoFire). // 2) AUTO-FIRE: the nearest VISIBLE hostile in range (game.autoFire).