fix: harden turn flow interaction UX
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user