fix: harden turn flow interaction UX

This commit is contained in:
2026-07-20 23:10:52 +09:00
parent 3961282d82
commit be5fe3e880
7 changed files with 1187 additions and 90 deletions

View File

@@ -3945,6 +3945,13 @@ export class BattleScene extends Phaser.Scene {
if (this.tacticalCommandCutInObjects.length > 0) {
return;
}
if (this.battleEventObjects.length > 0) {
this.hideBattleEventBanner();
return;
}
if (this.saveSlotPanelObjects.length > 0) {
return;
}
if (this.tacticalInitiativePanelObjects.length > 0) {
return;
}
@@ -3961,6 +3968,11 @@ export class BattleScene extends Phaser.Scene {
if (this.tacticalCommandCutInObjects.length > 0) {
return;
}
if (this.battleEventObjects.length > 0) {
soundDirector.playSelect();
this.hideBattleEventBanner();
return;
}
if (this.resultSortieOrderVisible) {
soundDirector.playSelect();
this.hideResultSortieOrder();
@@ -4029,9 +4041,15 @@ export class BattleScene extends Phaser.Scene {
}
});
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => {
if (this.battleEventObjects.length > 0) {
return;
}
if (this.handleSaveSlotPanelKey(event)) {
return;
}
if (this.saveSlotPanelObjects.length > 0) {
return;
}
if (this.handleTacticalInitiativeKey(event)) {
return;
}
@@ -4041,7 +4059,14 @@ export class BattleScene extends Phaser.Scene {
this.handleSideQuickTabKey(event);
});
this.input.keyboard?.on('keydown-SPACE', (event: KeyboardEvent) => {
if (this.activeFaction !== 'enemy' || this.fastForwardHeld) {
if (
this.battleEventObjects.length > 0 ||
this.saveSlotPanelObjects.length > 0 ||
this.tacticalInitiativePanelObjects.length > 0 ||
this.tacticalCommandCutInObjects.length > 0 ||
this.activeFaction !== 'enemy' ||
this.fastForwardHeld
) {
return;
}
event.preventDefault();
@@ -4056,7 +4081,12 @@ export class BattleScene extends Phaser.Scene {
this.renderBattleSpeedControl();
});
this.input.keyboard?.on('keydown-PERIOD', () => {
if (this.tacticalInitiativePanelObjects.length > 0 || this.tacticalCommandCutInObjects.length > 0) {
if (
this.battleEventObjects.length > 0 ||
this.saveSlotPanelObjects.length > 0 ||
this.tacticalInitiativePanelObjects.length > 0 ||
this.tacticalCommandCutInObjects.length > 0
) {
return;
}
this.cycleBattleSpeed();
@@ -6553,9 +6583,11 @@ export class BattleScene extends Phaser.Scene {
this.battleOutcome ||
this.phase === 'animating' ||
this.phase === 'resolved' ||
this.battleEventObjects.length > 0 ||
this.commandMenuObjects.length > 0 ||
this.mapMenuObjects.length > 0 ||
this.tacticalInitiativePanelObjects.length > 0 ||
this.tacticalCommandCutInObjects.length > 0 ||
this.saveSlotPanelObjects.length > 0 ||
this.turnPromptObjects.length > 0 ||
this.combatCutInObjects.length > 0 ||
@@ -6913,9 +6945,11 @@ export class BattleScene extends Phaser.Scene {
this.battleOutcome ||
this.phase === 'animating' ||
this.phase === 'resolved' ||
this.battleEventObjects.length > 0 ||
this.commandMenuObjects.length > 0 ||
this.mapMenuObjects.length > 0 ||
this.tacticalInitiativePanelObjects.length > 0 ||
this.tacticalCommandCutInObjects.length > 0 ||
this.saveSlotPanelObjects.length > 0 ||
this.turnPromptObjects.length > 0 ||
this.combatCutInObjects.length > 0 ||
@@ -7214,6 +7248,7 @@ export class BattleScene extends Phaser.Scene {
private handleBattleKeyboardNavigation(event: KeyboardEvent) {
if (
(this.phase !== 'moving' && this.phase !== 'targeting') ||
this.battleEventObjects.length > 0 ||
this.commandMenuObjects.length > 0 ||
this.mapMenuObjects.length > 0 ||
this.tacticalInitiativePanelObjects.length > 0 ||
@@ -7357,7 +7392,7 @@ export class BattleScene extends Phaser.Scene {
return;
}
if (this.turnPromptMode) {
if (this.battleEventObjects.length > 0 || this.saveSlotPanelObjects.length > 0 || this.turnPromptMode) {
return;
}
@@ -7520,6 +7555,9 @@ export class BattleScene extends Phaser.Scene {
}
private async moveSelectedUnit(x: number, y: number, pointer?: Phaser.Input.Pointer) {
if (this.battleEventObjects.length > 0) {
return;
}
if (this.isFirstBattleTutorialActive()) {
const path = this.firstBattleTutorialPath!;
if (this.firstBattleTutorialStep !== 'move') {
@@ -7567,6 +7605,8 @@ export class BattleScene extends Phaser.Scene {
const targetX = this.tileCenterX(x);
const targetY = this.tileCenterY(y);
const commandX = pointer?.x ?? targetX;
const commandY = pointer?.y ?? targetY;
if (!stayingInPlace) {
await this.moveUnitViewAsync(unit, x, y, view, 260, fromX, fromY);
if (this.battleOutcome || this.selectedUnit?.id !== unit.id || this.pendingMove?.unit.id !== unit.id) {
@@ -7574,8 +7614,6 @@ export class BattleScene extends Phaser.Scene {
}
this.phase = 'command';
}
const commandX = pointer?.x ?? targetX;
const commandY = pointer?.y ?? targetY;
this.showCommandMenu(unit, commandX, commandY);
this.renderUnitDetail(unit, '명령 선택 · 우클릭은 이동 취소');
@@ -12873,22 +12911,24 @@ export class BattleScene extends Phaser.Scene {
);
}
this.addResultButton('다시 하기', left + panelWidth - 276, top + 612, 124, () => {
soundDirector.playSelect();
this.scene.restart({
battleId: battleScenario.id,
selectedSortieUnitIds: [...this.launchSortieUnitIds],
sortieFormationAssignments: { ...this.launchSortieFormationAssignments },
sortieItemAssignments: this.normalizeLaunchSortieItemAssignments(this.launchSortieItemAssignments),
sortieOrderId: this.launchSortieOrderId,
sortieResonanceBondId: this.launchSortieResonanceBondId,
sortieRecommendation: this.launchSortieRecommendation
? cloneCampaignSortieRecommendationSnapshot(this.launchSortieRecommendation)
: undefined
this.beginResultNavigation(() => {
this.scene.restart({
battleId: battleScenario.id,
selectedSortieUnitIds: [...this.launchSortieUnitIds],
sortieFormationAssignments: { ...this.launchSortieFormationAssignments },
sortieItemAssignments: this.normalizeLaunchSortieItemAssignments(this.launchSortieItemAssignments),
sortieOrderId: this.launchSortieOrderId,
sortieResonanceBondId: this.launchSortieResonanceBondId,
sortieRecommendation: this.launchSortieRecommendation
? cloneCampaignSortieRecommendationSnapshot(this.launchSortieRecommendation)
: undefined
});
});
}, depth + 3, outcome === 'defeat' ? 'primary' : 'secondary', 'retry');
this.addResultButton('타이틀로', left + panelWidth - 140, top + 612, 124, () => {
soundDirector.playSelect();
this.scene.start('TitleScene');
this.beginResultNavigation(() => {
this.scene.start('TitleScene');
});
}, depth + 3, 'ghost', 'title');
void this.playResultSettlementAnimation(animationToken);
@@ -12911,13 +12951,53 @@ export class BattleScene extends Phaser.Scene {
this.resultContinueButton?.label.setText(this.resultContinueCtaLabel());
}
private setResultNavigationButtonsEnabled(enabled: boolean) {
this.resultActionButtons.forEach((button) => {
if (!button.background.active) {
return;
}
if (enabled) {
button.background.setInteractive({ useHandCursor: true });
} else {
button.background.disableInteractive();
}
button.background.setAlpha(enabled ? 1 : 0.72);
button.label.setAlpha(enabled ? 1 : 0.72);
});
}
private recoverResultNavigation() {
this.resultNavigationPending = false;
this.setResultNavigationButtonsEnabled(true);
}
private beginResultNavigation(navigate: () => void | Promise<unknown>) {
if (this.resultNavigationPending) {
return false;
}
this.resultNavigationPending = true;
this.setResultNavigationButtonsEnabled(false);
soundDirector.playSelect();
try {
const pending = navigate();
if (pending instanceof Promise) {
void pending.catch(() => this.recoverResultNavigation());
}
} catch (error) {
this.recoverResultNavigation();
throw error;
}
return true;
}
private handleResultContinue() {
if (this.resultNavigationPending) {
return;
}
soundDirector.playSelect();
if (this.resultSettlementStatus !== 'complete') {
soundDirector.playSelect();
this.completeResultSettlement(true);
this.resultContinueLockedUntil = this.time.now + 420;
const button = this.resultContinueButton;
@@ -12942,31 +13022,14 @@ export class BattleScene extends Phaser.Scene {
}
const { destination } = this.resultContinueRoute();
this.resultNavigationPending = true;
this.resultActionButtons.forEach((button) => {
button.background.disableInteractive();
button.background.setAlpha(0.72);
button.label.setAlpha(0.72);
});
const recoverNavigation = () => {
this.resultNavigationPending = false;
this.resultActionButtons.forEach((button) => {
if (!button.background.active) {
return;
}
button.background.setInteractive({ useHandCursor: true });
button.background.setAlpha(1);
button.label.setAlpha(1);
});
};
if (destination === 'victory-story') {
void startLazyScene(this, 'StoryScene', {
this.beginResultNavigation(() => startLazyScene(this, 'StoryScene', {
pages: firstBattleVictoryPages,
nextScene: 'CampScene'
}).catch(recoverNavigation);
}));
return;
}
void startLazyScene(this, 'CampScene').catch(recoverNavigation);
this.beginResultNavigation(() => startLazyScene(this, 'CampScene'));
}
private hideBattleResult() {
@@ -13225,11 +13288,13 @@ export class BattleScene extends Phaser.Scene {
}
private openResultSortieImprovement(outcome: BattleOutcome) {
if (this.resultNavigationPending) {
return;
}
const targetBattleId = this.resultSortieImprovementTargetBattleId(outcome);
if (!targetBattleId || !this.launchSortieRecommendation) {
return;
}
soundDirector.playSelect();
const campSceneData = {
openSortiePrep: true,
openSortieImprovement: true,
@@ -13240,15 +13305,16 @@ export class BattleScene extends Phaser.Scene {
}
: {})
};
this.hideResultFormationReview();
if (outcome === 'victory' && battleScenario.id === 'first-battle-zhuo-commandery') {
void startLazyScene(this, 'StoryScene', {
this.beginResultNavigation(() => startLazyScene(this, 'StoryScene', {
pages: firstBattleVictoryPages,
nextScene: 'CampScene',
nextSceneData: campSceneData
});
}));
return;
}
void startLazyScene(this, 'CampScene', campSceneData);
this.beginResultNavigation(() => startLazyScene(this, 'CampScene', campSceneData));
}
private publishFirstBattleReport(outcome: BattleOutcome) {
@@ -16484,6 +16550,7 @@ export class BattleScene extends Phaser.Scene {
this.firstBattleTutorialFeedback = '';
this.firstBattleTutorialTargetPreviewLocked = false;
this.firstBattleTutorialCompletion = undefined;
this.hideBattleEventBanner();
this.ensureFirstBattleTutorialPathVisible();
this.renderFirstBattleTutorial();
}
@@ -16987,10 +17054,16 @@ export class BattleScene extends Phaser.Scene {
const left = this.layout.gridX + Math.max(16, (this.layout.gridWidth - width) / 2);
const top = this.layout.gridY + 18;
const depth = 45;
const inputShade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x000000, 0.001);
inputShade.setOrigin(0);
inputShade.setDepth(depth - 1);
inputShade.setInteractive();
this.battleEventObjects.push(inputShade);
const panel = this.add.rectangle(left, top, width, height, 0x101821, 0.94);
panel.setOrigin(0);
panel.setDepth(depth);
panel.setStrokeStyle(2, palette.gold, 0.86);
panel.setInteractive();
this.battleEventObjects.push(panel);
const titleText = this.add.text(left + 20, top + 14, title, {
@@ -17096,10 +17169,16 @@ export class BattleScene extends Phaser.Scene {
const left = this.layout.gridX + Math.max(16, (this.layout.gridWidth - width) / 2);
const top = this.layout.gridY + 18;
const depth = 45;
const inputShade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x000000, 0.001);
inputShade.setOrigin(0);
inputShade.setDepth(depth - 1);
inputShade.setInteractive();
this.battleEventObjects.push(inputShade);
const panel = this.add.rectangle(left, top, width, height, 0x0c141b, 0.97);
panel.setOrigin(0);
panel.setDepth(depth);
panel.setStrokeStyle(2, palette.gold, 0.9);
panel.setInteractive();
this.battleEventObjects.push(panel);
const commandSeal = this.add.rectangle(left + 29, top + 28, 30, 30, 0x651f1b, 0.98);
@@ -17414,6 +17493,11 @@ export class BattleScene extends Phaser.Scene {
return;
}
if (this.battleEventObjects.length > 0) {
this.hideBattleEventBanner();
return;
}
if (this.tacticalInitiativePanelObjects.length > 0) {
soundDirector.playSelect();
this.hideTacticalInitiativePanel();
@@ -17473,6 +17557,11 @@ export class BattleScene extends Phaser.Scene {
return;
}
if (this.battleEventObjects.length > 0) {
this.hideBattleEventBanner();
return;
}
if (this.tacticalInitiativePanelObjects.length > 0) {
return;
}
@@ -17597,7 +17686,13 @@ export class BattleScene extends Phaser.Scene {
const buttonHeight = 34;
const padding = 8;
const menuHeight = padding * 2 + actions.length * buttonHeight;
const left = this.layout.mapX + 84;
const mapLeft = this.layout.mapX + 16;
const mapRight = this.layout.mapX + this.layout.mapWidth - 16;
const pointerGap = 12;
const preferredLeft = pointerX + pointerGap + menuWidth <= mapRight
? pointerX + pointerGap
: pointerX - menuWidth - pointerGap;
const left = Phaser.Math.Clamp(preferredLeft, mapLeft, mapRight - menuWidth);
const top = Phaser.Math.Clamp(pointerY - menuHeight / 2, this.layout.mapY + 16, this.layout.mapY + this.layout.mapHeight - menuHeight - 16);
this.mapMenuLayout = { left, top, width: menuWidth, buttonHeight, padding, actions };
@@ -17605,6 +17700,7 @@ export class BattleScene extends Phaser.Scene {
panel.setOrigin(0);
panel.setDepth(20);
panel.setStrokeStyle(2, palette.gold, 0.86);
panel.setInteractive();
this.mapMenuObjects.push(panel);
actions.forEach((action, index) => {
@@ -17789,6 +17885,12 @@ export class BattleScene extends Phaser.Scene {
const top = this.layout.mapY + this.battleUiLength(82);
const campaignSlots = listCampaignSaveSlots();
const shade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.54);
shade.setOrigin(0);
shade.setDepth(depth - 1);
shade.setInteractive();
this.saveSlotPanelObjects.push(shade);
const panel = this.add.rectangle(left, top, width, height, 0x0f1720, 0.98);
panel.setOrigin(0);
panel.setDepth(depth);
@@ -18569,8 +18671,7 @@ export class BattleScene extends Phaser.Scene {
await this.delay(180);
await this.presentCombatResult(result.counter);
}
await this.showCombatExchangeMapResults(result);
await this.delay(result.counter ? 220 : 160);
await this.showCombatExchangeMapResults(result, true);
return this.formatEnemyCombatResult(result, plan.behavior);
}
@@ -18589,8 +18690,8 @@ export class BattleScene extends Phaser.Scene {
this.centerCameraOnTile(Math.floor((enemy.x + target.x) / 2), Math.floor((enemy.y + target.y) / 2));
const result = this.resolveCombatAction(enemy, target, usable.command, false, usable);
await this.presentCombatResult(result);
this.showCombatMapResult(result);
await this.delay(160);
const popupDuration = this.showCombatMapResult(result);
await this.waitSceneDuration(popupDuration);
return this.formatEnemyCombatResult(result, behavior);
}
@@ -22323,6 +22424,12 @@ export class BattleScene extends Phaser.Scene {
});
}
private waitSceneDuration(ms: number) {
return new Promise<void>((resolve) => {
this.time.delayedCall(ms, () => resolve());
});
}
private moveUnitViewAsync(
unit: UnitData,
x: number,
@@ -22330,7 +22437,8 @@ export class BattleScene extends Phaser.Scene {
view = this.unitViews.get(unit.id),
duration = 260,
fromX = unit.x,
fromY = unit.y
fromY = unit.y,
showSortieTrail = true
) {
return new Promise<void>((resolve) => {
if (!view) {
@@ -22345,7 +22453,9 @@ export class BattleScene extends Phaser.Scene {
const direction = this.directionFromDelta(x - fromX, y - fromY);
const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y);
const movementDuration = this.movementDuration(duration, movementDistance);
this.showSortieFlankMovementTrail(unit, startX, startY, targetX, targetY, movementDuration);
if (showSortieTrail) {
this.showSortieFlankMovementTrail(unit, startX, startY, targetX, targetY, movementDuration);
}
this.tweens.killTweensOf([view.sprite, view.hitZone, view.label]);
if (view.roleSeal) {
this.tweens.killTweensOf(view.roleSeal);
@@ -23191,7 +23301,7 @@ export class BattleScene extends Phaser.Scene {
const view = this.unitViews.get(unit.id);
unit.x = fromX;
unit.y = fromY;
this.phase = 'moving';
this.phase = 'animating';
this.selectedUnit = unit;
this.pendingMove = undefined;
this.hideCommandMenu();
@@ -23201,7 +23311,29 @@ export class BattleScene extends Phaser.Scene {
this.refreshEnemyIntentForecast();
soundDirector.playSelect();
this.moveUnitView(unit, fromX, fromY, view, 180, toX, toY, false);
this.clearMarkers();
this.renderUnitDetail(
unit,
`이동을 취소하는 중입니다.\n${toX + 1}, ${toY + 1}에서 원래 위치 ${fromX + 1}, ${fromY + 1}로 되돌아갑니다.`
);
void this.completePendingMoveCancellation(unit, fromX, fromY, toX, toY, view);
return true;
}
private async completePendingMoveCancellation(
unit: UnitData,
fromX: number,
fromY: number,
toX: number,
toY: number,
view?: UnitView
) {
await this.moveUnitViewAsync(unit, fromX, fromY, view, 180, toX, toY, false);
if (this.battleOutcome || this.phase !== 'animating' || this.selectedUnit?.id !== unit.id) {
return;
}
this.phase = 'moving';
this.clearMarkers();
this.showMoveRange(unit);
this.renderUnitDetail(
@@ -23211,7 +23343,6 @@ export class BattleScene extends Phaser.Scene {
if (this.firstBattleTutorialStep === 'attack-command') {
this.setFirstBattleTutorialStep('move', '이동을 취소해 권장 이동칸 단계로 돌아왔습니다.');
}
return true;
}
private moveUnitView(
@@ -23907,19 +24038,25 @@ export class BattleScene extends Phaser.Scene {
});
}
private async showCombatExchangeMapResults(result: CombatResult) {
this.showCombatMapResult(result);
private async showCombatExchangeMapResults(result: CombatResult, waitUntilReadable = false) {
let popupDuration = this.showCombatMapResult(result);
if (!result.counter) {
if (waitUntilReadable) {
await this.waitSceneDuration(popupDuration);
}
return;
}
await this.delay(110);
this.showCombatMapResult(result.counter);
popupDuration = this.showCombatMapResult(result.counter);
if (waitUntilReadable) {
await this.waitSceneDuration(popupDuration);
}
}
private showCombatMapResult(result: CombatResult) {
if (!result.hit) {
this.showMapResultPopup(
return this.showMapResultPopup(
result.defender,
[this.missFeedbackLabel(result.action, result.usable)],
'#d9f1ff',
@@ -23927,7 +24064,6 @@ export class BattleScene extends Phaser.Scene {
19,
26
);
return;
}
const effectLine = [
@@ -23948,7 +24084,7 @@ export class BattleScene extends Phaser.Scene {
this.truncateUiText(effectLine, 28)
].filter(Boolean);
this.showMapResultPopup(
return this.showMapResultPopup(
result.defender,
lines,
result.critical ? '#ff8f5f' : '#ffdf7b',
@@ -24003,12 +24139,12 @@ export class BattleScene extends Phaser.Scene {
private showMapResultPopup(unit: UnitData, lines: string[], color: string, stroke: string, fontSize: number, lift: number) {
const view = this.unitViews.get(unit.id);
if (!view) {
return;
return 0;
}
const visibleLines = lines.filter(Boolean);
if (visibleLines.length === 0) {
return;
return 0;
}
const popup = this.add.text(
@@ -24078,6 +24214,7 @@ export class BattleScene extends Phaser.Scene {
}
}
});
return duration + delay;
}
private showObjectiveMapFeedback(

View File

@@ -204,7 +204,7 @@ import {
import { clearCampaignBattleSavesForSlot } from '../state/battleSaveKeys';
import { releaseTextureSource } from '../render/loaderLifecycle';
import { palette } from '../ui/palette';
import { startLazyScene, type LazySceneKey } from './lazyScenes';
import { ensureLazyScene, type LazySceneKey } from './lazyScenes';
const campLegacyCanvasWidth = 1280;
const campLegacyCanvasHeight = 720;
@@ -11321,6 +11321,7 @@ export class CampScene extends Phaser.Scene {
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
private saveSlotObjects: Phaser.GameObjects.GameObject[] = [];
private saveSlotConfirmObjects: Phaser.GameObjects.GameObject[] = [];
private navigationBlockerObjects: Phaser.GameObjects.GameObject[] = [];
private pendingSaveSlot?: number;
private pendingVictoryRewardReport?: CampaignVictoryRewardReport;
private victoryRewardObjects: Phaser.GameObjects.GameObject[] = [];
@@ -11442,6 +11443,7 @@ export class CampScene extends Phaser.Scene {
this.sortieOrderToggleButton = undefined;
this.saveSlotObjects = [];
this.saveSlotConfirmObjects = [];
this.navigationBlockerObjects = [];
this.pendingSaveSlot = undefined;
this.pendingVictoryRewardReport = undefined;
this.victoryRewardObjects = [];
@@ -12739,6 +12741,10 @@ export class CampScene extends Phaser.Scene {
}
private showCampSaveSlotPanel() {
if (this.navigationPending) {
return;
}
if (this.saveSlotObjects.length > 0) {
this.hideCampSaveSlotPanel();
return;
@@ -12752,6 +12758,35 @@ export class CampScene extends Phaser.Scene {
const activeSlot = this.campaign?.activeSaveSlot ?? getCampaignState().activeSaveSlot;
const slots = listCampaignSaveSlots();
const shade = this.add.rectangle(0, 0, campLegacyCanvasWidth, campLegacyCanvasHeight, 0x020406, 0.28);
shade.setOrigin(0);
shade.setDepth(depth - 1);
shade.setInteractive();
shade.on(
'pointerdown',
(
_pointer: Phaser.Input.Pointer,
_localX: number,
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
soundDirector.playSelect();
this.hideCampSaveSlotPanel();
}
);
shade.on(
'wheel',
(
_pointer: Phaser.Input.Pointer,
_deltaX: number,
_deltaY: number,
_deltaZ: number,
event?: { stopPropagation: () => void }
) => event?.stopPropagation()
);
this.trackCampSaveSlot(shade);
const panel = this.add.rectangle(left, top, width, height, 0x0f1720, 0.98);
panel.setOrigin(0);
panel.setDepth(depth);
@@ -12980,6 +13015,11 @@ export class CampScene extends Phaser.Scene {
}
private handleSaveSlotKey(event: KeyboardEvent) {
if (this.navigationPending) {
event.preventDefault();
return;
}
if (this.victoryRewardObjects.length > 0) {
return;
}
@@ -13491,6 +13531,10 @@ export class CampScene extends Phaser.Scene {
}
private handleEscapeKey() {
if (this.navigationPending) {
return;
}
if (this.victoryRewardObjects.length > 0) {
this.completeVictoryRewardAcknowledgement('close');
return;
@@ -19877,19 +19921,26 @@ export class CampScene extends Phaser.Scene {
}
const previousStep = getCampaignState().step;
markCampaignStep(step);
this.campaign = getCampaignState();
this.startCampNavigation(key, data, () => {
if (getCampaignState().step === step) {
markCampaignStep(previousStep);
this.startCampNavigation(
key,
data,
() => {
markCampaignStep(step);
this.campaign = getCampaignState();
},
() => {
if (getCampaignState().step === step) {
markCampaignStep(previousStep);
}
this.campaign = getCampaignState();
}
this.campaign = getCampaignState();
});
);
}
private startCampNavigation(
key: LazySceneKey,
data?: Record<string, unknown>,
commitState?: () => void,
recoverState?: () => void
) {
if (this.navigationPending) {
@@ -19897,14 +19948,94 @@ export class CampScene extends Phaser.Scene {
}
this.navigationPending = true;
void startLazyScene(this, key, data).catch((error: unknown) => {
this.navigationPending = false;
recoverState?.();
console.error(`[CampScene] ${key} 전환에 실패했습니다.`, error);
if (this.sys.isActive()) {
this.showCampNotice('다음 장면을 불러오지 못했습니다. 잠시 후 다시 시도하십시오.');
this.showCampNavigationBlocker();
let stateCommitted = false;
void (async () => {
try {
await ensureLazyScene(this.game, key);
if (!this.sys.isActive()) {
this.navigationPending = false;
this.hideCampNavigationBlocker();
return;
}
if (commitState) {
stateCommitted = true;
commitState();
}
this.scene.start(key, data);
} catch (error: unknown) {
this.navigationPending = false;
this.hideCampNavigationBlocker();
if (stateCommitted) {
recoverState?.();
}
console.error(`[CampScene] ${key} 전환에 실패했습니다.`, error);
if (this.sys.isActive()) {
this.showCampNotice('다음 장면을 불러오지 못했습니다. 잠시 후 다시 시도하십시오.');
}
}
});
})();
}
private showCampNavigationBlocker() {
this.hideCampNavigationBlocker();
const depth = 120;
const shade = this.add.rectangle(0, 0, campLegacyCanvasWidth, campLegacyCanvasHeight, 0x020406, 0.68);
shade.setOrigin(0);
shade.setDepth(depth);
shade.setInteractive();
shade.on(
'pointerdown',
(
_pointer: Phaser.Input.Pointer,
_localX: number,
_localY: number,
event?: { stopPropagation: () => void }
) => event?.stopPropagation()
);
shade.on(
'wheel',
(
_pointer: Phaser.Input.Pointer,
_deltaX: number,
_deltaY: number,
_deltaZ: number,
event?: { stopPropagation: () => void }
) => event?.stopPropagation()
);
this.trackCampNavigationBlocker(shade);
const panel = this.add.rectangle(campLegacyCanvasWidth / 2, campLegacyCanvasHeight / 2, 330, 104, 0x111a24, 0.99);
panel.setDepth(depth + 1);
panel.setStrokeStyle(2, palette.gold, 0.9);
panel.setInteractive();
this.trackCampNavigationBlocker(panel);
const title = this.add.text(
campLegacyCanvasWidth / 2,
campLegacyCanvasHeight / 2 - 18,
'불러오는 중…',
this.textStyle(22, '#f2e3bf', true)
);
title.setOrigin(0.5);
title.setDepth(depth + 2);
this.trackCampNavigationBlocker(title);
const guide = this.add.text(
campLegacyCanvasWidth / 2,
campLegacyCanvasHeight / 2 + 20,
'편성과 진행을 안전하게 준비하고 있습니다.',
this.textStyle(12, '#aeb7c2')
);
guide.setOrigin(0.5);
guide.setDepth(depth + 2);
this.trackCampNavigationBlocker(guide);
}
private hideCampNavigationBlocker() {
this.navigationBlockerObjects.forEach((object) => object.destroy());
this.navigationBlockerObjects = [];
}
private isFinalEpilogueFlow(flow = this.currentSortieFlow()) {
@@ -23745,6 +23876,12 @@ export class CampScene extends Phaser.Scene {
return object;
}
private trackCampNavigationBlocker<T extends Phaser.GameObjects.GameObject>(object: T) {
this.scaleLegacyCampUi(object);
this.navigationBlockerObjects.push(object);
return object;
}
private trackVictoryReward<T extends Phaser.GameObjects.GameObject>(object: T) {
this.scaleLegacyCampUi(object);
this.victoryRewardObjects.push(object);

View File

@@ -12,6 +12,7 @@ const uiPx = (value: number) => `${Math.round(ui(value))}px`;
export class EndingScene extends Phaser.Scene {
private readonly backgroundKey = 'story-wuzhang-jiangwei-inheritance';
private ready = false;
private navigating = false;
constructor() {
super('EndingScene');
@@ -19,6 +20,9 @@ export class EndingScene extends Phaser.Scene {
create() {
this.ready = false;
this.navigating = false;
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => this.returnToTitle(event));
this.input.keyboard?.on('keydown-SPACE', (event: KeyboardEvent) => this.returnToTitle(event));
this.createFallbackTexture();
this.renderEnding();
this.ensureBackgroundLoaded(() => this.renderEnding());
@@ -170,16 +174,20 @@ export class EndingScene extends Phaser.Scene {
shadow: { offsetX: 0, offsetY: ui(2), color: '#020408', blur: ui(6), fill: true }
}).setOrigin(0.5);
this.addButton(width / 2, height - ui(52), '타이틀로', () => {
soundDirector.playSelect();
this.scene.start('TitleScene');
});
this.input.keyboard?.on('keydown-ENTER', () => this.scene.start('TitleScene'));
this.input.keyboard?.on('keydown-SPACE', () => this.scene.start('TitleScene'));
this.addButton(width / 2, height - ui(52), '타이틀로', () => this.returnToTitle());
soundDirector.playMusic('oath-theme');
}
private returnToTitle(event?: KeyboardEvent) {
if (event?.repeat || this.navigating) {
return;
}
this.navigating = true;
soundDirector.playSelect();
this.scene.start('TitleScene');
}
private addButton(x: number, y: number, label: string, action: () => void) {
const bg = this.add.rectangle(x, y, ui(160), ui(42), 0x1a2630, 0.96);
bg.setStrokeStyle(ui(1), palette.gold, 0.82);

View File

@@ -74,7 +74,7 @@ export class TitleScene extends Phaser.Scene {
this.input.once('pointerdown', unlockAudio);
this.input.keyboard?.once('keydown', unlockAudio);
this.input.keyboard?.on('keydown-ENTER', () => this.handleEnterKey());
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => this.handleEnterKey(event));
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => this.handleSaveSlotKey(event));
this.input.keyboard?.on('keydown-ESC', () => this.handleEscapeKey());
}
@@ -636,7 +636,11 @@ export class TitleScene extends Phaser.Scene {
this.saveSlotPanel = undefined;
}
private handleEnterKey() {
private handleEnterKey(event?: KeyboardEvent) {
if (event?.repeat || this.navigating) {
return;
}
if (this.saveSlotPanel) {
return;
}
@@ -764,6 +768,9 @@ export class TitleScene extends Phaser.Scene {
}
private startGame() {
if (this.navigating) {
return;
}
soundDirector.start();
soundDirector.resume();
soundDirector.playSelect();
@@ -772,6 +779,9 @@ export class TitleScene extends Phaser.Scene {
}
private continueGame(slot?: number) {
if (this.navigating) {
return;
}
soundDirector.start();
soundDirector.resume();
soundDirector.playSelect();