diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 104e18f..8ca9745 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -79,7 +79,42 @@ try { throw new Error(`Expected ready units to loop idle frames: ${JSON.stringify(readyUnits)}`); } - console.log(`Verified title-to-battle flow and debug API at ${targetUrl}`); + await page.keyboard.press('F9'); + await page.waitForTimeout(100); + + await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; + }); + + const victoryState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const livingEnemiesAfterVictory = victoryState.units.filter((unit) => unit.faction === 'enemy' && unit.hp > 0); + if (livingEnemiesAfterVictory.length > 0) { + throw new Error(`Expected no living enemies after forced victory: ${JSON.stringify(livingEnemiesAfterVictory)}`); + } + + await page.screenshot({ path: 'dist/verification-battle-result-victory.png', fullPage: true }); + + await page.evaluate(() => window.__HEROS_DEBUG__?.goToBattle()); + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + + await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('defeat')); + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.battleOutcome === 'defeat' && state?.phase === 'resolved' && state?.resultVisible === true; + }); + + const defeatState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const liuBei = defeatState.units.find((unit) => unit.id === 'liu-bei'); + if (!liuBei || liuBei.hp > 0) { + throw new Error(`Expected Liu Bei to be defeated after forced defeat: ${JSON.stringify(liuBei)}`); + } + + console.log(`Verified title-to-battle flow, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 4dcd130..bde17db 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -59,13 +59,14 @@ type UnitView = { type UnitDirection = 'south' | 'east' | 'north' | 'west'; -type BattlePhase = 'idle' | 'moving' | 'command' | 'targeting' | 'animating'; +type BattlePhase = 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved'; type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait'; type DamageCommand = Exclude; type RosterTab = 'ally' | 'enemy'; type ActiveFaction = 'ally' | 'enemy'; type MapMenuAction = 'endTurn' | 'save' | 'load' | 'roster' | 'bond' | 'situation' | 'bgm' | 'close'; type EnemyAiBehavior = 'aggressive' | 'guard' | 'hold'; +type BattleOutcome = 'victory' | 'defeat'; type CommandButton = { command: BattleCommand; @@ -151,6 +152,8 @@ type BattleSaveState = { const maxEquipmentLevel = 9; const maxCharacterLevel = 99; const battleSaveStorageKey = 'heros-web:first-battle-state'; +const initialFirstBattleUnits = firstBattleUnits.map(cloneUnitData); +const initialFirstBattleBonds = firstBattleBonds.map(cloneBattleBond); const attackRangeByClass: Partial> = { archer: 2 @@ -216,6 +219,21 @@ const statLabels: Array<{ key: keyof UnitStats; label: string }> = [ { key: 'luck', label: '운' } ]; +function cloneUnitData(unit: UnitData): UnitData { + return { + ...unit, + stats: { ...unit.stats }, + equipment: JSON.parse(JSON.stringify(unit.equipment)) as UnitData['equipment'] + }; +} + +function cloneBattleBond(bond: BattleBond): BattleBond { + return { + ...bond, + unitIds: [...bond.unitIds] as [string, string] + }; +} + export class BattleScene extends Phaser.Scene { private layout!: BattleLayout; private phase: BattlePhase = 'idle'; @@ -234,6 +252,7 @@ export class BattleScene extends Phaser.Scene { private alertDismissEvent?: Phaser.Time.TimerEvent; private turnPromptObjects: Phaser.GameObjects.GameObject[] = []; private combatCutInObjects: Phaser.GameObjects.GameObject[] = []; + private resultObjects: Phaser.GameObjects.GameObject[] = []; private unitViews = new Map(); private actedUnitIds = new Set(); private bondStates = new Map(); @@ -241,6 +260,7 @@ export class BattleScene extends Phaser.Scene { private battleLog: string[] = []; private enemyHomeTiles = new Map(); private pendingMove?: PendingMove; + private battleOutcome?: BattleOutcome; private debugOverlay?: Phaser.GameObjects.Text; constructor() { @@ -248,11 +268,19 @@ export class BattleScene extends Phaser.Scene { } create() { + this.resetBattleData(); const { width, height } = this.scale; this.layout = this.createLayout(width, height); this.bondStates = this.createBondStates(); this.enemyHomeTiles = this.createEnemyHomeTiles(); this.battleLog = []; + this.actedUnitIds.clear(); + this.attackIntents = []; + this.selectedUnit = undefined; + this.pendingMove = undefined; + this.targetingAction = undefined; + this.battleOutcome = undefined; + this.phase = 'idle'; soundDirector.playMusic('battle-prep'); this.input.mouse?.disableContextMenu(); this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { @@ -275,6 +303,11 @@ export class BattleScene extends Phaser.Scene { this.renderRosterPanel('ally', '행동할 장수를 선택하세요.'); } + private resetBattleData() { + firstBattleUnits.splice(0, firstBattleUnits.length, ...initialFirstBattleUnits.map(cloneUnitData)); + firstBattleBonds.splice(0, firstBattleBonds.length, ...initialFirstBattleBonds.map(cloneBattleBond)); + } + private createLayout(width: number, height: number): BattleLayout { const margin = 24; const gap = 20; @@ -415,6 +448,10 @@ export class BattleScene extends Phaser.Scene { this.hideMapMenu(); this.hideTurnEndPrompt(); + if (this.battleOutcome) { + return; + } + if (this.phase === 'animating') { return; } @@ -855,13 +892,17 @@ export class BattleScene extends Phaser.Scene { this.hideTurnEndPrompt(); this.applyActedStyle(unit); soundDirector.playSelect(); + this.pushBattleLog(message); + + if (this.resolveBattleOutcomeIfNeeded()) { + return; + } const remaining = this.remainingAllyCount(); const turnHint = remaining > 0 ? `${remaining}명의 아군이 아직 행동할 수 있습니다.` : '모든 아군이 행동했습니다. 턴 종료 여부를 선택하세요.'; - this.pushBattleLog(message); this.renderRosterPanel('ally', `${message}\n${turnHint}`); if (remaining === 0) { @@ -869,6 +910,289 @@ export class BattleScene extends Phaser.Scene { } } + private resolveBattleOutcomeIfNeeded() { + if (this.battleOutcome) { + return true; + } + + const liuBei = firstBattleUnits.find((unit) => unit.id === 'liu-bei'); + if (!liuBei || liuBei.hp <= 0) { + this.completeBattle('defeat'); + return true; + } + + const enemiesAlive = firstBattleUnits.some((unit) => unit.faction === 'enemy' && unit.hp > 0); + if (!enemiesAlive) { + this.completeBattle('victory'); + return true; + } + + return false; + } + + private completeBattle(outcome: BattleOutcome) { + if (this.battleOutcome) { + return; + } + + this.battleOutcome = outcome; + this.phase = 'resolved'; + this.selectedUnit = undefined; + this.pendingMove = undefined; + this.targetingAction = undefined; + this.clearMarkers(); + this.hideCommandMenu(); + this.hideMapMenu(); + this.hideTurnEndPrompt(); + this.hideBattleAlert(); + + const message = + outcome === 'victory' + ? '승리! 황건적을 모두 물리쳤습니다.' + : '패배... 유비가 패주했습니다.'; + this.pushBattleLog(message); + this.renderSituationPanel(message); + soundDirector.playEffect(outcome === 'victory' ? 'exp-gain' : 'combat-impact', { + volume: outcome === 'victory' ? 0.38 : 0.28, + stopAfterMs: 1200 + }); + this.showBattleResult(outcome); + } + + private showBattleResult(outcome: BattleOutcome) { + this.hideBattleResult(); + + const depth = 120; + const panelWidth = 920; + const panelHeight = 590; + const left = Math.floor((this.scale.width - panelWidth) / 2); + const top = Math.floor((this.scale.height - panelHeight) / 2); + const allies = firstBattleUnits.filter((unit) => unit.faction === 'ally'); + const defeatedEnemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp <= 0).length; + const totalEnemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy').length; + const aliveAllies = allies.filter((unit) => unit.hp > 0).length; + + const shade = this.trackResultObject(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.68)); + shade.setOrigin(0); + shade.setDepth(depth); + shade.setInteractive(); + + const panel = this.trackResultObject(this.add.rectangle(left, top, panelWidth, panelHeight, 0x101821, 0.98)); + panel.setOrigin(0); + panel.setDepth(depth + 1); + panel.setStrokeStyle(3, outcome === 'victory' ? palette.gold : 0xb86b55, 0.94); + + const title = this.trackResultObject(this.add.text(left + panelWidth / 2, top + 26, outcome === 'victory' ? '전투 승리' : '전투 패배', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '34px', + color: outcome === 'victory' ? '#f4dfad' : '#ffb6a6', + fontStyle: '700', + stroke: '#05070a', + strokeThickness: 5 + })); + title.setOrigin(0.5, 0); + title.setDepth(depth + 2); + + const subtitleText = + outcome === 'victory' + ? '탁현 의용군은 첫 싸움에서 황건적을 몰아내고 마을을 지켜냈습니다.' + : '유비가 전장을 이탈했습니다. 진형을 가다듬고 다시 도전해야 합니다.'; + const subtitle = this.trackResultObject(this.add.text(left + panelWidth / 2, top + 72, subtitleText, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '17px', + color: '#d4dce6', + align: 'center' + })); + subtitle.setOrigin(0.5, 0); + subtitle.setDepth(depth + 2); + + const metricTop = top + 112; + const metricWidth = 196; + const metricGap = 18; + this.renderResultMetric('소요 턴', `${this.turnNumber}턴`, left + 44, metricTop, metricWidth, depth + 2); + this.renderResultMetric('생존 아군', `${aliveAllies} / ${allies.length}`, left + 44 + (metricWidth + metricGap), metricTop, metricWidth, depth + 2); + this.renderResultMetric('격파 적군', `${defeatedEnemies} / ${totalEnemies}`, left + 44 + (metricWidth + metricGap) * 2, metricTop, metricWidth, depth + 2); + this.renderResultMetric('전투 보상', outcome === 'victory' ? '획득' : '없음', left + 44 + (metricWidth + metricGap) * 3, metricTop, metricWidth, depth + 2); + + const unitTitle = this.trackResultObject(this.add.text(left + 44, top + 178, '장수 성장', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '21px', + color: '#f2e3bf', + fontStyle: '700' + })); + unitTitle.setDepth(depth + 2); + + allies.forEach((unit, index) => { + this.renderResultUnitRow(unit, left + 44, top + 214 + index * 66, panelWidth - 88, depth + 2); + }); + + const bondTitleTop = top + 424; + const bondTitle = this.trackResultObject(this.add.text(left + 44, bondTitleTop, '공명 변화', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '21px', + color: '#f2e3bf', + fontStyle: '700' + })); + bondTitle.setDepth(depth + 2); + + Array.from(this.bondStates.values()).forEach((bond, index) => { + this.renderResultBondRow(bond, left + 44 + index * 278, bondTitleTop + 38, 258, depth + 2); + }); + + const rewardLines = + outcome === 'victory' + ? ['군자금 300', '마을 민심 +1', '의용군 명망 +1'] + : ['보상 없음', '유비를 보호하며 다시 진군하세요.']; + const reward = this.trackResultObject(this.add.text(left + panelWidth - 44, top + 520, rewardLines.join(' '), { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: outcome === 'victory' ? '#d8b15f' : '#aeb7c2', + fontStyle: '700' + })); + reward.setOrigin(1, 0); + reward.setDepth(depth + 2); + + this.addResultButton('다시 하기', left + panelWidth - 286, top + 552, 124, () => { + soundDirector.playSelect(); + this.scene.restart(); + }, depth + 3); + this.addResultButton('타이틀로', left + panelWidth - 150, top + 552, 124, () => { + soundDirector.playSelect(); + this.scene.start('TitleScene'); + }, depth + 3); + } + + private hideBattleResult() { + this.resultObjects.forEach((object) => object.destroy()); + this.resultObjects = []; + } + + private renderResultMetric(label: string, value: string, x: number, y: number, width: number, depth: number) { + const bg = this.trackResultObject(this.add.rectangle(x, y, width, 48, 0x16212d, 0.94)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, 0x53606c, 0.6); + + const labelText = this.trackResultObject(this.add.text(x + 12, y + 8, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: '#9fb0bf' + })); + labelText.setDepth(depth + 1); + + const valueText = this.trackResultObject(this.add.text(x + width - 12, y + 22, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '18px', + color: '#f2e3bf', + fontStyle: '700' + })); + valueText.setOrigin(1, 0); + valueText.setDepth(depth + 1); + } + + private renderResultUnitRow(unit: UnitData, x: number, y: number, width: number, depth: number) { + const alive = unit.hp > 0; + const bg = this.trackResultObject(this.add.rectangle(x, y, width, 56, 0x101820, alive ? 0.94 : 0.66)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, alive ? palette.blue : 0x646464, alive ? 0.62 : 0.5); + + const name = this.trackResultObject(this.add.text(x + 14, y + 8, `${unit.name} Lv ${unit.level}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '17px', + color: alive ? '#e9f0f8' : '#9a9a9a', + fontStyle: '700' + })); + name.setDepth(depth + 1); + + const classText = this.trackResultObject(this.add.text(x + 150, y + 10, getUnitClass(unit.classKey).name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '14px', + color: alive ? '#9fb0bf' : '#777777' + })); + classText.setDepth(depth + 1); + + const hpText = this.trackResultObject(this.add.text(x + 270, y + 9, `HP ${unit.hp}/${unit.maxHp}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '14px', + color: alive ? '#f0e4c8' : '#8c8c8c', + fontStyle: '700' + })); + hpText.setDepth(depth + 1); + + const expText = this.trackResultObject(this.add.text(x + 392, y + 9, `경험 ${unit.exp}/100`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '14px', + color: '#d8b15f', + fontStyle: '700' + })); + expText.setDepth(depth + 1); + + const equipmentText = equipmentSlots + .map((slot) => { + const state = unit.equipment[slot]; + const item = getItem(state.itemId); + return `${equipmentSlotLabels[slot]} ${item.name} Lv${state.level} ${state.exp}/${equipmentExpToNext(state.level)}`; + }) + .join(' '); + const equipment = this.trackResultObject(this.add.text(x + 14, y + 32, equipmentText, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: alive ? '#c8d2dd' : '#7e858d', + wordWrap: { width: width - 28, useAdvancedWrap: true } + })); + equipment.setDepth(depth + 1); + } + + private renderResultBondRow(bond: BondState, x: number, y: number, width: number, depth: number) { + const bg = this.trackResultObject(this.add.rectangle(x, y, width, 58, 0x101820, 0.88)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, bond.battleExp > 0 ? palette.gold : 0x53606c, bond.battleExp > 0 ? 0.72 : 0.5); + + const [first, second] = bond.unitIds.map((unitId) => this.unitName(unitId)); + const title = this.trackResultObject(this.add.text(x + 12, y + 8, `${first} · ${second}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '14px', + color: '#e8dfca', + fontStyle: '700' + })); + title.setDepth(depth + 1); + + const value = this.trackResultObject(this.add.text(x + 12, y + 31, `공명 ${bond.level} 경험 ${bond.exp}/100 전투 +${bond.battleExp}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: bond.battleExp > 0 ? '#d8b15f' : '#9fb0bf' + })); + value.setDepth(depth + 1); + } + + private addResultButton(label: string, x: number, y: number, width: number, action: () => void, depth: number) { + const bg = this.trackResultObject(this.add.rectangle(x, y, width, 34, 0x1a2630, 0.96)); + bg.setDepth(depth); + bg.setStrokeStyle(1, label === '다시 하기' ? palette.blue : palette.gold, 0.78); + bg.setInteractive({ useHandCursor: true }); + bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98)); + bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96)); + bg.on('pointerdown', action); + + const text = this.trackResultObject(this.add.text(x, y, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '16px', + color: '#f2e3bf', + fontStyle: '700' + })); + text.setOrigin(0.5); + text.setDepth(depth + 1); + text.setInteractive({ useHandCursor: true }); + text.on('pointerdown', action); + } + + private trackResultObject(object: T) { + this.resultObjects.push(object); + return object; + } + private attackableTargets(attacker: UnitData) { return this.damageableTargets(attacker, 'attack'); } @@ -1045,6 +1369,10 @@ export class BattleScene extends Phaser.Scene { } private handleRightClick(pointer: Phaser.Input.Pointer) { + if (this.battleOutcome) { + return; + } + if (this.phase === 'animating') { return; } @@ -1066,6 +1394,10 @@ export class BattleScene extends Phaser.Scene { } private handleLeftClick(pointer: Phaser.Input.Pointer) { + if (this.battleOutcome) { + return; + } + if (this.phase !== 'idle' || !this.isPointerOnEmptyMapTile(pointer)) { return; } @@ -1283,9 +1615,11 @@ export class BattleScene extends Phaser.Scene { this.hideCommandMenu(); this.hideTurnEndPrompt(); this.hideCombatCutIn(); + this.hideBattleResult(); this.selectedUnit = undefined; this.pendingMove = undefined; this.targetingAction = undefined; + this.battleOutcome = undefined; this.phase = 'idle'; this.turnNumber = Math.max(1, state.turnNumber || 1); @@ -1356,6 +1690,9 @@ export class BattleScene extends Phaser.Scene { this.renderSituationPanel('이미 적군 행동 중입니다.'); return; } + if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) { + return; + } if (this.phase === 'command' || this.phase === 'targeting' || this.phase === 'animating') { this.setInfo('현재 장수의 행동을 먼저 결정하세요.'); return; @@ -1372,30 +1709,39 @@ export class BattleScene extends Phaser.Scene { this.updateTurnText(); this.renderSituationPanel('아군 턴을 종료했습니다.\n적군이 행동을 시작합니다.'); void this.runEnemyTurn(); - return; - - this.activeFaction = 'enemy'; - this.updateTurnText(); - const pressureMessage = this.resolveEnemyPressure(); - this.renderSituationPanel(`아군 턴을 종료했습니다.\n${pressureMessage}`); - - this.time.delayedCall(700, () => this.beginNextAllyTurn()); } private async runEnemyTurn() { + if (this.battleOutcome) { + return; + } + const enemies = firstBattleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0); const messages: string[] = []; for (const enemy of enemies) { + if (this.battleOutcome) { + return; + } + const message = await this.executeEnemyAction(enemy); if (message) { messages.push(message); this.pushBattleLog(message); this.renderSituationPanel(messages.slice(-3).join('\n')); } + + if (this.resolveBattleOutcomeIfNeeded()) { + return; + } + await this.delay(260); } + if (this.resolveBattleOutcomeIfNeeded()) { + return; + } + this.time.delayedCall(520, () => this.beginNextAllyTurn()); } @@ -1871,6 +2217,10 @@ export class BattleScene extends Phaser.Scene { } private beginNextAllyTurn() { + if (this.battleOutcome || this.resolveBattleOutcomeIfNeeded()) { + return; + } + this.turnNumber += 1; this.activeFaction = 'ally'; this.actedUnitIds.clear(); @@ -2901,6 +3251,8 @@ export class BattleScene extends Phaser.Scene { turnNumber: this.turnNumber, activeFaction: this.activeFaction, phase: this.phase, + battleOutcome: this.battleOutcome ?? null, + resultVisible: this.resultObjects.length > 0, selectedUnitId: this.selectedUnit?.id ?? null, pendingMove: this.pendingMove ? { @@ -2939,6 +3291,29 @@ export class BattleScene extends Phaser.Scene { }; } + debugForceBattleOutcome(outcome: BattleOutcome = 'victory') { + if (!import.meta.env.DEV || this.battleOutcome) { + return; + } + + if (outcome === 'victory') { + firstBattleUnits + .filter((unit) => unit.faction === 'enemy') + .forEach((unit) => { + unit.hp = 0; + this.applyDefeatedStyle(unit); + }); + } else { + const liuBei = firstBattleUnits.find((unit) => unit.id === 'liu-bei'); + if (liuBei) { + liuBei.hp = 0; + this.applyDefeatedStyle(liuBei); + } + } + + this.completeBattle(outcome); + } + toggleDebugOverlay() { if (this.debugOverlay) { this.debugOverlay.destroy(); diff --git a/src/main.ts b/src/main.ts index ff7ef55..72c7a3d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ declare global { type DebugBattleScene = Phaser.Scene & { getDebugState?: () => unknown; toggleDebugOverlay?: () => void; + debugForceBattleOutcome?: (outcome: 'victory' | 'defeat') => void; }; type HerosDebugApi = { @@ -24,6 +25,7 @@ type HerosDebugApi = { activeScenes: () => string[]; battle: () => unknown; goToBattle: () => void; + forceBattleOutcome: (outcome: 'victory' | 'defeat') => void; scene: (key: string) => Phaser.Scene | undefined; toggleBattleOverlay: () => void; }; @@ -63,6 +65,9 @@ function createDebugApi(game: Phaser.Game): HerosDebugApi { goToBattle: () => { game.scene.start('BattleScene'); }, + forceBattleOutcome: (outcome) => { + battleScene()?.debugForceBattleOutcome?.(outcome); + }, scene, toggleBattleOverlay: () => { battleScene()?.toggleDebugOverlay?.();