diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 2dc2852..452c8c4 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -237,7 +237,7 @@ try { throw new Error(`Expected positional bond bonuses and reduced counter damage: ${JSON.stringify(combatMechanicsProbe)}`); } - const moveWaitTurnPromptState = await page.evaluate(() => { + const moveWaitTurnPromptStates = await page.evaluate(() => { const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene'); const state = window.__HEROS_DEBUG__?.battle(); const lastAlly = state?.units?.find((unit) => unit.id === 'zhang-fei'); @@ -261,11 +261,26 @@ try { scene.selectUnit(liveLastAlly); scene.moveSelectedUnit(liveLastAlly.x, liveLastAlly.y); + const postMoveState = window.__HEROS_DEBUG__?.battle(); scene.completeUnitAction(liveLastAlly, 'wait'); - return window.__HEROS_DEBUG__?.battle(); + const postWaitState = window.__HEROS_DEBUG__?.battle(); + return { postMoveState, postWaitState }; }); - if (!moveWaitTurnPromptState?.turnPromptVisible || moveWaitTurnPromptState.phase !== 'idle') { - throw new Error(`Expected move-wait completion to show turn-end prompt: ${JSON.stringify(moveWaitTurnPromptState)}`); + if ( + !moveWaitTurnPromptStates?.postMoveState?.turnPromptVisible || + moveWaitTurnPromptStates.postMoveState.turnPromptMode !== 'post-move' || + moveWaitTurnPromptStates.postMoveState.phase !== 'command' || + moveWaitTurnPromptStates.postMoveState.pendingMove?.unitId !== 'zhang-fei' || + moveWaitTurnPromptStates.postMoveState.actedUnitIds?.includes('zhang-fei') + ) { + throw new Error(`Expected final ally move to show post-move turn prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`); + } + if ( + !moveWaitTurnPromptStates.postWaitState?.turnPromptVisible || + moveWaitTurnPromptStates.postWaitState.turnPromptMode !== 'turn-end' || + moveWaitTurnPromptStates.postWaitState.phase !== 'idle' + ) { + throw new Error(`Expected move-wait completion to show turn-end prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`); } await page.evaluate(() => { diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index a08edac..6a3a699 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1015,6 +1015,17 @@ type MapMenuAction = 'endTurn' | 'threat' | 'save' | 'load' | 'roster' | 'bond' type EnemyAiBehavior = 'aggressive' | 'guard' | 'hold'; type BattleOutcome = 'victory' | 'defeat'; type SaveSlotMode = 'save' | 'load'; +type TurnPromptMode = 'turn-end' | 'post-move'; + +type TurnEndPromptOptions = { + mode?: TurnPromptMode; + title?: string; + body?: string; + primaryLabel?: string; + secondaryLabel?: string; + primaryAction?: () => void; + secondaryAction?: () => void; +}; type BattleSceneData = { battleId?: string; @@ -2716,6 +2727,7 @@ export class BattleScene extends Phaser.Scene { private alertDismissEvent?: Phaser.Time.TimerEvent; private battleEventObjects: Phaser.GameObjects.GameObject[] = []; private turnPromptObjects: Phaser.GameObjects.GameObject[] = []; + private turnPromptMode?: TurnPromptMode; private combatCutInObjects: Phaser.GameObjects.GameObject[] = []; private resultObjects: Phaser.GameObjects.GameObject[] = []; private resultGaugeAnimations: ResultGaugeAnimation[] = []; @@ -3403,8 +3415,38 @@ export class BattleScene extends Phaser.Scene { this.moveUnitView(unit, x, y, view); } this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY); - const finalAllyHint = this.remainingAllyCount() === 1 ? '\n마지막 미행동 장수입니다. 대기를 선택하면 턴 종료 확인이 열립니다.' : ''; + const isFinalAlly = this.remainingAllyCount() === 1; + const finalAllyHint = isFinalAlly ? '\n마지막 미행동 장수입니다. 바로 턴 종료 확인을 선택할 수 있습니다.' : ''; this.renderUnitDetail(unit, `공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.${finalAllyHint}`); + + if (isFinalAlly) { + this.showPostMoveTurnEndPrompt(unit); + return; + } + + this.hideTurnEndPrompt(); + } + + private showPostMoveTurnEndPrompt(unit: UnitData) { + this.showTurnEndPrompt(undefined, { + mode: 'post-move', + title: '마지막 장수 이동 완료', + body: '대기로 행동을 마치고 턴을 종료할까요?', + primaryLabel: '대기 후 턴 종료', + secondaryLabel: '명령 선택', + primaryAction: () => { + if (this.phase !== 'command' || this.selectedUnit?.id !== unit.id || this.actedUnitIds.has(unit.id)) { + this.hideTurnEndPrompt(); + return; + } + + this.completeUnitAction(unit, 'wait'); + if (this.activeFaction === 'ally' && !this.battleOutcome) { + this.endAllyTurn(); + } + }, + secondaryAction: () => this.hideTurnEndPrompt() + }); } private canMoveTo(unit: UnitData, x: number, y: number) { @@ -3768,6 +3810,7 @@ export class BattleScene extends Phaser.Scene { return; } if (choicePointer.leftButtonDown()) { + this.hideTurnEndPrompt(); if (command === 'strategy' || command === 'item') { this.showUsableMenu(unit, command, choicePointer.x, choicePointer.y); return; @@ -4013,6 +4056,7 @@ export class BattleScene extends Phaser.Scene { return; } + this.hideTurnEndPrompt(); this.phase = 'targeting'; this.targetingAction = action; this.selectedUsable = usable; @@ -4041,6 +4085,7 @@ export class BattleScene extends Phaser.Scene { return; } + this.hideTurnEndPrompt(); this.phase = 'targeting'; this.targetingAction = usable.command; this.selectedUsable = usable; @@ -8014,6 +8059,7 @@ export class BattleScene extends Phaser.Scene { this.selectedUnit = unit; this.pendingMove = undefined; this.hideCommandMenu(); + this.hideTurnEndPrompt(); this.updateMiniMap(); soundDirector.playSelect(); @@ -8396,11 +8442,12 @@ export class BattleScene extends Phaser.Scene { }); } - private showTurnEndPrompt(message?: string) { + private showTurnEndPrompt(message?: string, options: TurnEndPromptOptions = {}) { this.hideTurnEndPrompt(); + this.turnPromptMode = options.mode ?? 'turn-end'; - const width = 360; - const height = 156; + const width = 388; + const height = 162; const left = this.layout.mapX + this.layout.mapWidth / 2 - width / 2; const top = this.layout.mapY + this.layout.mapHeight / 2 - height / 2; const panel = this.add.rectangle(left, top, width, height, 0x101821, 0.96); @@ -8409,7 +8456,7 @@ export class BattleScene extends Phaser.Scene { panel.setStrokeStyle(2, palette.gold, 0.9); this.turnPromptObjects.push(panel); - const title = this.add.text(left + width / 2, top + 26, '모든 아군의 행동이 종료되었습니다.', { + const title = this.add.text(left + width / 2, top + 26, options.title ?? '모든 아군의 행동이 종료되었습니다.', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f2e3bf', @@ -8419,31 +8466,38 @@ export class BattleScene extends Phaser.Scene { title.setDepth(41); this.turnPromptObjects.push(title); - const body = this.add.text(left + width / 2, top + 58, '턴을 종료하시겠습니까?', { + const body = this.add.text(left + width / 2, top + 58, options.body ?? '턴을 종료하시겠습니까?', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '17px', - color: '#d4dce6' + color: '#d4dce6', + align: 'center', + wordWrap: { width: width - 44, useAdvancedWrap: true } }); body.setOrigin(0.5); body.setDepth(41); this.turnPromptObjects.push(body); + const primaryLabel = options.primaryLabel ?? '턴 종료'; + const secondaryLabel = options.secondaryLabel ?? '전장 확인'; + const primaryAction = options.primaryAction ?? (() => this.endAllyTurn()); + const secondaryAction = options.secondaryAction ?? (() => this.hideTurnEndPrompt()); + const buttonWidth = 136; const buttons = [ - { label: '턴 종료', x: left + 106, action: () => this.endAllyTurn() }, - { label: '전장 확인', x: left + 254, action: () => this.hideTurnEndPrompt() } + { label: primaryLabel, x: left + 119, action: primaryAction, primary: true }, + { label: secondaryLabel, x: left + 269, action: secondaryAction, primary: false } ]; - buttons.forEach(({ label, x, action }) => { - const bg = this.add.rectangle(x, top + 112, 116, 36, 0x1a2630, 0.95); + buttons.forEach(({ label, x, action, primary }) => { + const bg = this.add.rectangle(x, top + 116, buttonWidth, 36, 0x1a2630, 0.95); bg.setDepth(41); - bg.setStrokeStyle(1, label === '턴 종료' ? palette.gold : palette.blue, 0.72); + bg.setStrokeStyle(1, primary ? palette.gold : palette.blue, 0.72); bg.setInteractive({ useHandCursor: true }); bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98)); bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.95)); bg.on('pointerdown', action); this.turnPromptObjects.push(bg); - const text = this.add.text(x, top + 112, label, { + const text = this.add.text(x, top + 116, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#f2e3bf', @@ -8462,6 +8516,7 @@ export class BattleScene extends Phaser.Scene { private hideTurnEndPrompt() { this.turnPromptObjects.forEach((object) => object.destroy()); this.turnPromptObjects = []; + this.turnPromptMode = undefined; } private pushBattleLog(message: string) { @@ -9180,6 +9235,7 @@ export class BattleScene extends Phaser.Scene { mapBackgroundReady: this.mapBackground?.texture.key === battleScenario.mapTextureKey, resultVisible: this.resultObjects.length > 0, turnPromptVisible: this.turnPromptObjects.length > 0, + turnPromptMode: this.turnPromptMode ?? null, markerCount: this.markers.length, threatMarkerCount: this.markers.filter((marker) => marker.getData('markerType') === 'threat').length, enemyMoveMarkerCount: this.markers.filter((marker) => marker.getData('markerType') === 'enemy-move').length,