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

@@ -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);