fix: refine turn completion and interaction flow
This commit is contained in:
@@ -35,13 +35,14 @@ try {
|
|||||||
validateNumericProperties('gold', { min: 1, max: 20000 });
|
validateNumericProperties('gold', { min: 1, max: 20000 });
|
||||||
validateNumericProperties('bondExp', { min: 0, max: 200 });
|
validateNumericProperties('bondExp', { min: 0, max: 200 });
|
||||||
validateNumericProperties('rewardExp', { min: 1, max: 200 });
|
validateNumericProperties('rewardExp', { min: 1, max: 200 });
|
||||||
|
const interactionUxGuardCount = validateCampInteractionUxGuards();
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
|
throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${rewardBondLinks} reward-bond links, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.`
|
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${rewardBondLinks} reward-bond links, ${itemRewardArrays.length} camp item reward lists, camp reward numeric fields, and ${interactionUxGuardCount} camp interaction UX guards.`
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await server.close();
|
await server.close();
|
||||||
@@ -87,6 +88,125 @@ function validateNumericProperties(propertyName, { min, max }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCampInteractionUxGuards() {
|
||||||
|
const visitRewardMethod = extractPrivateMethod(source, 'showVisitReward');
|
||||||
|
const dialogueRewardMethod = extractPrivateMethod(source, 'showDialogueReward');
|
||||||
|
const noticeMethod = extractPrivateMethod(source, 'showCampNotice');
|
||||||
|
const noticeDurationMethod = extractPrivateMethod(source, 'campNoticeHoldDuration');
|
||||||
|
const enterKeyMethod = extractPrivateMethod(source, 'handleSortieEnterKey');
|
||||||
|
const navigationMethod = extractPrivateMethod(source, 'startCampNavigation');
|
||||||
|
const steppedNavigationMethod = extractPrivateMethod(source, 'startCampNavigationWithCampaignStep');
|
||||||
|
const startStoryMethod = extractPrivateMethod(source, 'startVictoryStory');
|
||||||
|
const saveMethod = extractPrivateMethod(source, 'saveCampToSlot');
|
||||||
|
let checkedGuardCount = 0;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
visitRewardMethod.includes('획득 내역') &&
|
||||||
|
visitRewardMethod.includes('${this.visitRewardText(choice)}') &&
|
||||||
|
visitRewardMethod.includes('응답 · ${choice.response}') &&
|
||||||
|
!visitRewardMethod.includes('delayedCall'),
|
||||||
|
'visit rewards must display the acquisition details and response together without delayed replacement'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
dialogueRewardMethod.includes('획득 내역') &&
|
||||||
|
dialogueRewardMethod.includes('공명 +${rewardExp}') &&
|
||||||
|
dialogueRewardMethod.includes('응답 · ${choice.response}') &&
|
||||||
|
!dialogueRewardMethod.includes('delayedCall'),
|
||||||
|
'dialogue rewards must display the acquisition details and response together without delayed replacement'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
noticeMethod.includes('delay: this.campNoticeHoldDuration(message)') &&
|
||||||
|
noticeMethod.includes('text.height + 28'),
|
||||||
|
'camp notices must size multi-line content and use a message-aware hold duration'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
const minimumHoldMs = Number(/const campNoticeMinimumHoldMs = (\d+);/.exec(source)?.[1] ?? 0);
|
||||||
|
expectCampUx(
|
||||||
|
minimumHoldMs >= 2500 &&
|
||||||
|
noticeDurationMethod.includes('readableCharacterCount * campNoticeCharacterHoldMs') &&
|
||||||
|
noticeDurationMethod.includes('campNoticeMaximumHoldMs'),
|
||||||
|
`camp notice timing must retain a readable minimum and scale with body length (found ${minimumHoldMs}ms)`
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
source.includes('private navigationPending = false;') &&
|
||||||
|
enterKeyMethod.includes('event.repeat || this.navigationPending'),
|
||||||
|
'sortie Enter handling must ignore key-repeat events and pending navigation'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
navigationMethod.includes('if (this.navigationPending)') &&
|
||||||
|
navigationMethod.includes('this.navigationPending = true;') &&
|
||||||
|
navigationMethod.includes('startLazyScene(this, key, data)') &&
|
||||||
|
navigationMethod.includes('this.navigationPending = false;'),
|
||||||
|
'lazy camp navigation must lock before loading and unlock after a failed transition'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
startStoryMethod.includes('if (this.navigationPending)') &&
|
||||||
|
!startStoryMethod.includes('startLazyScene(') &&
|
||||||
|
((startStoryMethod.match(/this\.startCampNavigation\(/g)?.length ?? 0) +
|
||||||
|
(startStoryMethod.match(/this\.startCampNavigationWithCampaignStep\(/g)?.length ?? 0)) >= 4,
|
||||||
|
'every camp story, battle, and ending transition must route through the scene navigation lock'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
expectCampUx(
|
||||||
|
steppedNavigationMethod.includes('const previousStep = getCampaignState().step') &&
|
||||||
|
steppedNavigationMethod.includes('markCampaignStep(step)') &&
|
||||||
|
steppedNavigationMethod.includes('getCampaignState().step === step') &&
|
||||||
|
steppedNavigationMethod.includes('markCampaignStep(previousStep)') &&
|
||||||
|
navigationMethod.includes('recoverState?.()'),
|
||||||
|
'campaign-step navigation must restore the previous step when lazy scene loading fails'
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
const transientUndoFields = [
|
||||||
|
'sortieSwapUndoState',
|
||||||
|
'sortieRecommendationUndoState',
|
||||||
|
'sortiePresetUndoState',
|
||||||
|
'sortieGuidedImprovementUndoState',
|
||||||
|
'reportFormationHistoryUndoState'
|
||||||
|
];
|
||||||
|
const clearedUndoField = transientUndoFields.find((field) => new RegExp(`this\\.${field}\\s*=\\s*undefined`).test(saveMethod));
|
||||||
|
expectCampUx(
|
||||||
|
!clearedUndoField && !saveMethod.includes('persistSortieSelection('),
|
||||||
|
`manual camp saves must preserve applicable undo state${clearedUndoField ? ` (cleared ${clearedUndoField})` : ''}`
|
||||||
|
);
|
||||||
|
checkedGuardCount += 1;
|
||||||
|
|
||||||
|
return checkedGuardCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectCampUx(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
errors.push(`${sourcePath}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPrivateMethod(text, methodName) {
|
||||||
|
const match = new RegExp(`\\bprivate\\s+${methodName}\\s*\\(`).exec(text);
|
||||||
|
if (!match) {
|
||||||
|
errors.push(`${sourcePath}: cannot find private method ${methodName}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const bodyStart = text.indexOf('{', match.index + match[0].length);
|
||||||
|
if (bodyStart < 0) {
|
||||||
|
errors.push(`${sourcePath}:${lineNumber(match.index)} cannot find body for private method ${methodName}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const bodyEnd = findMatchingDelimiter(text, bodyStart, '{', '}');
|
||||||
|
return text.slice(bodyStart, bodyEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
function collectCampBattleIdEntries(text, defaultBattleScenarioId) {
|
function collectCampBattleIdEntries(text, defaultBattleScenarioId) {
|
||||||
const objectSource = extractConstObject(text, 'campBattleIds');
|
const objectSource = extractConstObject(text, 'campBattleIds');
|
||||||
const objectOffset = text.indexOf(objectSource);
|
const objectOffset = text.indexOf(objectSource);
|
||||||
|
|||||||
@@ -810,13 +810,13 @@ try {
|
|||||||
throw new Error(`Expected positional bond bonuses and reduced counter damage: ${JSON.stringify(combatMechanicsProbe)}`);
|
throw new Error(`Expected positional bond bonuses and reduced counter damage: ${JSON.stringify(combatMechanicsProbe)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveWaitTurnPromptStates = await page.evaluate(() => {
|
const finalAllyActionPromptStates = await page.evaluate(async () => {
|
||||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
const state = window.__HEROS_DEBUG__?.battle();
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
const lastAlly = state?.units?.find((unit) => unit.id === 'zhang-fei');
|
const lastAlly = state?.units?.find((unit) => unit.id === 'zhang-fei');
|
||||||
|
|
||||||
if (!scene || !state || !lastAlly) {
|
if (!scene || !state || !lastAlly) {
|
||||||
throw new Error('Expected Zhang Fei and battle scene for move-wait turn prompt test.');
|
throw new Error('Expected Zhang Fei and battle scene for final ally action prompt test.');
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.hideTurnEndPrompt();
|
scene.hideTurnEndPrompt();
|
||||||
@@ -829,31 +829,79 @@ try {
|
|||||||
|
|
||||||
const liveLastAlly = scene.debugUnitById(lastAlly.id);
|
const liveLastAlly = scene.debugUnitById(lastAlly.id);
|
||||||
if (!liveLastAlly) {
|
if (!liveLastAlly) {
|
||||||
throw new Error('Expected live Zhang Fei unit for move-wait turn prompt test.');
|
throw new Error('Expected live Zhang Fei unit for final ally action prompt test.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalPosition = { x: liveLastAlly.x, y: liveLastAlly.y };
|
||||||
|
const destination = scene.reachableTiles(liveLastAlly).find((tile) => tile.x !== liveLastAlly.x || tile.y !== liveLastAlly.y);
|
||||||
|
if (!destination) {
|
||||||
|
throw new Error('Expected a non-origin reachable tile for final ally movement timing test.');
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.selectUnit(liveLastAlly);
|
scene.selectUnit(liveLastAlly);
|
||||||
scene.moveSelectedUnit(liveLastAlly.x, liveLastAlly.y);
|
const movement = scene.moveSelectedUnit(destination.x, destination.y);
|
||||||
|
const duringMoveState = window.__HEROS_DEBUG__?.battle();
|
||||||
|
await movement;
|
||||||
const postMoveState = window.__HEROS_DEBUG__?.battle();
|
const postMoveState = window.__HEROS_DEBUG__?.battle();
|
||||||
scene.completeUnitAction(liveLastAlly, 'wait');
|
scene.completeUnitAction(liveLastAlly, 'wait');
|
||||||
const postWaitState = window.__HEROS_DEBUG__?.battle();
|
const postWaitState = window.__HEROS_DEBUG__?.battle();
|
||||||
return { postMoveState, postWaitState };
|
scene.input.keyboard?.emit('keydown-ENTER', { repeat: true });
|
||||||
|
const postRepeatedEnterState = window.__HEROS_DEBUG__?.battle();
|
||||||
|
const modalBlockerActive = scene.turnPromptObjects.some((object) => object.depth === 39 && object.input?.enabled === true);
|
||||||
|
const otherAlly = state.units.find((unit) => unit.faction === 'ally' && unit.hp > 0 && unit.id !== liveLastAlly.id);
|
||||||
|
const liveOtherAlly = otherAlly ? scene.debugUnitById(otherAlly.id) : undefined;
|
||||||
|
if (liveOtherAlly) {
|
||||||
|
scene.selectUnit(liveOtherAlly);
|
||||||
|
}
|
||||||
|
const postBlockedSelectionState = window.__HEROS_DEBUG__?.battle();
|
||||||
|
liveLastAlly.x = originalPosition.x;
|
||||||
|
liveLastAlly.y = originalPosition.y;
|
||||||
|
scene.positionUnitView(liveLastAlly);
|
||||||
|
return {
|
||||||
|
duringMoveState,
|
||||||
|
postMoveState,
|
||||||
|
postWaitState,
|
||||||
|
postRepeatedEnterState,
|
||||||
|
postBlockedSelectionState,
|
||||||
|
modalBlockerActive
|
||||||
|
};
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
!moveWaitTurnPromptStates?.postMoveState?.turnPromptVisible ||
|
finalAllyActionPromptStates?.duringMoveState?.phase !== 'animating' ||
|
||||||
moveWaitTurnPromptStates.postMoveState.turnPromptMode !== 'post-move' ||
|
finalAllyActionPromptStates.duringMoveState.commandMenuVisible ||
|
||||||
moveWaitTurnPromptStates.postMoveState.phase !== 'command' ||
|
finalAllyActionPromptStates.duringMoveState.turnPromptVisible
|
||||||
moveWaitTurnPromptStates.postMoveState.pendingMove?.unitId !== 'zhang-fei' ||
|
|
||||||
moveWaitTurnPromptStates.postMoveState.actedUnitIds?.includes('zhang-fei')
|
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected final ally move to show post-move turn prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`);
|
throw new Error(`Expected movement animation to lock command input until arrival: ${JSON.stringify(finalAllyActionPromptStates)}`);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!moveWaitTurnPromptStates.postWaitState?.turnPromptVisible ||
|
finalAllyActionPromptStates?.postMoveState?.turnPromptVisible ||
|
||||||
moveWaitTurnPromptStates.postWaitState.turnPromptMode !== 'turn-end' ||
|
finalAllyActionPromptStates.postMoveState.turnPromptMode !== null ||
|
||||||
moveWaitTurnPromptStates.postWaitState.phase !== 'idle'
|
!finalAllyActionPromptStates.postMoveState.commandMenuVisible ||
|
||||||
|
finalAllyActionPromptStates.postMoveState.phase !== 'command' ||
|
||||||
|
finalAllyActionPromptStates.postMoveState.pendingMove?.unitId !== 'zhang-fei' ||
|
||||||
|
finalAllyActionPromptStates.postMoveState.actedUnitIds?.includes('zhang-fei')
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected move-wait completion to show turn-end prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`);
|
throw new Error(`Expected final ally move to keep command selection open without a turn prompt: ${JSON.stringify(finalAllyActionPromptStates)}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!finalAllyActionPromptStates.postWaitState?.turnPromptVisible ||
|
||||||
|
finalAllyActionPromptStates.postWaitState.turnPromptMode !== 'turn-end' ||
|
||||||
|
finalAllyActionPromptStates.postWaitState.commandMenuVisible ||
|
||||||
|
finalAllyActionPromptStates.postWaitState.phase !== 'idle' ||
|
||||||
|
!finalAllyActionPromptStates.postWaitState.actedUnitIds?.includes('zhang-fei')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected final ally action completion to show the turn-end prompt: ${JSON.stringify(finalAllyActionPromptStates)}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!finalAllyActionPromptStates.postRepeatedEnterState?.turnPromptVisible ||
|
||||||
|
finalAllyActionPromptStates.postRepeatedEnterState.turnPromptMode !== 'turn-end' ||
|
||||||
|
finalAllyActionPromptStates.postRepeatedEnterState.activeFaction !== 'ally' ||
|
||||||
|
!finalAllyActionPromptStates.modalBlockerActive ||
|
||||||
|
!finalAllyActionPromptStates.postBlockedSelectionState?.turnPromptVisible ||
|
||||||
|
finalAllyActionPromptStates.postBlockedSelectionState.turnPromptMode !== 'turn-end' ||
|
||||||
|
finalAllyActionPromptStates.postBlockedSelectionState.selectedUnitId
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected repeated Enter input and underlying battlefield selection to leave the turn-end prompt open: ${JSON.stringify(finalAllyActionPromptStates)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
@@ -1028,6 +1076,36 @@ try {
|
|||||||
throw new Error(`Expected leader objective to resolve after victory: ${JSON.stringify(victoryState.objectives)}`);
|
throw new Error(`Expected leader objective to resolve after victory: ${JSON.stringify(victoryState.objectives)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resultDoubleActivationState = await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
if (!scene) {
|
||||||
|
throw new Error('Expected BattleScene for result double-activation guard test.');
|
||||||
|
}
|
||||||
|
scene.resultSettlementStatus = 'pending';
|
||||||
|
scene.refreshResultContinueCta();
|
||||||
|
scene.handleResultContinue();
|
||||||
|
scene.handleResultContinue();
|
||||||
|
return {
|
||||||
|
activeScenes: window.__HEROS_DEBUG__?.activeScenes?.() ?? [],
|
||||||
|
battle: window.__HEROS_DEBUG__?.battle()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const guardedContinueAction = resultDoubleActivationState.battle?.resultActions?.find((action) => action.kind === 'continue');
|
||||||
|
if (
|
||||||
|
resultDoubleActivationState.battle?.resultSettlement?.status !== 'complete' ||
|
||||||
|
resultDoubleActivationState.battle?.resultVisible !== true ||
|
||||||
|
!resultDoubleActivationState.activeScenes.includes('BattleScene') ||
|
||||||
|
resultDoubleActivationState.activeScenes.includes('StoryScene') ||
|
||||||
|
resultDoubleActivationState.activeScenes.includes('CampScene') ||
|
||||||
|
guardedContinueAction?.interactive !== false
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected rapid result activation to finish settlement without leaving the result screen: ${JSON.stringify(resultDoubleActivationState)}`);
|
||||||
|
}
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const continueAction = window.__HEROS_DEBUG__?.battle()?.resultActions?.find((action) => action.kind === 'continue');
|
||||||
|
return continueAction?.interactive === true;
|
||||||
|
});
|
||||||
|
|
||||||
await page.screenshot({ path: 'dist/verification-battle-result-victory.png', fullPage: true });
|
await page.screenshot({ path: 'dist/verification-battle-result-victory.png', fullPage: true });
|
||||||
|
|
||||||
await page.mouse.click(738, 642);
|
await page.mouse.click(738, 642);
|
||||||
|
|||||||
@@ -1398,7 +1398,7 @@ type MapMenuAction = 'endTurn' | 'threat' | 'save' | 'load' | 'roster' | 'bond'
|
|||||||
type EnemyAiBehavior = 'aggressive' | 'guard' | 'hold';
|
type EnemyAiBehavior = 'aggressive' | 'guard' | 'hold';
|
||||||
type BattleOutcome = 'victory' | 'defeat';
|
type BattleOutcome = 'victory' | 'defeat';
|
||||||
type SaveSlotMode = 'save' | 'load';
|
type SaveSlotMode = 'save' | 'load';
|
||||||
type TurnPromptMode = 'turn-end' | 'post-move' | 'load-confirm' | 'save-overwrite-confirm';
|
type TurnPromptMode = 'turn-end' | 'load-confirm' | 'save-overwrite-confirm';
|
||||||
|
|
||||||
const sideQuickTabDefinitions: ReadonlyArray<{
|
const sideQuickTabDefinitions: ReadonlyArray<{
|
||||||
id: SideQuickTabId;
|
id: SideQuickTabId;
|
||||||
@@ -3531,6 +3531,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private scenarioCombatAssetSettlementCount = 0;
|
private scenarioCombatAssetSettlementCount = 0;
|
||||||
private resumeBattleSaveSlot?: number;
|
private resumeBattleSaveSlot?: number;
|
||||||
private pendingDeploymentConfirmation = false;
|
private pendingDeploymentConfirmation = false;
|
||||||
|
private pendingDeploymentSignature?: string;
|
||||||
private turnNumber = 1;
|
private turnNumber = 1;
|
||||||
private activeFaction: ActiveFaction = 'ally';
|
private activeFaction: ActiveFaction = 'ally';
|
||||||
private selectedUnit?: UnitData;
|
private selectedUnit?: UnitData;
|
||||||
@@ -3638,6 +3639,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private resultSettlementTweens: Phaser.Tweens.Tween[] = [];
|
private resultSettlementTweens: Phaser.Tweens.Tween[] = [];
|
||||||
private resultSettlementTransientObjects: Phaser.GameObjects.GameObject[] = [];
|
private resultSettlementTransientObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private resultContinueButton?: ResultButtonView;
|
private resultContinueButton?: ResultButtonView;
|
||||||
|
private resultContinueLockedUntil = 0;
|
||||||
|
private resultNavigationPending = false;
|
||||||
private resultCoreResonanceSummaryText?: Phaser.GameObjects.Text;
|
private resultCoreResonanceSummaryText?: Phaser.GameObjects.Text;
|
||||||
private resultFormationReviewObjects: Phaser.GameObjects.GameObject[] = [];
|
private resultFormationReviewObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private resultFormationReviewVisible = false;
|
private resultFormationReviewVisible = false;
|
||||||
@@ -3735,6 +3738,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.scenarioCombatAssetLoadCompletedAt = undefined;
|
this.scenarioCombatAssetLoadCompletedAt = undefined;
|
||||||
this.scenarioCombatAssetSettlementCount = 0;
|
this.scenarioCombatAssetSettlementCount = 0;
|
||||||
this.pendingDeploymentConfirmation = false;
|
this.pendingDeploymentConfirmation = false;
|
||||||
|
this.pendingDeploymentSignature = undefined;
|
||||||
this.rosterTab = 'ally';
|
this.rosterTab = 'ally';
|
||||||
this.rosterPage = 0;
|
this.rosterPage = 0;
|
||||||
this.bondPage = 0;
|
this.bondPage = 0;
|
||||||
@@ -3934,7 +3938,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.keyboardBattleCursor = undefined;
|
this.keyboardBattleCursor = undefined;
|
||||||
this.updatePointerFeedback(pointer);
|
this.updatePointerFeedback(pointer);
|
||||||
});
|
});
|
||||||
this.input.keyboard?.on('keydown-ENTER', () => {
|
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => {
|
||||||
|
if (event.repeat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.tacticalCommandCutInObjects.length > 0) {
|
if (this.tacticalCommandCutInObjects.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4223,8 +4230,15 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.scenarioCombatAssetSettlementCount += 1;
|
this.scenarioCombatAssetSettlementCount += 1;
|
||||||
this.flushScenarioCombatAssetCallbacks();
|
this.flushScenarioCombatAssetCallbacks();
|
||||||
if (this.pendingDeploymentConfirmation && this.phase === 'deployment') {
|
if (this.pendingDeploymentConfirmation && this.phase === 'deployment') {
|
||||||
|
const deploymentUnchanged = this.pendingDeploymentSignature === this.deploymentSignature();
|
||||||
this.pendingDeploymentConfirmation = false;
|
this.pendingDeploymentConfirmation = false;
|
||||||
this.confirmPreBattleDeployment();
|
this.pendingDeploymentSignature = undefined;
|
||||||
|
if (deploymentUnchanged) {
|
||||||
|
this.confirmPreBattleDeployment();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deploymentNotice = '준비 중 배치가 바뀌었습니다. 현재 배치를 확인한 뒤 전투 시작을 다시 눌러주세요.';
|
||||||
|
this.renderDeploymentPanel();
|
||||||
}
|
}
|
||||||
}, watchdogMs, generation);
|
}, watchdogMs, generation);
|
||||||
}, { watchdogMs });
|
}, { watchdogMs });
|
||||||
@@ -7307,7 +7321,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cursor.mode === 'move' && this.phase === 'moving') {
|
if (cursor.mode === 'move' && this.phase === 'moving') {
|
||||||
this.moveSelectedUnit(cursor.x, cursor.y);
|
void this.moveSelectedUnit(cursor.x, cursor.y);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.phase !== 'targeting' || !cursor.targetId || !this.targetingAction) {
|
if (this.phase !== 'targeting' || !cursor.targetId || !this.targetingAction) {
|
||||||
@@ -7343,6 +7357,10 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.turnPromptMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tutorialPath = this.firstBattleTutorialPath;
|
const tutorialPath = this.firstBattleTutorialPath;
|
||||||
const tutorialStep = this.firstBattleTutorialStep;
|
const tutorialStep = this.firstBattleTutorialStep;
|
||||||
const completingTutorialSelect = Boolean(
|
const completingTutorialSelect = Boolean(
|
||||||
@@ -7385,7 +7403,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.phase === 'moving' && this.selectedUnit?.id === unit.id) {
|
if (this.phase === 'moving' && this.selectedUnit?.id === unit.id) {
|
||||||
this.moveSelectedUnit(unit.x, unit.y);
|
void this.moveSelectedUnit(unit.x, unit.y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7494,14 +7512,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
marker.on('pointerout', () => marker.setFillStyle(palette.blue, fillAlpha));
|
marker.on('pointerout', () => marker.setFillStyle(palette.blue, fillAlpha));
|
||||||
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||||
if (pointer.leftButtonDown()) {
|
if (pointer.leftButtonDown()) {
|
||||||
this.moveSelectedUnit(x, y, pointer);
|
void this.moveSelectedUnit(x, y, pointer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.markers.push(marker);
|
this.markers.push(marker);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private moveSelectedUnit(x: number, y: number, pointer?: Phaser.Input.Pointer) {
|
private async moveSelectedUnit(x: number, y: number, pointer?: Phaser.Input.Pointer) {
|
||||||
if (this.isFirstBattleTutorialActive()) {
|
if (this.isFirstBattleTutorialActive()) {
|
||||||
const path = this.firstBattleTutorialPath!;
|
const path = this.firstBattleTutorialPath!;
|
||||||
if (this.firstBattleTutorialStep !== 'move') {
|
if (this.firstBattleTutorialStep !== 'move') {
|
||||||
@@ -7513,7 +7531,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.selectedUnit || !this.canMoveTo(this.selectedUnit, x, y)) {
|
if (this.phase !== 'moving' || !this.selectedUnit || !this.canMoveTo(this.selectedUnit, x, y)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7530,7 +7548,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const stayingInPlace = fromX === x && fromY === y;
|
const stayingInPlace = fromX === x && fromY === y;
|
||||||
unit.x = x;
|
unit.x = x;
|
||||||
unit.y = y;
|
unit.y = y;
|
||||||
this.phase = 'command';
|
this.phase = stayingInPlace ? 'command' : 'animating';
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
this.updateMiniMap();
|
this.updateMiniMap();
|
||||||
this.refreshEnemyIntentForecast();
|
this.refreshEnemyIntentForecast();
|
||||||
@@ -7550,18 +7568,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const targetX = this.tileCenterX(x);
|
const targetX = this.tileCenterX(x);
|
||||||
const targetY = this.tileCenterY(y);
|
const targetY = this.tileCenterY(y);
|
||||||
if (!stayingInPlace) {
|
if (!stayingInPlace) {
|
||||||
this.moveUnitView(unit, x, y, view, 260, fromX, fromY);
|
await this.moveUnitViewAsync(unit, x, y, view, 260, fromX, fromY);
|
||||||
|
if (this.battleOutcome || this.selectedUnit?.id !== unit.id || this.pendingMove?.unit.id !== unit.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.phase = 'command';
|
||||||
}
|
}
|
||||||
const commandX = pointer?.x ?? targetX;
|
const commandX = pointer?.x ?? targetX;
|
||||||
const commandY = pointer?.y ?? targetY;
|
const commandY = pointer?.y ?? targetY;
|
||||||
const isFinalAlly = this.remainingAllyCount() === 1;
|
|
||||||
|
|
||||||
if (isFinalAlly) {
|
|
||||||
this.hideCommandMenu();
|
|
||||||
this.renderUnitDetail(unit, '마지막 장수 · 명령 또는 턴 종료');
|
|
||||||
this.showPostMoveTurnEndPrompt(unit, commandX, commandY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showCommandMenu(unit, commandX, commandY);
|
this.showCommandMenu(unit, commandX, commandY);
|
||||||
this.renderUnitDetail(unit, '명령 선택 · 우클릭은 이동 취소');
|
this.renderUnitDetail(unit, '명령 선택 · 우클릭은 이동 취소');
|
||||||
@@ -7572,41 +7586,6 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private showPostMoveTurnEndPrompt(unit: UnitData, commandX = this.tileCenterX(unit.x), commandY = this.tileCenterY(unit.y)) {
|
|
||||||
this.hideCommandMenu();
|
|
||||||
this.showTurnEndPrompt(undefined, {
|
|
||||||
mode: 'post-move',
|
|
||||||
title: '마지막 장수 이동 완료',
|
|
||||||
body: this.pendingMove?.unit.id === unit.id
|
|
||||||
? `이동 결과 · ${this.moveIntentRiskCompactLine(this.pendingMove.intentRisk)}\n${this.enemyIntentTurnEndBody()}`
|
|
||||||
: this.enemyIntentTurnEndBody(),
|
|
||||||
primaryLabel: '대기 후 턴 종료',
|
|
||||||
secondaryLabel: '명령 선택',
|
|
||||||
primaryAction: () => {
|
|
||||||
if (this.phase !== 'command' || this.selectedUnit?.id !== unit.id || this.actedUnitIds.has(unit.id)) {
|
|
||||||
this.hideTurnEndPrompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.completeUnitAction(unit, 'wait');
|
|
||||||
if (this.activeFaction === 'ally' && !this.battleOutcome) {
|
|
||||||
this.endAllyTurn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryAction: () => {
|
|
||||||
if (this.phase !== 'command' || this.selectedUnit?.id !== unit.id || this.actedUnitIds.has(unit.id)) {
|
|
||||||
this.hideTurnEndPrompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.suppressNextLeftClick = true;
|
|
||||||
this.hideTurnEndPrompt();
|
|
||||||
this.showCommandMenu(unit, commandX, commandY);
|
|
||||||
this.renderUnitDetail(unit, '공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private canMoveTo(unit: UnitData, x: number, y: number) {
|
private canMoveTo(unit: UnitData, x: number, y: number) {
|
||||||
return this.reachableTiles(unit).some((tile) => tile.x === x && tile.y === y);
|
return this.reachableTiles(unit).some((tile) => tile.x === x && tile.y === y);
|
||||||
}
|
}
|
||||||
@@ -8386,7 +8365,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
roleBounds.push({ x: left, y: rowTop, width, height: roleRowHeight });
|
roleBounds.push({ x: left, y: rowTop, width, height: roleRowHeight });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderDeploymentButton(left, startButtonTop, width, '전투 시작', palette.gold, () => this.confirmPreBattleDeployment());
|
this.renderDeploymentButton(
|
||||||
|
left,
|
||||||
|
startButtonTop,
|
||||||
|
width,
|
||||||
|
this.pendingDeploymentConfirmation ? '전투 준비 중' : '전투 시작',
|
||||||
|
palette.gold,
|
||||||
|
() => this.confirmPreBattleDeployment()
|
||||||
|
);
|
||||||
this.renderDeploymentButton(
|
this.renderDeploymentButton(
|
||||||
left,
|
left,
|
||||||
restoreButtonTop,
|
restoreButtonTop,
|
||||||
@@ -8625,7 +8611,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
if (this.scenarioCombatAssetStatus !== 'ready' && this.scenarioCombatAssetStatus !== 'degraded') {
|
if (this.scenarioCombatAssetStatus !== 'ready' && this.scenarioCombatAssetStatus !== 'degraded') {
|
||||||
this.pendingDeploymentConfirmation = true;
|
this.pendingDeploymentConfirmation = true;
|
||||||
this.deploymentNotice = '\uC804\uD22C \uB3D9\uC791\uC744 \uC900\uBE44\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uC900\uBE44\uAC00 \uB05D\uB098\uBA74 \uC790\uB3D9\uC73C\uB85C \uC804\uD22C\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.';
|
this.pendingDeploymentSignature = this.deploymentSignature();
|
||||||
|
this.deploymentNotice = '전투 동작을 준비하고 있습니다. 배치를 바꾸면 준비 완료 후 현재 배치를 다시 확인합니다.';
|
||||||
this.renderDeploymentPanel();
|
this.renderDeploymentPanel();
|
||||||
this.ensureScenarioCombatAssets();
|
this.ensureScenarioCombatAssets();
|
||||||
return;
|
return;
|
||||||
@@ -8646,6 +8633,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deploymentSignature() {
|
||||||
|
return this.deploymentEligibleUnits()
|
||||||
|
.map((unit) => `${unit.id}:${unit.x},${unit.y}`)
|
||||||
|
.sort()
|
||||||
|
.join('|');
|
||||||
|
}
|
||||||
|
|
||||||
private updateDeploymentPointerFeedback(tile: { x: number; y: number }, force: boolean) {
|
private updateDeploymentPointerFeedback(tile: { x: number; y: number }, force: boolean) {
|
||||||
const selected = this.deploymentSelectedUnit();
|
const selected = this.deploymentSelectedUnit();
|
||||||
const occupant = this.unitAtTile(tile.x, tile.y);
|
const occupant = this.unitAtTile(tile.x, tile.y);
|
||||||
@@ -11509,7 +11503,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
await this.presentCombatResult(result.counter);
|
await this.presentCombatResult(result.counter);
|
||||||
}
|
}
|
||||||
await this.showCombatExchangeMapResults(result);
|
await this.showCombatExchangeMapResults(result);
|
||||||
await this.delay(result.counter ? 220 : 160);
|
// Keep the result anchored on its unit long enough to read before auto-focusing the next ally.
|
||||||
|
await this.delay(620);
|
||||||
this.finishUnitAction(attacker, this.formatCombatResult(result), {
|
this.finishUnitAction(attacker, this.formatCombatResult(result), {
|
||||||
defeatedEnemyIds: result.defeated ? [result.defender.id] : []
|
defeatedEnemyIds: result.defeated ? [result.defender.id] : []
|
||||||
});
|
});
|
||||||
@@ -11537,7 +11532,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
await this.presentSupportResult(result);
|
await this.presentSupportResult(result);
|
||||||
this.showSupportMapResult(result);
|
this.showSupportMapResult(result);
|
||||||
await this.delay(160);
|
// Keep the support result visible before the camera moves to the next actionable ally.
|
||||||
|
await this.delay(620);
|
||||||
this.finishUnitAction(user, this.formatSupportResult(result));
|
this.finishUnitAction(user, this.formatSupportResult(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12686,6 +12682,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.hideBattleResult();
|
this.hideBattleResult();
|
||||||
this.resultAnimationToken += 1;
|
this.resultAnimationToken += 1;
|
||||||
this.resultGaugeAnimations = [];
|
this.resultGaugeAnimations = [];
|
||||||
|
this.resultContinueLockedUntil = 0;
|
||||||
|
this.resultNavigationPending = false;
|
||||||
const animationToken = this.resultAnimationToken;
|
const animationToken = this.resultAnimationToken;
|
||||||
|
|
||||||
const depth = 120;
|
const depth = 120;
|
||||||
@@ -12914,21 +12912,61 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleResultContinue() {
|
private handleResultContinue() {
|
||||||
|
if (this.resultNavigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
if (this.resultSettlementStatus !== 'complete') {
|
if (this.resultSettlementStatus !== 'complete') {
|
||||||
this.completeResultSettlement(true);
|
this.completeResultSettlement(true);
|
||||||
|
this.resultContinueLockedUntil = this.time.now + 420;
|
||||||
|
const button = this.resultContinueButton;
|
||||||
|
if (button) {
|
||||||
|
button.background.disableInteractive();
|
||||||
|
button.background.setAlpha(0.72);
|
||||||
|
button.label.setAlpha(0.72);
|
||||||
|
this.time.delayedCall(420, () => {
|
||||||
|
if (!button.background.active || this.resultContinueButton !== button || this.resultNavigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.background.setInteractive({ useHandCursor: true });
|
||||||
|
button.background.setAlpha(1);
|
||||||
|
button.label.setAlpha(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.time.now < this.resultContinueLockedUntil) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { destination } = this.resultContinueRoute();
|
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') {
|
if (destination === 'victory-story') {
|
||||||
void startLazyScene(this, 'StoryScene', {
|
void startLazyScene(this, 'StoryScene', {
|
||||||
pages: firstBattleVictoryPages,
|
pages: firstBattleVictoryPages,
|
||||||
nextScene: 'CampScene'
|
nextScene: 'CampScene'
|
||||||
});
|
}).catch(recoverNavigation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void startLazyScene(this, 'CampScene');
|
void startLazyScene(this, 'CampScene').catch(recoverNavigation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideBattleResult() {
|
private hideBattleResult() {
|
||||||
@@ -12957,6 +12995,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.resultSettlementSkipped = false;
|
this.resultSettlementSkipped = false;
|
||||||
this.resultSettlementProgress = 0;
|
this.resultSettlementProgress = 0;
|
||||||
this.resultContinueButton = undefined;
|
this.resultContinueButton = undefined;
|
||||||
|
this.resultContinueLockedUntil = 0;
|
||||||
|
this.resultNavigationPending = false;
|
||||||
this.resultCoreResonanceSummaryText = undefined;
|
this.resultCoreResonanceSummaryText = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24111,6 +24151,21 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.hideTurnEndPrompt();
|
this.hideTurnEndPrompt();
|
||||||
this.turnPromptMode = options.mode ?? 'turn-end';
|
this.turnPromptMode = options.mode ?? 'turn-end';
|
||||||
|
|
||||||
|
const shade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x03070b, 0.46);
|
||||||
|
shade.setOrigin(0);
|
||||||
|
shade.setDepth(39);
|
||||||
|
shade.setInteractive();
|
||||||
|
shade.on(
|
||||||
|
'pointerdown',
|
||||||
|
(
|
||||||
|
_pointer: Phaser.Input.Pointer,
|
||||||
|
_localX: number,
|
||||||
|
_localY: number,
|
||||||
|
event?: { stopPropagation: () => void }
|
||||||
|
) => event?.stopPropagation()
|
||||||
|
);
|
||||||
|
this.turnPromptObjects.push(shade);
|
||||||
|
|
||||||
const width = this.battleUiLength(388);
|
const width = this.battleUiLength(388);
|
||||||
const height = this.battleUiLength(162);
|
const height = this.battleUiLength(162);
|
||||||
const left = this.layout.mapX + this.layout.mapWidth / 2 - width / 2;
|
const left = this.layout.mapX + this.layout.mapWidth / 2 - width / 2;
|
||||||
@@ -24988,6 +25043,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (this.activeFaction === 'ally' && !this.battleOutcome && this.remainingAllyCount() === 0) {
|
||||||
|
this.renderSituationTurnEndButton(
|
||||||
|
left + width - this.battleUiLength(92),
|
||||||
|
top,
|
||||||
|
this.battleUiLength(92)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.renderSituationLine('현재 턴', `${this.turnNumber}턴 / ${factionLabels[this.activeFaction]}`, left, top + this.battleUiLength(32), width, battleFhdUiScale);
|
this.renderSituationLine('현재 턴', `${this.turnNumber}턴 / ${factionLabels[this.activeFaction]}`, left, top + this.battleUiLength(32), width, battleFhdUiScale);
|
||||||
this.renderSituationLine('아군 행동', `${actedCount} / ${allyCount}`, left, top + this.battleUiLength(60), width, battleFhdUiScale);
|
this.renderSituationLine('아군 행동', `${actedCount} / ${allyCount}`, left, top + this.battleUiLength(60), width, battleFhdUiScale);
|
||||||
this.renderSituationLine('남은 적', `${enemyCount}`, left, top + this.battleUiLength(88), width, battleFhdUiScale);
|
this.renderSituationLine('남은 적', `${enemyCount}`, left, top + this.battleUiLength(88), width, battleFhdUiScale);
|
||||||
@@ -25015,6 +25078,38 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.playSidePanelTransition(transition);
|
this.playSidePanelTransition(transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderSituationTurnEndButton(x: number, y: number, width: number) {
|
||||||
|
const height = this.battleUiLength(24);
|
||||||
|
const button = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x4a371d, 0.98));
|
||||||
|
button.setOrigin(0);
|
||||||
|
button.setDepth(32);
|
||||||
|
button.setStrokeStyle(this.battleUiLength(1), palette.gold, 0.9);
|
||||||
|
|
||||||
|
const text = this.trackSideObject(this.add.text(x + width / 2, y + height / 2, '턴 종료', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: this.battleUiFontSize(11),
|
||||||
|
color: '#fff2b8',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
text.setDepth(33);
|
||||||
|
|
||||||
|
const activate = () => {
|
||||||
|
if (this.activeFaction !== 'ally' || this.battleOutcome || this.remainingAllyCount() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.suppressNextLeftClick = true;
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.showTurnEndPrompt();
|
||||||
|
};
|
||||||
|
[button, text].forEach((object) => {
|
||||||
|
object.setInteractive({ useHandCursor: true });
|
||||||
|
object.on('pointerdown', activate);
|
||||||
|
});
|
||||||
|
button.on('pointerover', () => button.setFillStyle(0x654c25, 0.99));
|
||||||
|
button.on('pointerout', () => button.setFillStyle(0x4a371d, 0.98));
|
||||||
|
}
|
||||||
|
|
||||||
private compactSituationMessage(message: string) {
|
private compactSituationMessage(message: string) {
|
||||||
return message
|
return message
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|||||||
@@ -204,11 +204,16 @@ import {
|
|||||||
import { clearCampaignBattleSavesForSlot } from '../state/battleSaveKeys';
|
import { clearCampaignBattleSavesForSlot } from '../state/battleSaveKeys';
|
||||||
import { releaseTextureSource } from '../render/loaderLifecycle';
|
import { releaseTextureSource } from '../render/loaderLifecycle';
|
||||||
import { palette } from '../ui/palette';
|
import { palette } from '../ui/palette';
|
||||||
import { startLazyScene } from './lazyScenes';
|
import { startLazyScene, type LazySceneKey } from './lazyScenes';
|
||||||
|
|
||||||
const campLegacyCanvasWidth = 1280;
|
const campLegacyCanvasWidth = 1280;
|
||||||
const campLegacyCanvasHeight = 720;
|
const campLegacyCanvasHeight = 720;
|
||||||
const campFhdUiScale = 1.5;
|
const campFhdUiScale = 1.5;
|
||||||
|
const campNoticeMinimumHoldMs = 2600;
|
||||||
|
const campNoticeMaximumHoldMs = 9000;
|
||||||
|
const campNoticeBaseHoldMs = 1800;
|
||||||
|
const campNoticeCharacterHoldMs = 55;
|
||||||
|
const campNoticeParagraphPauseMs = 500;
|
||||||
|
|
||||||
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'equipment' | 'progress';
|
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'equipment' | 'progress';
|
||||||
|
|
||||||
@@ -11383,6 +11388,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private openSortieImprovementOnCreate = false;
|
private openSortieImprovementOnCreate = false;
|
||||||
private retrySortieBattleId?: BattleScenarioId;
|
private retrySortieBattleId?: BattleScenarioId;
|
||||||
private skipIntroStoryForSortie = false;
|
private skipIntroStoryForSortie = false;
|
||||||
|
private navigationPending = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('CampScene');
|
super('CampScene');
|
||||||
@@ -11398,6 +11404,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.releaseOwnedCampTextures());
|
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.releaseOwnedCampTextures());
|
||||||
|
this.navigationPending = false;
|
||||||
this.contentObjects = [];
|
this.contentObjects = [];
|
||||||
this.campRosterPage = 0;
|
this.campRosterPage = 0;
|
||||||
this.campRosterLayout = undefined;
|
this.campRosterLayout = undefined;
|
||||||
@@ -12958,11 +12965,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private saveCampToSlot(slot: number) {
|
private saveCampToSlot(slot: number) {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
const returnToSortiePrep = this.sortieObjects.length > 0;
|
const returnToSortiePrep = this.sortieObjects.length > 0;
|
||||||
this.sortieSwapUndoState = undefined;
|
|
||||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||||
this.sortieRecommendationUndoState = undefined;
|
|
||||||
this.closeSortieRecommendationBrowserState();
|
this.closeSortieRecommendationBrowserState();
|
||||||
this.sortiePresetUndoState = undefined;
|
|
||||||
this.closeSortiePresetBrowserState();
|
this.closeSortiePresetBrowserState();
|
||||||
this.hideCampSaveSlotPanel();
|
this.hideCampSaveSlotPanel();
|
||||||
clearCampaignBattleSavesForSlot(slot);
|
clearCampaignBattleSavesForSlot(slot);
|
||||||
@@ -13536,6 +13540,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleSortieEnterKey(event: KeyboardEvent) {
|
private handleSortieEnterKey(event: KeyboardEvent) {
|
||||||
|
if (event.repeat || this.navigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.sortieObjects.length === 0 ||
|
this.sortieObjects.length === 0 ||
|
||||||
this.victoryRewardObjects.length > 0 ||
|
this.victoryRewardObjects.length > 0 ||
|
||||||
@@ -19757,6 +19764,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startVictoryStory() {
|
private startVictoryStory() {
|
||||||
|
if (this.navigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const flow = this.currentSortieFlow();
|
const flow = this.currentSortieFlow();
|
||||||
if (flow.nextBattleId && !this.hasCurrentSortieOrderSelection()) {
|
if (flow.nextBattleId && !this.hasCurrentSortieOrderSelection()) {
|
||||||
this.sortiePrepStep = 'formation';
|
this.sortiePrepStep = 'formation';
|
||||||
@@ -19785,7 +19795,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
if (this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep) {
|
if (this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep) {
|
||||||
this.hideSortiePrep();
|
this.hideSortiePrep();
|
||||||
void startLazyScene(this, 'EndingScene');
|
this.startCampNavigation('EndingScene');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19796,16 +19806,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.showCampNotice(flow.unavailableNotice ?? '현재 군영 정비를 먼저 마치세요. 대화, 보급, 편성을 점검할 수 있습니다.');
|
this.showCampNotice(flow.unavailableNotice ?? '현재 군영 정비를 먼저 마치세요. 대화, 보급, 편성을 점검할 수 있습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markCampaignStep(flow.campaignStep);
|
this.startCampNavigationWithCampaignStep(flow.campaignStep, 'StoryScene', {
|
||||||
this.campaign = getCampaignState();
|
|
||||||
void startLazyScene(this, 'StoryScene', {
|
|
||||||
pages: flow.pages,
|
pages: flow.pages,
|
||||||
nextScene: flow.campaignStep === 'ending-complete' ? 'EndingScene' : 'CampScene'
|
nextScene: flow.campaignStep === 'ending-complete' ? 'EndingScene' : 'CampScene'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (flow.pages.length > 0) {
|
if (flow.pages.length > 0) {
|
||||||
void startLazyScene(this, 'StoryScene', {
|
this.startCampNavigation('StoryScene', {
|
||||||
pages: flow.pages,
|
pages: flow.pages,
|
||||||
nextScene: 'CampScene'
|
nextScene: 'CampScene'
|
||||||
});
|
});
|
||||||
@@ -19823,7 +19831,6 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
markCampaignStep(flow.campaignStep);
|
|
||||||
const selectedSortieUnitIds = [...this.selectedSortieUnitIds];
|
const selectedSortieUnitIds = [...this.selectedSortieUnitIds];
|
||||||
const sortieFormationAssignments = { ...this.sortieFormationAssignments };
|
const sortieFormationAssignments = { ...this.sortieFormationAssignments };
|
||||||
const sortieItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
const sortieItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
||||||
@@ -19833,7 +19840,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const sortieRecommendation = this.currentSortieRecommendationSelection();
|
const sortieRecommendation = this.currentSortieRecommendationSelection();
|
||||||
if (this.skipIntroStoryForSortie) {
|
if (this.skipIntroStoryForSortie) {
|
||||||
void startLazyScene(this, 'BattleScene', {
|
this.startCampNavigationWithCampaignStep(flow.campaignStep, 'BattleScene', {
|
||||||
battleId: flow.nextBattleId,
|
battleId: flow.nextBattleId,
|
||||||
selectedSortieUnitIds,
|
selectedSortieUnitIds,
|
||||||
sortieFormationAssignments,
|
sortieFormationAssignments,
|
||||||
@@ -19845,7 +19852,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void startLazyScene(this, 'StoryScene', {
|
this.startCampNavigationWithCampaignStep(flow.campaignStep, 'StoryScene', {
|
||||||
pages: flow.pages,
|
pages: flow.pages,
|
||||||
nextScene: 'BattleScene',
|
nextScene: 'BattleScene',
|
||||||
nextSceneData: {
|
nextSceneData: {
|
||||||
@@ -19860,6 +19867,46 @@ export class CampScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startCampNavigationWithCampaignStep(
|
||||||
|
step: CampaignStep,
|
||||||
|
key: LazySceneKey,
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
if (this.navigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStep = getCampaignState().step;
|
||||||
|
markCampaignStep(step);
|
||||||
|
this.campaign = getCampaignState();
|
||||||
|
this.startCampNavigation(key, data, () => {
|
||||||
|
if (getCampaignState().step === step) {
|
||||||
|
markCampaignStep(previousStep);
|
||||||
|
}
|
||||||
|
this.campaign = getCampaignState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private startCampNavigation(
|
||||||
|
key: LazySceneKey,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
recoverState?: () => void
|
||||||
|
) {
|
||||||
|
if (this.navigationPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('다음 장면을 불러오지 못했습니다. 잠시 후 다시 시도하십시오.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private isFinalEpilogueFlow(flow = this.currentSortieFlow()) {
|
private isFinalEpilogueFlow(flow = this.currentSortieFlow()) {
|
||||||
return flow.campaignStep === 'ending-complete' && !flow.nextBattleId;
|
return flow.campaignStep === 'ending-complete' && !flow.nextBattleId;
|
||||||
}
|
}
|
||||||
@@ -22881,8 +22928,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showVisitReward(choice: CampVisitChoice) {
|
private showVisitReward(choice: CampVisitChoice) {
|
||||||
this.showCampNotice(`${choice.label} · ${this.visitRewardText(choice)}`);
|
this.showCampNotice(`획득 내역 · ${choice.label} · ${this.visitRewardText(choice)}\n응답 · ${choice.response}`);
|
||||||
this.time.delayedCall(120, () => this.showCampNotice(choice.response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private visitRewardText(choice: CampVisitChoice) {
|
private visitRewardText(choice: CampVisitChoice) {
|
||||||
@@ -22919,8 +22965,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
||||||
this.showCampNotice(`${choice.label} · 공명 +${rewardExp}`);
|
this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}`);
|
||||||
this.time.delayedCall(120, () => this.showCampNotice(choice.response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showCampNotice(message: string) {
|
private showCampNotice(message: string) {
|
||||||
@@ -22928,10 +22973,8 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.dialogueObjects.forEach((object) => object.active && object.destroy());
|
this.dialogueObjects.forEach((object) => object.active && object.destroy());
|
||||||
this.dialogueObjects = [];
|
this.dialogueObjects = [];
|
||||||
const depth = this.sortieObjects.length > 0 ? 72 : 30;
|
const depth = this.sortieObjects.length > 0 ? 72 : 30;
|
||||||
const noticeWidth = Math.min(640, Math.max(360, message.length * 13));
|
const longestLineLength = Math.max(...message.split('\n').map((line) => line.length));
|
||||||
const box = this.scaleLegacyCampUi(this.add.rectangle(campLegacyCanvasWidth / 2, 104, noticeWidth, 50, 0x101820, 0.96));
|
const noticeWidth = Math.min(760, Math.max(420, longestLineLength * 13));
|
||||||
box.setStrokeStyle(2, palette.gold, 0.84);
|
|
||||||
box.setDepth(depth);
|
|
||||||
const text = this.scaleLegacyCampUi(this.add.text(campLegacyCanvasWidth / 2, 104, message, {
|
const text = this.scaleLegacyCampUi(this.add.text(campLegacyCanvasWidth / 2, 104, message, {
|
||||||
...this.textStyle(14, '#f2e3bf', true),
|
...this.textStyle(14, '#f2e3bf', true),
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -22939,12 +22982,17 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}));
|
}));
|
||||||
text.setOrigin(0.5);
|
text.setOrigin(0.5);
|
||||||
text.setDepth(depth + 1);
|
text.setDepth(depth + 1);
|
||||||
|
text.setLineSpacing(5);
|
||||||
|
const noticeHeight = Math.max(56, text.height + 28);
|
||||||
|
const box = this.scaleLegacyCampUi(this.add.rectangle(campLegacyCanvasWidth / 2, 104, noticeWidth, noticeHeight, 0x101820, 0.96));
|
||||||
|
box.setStrokeStyle(2, palette.gold, 0.84);
|
||||||
|
box.setDepth(depth);
|
||||||
const noticeObjects: Phaser.GameObjects.GameObject[] = [box, text];
|
const noticeObjects: Phaser.GameObjects.GameObject[] = [box, text];
|
||||||
this.dialogueObjects = noticeObjects;
|
this.dialogueObjects = noticeObjects;
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: noticeObjects,
|
targets: noticeObjects,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
delay: 1300,
|
delay: this.campNoticeHoldDuration(message),
|
||||||
duration: 320,
|
duration: 320,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
noticeObjects.forEach((object) => object.active && object.destroy());
|
noticeObjects.forEach((object) => object.active && object.destroy());
|
||||||
@@ -22955,6 +23003,18 @@ export class CampScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private campNoticeHoldDuration(message: string) {
|
||||||
|
const readableCharacterCount = message.replace(/\s/g, '').length;
|
||||||
|
const paragraphCount = Math.max(1, message.split('\n').filter((line) => line.trim().length > 0).length);
|
||||||
|
return Phaser.Math.Clamp(
|
||||||
|
campNoticeBaseHoldMs +
|
||||||
|
readableCharacterCount * campNoticeCharacterHoldMs +
|
||||||
|
(paragraphCount - 1) * campNoticeParagraphPauseMs,
|
||||||
|
campNoticeMinimumHoldMs,
|
||||||
|
campNoticeMaximumHoldMs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private selectedUnit() {
|
private selectedUnit() {
|
||||||
return this.currentUnits().find((unit) => unit.id === this.selectedUnitId);
|
return this.currentUnits().find((unit) => unit.id === this.selectedUnitId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user