feat: complete keyboard flows and stabilize camp focus
This commit is contained in:
@@ -1728,6 +1728,24 @@ type CommandButton = {
|
||||
command: BattleCommand;
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
text: Phaser.GameObjects.Text;
|
||||
available: boolean;
|
||||
accent: number;
|
||||
};
|
||||
|
||||
type UsableMenuButton = {
|
||||
usable: BattleUsable;
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
text: Phaser.GameObjects.Text;
|
||||
available: boolean;
|
||||
accent: number;
|
||||
};
|
||||
|
||||
type KeyboardCommandMenuEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
available: boolean;
|
||||
accent: number;
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
};
|
||||
|
||||
type PendingMove = {
|
||||
@@ -3839,7 +3857,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
private sidePanelTransitionActive = false;
|
||||
private sidePanelTransitionLast?: SidePanelTransitionSnapshot;
|
||||
private commandButtons: CommandButton[] = [];
|
||||
private usableMenuButtons: UsableMenuButton[] = [];
|
||||
private commandMenuObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private commandMenuMode?: 'command' | 'usable';
|
||||
private commandMenuUnitId?: string;
|
||||
private commandMenuParentCommand?: UsableCommand;
|
||||
private commandMenuAnchor?: { x: number; y: number };
|
||||
private commandMenuFocusedIndex = -1;
|
||||
private commandMenuFocusMarker?: Phaser.GameObjects.Text;
|
||||
private deploymentObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private deploymentSelectedUnitId?: string;
|
||||
private deploymentNotice = '추천 배치는 이미 적용되어 있습니다. 필요하면 장수를 선택해 시작 구역 안에서 위치를 바꾸세요.';
|
||||
@@ -4069,7 +4094,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.terrainPanelLayout = undefined;
|
||||
this.sidePanelObjects = [];
|
||||
this.commandButtons = [];
|
||||
this.usableMenuButtons = [];
|
||||
this.commandMenuObjects = [];
|
||||
this.commandMenuMode = undefined;
|
||||
this.commandMenuUnitId = undefined;
|
||||
this.commandMenuParentCommand = undefined;
|
||||
this.commandMenuAnchor = undefined;
|
||||
this.commandMenuFocusedIndex = -1;
|
||||
this.commandMenuFocusMarker = undefined;
|
||||
this.deploymentObjects = [];
|
||||
this.mapMenuObjects = [];
|
||||
this.mapMenuLayout = undefined;
|
||||
@@ -4351,6 +4383,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (this.tacticalInitiativePanelObjects.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (this.commandMenuObjects.length > 0) {
|
||||
this.activateCommandMenuFocusedAction();
|
||||
return;
|
||||
}
|
||||
if (this.turnPromptObjects.length > 0) {
|
||||
this.activateTurnPromptFocusedAction();
|
||||
return;
|
||||
@@ -4404,6 +4440,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.commandMenuObjects.length > 0) {
|
||||
if (this.returnFromUsableMenu()) {
|
||||
return;
|
||||
}
|
||||
if (this.cancelPendingMove()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cancelDeploymentSelection()) {
|
||||
soundDirector.playSelect();
|
||||
return;
|
||||
@@ -4452,6 +4497,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (this.handleTacticalInitiativeKey(event)) {
|
||||
return;
|
||||
}
|
||||
if (this.handleCommandMenuNavigationKey(event)) {
|
||||
return;
|
||||
}
|
||||
if (this.handleBattleKeyboardNavigation(event)) {
|
||||
return;
|
||||
}
|
||||
@@ -8392,6 +8440,183 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.lastPointerFeedbackKey = undefined;
|
||||
}
|
||||
|
||||
private activeCommandMenuEntries(): KeyboardCommandMenuEntry[] {
|
||||
if (this.commandMenuMode === 'command') {
|
||||
return this.commandButtons
|
||||
.filter(({ background }) => background.active)
|
||||
.map(({ command, background, available, accent }) => ({
|
||||
id: command,
|
||||
label: commandLabels[command],
|
||||
available,
|
||||
accent,
|
||||
background
|
||||
}));
|
||||
}
|
||||
if (this.commandMenuMode === 'usable') {
|
||||
return this.usableMenuButtons
|
||||
.filter(({ background }) => background.active)
|
||||
.map(({ usable, background, available, accent }) => ({
|
||||
id: usable.id,
|
||||
label: usable.name,
|
||||
available,
|
||||
accent,
|
||||
background
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private setCommandMenuFocus(index: number) {
|
||||
const entries = this.activeCommandMenuEntries();
|
||||
const entry = entries[index];
|
||||
if (!entry?.available) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.commandMenuFocusedIndex = index;
|
||||
entries.forEach((candidate, candidateIndex) => {
|
||||
candidate.background.setStrokeStyle(
|
||||
candidateIndex === index ? 3 : 1,
|
||||
candidateIndex === index ? 0xffffff : candidate.accent,
|
||||
candidateIndex === index ? 1 : candidate.available ? 0.74 : 0.46
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.commandMenuFocusMarker?.active) {
|
||||
const marker = this.add.text(0, 0, '▶', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#fff8df',
|
||||
fontStyle: '700',
|
||||
stroke: '#05080b',
|
||||
strokeThickness: 4
|
||||
});
|
||||
marker.setName('command-keyboard-focus-marker');
|
||||
marker.setOrigin(0, 0.5);
|
||||
marker.setDepth(19);
|
||||
this.commandMenuFocusMarker = marker;
|
||||
this.commandMenuObjects.push(marker);
|
||||
}
|
||||
|
||||
const bounds = entry.background.getBounds();
|
||||
this.commandMenuFocusMarker
|
||||
.setPosition(bounds.x + 8, bounds.centerY)
|
||||
.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private focusFirstAvailableCommandMenuItem() {
|
||||
const entries = this.activeCommandMenuEntries();
|
||||
const firstAvailableIndex = entries.findIndex(({ available }) => available);
|
||||
this.commandMenuFocusedIndex = -1;
|
||||
this.commandMenuFocusMarker?.setVisible(false);
|
||||
if (firstAvailableIndex >= 0) {
|
||||
this.setCommandMenuFocus(firstAvailableIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private moveCommandMenuFocus(step: -1 | 1) {
|
||||
const entries = this.activeCommandMenuEntries();
|
||||
if (entries.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let index = this.commandMenuFocusedIndex;
|
||||
for (let attempt = 0; attempt < entries.length; attempt += 1) {
|
||||
index = (index + step + entries.length) % entries.length;
|
||||
if (entries[index]?.available) {
|
||||
const changed = index !== this.commandMenuFocusedIndex;
|
||||
this.setCommandMenuFocus(index);
|
||||
if (changed) {
|
||||
soundDirector.playHover();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private handleCommandMenuNavigationKey(event: KeyboardEvent) {
|
||||
if (this.commandMenuObjects.length === 0 || !this.commandMenuMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const step =
|
||||
event.code === 'ArrowUp'
|
||||
? -1
|
||||
: event.code === 'ArrowDown'
|
||||
? 1
|
||||
: event.code === 'Tab'
|
||||
? event.shiftKey
|
||||
? -1
|
||||
: 1
|
||||
: undefined;
|
||||
if (step !== undefined) {
|
||||
event.preventDefault();
|
||||
this.moveCommandMenuFocus(step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private activateCommandMenuFocusedAction() {
|
||||
const entries = this.activeCommandMenuEntries();
|
||||
const entry = entries[this.commandMenuFocusedIndex];
|
||||
const unit = battleUnits.find(
|
||||
(candidate) =>
|
||||
candidate.id === this.commandMenuUnitId &&
|
||||
candidate.id === this.selectedUnit?.id &&
|
||||
candidate.hp > 0
|
||||
);
|
||||
if (!entry?.available || !unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bounds = entry.background.getBounds();
|
||||
if (this.commandMenuMode === 'command') {
|
||||
const button = this.commandButtons[this.commandMenuFocusedIndex];
|
||||
if (!button || button.command !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
return this.activateCommandMenuChoice(unit, button.command, bounds.centerX, bounds.centerY);
|
||||
}
|
||||
|
||||
const button = this.usableMenuButtons[this.commandMenuFocusedIndex];
|
||||
if (!button || button.usable.id !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
return this.activateUsableMenuChoice(unit, button.usable);
|
||||
}
|
||||
|
||||
private returnFromUsableMenu() {
|
||||
if (this.commandMenuMode !== 'usable') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unit = battleUnits.find(
|
||||
(candidate) =>
|
||||
candidate.id === this.commandMenuUnitId &&
|
||||
candidate.id === this.selectedUnit?.id &&
|
||||
candidate.hp > 0
|
||||
);
|
||||
const parentCommand = this.commandMenuParentCommand;
|
||||
const anchor = this.commandMenuAnchor;
|
||||
if (!unit || !parentCommand || !anchor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
soundDirector.playSelect();
|
||||
this.showCommandMenu(unit, anchor.x, anchor.y);
|
||||
const parentIndex = this.commandButtons.findIndex(({ command, available }) => command === parentCommand && available);
|
||||
if (parentIndex >= 0) {
|
||||
this.setCommandMenuFocus(parentIndex);
|
||||
}
|
||||
this.renderUnitDetail(
|
||||
unit,
|
||||
`${commandLabels[parentCommand]} 선택을 취소했습니다.\n키보드: ↑↓/Tab 이동 · Enter 선택 · Esc 이동 취소`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private handleBattleKeyboardNavigation(event: KeyboardEvent) {
|
||||
if (
|
||||
(this.phase !== 'moving' && this.phase !== 'targeting') ||
|
||||
@@ -8763,7 +8988,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.showCommandMenu(unit, commandX, commandY);
|
||||
this.renderUnitDetail(unit, '명령 선택 · 우클릭은 이동 취소');
|
||||
this.renderUnitDetail(
|
||||
unit,
|
||||
'명령 선택 · 우클릭은 이동 취소\n키보드: ↑↓/Tab 이동 · Enter 선택 · Esc 이동 취소'
|
||||
);
|
||||
this.hideTurnEndPrompt();
|
||||
if (this.firstBattleTutorialStep === 'move') {
|
||||
this.suppressNextLeftClick = true;
|
||||
@@ -10951,6 +11179,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) {
|
||||
this.hideCommandMenu();
|
||||
this.commandMenuMode = 'command';
|
||||
this.commandMenuUnitId = unit.id;
|
||||
this.commandMenuAnchor = { x: pointerX, y: pointerY };
|
||||
|
||||
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
|
||||
const menuWidth = 330;
|
||||
@@ -11064,27 +11295,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
if (choicePointer.leftButtonDown()) {
|
||||
this.suppressNextLeftClick = true;
|
||||
if (!this.acceptFirstBattleTutorialCommand(command)) {
|
||||
return;
|
||||
}
|
||||
this.hideTurnEndPrompt();
|
||||
if (command === 'strategy' || command === 'item') {
|
||||
this.showUsableMenu(unit, command, choicePointer.x, choicePointer.y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command !== 'wait') {
|
||||
this.beginDamageTargeting(unit, command);
|
||||
if (command === 'attack' && this.phase === 'targeting' && this.firstBattleTutorialStep === 'attack-command') {
|
||||
this.setFirstBattleTutorialStep('target-preview');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.completeUnitAction(unit, command);
|
||||
this.activateCommandMenuChoice(unit, command, choicePointer.x, choicePointer.y);
|
||||
}
|
||||
};
|
||||
const showDetail = () => {
|
||||
if (info.available) {
|
||||
this.setCommandMenuFocus(index);
|
||||
}
|
||||
background.setFillStyle(info.available ? 0x283947 : 0x27303a, 0.98);
|
||||
this.renderUnitDetail(unit, `${commandLabels[command]}\n${info.detail}`);
|
||||
};
|
||||
@@ -11097,8 +11314,47 @@ export class BattleScene extends Phaser.Scene {
|
||||
text.on('pointerout', clearHover);
|
||||
text.on('pointerdown', chooseCommand);
|
||||
|
||||
this.commandButtons.push({ command, background, text });
|
||||
this.commandButtons.push({
|
||||
command,
|
||||
background,
|
||||
text,
|
||||
available: info.available,
|
||||
accent
|
||||
});
|
||||
});
|
||||
this.focusFirstAvailableCommandMenuItem();
|
||||
this.renderUnitDetail(
|
||||
unit,
|
||||
'명령 선택\n키보드: ↑↓/Tab 이동 · Enter 선택 · Esc 이동 취소'
|
||||
);
|
||||
}
|
||||
|
||||
private activateCommandMenuChoice(
|
||||
unit: UnitData,
|
||||
command: BattleCommand,
|
||||
anchorX: number,
|
||||
anchorY: number
|
||||
) {
|
||||
if (!this.acceptFirstBattleTutorialCommand(command)) {
|
||||
return false;
|
||||
}
|
||||
this.hideTurnEndPrompt();
|
||||
if (command === 'strategy' || command === 'item') {
|
||||
this.showUsableMenu(unit, command, anchorX, anchorY);
|
||||
soundDirector.playSelect();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command !== 'wait') {
|
||||
this.beginDamageTargeting(unit, command);
|
||||
if (command === 'attack' && this.phase === 'targeting' && this.firstBattleTutorialStep === 'attack-command') {
|
||||
this.setFirstBattleTutorialStep('target-preview');
|
||||
}
|
||||
return this.phase === 'targeting';
|
||||
}
|
||||
|
||||
this.completeUnitAction(unit, command);
|
||||
return true;
|
||||
}
|
||||
|
||||
private commandMenuPosition(pointerX: number, pointerY: number, width: number, height: number) {
|
||||
@@ -11477,6 +11733,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
text.destroy();
|
||||
});
|
||||
this.commandButtons = [];
|
||||
this.usableMenuButtons = [];
|
||||
this.commandMenuMode = undefined;
|
||||
this.commandMenuUnitId = undefined;
|
||||
this.commandMenuParentCommand = undefined;
|
||||
this.commandMenuAnchor = undefined;
|
||||
this.commandMenuFocusedIndex = -1;
|
||||
this.commandMenuFocusMarker = undefined;
|
||||
}
|
||||
|
||||
private showUsableMenu(unit: UnitData, command: UsableCommand, pointerX: number, pointerY: number) {
|
||||
@@ -11487,6 +11750,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.hideCommandMenu();
|
||||
this.commandMenuMode = 'usable';
|
||||
this.commandMenuUnitId = unit.id;
|
||||
this.commandMenuParentCommand = command;
|
||||
this.commandMenuAnchor = { x: pointerX, y: pointerY };
|
||||
|
||||
const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) }));
|
||||
const readyCount = forecasts.filter(({ forecast }) => forecast.available).length;
|
||||
@@ -11645,7 +11912,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
const choose = (choicePointer: Phaser.Input.Pointer) => {
|
||||
if (choicePointer.rightButtonDown()) {
|
||||
this.showCommandMenu(unit, pointerX, pointerY);
|
||||
this.returnFromUsableMenu();
|
||||
return;
|
||||
}
|
||||
if (choicePointer.leftButtonDown()) {
|
||||
@@ -11654,10 +11921,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
showDetail();
|
||||
return;
|
||||
}
|
||||
this.beginUsableTargeting(unit, usable);
|
||||
this.activateUsableMenuChoice(unit, usable);
|
||||
}
|
||||
};
|
||||
const showDetail = () => {
|
||||
if (forecast.available) {
|
||||
this.setCommandMenuFocus(index);
|
||||
}
|
||||
bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98);
|
||||
this.renderUnitDetail(unit, this.usableDetailText(unit, usable));
|
||||
};
|
||||
@@ -11673,7 +11943,29 @@ export class BattleScene extends Phaser.Scene {
|
||||
name.on('pointerover', showDetail);
|
||||
name.on('pointerout', clearHover);
|
||||
name.on('pointerdown', choose);
|
||||
this.usableMenuButtons.push({
|
||||
usable,
|
||||
background: bg,
|
||||
text: name,
|
||||
available: forecast.available,
|
||||
accent
|
||||
});
|
||||
});
|
||||
this.focusFirstAvailableCommandMenuItem();
|
||||
this.renderUnitDetail(
|
||||
unit,
|
||||
`${commandLabels[command]} 선택\n키보드: ↑↓/Tab 이동 · Enter 선택 · Esc 명령으로`
|
||||
);
|
||||
}
|
||||
|
||||
private activateUsableMenuChoice(unit: UnitData, usable: BattleUsable) {
|
||||
const forecast = this.usableMenuForecast(unit, usable);
|
||||
if (!forecast.available) {
|
||||
this.renderUnitDetail(unit, this.usableDetailText(unit, usable));
|
||||
return false;
|
||||
}
|
||||
this.beginUsableTargeting(unit, usable);
|
||||
return this.phase === 'targeting';
|
||||
}
|
||||
|
||||
private availableUsables(unit: UnitData, command: UsableCommand) {
|
||||
@@ -26473,7 +26765,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.clearMarkers();
|
||||
soundDirector.playSelect();
|
||||
this.showCommandMenu(unit, view?.sprite.x ?? this.tileCenterX(unit.x), view?.sprite.y ?? this.tileCenterY(unit.y));
|
||||
this.renderUnitDetail(unit, '대상 선택을 취소했습니다.\n공격, 책략, 도구, 대기 중 하나를 다시 선택하세요.\n우클릭하면 이동 취소로 돌아갑니다.');
|
||||
this.renderUnitDetail(
|
||||
unit,
|
||||
'대상 선택을 취소했습니다.\n공격, 책략, 도구, 대기 중 하나를 다시 선택하세요.\n키보드: ↑↓/Tab 이동 · Enter 선택 · Esc 이동 취소'
|
||||
);
|
||||
if (this.firstBattleTutorialStep === 'target-preview') {
|
||||
this.setFirstBattleTutorialStep('attack-command', '대상 선택을 취소해 공격 명령 단계로 돌아왔습니다.');
|
||||
}
|
||||
@@ -30783,6 +31078,80 @@ export class BattleScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
private commandMenuKeyboardDebugState() {
|
||||
const entries = this.activeCommandMenuEntries();
|
||||
const focused = entries[this.commandMenuFocusedIndex];
|
||||
const guidance = this.sidePanelObjects.find(
|
||||
(object): object is Phaser.GameObjects.Text =>
|
||||
object instanceof Phaser.GameObjects.Text &&
|
||||
object.active &&
|
||||
object.visible &&
|
||||
object.text.includes('↑↓/Tab')
|
||||
);
|
||||
const toBounds = (background?: Phaser.GameObjects.Rectangle) => {
|
||||
const bounds = background?.active ? background.getBounds() : undefined;
|
||||
return bounds
|
||||
? { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
|
||||
: null;
|
||||
};
|
||||
const markerBounds =
|
||||
this.commandMenuFocusMarker?.active && this.commandMenuFocusMarker.visible
|
||||
? this.commandMenuFocusMarker.getBounds()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
visible: this.commandMenuObjects.length > 0 && Boolean(this.commandMenuMode),
|
||||
mode: this.commandMenuMode ?? null,
|
||||
unitId: this.commandMenuUnitId ?? null,
|
||||
parentCommand: this.commandMenuParentCommand ?? null,
|
||||
focusedIndex: this.commandMenuFocusedIndex,
|
||||
guidance: guidance
|
||||
? {
|
||||
text: guidance.text,
|
||||
bounds: {
|
||||
x: guidance.getBounds().x,
|
||||
y: guidance.getBounds().y,
|
||||
width: guidance.getBounds().width,
|
||||
height: guidance.getBounds().height
|
||||
}
|
||||
}
|
||||
: null,
|
||||
focusedItem: focused
|
||||
? {
|
||||
id: focused.id,
|
||||
label: focused.label,
|
||||
available: focused.available,
|
||||
bounds: toBounds(focused.background),
|
||||
lineWidth: focused.background.lineWidth
|
||||
}
|
||||
: null,
|
||||
items: entries.map((entry, index) => ({
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
available: entry.available,
|
||||
focused: index === this.commandMenuFocusedIndex,
|
||||
bounds: toBounds(entry.background),
|
||||
lineWidth: entry.background.lineWidth
|
||||
})),
|
||||
focusMarker: markerBounds
|
||||
? {
|
||||
visible: true,
|
||||
text: this.commandMenuFocusMarker?.text ?? '',
|
||||
bounds: {
|
||||
x: markerBounds.x,
|
||||
y: markerBounds.y,
|
||||
width: markerBounds.width,
|
||||
height: markerBounds.height
|
||||
}
|
||||
}
|
||||
: {
|
||||
visible: false,
|
||||
text: this.commandMenuFocusMarker?.text ?? '',
|
||||
bounds: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private battleHudDebugState() {
|
||||
const miniMap = this.miniMapLayout;
|
||||
const selectedDot = this.selectedUnit ? this.miniMapUnitDots.get(this.selectedUnit.id) : undefined;
|
||||
@@ -31868,6 +32237,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.mapBackground?.active && this.mapBackground.texture.key === battleScenario.mapTextureKey
|
||||
),
|
||||
commandMenuVisible: this.commandMenuObjects.length > 0,
|
||||
commandMenuKeyboard: this.commandMenuKeyboardDebugState(),
|
||||
mapMenuVisible: this.mapMenuObjects.length > 0,
|
||||
saveSlotPanelVisible: this.saveSlotPanelObjects.length > 0,
|
||||
saveSlotPanelMode: this.saveSlotPanelMode ?? null,
|
||||
|
||||
@@ -261,6 +261,7 @@ import {
|
||||
} from '../state/thirdCampPreparationActions';
|
||||
import { clearCampaignBattleSavesForSlot } from '../state/battleSaveKeys';
|
||||
import { releaseTextureSource } from '../render/loaderLifecycle';
|
||||
import { isVisualMotionReduced } from '../settings/visualMotion';
|
||||
import { palette } from '../ui/palette';
|
||||
import { ensureLazyScene, type LazySceneKey } from './lazyScenes';
|
||||
|
||||
@@ -11461,6 +11462,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private campSkinTransitionRevision = 0;
|
||||
private campSkinTransitionCompletedRevision = 0;
|
||||
private campSkinTransitionActive = false;
|
||||
private visualMotionReduced = false;
|
||||
private ownedCampTextureKeys = new Set<string>();
|
||||
private campSkinTransitionLast?: {
|
||||
kind: 'backdrop-fade';
|
||||
@@ -11593,6 +11595,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private retrySortieBattleId?: BattleScenarioId;
|
||||
private skipIntroStoryForSortie = false;
|
||||
private navigationPending = false;
|
||||
private campNoticeRemovalTimer?: Phaser.Time.TimerEvent;
|
||||
|
||||
constructor() {
|
||||
super('CampScene');
|
||||
@@ -11615,6 +11618,10 @@ export class CampScene extends Phaser.Scene {
|
||||
create() {
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.releaseOwnedCampTextures());
|
||||
this.navigationPending = false;
|
||||
this.visualMotionReduced = isVisualMotionReduced();
|
||||
this.activeTab = 'status';
|
||||
this.selectedCityLocation = 'information';
|
||||
this.campNoticeRemovalTimer = undefined;
|
||||
this.contentObjects = [];
|
||||
this.campRosterPage = 0;
|
||||
this.campRosterLayout = undefined;
|
||||
@@ -11755,8 +11762,6 @@ export class CampScene extends Phaser.Scene {
|
||||
: !dialogueComplete
|
||||
? 'dialogue'
|
||||
: 'market';
|
||||
} else if (this.activeTab === 'city') {
|
||||
this.activeTab = 'status';
|
||||
}
|
||||
const pendingDirectExplorationVisit = this.availableCampVisits().find(
|
||||
(visit) =>
|
||||
@@ -11766,6 +11771,8 @@ export class CampScene extends Phaser.Scene {
|
||||
if (!cityStay && pendingDirectExplorationVisit) {
|
||||
this.selectedVisitId = pendingDirectExplorationVisit.id;
|
||||
this.activeTab = 'visit';
|
||||
} else if (!cityStay && pendingPriorityReturnDialogueId) {
|
||||
this.activeTab = 'dialogue';
|
||||
}
|
||||
this.initializeCityEquipmentContext();
|
||||
soundDirector.playSoundscape({
|
||||
@@ -11865,9 +11872,9 @@ export class CampScene extends Phaser.Scene {
|
||||
fontStyle: '700'
|
||||
})).setOrigin(0.5).setDepth(-17);
|
||||
|
||||
const transitionDurationMs = 420;
|
||||
const transitionDurationMs = this.visualMotionReduced ? 0 : 420;
|
||||
const transitionRevision = ++this.campSkinTransitionRevision;
|
||||
this.campSkinTransitionActive = true;
|
||||
this.campSkinTransitionActive = !this.visualMotionReduced;
|
||||
this.campSkinTransitionLast = {
|
||||
kind: 'backdrop-fade',
|
||||
durationMs: transitionDurationMs,
|
||||
@@ -11875,19 +11882,24 @@ export class CampScene extends Phaser.Scene {
|
||||
fromAlpha: 0,
|
||||
toAlpha: skin.backdropAlpha
|
||||
};
|
||||
this.tweens.add({
|
||||
targets: backdrop,
|
||||
alpha: skin.backdropAlpha,
|
||||
duration: transitionDurationMs,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => {
|
||||
if (this.campSkinBackdrop !== backdrop) {
|
||||
return;
|
||||
if (this.visualMotionReduced) {
|
||||
backdrop.setAlpha(skin.backdropAlpha);
|
||||
this.campSkinTransitionCompletedRevision = transitionRevision;
|
||||
} else {
|
||||
this.tweens.add({
|
||||
targets: backdrop,
|
||||
alpha: skin.backdropAlpha,
|
||||
duration: transitionDurationMs,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => {
|
||||
if (this.campSkinBackdrop !== backdrop) {
|
||||
return;
|
||||
}
|
||||
this.campSkinTransitionCompletedRevision = transitionRevision;
|
||||
this.campSkinTransitionActive = false;
|
||||
}
|
||||
this.campSkinTransitionCompletedRevision = transitionRevision;
|
||||
this.campSkinTransitionActive = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.renderStaticButtons();
|
||||
this.render();
|
||||
@@ -25162,21 +25174,30 @@ export class CampScene extends Phaser.Scene {
|
||||
box.setDepth(depth);
|
||||
const noticeObjects: Phaser.GameObjects.GameObject[] = [box, text];
|
||||
this.dialogueObjects = noticeObjects;
|
||||
this.tweens.add({
|
||||
targets: noticeObjects,
|
||||
alpha: 0,
|
||||
delay: this.campNoticeHoldDuration(message),
|
||||
duration: 320,
|
||||
onComplete: () => {
|
||||
noticeObjects.forEach((object) => object.active && object.destroy());
|
||||
if (this.dialogueObjects === noticeObjects) {
|
||||
this.dialogueObjects = [];
|
||||
}
|
||||
const removeNotice = () => {
|
||||
noticeObjects.forEach((object) => object.active && object.destroy());
|
||||
if (this.dialogueObjects === noticeObjects) {
|
||||
this.dialogueObjects = [];
|
||||
}
|
||||
});
|
||||
this.campNoticeRemovalTimer = undefined;
|
||||
};
|
||||
const holdDuration = this.campNoticeHoldDuration(message);
|
||||
if (this.visualMotionReduced) {
|
||||
this.campNoticeRemovalTimer = this.time.delayedCall(holdDuration, removeNotice);
|
||||
} else {
|
||||
this.tweens.add({
|
||||
targets: noticeObjects,
|
||||
alpha: 0,
|
||||
delay: holdDuration,
|
||||
duration: 320,
|
||||
onComplete: removeNotice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private clearCampNotice() {
|
||||
this.campNoticeRemovalTimer?.remove(false);
|
||||
this.campNoticeRemovalTimer = undefined;
|
||||
this.tweens.killTweensOf(this.dialogueObjects);
|
||||
this.dialogueObjects.forEach((object) => object.active && object.destroy());
|
||||
this.dialogueObjects = [];
|
||||
@@ -26228,6 +26249,7 @@ export class CampScene extends Phaser.Scene {
|
||||
: undefined;
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
visualMotionReduced: this.visualMotionReduced,
|
||||
victoryRewardAcknowledgement: {
|
||||
visible: this.victoryRewardObjects.length > 0,
|
||||
battleId: this.pendingVictoryRewardReport?.battleId ?? null,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
updateAudioPreference
|
||||
} from '../settings/audioPreferences';
|
||||
import {
|
||||
isVisualMotionReduced,
|
||||
loadVisualMotionMode,
|
||||
nextVisualMotionMode,
|
||||
saveVisualMotionMode,
|
||||
@@ -43,11 +44,31 @@ const fhdUiScale = 1.5;
|
||||
const ui = (value: number) => value * fhdUiScale;
|
||||
const uiPx = (value: number) => `${Math.round(ui(value))}px`;
|
||||
|
||||
type TitleFocusScope = 'main' | 'settings' | 'save-slots' | 'new-game-confirm';
|
||||
|
||||
type TitleFocusItem = {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
marker: Phaser.GameObjects.Text;
|
||||
activate: () => void;
|
||||
render: (focused: boolean) => void;
|
||||
bounds: () => Phaser.Geom.Rectangle;
|
||||
};
|
||||
|
||||
export class TitleScene extends Phaser.Scene {
|
||||
private focusedButton?: Phaser.GameObjects.Text;
|
||||
private settingsPanel?: Phaser.GameObjects.Container;
|
||||
private newGameConfirmPanel?: Phaser.GameObjects.Container;
|
||||
private saveSlotPanel?: Phaser.GameObjects.Container;
|
||||
private readonly focusItems = new Map<TitleFocusScope, TitleFocusItem[]>();
|
||||
private focusScope: TitleFocusScope = 'main';
|
||||
private focusedItemId?: string;
|
||||
private titleBackground?: Phaser.GameObjects.Image;
|
||||
private titleBackgroundCoverScale = 1;
|
||||
private titleMist?: Phaser.GameObjects.Ellipse;
|
||||
private titlePetals: Phaser.GameObjects.Image[] = [];
|
||||
private titleMotionTweens: Phaser.Tweens.Tween[] = [];
|
||||
private visualMotionReduced = false;
|
||||
private handledKeyboardEvents = new WeakSet<KeyboardEvent>();
|
||||
private navigating = false;
|
||||
|
||||
constructor() {
|
||||
@@ -55,10 +76,18 @@ export class TitleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
create() {
|
||||
this.focusedButton = undefined;
|
||||
this.settingsPanel = undefined;
|
||||
this.newGameConfirmPanel = undefined;
|
||||
this.saveSlotPanel = undefined;
|
||||
this.focusItems.clear();
|
||||
this.focusScope = 'main';
|
||||
this.focusedItemId = undefined;
|
||||
this.titleBackground = undefined;
|
||||
this.titleMist = undefined;
|
||||
this.titlePetals = [];
|
||||
this.titleMotionTweens = [];
|
||||
this.visualMotionReduced = isVisualMotionReduced();
|
||||
this.handledKeyboardEvents = new WeakSet<KeyboardEvent>();
|
||||
this.navigating = false;
|
||||
|
||||
const debugBattleId = this.debugBattleId();
|
||||
@@ -90,6 +119,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
const { width, height } = this.scale;
|
||||
this.drawBackground(width, height);
|
||||
this.drawAtmosphere(width, height);
|
||||
this.applyTitleMotionPreference(this.visualMotionReduced);
|
||||
this.drawTitleMark(width, height);
|
||||
this.drawMenu(width, height);
|
||||
|
||||
@@ -102,9 +132,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
|
||||
this.input.once('pointerdown', unlockAudio);
|
||||
this.input.keyboard?.once('keydown', unlockAudio);
|
||||
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());
|
||||
this.input.keyboard?.on('keydown', this.handleKeyboardInput, this);
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
|
||||
this.input.keyboard?.off('keydown', this.handleKeyboardInput, this);
|
||||
this.stopTitleMotionTweens();
|
||||
});
|
||||
}
|
||||
|
||||
private shouldOpenDebugSortiePrep() {
|
||||
@@ -173,18 +205,8 @@ export class TitleScene extends Phaser.Scene {
|
||||
const texture = this.textures.get('title-taoyuan').getSourceImage();
|
||||
const coverScale = Math.max(width / texture.width, height / texture.height);
|
||||
|
||||
background.setScale(coverScale * 1.08);
|
||||
background.setPosition(width / 2 - ui(18), height / 2 + ui(4));
|
||||
this.tweens.add({
|
||||
targets: background,
|
||||
x: width / 2 + ui(18),
|
||||
y: height / 2 - ui(6),
|
||||
scale: coverScale * 1.12,
|
||||
duration: 18000,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
this.titleBackground = background;
|
||||
this.titleBackgroundCoverScale = coverScale;
|
||||
|
||||
this.add.rectangle(0, 0, width, height, 0x080a0e, 0.2).setOrigin(0);
|
||||
this.add.image(0, 0, this.createShadeTexture(width, height)).setOrigin(0);
|
||||
@@ -224,15 +246,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
|
||||
private drawAtmosphere(width: number, height: number) {
|
||||
const mist = this.add.ellipse(width / 2, height * 0.78, width * 1.25, ui(190), 0xd8b15f, 0.04);
|
||||
this.tweens.add({
|
||||
targets: mist,
|
||||
alpha: 0.11,
|
||||
x: width / 2 + ui(22),
|
||||
duration: 8000,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
this.titleMist = mist;
|
||||
|
||||
for (let index = 0; index < 34; index += 1) {
|
||||
const petal = this.add.image(
|
||||
@@ -244,8 +258,63 @@ export class TitleScene extends Phaser.Scene {
|
||||
petal.setScale(scale * fhdUiScale);
|
||||
petal.setAlpha(Phaser.Math.FloatBetween(0.22, 0.62));
|
||||
petal.setRotation(Phaser.Math.FloatBetween(-1.2, 1.2));
|
||||
this.titlePetals.push(petal);
|
||||
}
|
||||
}
|
||||
|
||||
this.tweens.add({
|
||||
private applyTitleMotionPreference(reduced: boolean) {
|
||||
this.visualMotionReduced = reduced;
|
||||
this.stopTitleMotionTweens();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
this.titleBackground
|
||||
?.setPosition(width / 2, height / 2)
|
||||
.setScale(this.titleBackgroundCoverScale * 1.1);
|
||||
this.titleMist?.setPosition(width / 2, height * 0.78).setAlpha(reduced ? 0.07 : 0.04);
|
||||
|
||||
this.titlePetals.forEach((petal, index) => {
|
||||
const x = ((index * 211 + 97) % Math.max(1, width + ui(120))) - ui(60);
|
||||
const y = ((index * 137 + 43) % Math.max(1, height + ui(100))) - ui(80);
|
||||
petal
|
||||
.setVisible(true)
|
||||
.setPosition(x, y)
|
||||
.setRotation(-0.9 + (index % 9) * 0.21);
|
||||
});
|
||||
|
||||
if (reduced) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.titleBackground) {
|
||||
this.titleBackground
|
||||
.setPosition(width / 2 - ui(18), height / 2 + ui(4))
|
||||
.setScale(this.titleBackgroundCoverScale * 1.08);
|
||||
this.trackTitleMotionTween({
|
||||
targets: this.titleBackground,
|
||||
x: width / 2 + ui(18),
|
||||
y: height / 2 - ui(6),
|
||||
scale: this.titleBackgroundCoverScale * 1.12,
|
||||
duration: 18000,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
if (this.titleMist) {
|
||||
this.trackTitleMotionTween({
|
||||
targets: this.titleMist,
|
||||
alpha: 0.11,
|
||||
x: width / 2 + ui(22),
|
||||
duration: 8000,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
this.titlePetals.forEach((petal) => {
|
||||
this.trackTitleMotionTween({
|
||||
targets: petal,
|
||||
x: petal.x + Phaser.Math.Between(ui(80), ui(220)),
|
||||
y: height + Phaser.Math.Between(ui(40), ui(180)),
|
||||
@@ -258,7 +327,18 @@ export class TitleScene extends Phaser.Scene {
|
||||
petal.setPosition(Phaser.Math.Between(-ui(120), width), Phaser.Math.Between(-ui(180), -ui(40)));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private trackTitleMotionTween(config: Phaser.Types.Tweens.TweenBuilderConfig) {
|
||||
const tween = this.tweens.add(config);
|
||||
this.titleMotionTweens.push(tween);
|
||||
return tween;
|
||||
}
|
||||
|
||||
private stopTitleMotionTweens() {
|
||||
this.titleMotionTweens.forEach((tween) => tween.stop());
|
||||
this.titleMotionTweens = [];
|
||||
}
|
||||
|
||||
private drawTitleMark(width: number, height: number) {
|
||||
@@ -303,13 +383,22 @@ export class TitleScene extends Phaser.Scene {
|
||||
this.add.rectangle(menuX, menuY - plateHeight / 2, ui(186), ui(2), palette.gold, 0.7);
|
||||
this.add.rectangle(menuX, menuY + plateHeight / 2, ui(186), ui(2), palette.gold, 0.36);
|
||||
|
||||
this.createMenuButton(menuX, menuY - ui(70), '새 게임', true, () => this.requestStartGame());
|
||||
this.createMenuButton(menuX, menuY, isEndingComplete ? '엔딩 보기' : '이어하기', canContinue, () => this.requestContinueGame());
|
||||
this.createMenuButton(menuX, menuY + ui(70), '설정', true, () => this.openSettingsPanel(menuX, menuY));
|
||||
this.createMenuButton('new-game', menuX, menuY - ui(70), '새 게임', true, () => this.requestStartGame());
|
||||
this.createMenuButton(
|
||||
'continue',
|
||||
menuX,
|
||||
menuY,
|
||||
isEndingComplete ? '엔딩 보기' : '이어하기',
|
||||
canContinue,
|
||||
() => this.requestContinueGame()
|
||||
);
|
||||
this.createMenuButton('settings', menuX, menuY + ui(70), '설정', true, () => this.openSettingsPanel(menuX, menuY));
|
||||
|
||||
if (campaign) {
|
||||
this.drawCampaignSaveBadge(menuX, menuY + ui(132), campaign, isEndingComplete);
|
||||
}
|
||||
|
||||
this.setFocusScope('main', canContinue ? 'continue' : 'new-game');
|
||||
}
|
||||
|
||||
private drawCampaignSaveBadge(
|
||||
@@ -404,6 +493,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private createMenuButton(
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
label: string,
|
||||
@@ -421,41 +511,49 @@ export class TitleScene extends Phaser.Scene {
|
||||
text.setOrigin(0.5);
|
||||
text.setShadow(0, ui(3), '#080a0e', ui(8), true, true);
|
||||
|
||||
if (!enabled) {
|
||||
return text;
|
||||
}
|
||||
const marker = this.add.text(x - ui(116), y, '▶', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(17),
|
||||
color: '#f6e6bd',
|
||||
fontStyle: '700'
|
||||
});
|
||||
marker.setOrigin(0.5).setVisible(false);
|
||||
|
||||
const hitArea = this.add.zone(x, y, ui(216), ui(58));
|
||||
hitArea.setOrigin(0.5);
|
||||
hitArea.setInteractive({ useHandCursor: true });
|
||||
hitArea.on('pointerover', () => {
|
||||
this.focusButton(text);
|
||||
soundDirector.playHover();
|
||||
});
|
||||
hitArea.on('pointerout', () => this.unfocusButton(text));
|
||||
hitArea.on('pointerdown', () => {
|
||||
soundDirector.start();
|
||||
soundDirector.resume();
|
||||
onSelect();
|
||||
const render = (focused: boolean) => {
|
||||
marker.setVisible(enabled && focused);
|
||||
text.setColor(!enabled ? '#7d8189' : focused ? '#111821' : '#f1e3c2');
|
||||
text.setBackgroundColor(enabled && focused ? '#d8b15f' : 'rgba(0,0,0,0)');
|
||||
};
|
||||
|
||||
this.registerFocusItem('main', {
|
||||
id,
|
||||
enabled,
|
||||
marker,
|
||||
activate: onSelect,
|
||||
render,
|
||||
bounds: () => hitArea.getBounds()
|
||||
});
|
||||
render(false);
|
||||
|
||||
if (enabled) {
|
||||
hitArea.setInteractive({ useHandCursor: true });
|
||||
hitArea.on('pointerover', () => this.focusItem('main', id, true));
|
||||
hitArea.on('pointerdown', () => {
|
||||
if (this.activeModalScope()) {
|
||||
return;
|
||||
}
|
||||
this.focusItem('main', id);
|
||||
soundDirector.start();
|
||||
soundDirector.resume();
|
||||
onSelect();
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private focusButton(text: Phaser.GameObjects.Text) {
|
||||
this.focusedButton = text;
|
||||
text.setColor('#111821');
|
||||
text.setBackgroundColor('#d8b15f');
|
||||
}
|
||||
|
||||
private unfocusButton(text: Phaser.GameObjects.Text) {
|
||||
if (this.focusedButton === text) {
|
||||
this.focusedButton = undefined;
|
||||
}
|
||||
text.setColor('#f1e3c2');
|
||||
text.setBackgroundColor('rgba(0,0,0,0)');
|
||||
}
|
||||
|
||||
private openSettingsPanel(x: number, y: number) {
|
||||
if (this.settingsPanel) {
|
||||
this.closeSettingsPanel();
|
||||
@@ -463,8 +561,9 @@ export class TitleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
soundDirector.playSelect();
|
||||
this.closeNewGameConfirmPanel();
|
||||
this.closeSaveSlotPanel();
|
||||
this.closeNewGameConfirmPanel(false);
|
||||
this.closeSaveSlotPanel(false);
|
||||
this.clearFocusScope('settings');
|
||||
|
||||
const panel = this.add.container(x, y + ui(126));
|
||||
const backdrop = this.add.rectangle(0, 0, ui(320), ui(468), 0x0b1118, 1);
|
||||
@@ -480,48 +579,98 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
title.setOrigin(0.5);
|
||||
|
||||
const soundText = this.createSettingsButton(0, -ui(126), this.soundLabel());
|
||||
soundText.on('pointerdown', () => {
|
||||
const preferences = loadAudioPreferences();
|
||||
const muted = !soundDirector.isMuted();
|
||||
soundDirector.setMuted(muted);
|
||||
saveAudioPreferences({ ...preferences, muted });
|
||||
soundText.setText(this.soundLabel());
|
||||
soundDirector.playSelect();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'sound',
|
||||
0,
|
||||
-ui(126),
|
||||
this.soundLabel(),
|
||||
(text) => {
|
||||
const preferences = loadAudioPreferences();
|
||||
const muted = !soundDirector.isMuted();
|
||||
soundDirector.setMuted(muted);
|
||||
saveAudioPreferences({ ...preferences, muted });
|
||||
text.setText(this.soundLabel());
|
||||
soundDirector.playSelect();
|
||||
}
|
||||
);
|
||||
|
||||
const musicText = this.createSettingsButton(0, -ui(84), this.audioCategoryLabel('music'));
|
||||
musicText.on('pointerdown', () => this.cycleAudioLevel('music', musicText));
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'music',
|
||||
0,
|
||||
-ui(84),
|
||||
this.audioCategoryLabel('music'),
|
||||
(text) => this.cycleAudioLevel('music', text)
|
||||
);
|
||||
|
||||
const effectsText = this.createSettingsButton(0, -ui(42), this.audioCategoryLabel('effects'));
|
||||
effectsText.on('pointerdown', () => this.cycleAudioLevel('effects', effectsText));
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'effects',
|
||||
0,
|
||||
-ui(42),
|
||||
this.audioCategoryLabel('effects'),
|
||||
(text) => this.cycleAudioLevel('effects', text)
|
||||
);
|
||||
|
||||
const ambienceText = this.createSettingsButton(0, 0, this.audioCategoryLabel('ambience'));
|
||||
ambienceText.on('pointerdown', () => this.cycleAudioLevel('ambience', ambienceText));
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'ambience',
|
||||
0,
|
||||
0,
|
||||
this.audioCategoryLabel('ambience'),
|
||||
(text) => this.cycleAudioLevel('ambience', text)
|
||||
);
|
||||
|
||||
const presentationText = this.createSettingsButton(0, ui(42), this.combatPresentationLabel());
|
||||
presentationText.on('pointerdown', () => {
|
||||
const nextMode = nextCombatPresentationMode(loadCombatPresentationMode());
|
||||
saveCombatPresentationMode(nextMode);
|
||||
presentationText.setText(this.combatPresentationLabel());
|
||||
soundDirector.playSelect();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'combat-presentation',
|
||||
0,
|
||||
ui(42),
|
||||
this.combatPresentationLabel(),
|
||||
(text) => {
|
||||
const nextMode = nextCombatPresentationMode(loadCombatPresentationMode());
|
||||
saveCombatPresentationMode(nextMode);
|
||||
text.setText(this.combatPresentationLabel());
|
||||
soundDirector.playSelect();
|
||||
}
|
||||
);
|
||||
|
||||
const motionText = this.createSettingsButton(0, ui(84), this.visualMotionLabel());
|
||||
motionText.on('pointerdown', () => {
|
||||
const nextMode = nextVisualMotionMode(loadVisualMotionMode());
|
||||
saveVisualMotionMode(nextMode);
|
||||
motionText.setText(this.visualMotionLabel());
|
||||
soundDirector.playSelect();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'visual-motion',
|
||||
0,
|
||||
ui(84),
|
||||
this.visualMotionLabel(),
|
||||
(text) => {
|
||||
const nextMode = nextVisualMotionMode(loadVisualMotionMode());
|
||||
saveVisualMotionMode(nextMode);
|
||||
this.applyTitleMotionPreference(nextMode === 'reduced');
|
||||
text.setText(this.visualMotionLabel());
|
||||
soundDirector.playSelect();
|
||||
}
|
||||
);
|
||||
|
||||
const close = this.createSettingsButton(0, ui(132), '닫기');
|
||||
close.on('pointerdown', () => {
|
||||
soundDirector.playSelect();
|
||||
this.closeSettingsPanel();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'settings',
|
||||
'close',
|
||||
0,
|
||||
ui(132),
|
||||
'닫기',
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.closeSettingsPanel();
|
||||
}
|
||||
);
|
||||
|
||||
const hint = this.add.text(0, ui(178), '항목을 눌러 단계 변경 · Esc 닫기', {
|
||||
const hint = this.add.text(0, ui(178), '↑↓/Tab 이동 · Enter/Space 변경\nEsc 닫기', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(11),
|
||||
color: '#9fb0bf',
|
||||
@@ -531,24 +680,70 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
hint.setOrigin(0.5);
|
||||
|
||||
panel.add([backdrop, title, soundText, musicText, effectsText, ambienceText, presentationText, motionText, close, hint]);
|
||||
panel.addAt([backdrop, title, hint], 0);
|
||||
panel.setAlpha(0);
|
||||
this.settingsPanel = panel;
|
||||
this.tweens.add({ targets: panel, alpha: 1, y: y + ui(116), duration: 160, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({
|
||||
targets: panel,
|
||||
alpha: 1,
|
||||
y: y + ui(116),
|
||||
duration: this.visualMotionReduced ? 0 : 160,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
this.setFocusScope('settings', 'sound');
|
||||
}
|
||||
|
||||
private createSettingsButton(x: number, y: number, label: string) {
|
||||
private createSettingsButton(
|
||||
panel: Phaser.GameObjects.Container,
|
||||
scope: Exclude<TitleFocusScope, 'main'>,
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
label: string,
|
||||
onSelect: (text: Phaser.GameObjects.Text) => void,
|
||||
baseColor = '#d8b15f'
|
||||
) {
|
||||
const text = this.add.text(x, y, label, {
|
||||
fontSize: uiPx(20),
|
||||
color: '#d8b15f',
|
||||
color: baseColor,
|
||||
fixedWidth: ui(210),
|
||||
align: 'center',
|
||||
padding: { top: ui(8), bottom: ui(8) }
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
text.on('pointerover', () => text.setColor('#f1e3c2'));
|
||||
text.on('pointerout', () => text.setColor('#d8b15f'));
|
||||
|
||||
const marker = this.add.text(x - ui(124), y, '▶', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(14),
|
||||
color: '#f6e6bd',
|
||||
fontStyle: '700'
|
||||
});
|
||||
marker.setOrigin(0.5).setVisible(false);
|
||||
|
||||
const render = (focused: boolean) => {
|
||||
marker.setVisible(focused);
|
||||
text.setColor(focused ? '#111821' : baseColor);
|
||||
text.setBackgroundColor(focused ? '#d8b15f' : 'rgba(0,0,0,0)');
|
||||
};
|
||||
|
||||
panel.add([marker, text]);
|
||||
this.registerFocusItem(scope, {
|
||||
id,
|
||||
enabled: true,
|
||||
marker,
|
||||
activate: () => onSelect(text),
|
||||
render,
|
||||
bounds: () => text.getBounds()
|
||||
});
|
||||
render(false);
|
||||
text.on('pointerover', () => this.focusItem(scope, id, true));
|
||||
text.on('pointerdown', () => {
|
||||
this.focusItem(scope, id);
|
||||
soundDirector.start();
|
||||
soundDirector.resume();
|
||||
onSelect(text);
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -584,9 +779,13 @@ export class TitleScene extends Phaser.Scene {
|
||||
return `화면 움직임: ${visualMotionModeLabel(loadVisualMotionMode())}`;
|
||||
}
|
||||
|
||||
private closeSettingsPanel() {
|
||||
private closeSettingsPanel(restoreFocus = true) {
|
||||
this.clearFocusScope('settings');
|
||||
this.settingsPanel?.destroy();
|
||||
this.settingsPanel = undefined;
|
||||
if (restoreFocus) {
|
||||
this.setFocusScope('main', 'settings');
|
||||
}
|
||||
}
|
||||
|
||||
private requestContinueGame() {
|
||||
@@ -614,12 +813,14 @@ export class TitleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
soundDirector.playSelect();
|
||||
this.closeSettingsPanel();
|
||||
this.closeNewGameConfirmPanel();
|
||||
this.closeSettingsPanel(false);
|
||||
this.closeNewGameConfirmPanel(false);
|
||||
this.clearFocusScope('save-slots');
|
||||
|
||||
const panel = this.add.container(x, y + ui(252));
|
||||
const backdrop = this.add.rectangle(0, 0, ui(318), ui(280), 0x0e151d, 0.95);
|
||||
backdrop.setStrokeStyle(ui(1), palette.gold, 0.58);
|
||||
backdrop.setInteractive();
|
||||
|
||||
const title = this.add.text(0, -ui(116), '이어갈 기록 선택 (1~3)', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
@@ -632,13 +833,20 @@ export class TitleScene extends Phaser.Scene {
|
||||
title.setOrigin(0.5);
|
||||
|
||||
const rows = listCampaignSaveSlots().map((slot, index) => this.createSaveSlotRow(slot, -ui(82) + index * ui(58)));
|
||||
const close = this.createSettingsButton(0, ui(96), '닫기');
|
||||
close.on('pointerdown', () => {
|
||||
soundDirector.playSelect();
|
||||
this.closeSaveSlotPanel();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'save-slots',
|
||||
'close',
|
||||
0,
|
||||
ui(96),
|
||||
'닫기',
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.closeSaveSlotPanel();
|
||||
}
|
||||
);
|
||||
|
||||
const hint = this.add.text(0, ui(126), '숫자키 1~3 선택 · Esc 닫기', {
|
||||
const hint = this.add.text(0, ui(120), '↑↓/Tab 이동 · Enter/Space 선택\n숫자키 1~3 · Esc 닫기', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(11),
|
||||
color: '#9fb0bf',
|
||||
@@ -648,10 +856,18 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
hint.setOrigin(0.5);
|
||||
|
||||
panel.add([backdrop, title, ...rows, close, hint]);
|
||||
panel.addAt([backdrop, title, ...rows, hint], 0);
|
||||
panel.setAlpha(0);
|
||||
this.saveSlotPanel = panel;
|
||||
this.tweens.add({ targets: panel, alpha: 1, y: y + ui(242), duration: 160, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({
|
||||
targets: panel,
|
||||
alpha: 1,
|
||||
y: y + ui(242),
|
||||
duration: this.visualMotionReduced ? 0 : 160,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
const activeSlot = loadCampaignState().activeSaveSlot;
|
||||
this.setFocusScope('save-slots', `slot-${activeSlot}`);
|
||||
}
|
||||
|
||||
private createSaveSlotRow(slot: CampaignSaveSlotSummary, y: number) {
|
||||
@@ -701,20 +917,41 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
keyText.setOrigin(0.5);
|
||||
|
||||
row.add([background, title, detail, meta, keycap, keyText]);
|
||||
const marker = this.add.text(-ui(143), 0, '▶', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(12),
|
||||
color: '#f6e6bd',
|
||||
fontStyle: '700'
|
||||
});
|
||||
marker.setOrigin(0.5).setVisible(false);
|
||||
|
||||
row.add([background, title, detail, meta, keycap, keyText, marker]);
|
||||
|
||||
const render = (focused: boolean) => {
|
||||
marker.setVisible(enabled && focused);
|
||||
background.setFillStyle(enabled && focused ? 0xd8b15f : enabled ? 0x1b2530 : 0x121820, enabled && focused ? 0.22 : enabled ? 0.9 : 0.56);
|
||||
background.setStrokeStyle(
|
||||
ui(focused ? 2 : 1),
|
||||
enabled ? palette.gold : 0x56616d,
|
||||
enabled && focused ? 0.9 : enabled ? 0.42 : 0.2
|
||||
);
|
||||
};
|
||||
|
||||
this.registerFocusItem('save-slots', {
|
||||
id: `slot-${slot.slot}`,
|
||||
enabled,
|
||||
marker,
|
||||
activate: () => this.activateSaveSlot(slot.slot),
|
||||
render,
|
||||
bounds: () => background.getBounds()
|
||||
});
|
||||
render(false);
|
||||
|
||||
if (enabled) {
|
||||
background.setInteractive({ useHandCursor: true });
|
||||
background.on('pointerover', () => {
|
||||
background.setFillStyle(0xd8b15f, 0.22);
|
||||
background.setStrokeStyle(ui(1), palette.gold, 0.72);
|
||||
soundDirector.playHover();
|
||||
});
|
||||
background.on('pointerout', () => {
|
||||
background.setFillStyle(0x1b2530, 0.9);
|
||||
background.setStrokeStyle(ui(1), palette.gold, 0.42);
|
||||
});
|
||||
background.on('pointerover', () => this.focusItem('save-slots', `slot-${slot.slot}`, true));
|
||||
background.on('pointerdown', () => {
|
||||
this.focusItem('save-slots', `slot-${slot.slot}`);
|
||||
this.activateSaveSlot(slot.slot);
|
||||
});
|
||||
}
|
||||
@@ -722,54 +959,156 @@ export class TitleScene extends Phaser.Scene {
|
||||
return row;
|
||||
}
|
||||
|
||||
private handleSaveSlotKey(event: KeyboardEvent) {
|
||||
if (!this.saveSlotPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slot = Number(event.key);
|
||||
if (!Number.isInteger(slot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.activateSaveSlot(slot);
|
||||
}
|
||||
|
||||
private activateSaveSlot(slot: number) {
|
||||
if (!listCampaignSaveSlots().some((summary) => summary.slot === slot && summary.exists)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeSaveSlotPanel();
|
||||
this.closeSaveSlotPanel(false);
|
||||
this.continueGame(slot);
|
||||
}
|
||||
|
||||
private closeSaveSlotPanel() {
|
||||
private closeSaveSlotPanel(restoreFocus = true) {
|
||||
this.clearFocusScope('save-slots');
|
||||
this.saveSlotPanel?.destroy();
|
||||
this.saveSlotPanel = undefined;
|
||||
if (restoreFocus) {
|
||||
this.setFocusScope('main', 'continue');
|
||||
}
|
||||
}
|
||||
|
||||
private handleEnterKey(event?: KeyboardEvent) {
|
||||
if (event?.repeat || this.navigating) {
|
||||
private registerFocusItem(scope: TitleFocusScope, item: TitleFocusItem) {
|
||||
const items = this.focusItems.get(scope) ?? [];
|
||||
const duplicateIndex = items.findIndex((candidate) => candidate.id === item.id);
|
||||
if (duplicateIndex >= 0) {
|
||||
items[duplicateIndex] = item;
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
this.focusItems.set(scope, items);
|
||||
}
|
||||
|
||||
private setFocusScope(scope: TitleFocusScope, preferredId?: string) {
|
||||
this.currentFocusItem()?.render(false);
|
||||
this.focusScope = scope;
|
||||
|
||||
const enabledItems = (this.focusItems.get(scope) ?? []).filter((item) => item.enabled);
|
||||
const nextItem = enabledItems.find((item) => item.id === preferredId) ?? enabledItems[0];
|
||||
this.focusedItemId = nextItem?.id;
|
||||
nextItem?.render(true);
|
||||
}
|
||||
|
||||
private clearFocusScope(scope: TitleFocusScope) {
|
||||
const items = this.focusItems.get(scope) ?? [];
|
||||
items.forEach((item) => item.render(false));
|
||||
this.focusItems.delete(scope);
|
||||
if (this.focusScope === scope) {
|
||||
this.focusedItemId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private focusItem(scope: TitleFocusScope, id: string, playHover = false) {
|
||||
const modalScope = this.activeModalScope();
|
||||
if (modalScope && scope !== modalScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.saveSlotPanel) {
|
||||
const nextItem = (this.focusItems.get(scope) ?? []).find((item) => item.id === id && item.enabled);
|
||||
if (!nextItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newGameConfirmPanel) {
|
||||
this.closeNewGameConfirmPanel();
|
||||
this.startGame();
|
||||
return;
|
||||
const changed = this.focusScope !== scope || this.focusedItemId !== id;
|
||||
this.currentFocusItem()?.render(false);
|
||||
this.focusScope = scope;
|
||||
this.focusedItemId = id;
|
||||
nextItem.render(true);
|
||||
if (changed && playHover) {
|
||||
soundDirector.playHover();
|
||||
}
|
||||
}
|
||||
|
||||
private activeModalScope(): Exclude<TitleFocusScope, 'main'> | undefined {
|
||||
if (this.settingsPanel) {
|
||||
return 'settings';
|
||||
}
|
||||
if (this.saveSlotPanel) {
|
||||
return 'save-slots';
|
||||
}
|
||||
if (this.newGameConfirmPanel) {
|
||||
return 'new-game-confirm';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private moveFocus(direction: 1 | -1) {
|
||||
const enabledItems = (this.focusItems.get(this.focusScope) ?? []).filter((item) => item.enabled);
|
||||
if (enabledItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateDefaultMenuAction();
|
||||
const currentIndex = enabledItems.findIndex((item) => item.id === this.focusedItemId);
|
||||
const nextIndex = currentIndex < 0
|
||||
? 0
|
||||
: (currentIndex + direction + enabledItems.length) % enabledItems.length;
|
||||
this.focusItem(this.focusScope, enabledItems[nextIndex].id, true);
|
||||
}
|
||||
|
||||
private currentFocusItem() {
|
||||
return (this.focusItems.get(this.focusScope) ?? []).find((item) => item.id === this.focusedItemId);
|
||||
}
|
||||
|
||||
private activateFocusedItem() {
|
||||
const item = this.currentFocusItem();
|
||||
if (!item?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
soundDirector.start();
|
||||
soundDirector.resume();
|
||||
item.activate();
|
||||
}
|
||||
|
||||
private handleKeyboardInput(event: KeyboardEvent) {
|
||||
if (this.handledKeyboardEvents.has(event)) {
|
||||
return;
|
||||
}
|
||||
this.handledKeyboardEvents.add(event);
|
||||
|
||||
if (event.repeat || this.navigating) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.handleEscapeKey();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.saveSlotPanel && /^[1-3]$/.test(event.key)) {
|
||||
event.preventDefault();
|
||||
const slot = Number(event.key);
|
||||
this.focusItem('save-slots', `slot-${slot}`);
|
||||
this.activateSaveSlot(slot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' || (event.key === 'Tab' && event.shiftKey)) {
|
||||
event.preventDefault();
|
||||
this.moveFocus(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
this.moveFocus(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.code === 'Space') {
|
||||
event.preventDefault();
|
||||
this.activateFocusedItem();
|
||||
}
|
||||
}
|
||||
|
||||
private handleEscapeKey() {
|
||||
@@ -811,12 +1150,14 @@ export class TitleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
soundDirector.playSelect();
|
||||
this.closeSettingsPanel();
|
||||
this.closeSaveSlotPanel();
|
||||
this.closeSettingsPanel(false);
|
||||
this.closeSaveSlotPanel(false);
|
||||
this.clearFocusScope('new-game-confirm');
|
||||
|
||||
const panel = this.add.container(x, y + ui(240));
|
||||
const backdrop = this.add.rectangle(0, 0, ui(286), ui(198), 0x0e151d, 0.94);
|
||||
backdrop.setStrokeStyle(ui(1), palette.gold, 0.58);
|
||||
backdrop.setInteractive();
|
||||
|
||||
const title = this.add.text(0, -ui(72), '새로 시작', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
@@ -838,7 +1179,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
message.setOrigin(0.5);
|
||||
|
||||
const hint = this.add.text(0, ui(13), 'Enter 새 게임 · Esc 취소', {
|
||||
const hint = this.add.text(0, ui(13), '↑↓ 선택 · Enter/Space 결정 · Esc 취소', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: uiPx(11),
|
||||
color: '#9fb0bf',
|
||||
@@ -848,37 +1189,100 @@ export class TitleScene extends Phaser.Scene {
|
||||
});
|
||||
hint.setOrigin(0.5);
|
||||
|
||||
const confirm = this.createSettingsButton(0, ui(48), '새 게임 시작');
|
||||
confirm.setColor('#f1e3c2');
|
||||
confirm.on('pointerdown', () => {
|
||||
this.closeNewGameConfirmPanel();
|
||||
this.startGame();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'new-game-confirm',
|
||||
'confirm',
|
||||
0,
|
||||
ui(48),
|
||||
'새 게임 시작',
|
||||
() => {
|
||||
this.closeNewGameConfirmPanel(false);
|
||||
this.startGame();
|
||||
},
|
||||
'#f1e3c2'
|
||||
);
|
||||
|
||||
const cancel = this.createSettingsButton(0, ui(88), '취소');
|
||||
cancel.on('pointerdown', () => {
|
||||
soundDirector.playSelect();
|
||||
this.closeNewGameConfirmPanel();
|
||||
});
|
||||
this.createSettingsButton(
|
||||
panel,
|
||||
'new-game-confirm',
|
||||
'cancel',
|
||||
0,
|
||||
ui(88),
|
||||
'취소',
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.closeNewGameConfirmPanel();
|
||||
}
|
||||
);
|
||||
|
||||
panel.add([backdrop, title, message, hint, confirm, cancel]);
|
||||
panel.addAt([backdrop, title, message, hint], 0);
|
||||
panel.setAlpha(0);
|
||||
this.newGameConfirmPanel = panel;
|
||||
this.tweens.add({ targets: panel, alpha: 1, y: y + ui(230), duration: 160, ease: 'Sine.easeOut' });
|
||||
this.tweens.add({
|
||||
targets: panel,
|
||||
alpha: 1,
|
||||
y: y + ui(230),
|
||||
duration: this.visualMotionReduced ? 0 : 160,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
this.setFocusScope('new-game-confirm', 'confirm');
|
||||
}
|
||||
|
||||
private closeNewGameConfirmPanel() {
|
||||
private closeNewGameConfirmPanel(restoreFocus = true) {
|
||||
this.clearFocusScope('new-game-confirm');
|
||||
this.newGameConfirmPanel?.destroy();
|
||||
this.newGameConfirmPanel = undefined;
|
||||
if (restoreFocus) {
|
||||
this.setFocusScope('main', 'new-game');
|
||||
}
|
||||
}
|
||||
|
||||
private activateDefaultMenuAction() {
|
||||
if (hasCampaignSave()) {
|
||||
this.requestContinueGame();
|
||||
return;
|
||||
}
|
||||
getDebugState() {
|
||||
const items = (this.focusItems.get(this.focusScope) ?? []).map((item) => {
|
||||
const bounds = item.bounds();
|
||||
return {
|
||||
id: item.id,
|
||||
enabled: item.enabled,
|
||||
focused: item.id === this.focusedItemId,
|
||||
focusIndicatorVisible: item.marker.visible,
|
||||
bounds: {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.startGame();
|
||||
return {
|
||||
scene: 'TitleScene',
|
||||
navigating: this.navigating,
|
||||
focus: {
|
||||
scope: this.focusScope,
|
||||
itemId: this.focusedItemId,
|
||||
items
|
||||
},
|
||||
panels: {
|
||||
settings: Boolean(this.settingsPanel),
|
||||
saveSlots: Boolean(this.saveSlotPanel),
|
||||
newGameConfirm: Boolean(this.newGameConfirmPanel)
|
||||
},
|
||||
motion: {
|
||||
mode: loadVisualMotionMode(),
|
||||
reduced: this.visualMotionReduced,
|
||||
activeInfiniteTweenCount: this.titleMotionTweens.length,
|
||||
background: this.titleBackground
|
||||
? {
|
||||
x: this.titleBackground.x,
|
||||
y: this.titleBackground.y,
|
||||
scale: this.titleBackground.scale
|
||||
}
|
||||
: undefined,
|
||||
mistAlpha: this.titleMist?.alpha,
|
||||
petalCount: this.titlePetals.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private startGame() {
|
||||
|
||||
@@ -21,6 +21,10 @@ type DebugBattleScene = Phaser.Scene & {
|
||||
debugForceBattleOutcome?: (outcome: 'victory' | 'defeat') => void;
|
||||
};
|
||||
|
||||
type DebugTitleScene = Phaser.Scene & {
|
||||
getDebugState?: () => unknown;
|
||||
};
|
||||
|
||||
type DebugCampScene = Phaser.Scene & {
|
||||
getDebugState?: () => unknown;
|
||||
};
|
||||
@@ -55,6 +59,7 @@ type HerosDebugApi = {
|
||||
game: Phaser.Game;
|
||||
activeScenes: () => string[];
|
||||
audio: () => ReturnType<typeof soundDirector.getDebugState>;
|
||||
title: () => unknown;
|
||||
battle: () => unknown;
|
||||
camp: () => unknown;
|
||||
village: () => unknown;
|
||||
@@ -115,6 +120,7 @@ if (debugEnabled) {
|
||||
|
||||
function createDebugApi(game: Phaser.Game): HerosDebugApi {
|
||||
const scene = (key: string) => game.scene.getScene(key);
|
||||
const titleScene = () => scene('TitleScene') as DebugTitleScene | undefined;
|
||||
const battleScene = () => scene('BattleScene') as DebugBattleScene | undefined;
|
||||
const campScene = () => scene('CampScene') as DebugCampScene | undefined;
|
||||
const villageScene = () => scene('PrologueVillageScene') as DebugVillageScene | undefined;
|
||||
@@ -127,6 +133,9 @@ function createDebugApi(game: Phaser.Game): HerosDebugApi {
|
||||
game,
|
||||
activeScenes: () => game.scene.getScenes(true).map((activeScene) => activeScene.scene.key),
|
||||
audio: () => soundDirector.getDebugState(),
|
||||
title: () => game.scene.isActive('TitleScene')
|
||||
? titleScene()?.getDebugState?.() ?? { active: false, reason: 'TitleScene debug state is unavailable.' }
|
||||
: { active: false, reason: 'TitleScene is not active yet.' },
|
||||
battle: () => game.scene.isActive('BattleScene')
|
||||
? battleScene()?.getDebugState?.() ?? { active: false, reason: 'BattleScene debug state is unavailable.' }
|
||||
: { active: false, reason: 'BattleScene is not active yet.' },
|
||||
|
||||
Reference in New Issue
Block a user