fix: sequence battle feedback and autosaves
This commit is contained in:
@@ -9,6 +9,7 @@ import { isVisualMotionReduced } from '../settings/visualMotion';
|
||||
import { battleMapAssets } from '../data/battleMapAssets';
|
||||
import { type BattleBond, type UnitData, type UnitStats } from '../data/scenario';
|
||||
import {
|
||||
battleScenarios,
|
||||
defaultBattleScenario,
|
||||
getBattleScenario,
|
||||
type BattleCampaignRewardDefinition,
|
||||
@@ -233,6 +234,7 @@ import {
|
||||
type BattleSaveUnitStats
|
||||
} from '../state/battleSaveState';
|
||||
import {
|
||||
campaignBattleResumeStorageKeys,
|
||||
clearCampaignBattleSavesForSlot,
|
||||
clearBattleSaveStorageForBattle,
|
||||
readBattleSaveStorageCandidate
|
||||
@@ -257,6 +259,7 @@ const battleFhdUiScale = 1.5;
|
||||
const battleEnvironmentTintDepth = 3.04;
|
||||
const battleEnvironmentHazeDepth = 3.12;
|
||||
const battleEnvironmentParticleDepth = 3.3;
|
||||
const battleFeedbackSettlePaddingMs = 24;
|
||||
const firstBattleTutorialId: CampaignTutorialId = 'first-battle-basic-controls';
|
||||
|
||||
type FirstBattlePreparationState = {
|
||||
@@ -1532,6 +1535,15 @@ type UnitActionPose = 'attack' | 'strategy' | 'item' | 'hurt' | 'celebrate';
|
||||
const levelUpCelebrationStepOffsets = [0, -7, -2, -9, 0, -6, -3, -8];
|
||||
|
||||
type BattlePhase = 'deployment' | 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved';
|
||||
type BattleAutosaveReason =
|
||||
| 'battle-start'
|
||||
| 'deployment-confirmed'
|
||||
| 'ally-action'
|
||||
| 'ally-turn-start'
|
||||
| 'pagehide'
|
||||
| 'hidden'
|
||||
| 'debug';
|
||||
type BattleAutosaveResult = 'saved' | 'skipped' | 'failed';
|
||||
type ScenarioCombatAssetStatus = 'idle' | 'loading' | 'ready' | 'degraded';
|
||||
type BattleCommand = 'attack' | UsableCommand | 'wait';
|
||||
type DamageCommand = Exclude<BattleCommand, 'wait'>;
|
||||
@@ -2370,6 +2382,10 @@ type IntentCounterplayEvent = {
|
||||
type IntentCounterplayActionOutcome = {
|
||||
defeatedEnemyIds?: string[];
|
||||
};
|
||||
type IntentCounterplaySettlement = {
|
||||
message: string;
|
||||
popupDuration: number;
|
||||
};
|
||||
|
||||
type TacticalInitiativeRole = BattleSaveTacticalCommandRole;
|
||||
type TacticalCommandSource = NonNullable<BattleSaveTacticalCommandState['source']>;
|
||||
@@ -3997,6 +4013,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private battleEventQueue: BattleSavePendingEvent[] = [];
|
||||
private activeBattleEvent?: BattleSavePendingEvent;
|
||||
private battleEventDismissTween?: Phaser.Tweens.Tween;
|
||||
private battleEventCompletionWaiters: Array<() => void> = [];
|
||||
private battleEventPresentationCount = 0;
|
||||
private deferBattleEventPresentation = false;
|
||||
private tacticalEventReactionHistory: TacticalEventReactionSnapshot[] = [];
|
||||
@@ -4016,6 +4033,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private turnPromptRiskRows: TurnEndRiskRowView[] = [];
|
||||
private mapResultPopupObjects: Phaser.GameObjects.Text[] = [];
|
||||
private mapResultPopupPeakCount = 0;
|
||||
private battleFeedbackReadableUntil = 0;
|
||||
private mapResultPopupLast?: {
|
||||
unitId: string;
|
||||
lines: string[];
|
||||
@@ -4164,6 +4182,22 @@ export class BattleScene extends Phaser.Scene {
|
||||
private launchCamaraderieMemoryBondId?: string;
|
||||
private launchCamaraderieMemoryBonus = 0;
|
||||
private storyHandoff?: CampaignStoryHandoffReference;
|
||||
private battleAutosaveWriteCount = 0;
|
||||
private battleAutosaveSkipCount = 0;
|
||||
private battleAutosaveFailureCount = 0;
|
||||
private battleAutosaveLastResult?: BattleAutosaveResult;
|
||||
private battleAutosaveLastReason?: BattleAutosaveReason;
|
||||
private battleAutosaveLastSlot?: number;
|
||||
private battleAutosaveLastSavedAt?: string;
|
||||
private battleAutosaveReady = false;
|
||||
private readonly handleBattlePageHide = () => {
|
||||
this.persistBattleAutosave('pagehide');
|
||||
};
|
||||
private readonly handleBattleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
this.persistBattleAutosave('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super('BattleScene');
|
||||
@@ -4237,6 +4271,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.turnPromptRiskSummaryText = undefined;
|
||||
this.turnPromptRiskFootnoteText = undefined;
|
||||
this.turnPromptRiskRows = [];
|
||||
this.battleEventCompletionWaiters = [];
|
||||
this.battleFeedbackReadableUntil = 0;
|
||||
this.turnText = undefined;
|
||||
this.battleTitleText = undefined;
|
||||
this.objectiveTrackerText = undefined;
|
||||
@@ -4354,6 +4390,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.resetSortieCooperationStats();
|
||||
this.launchCamaraderieMemoryBondId = undefined;
|
||||
this.launchCamaraderieMemoryBonus = 0;
|
||||
this.battleAutosaveWriteCount = 0;
|
||||
this.battleAutosaveSkipCount = 0;
|
||||
this.battleAutosaveFailureCount = 0;
|
||||
this.battleAutosaveLastResult = undefined;
|
||||
this.battleAutosaveLastReason = undefined;
|
||||
this.battleAutosaveLastSlot = undefined;
|
||||
this.battleAutosaveLastSavedAt = undefined;
|
||||
this.battleAutosaveReady = false;
|
||||
}
|
||||
|
||||
create() {
|
||||
@@ -4384,6 +4428,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.mapResultPopupObjects.forEach((object) => object.active && object.destroy());
|
||||
this.mapResultPopupObjects = [];
|
||||
this.hideBattleSoundCaption();
|
||||
this.battleAutosaveReady = false;
|
||||
this.uninstallBattleAutosaveHandlers();
|
||||
});
|
||||
this.resultFormationReviewVisible = false;
|
||||
this.resultFormationReviewFeedback = '';
|
||||
@@ -4478,6 +4524,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
this.input.mouse?.disableContextMenu();
|
||||
this.installBattleInputHandlers();
|
||||
this.installBattleAutosaveHandlers();
|
||||
this.installDebugHotkeys();
|
||||
|
||||
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
||||
@@ -4634,6 +4681,27 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.uninstallDebugHotkeys();
|
||||
}
|
||||
|
||||
private installBattleAutosaveHandlers() {
|
||||
this.uninstallBattleAutosaveHandlers();
|
||||
window.addEventListener('pagehide', this.handleBattlePageHide);
|
||||
document.addEventListener('visibilitychange', this.handleBattleVisibilityChange);
|
||||
this.events.once(
|
||||
Phaser.Scenes.Events.SHUTDOWN,
|
||||
this.uninstallBattleAutosaveHandlers,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
private uninstallBattleAutosaveHandlers() {
|
||||
this.events.off(
|
||||
Phaser.Scenes.Events.SHUTDOWN,
|
||||
this.uninstallBattleAutosaveHandlers,
|
||||
this
|
||||
);
|
||||
window.removeEventListener('pagehide', this.handleBattlePageHide);
|
||||
document.removeEventListener('visibilitychange', this.handleBattleVisibilityChange);
|
||||
}
|
||||
|
||||
private battleInputEventHandled(
|
||||
channel: string,
|
||||
event: KeyboardEvent,
|
||||
@@ -4975,6 +5043,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.time.delayedCall(180, () => {
|
||||
this.showOpeningBattleEvent();
|
||||
this.scheduleFirstBattleTutorial();
|
||||
this.battleAutosaveReady = true;
|
||||
this.persistBattleAutosave('battle-start');
|
||||
});
|
||||
this.renderRosterPanel('ally', '행동할 장수를 선택하세요.');
|
||||
this.refreshEnemyIntentForecast();
|
||||
@@ -11114,6 +11184,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.refreshEnemyIntentForecast();
|
||||
this.showOpeningBattleEvent();
|
||||
this.scheduleFirstBattleTutorial();
|
||||
this.battleAutosaveReady = true;
|
||||
this.persistBattleAutosave('deployment-confirmed');
|
||||
}
|
||||
|
||||
private deploymentSignature() {
|
||||
@@ -11949,8 +12021,18 @@ export class BattleScene extends Phaser.Scene {
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
soundDirector.playGrowthTick();
|
||||
this.showMapResultPopup(actor, [`의도 파훼 +${events.length}`, detail], '#ffdf7b', '#2b1606', 18, 36);
|
||||
return `의도 파훼 +${events.length} · ${actor.name} · ${compactDetail}`;
|
||||
const popupDuration = this.showMapResultPopup(
|
||||
actor,
|
||||
[`의도 파훼 +${events.length}`, detail],
|
||||
'#ffdf7b',
|
||||
'#2b1606',
|
||||
18,
|
||||
36
|
||||
);
|
||||
return {
|
||||
message: `의도 파훼 +${events.length} · ${actor.name} · ${compactDetail}`,
|
||||
popupDuration
|
||||
} satisfies IntentCounterplaySettlement;
|
||||
}
|
||||
|
||||
private recordIntentGuardCounterplay(
|
||||
@@ -13066,7 +13148,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
this.finishUnitAction(unit, this.commandResultMessage(unit, command));
|
||||
void this.finishUnitAction(unit, this.commandResultMessage(unit, command));
|
||||
}
|
||||
|
||||
private actionTargetingLabel(action: DamageCommand, usable?: BattleUsable) {
|
||||
@@ -14263,10 +14345,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
await this.delay(180);
|
||||
await this.presentCombatResult(result.counter);
|
||||
}
|
||||
await this.showCombatExchangeMapResults(result);
|
||||
// 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), {
|
||||
await this.showCombatExchangeMapResults(result, true);
|
||||
await this.finishUnitAction(attacker, this.formatCombatResult(result), {
|
||||
defeatedEnemyIds: result.defeated ? [result.defender.id] : []
|
||||
});
|
||||
}
|
||||
@@ -14292,10 +14372,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
const result = this.resolveSupportAction(user, target, usable);
|
||||
this.clearMarkers();
|
||||
await this.presentSupportResult(result);
|
||||
this.showSupportMapResult(result);
|
||||
// Keep the support result visible before the camera moves to the next actionable ally.
|
||||
await this.delay(620);
|
||||
this.finishUnitAction(user, this.formatSupportResult(result));
|
||||
const popupDuration = this.showSupportMapResult(result);
|
||||
await this.waitSceneDuration(popupDuration);
|
||||
await this.finishUnitAction(user, this.formatSupportResult(result));
|
||||
}
|
||||
|
||||
private renderAttackPreview(preview: CombatPreview, locked = false) {
|
||||
@@ -14359,11 +14438,19 @@ export class BattleScene extends Phaser.Scene {
|
||||
battleScenario.id,
|
||||
this.prologueVolunteerReassured
|
||||
);
|
||||
this.triggerBattleEvent(firstBattleVolunteerPromiseEventKey, '첫 교전', [
|
||||
const presentationWasDeferred =
|
||||
this.deferBattleEventPresentation;
|
||||
this.deferBattleEventPresentation = true;
|
||||
try {
|
||||
this.triggerBattleEvent(firstBattleVolunteerPromiseEventKey, '첫 교전', [
|
||||
`${attacker.name} · ${target.name} 공격을 시작합니다.`,
|
||||
'공격 후 경험치와 장비 경험치가 전투창에서 정산됩니다.',
|
||||
...(volunteerPromiseLine ? [volunteerPromiseLine] : [])
|
||||
], { playCue: false });
|
||||
], { playCue: false });
|
||||
} finally {
|
||||
this.deferBattleEventPresentation =
|
||||
presentationWasDeferred;
|
||||
}
|
||||
}
|
||||
|
||||
private battleBuffShortText(buff: BattleBuffState) {
|
||||
@@ -15336,11 +15423,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
return x + width + 6;
|
||||
}
|
||||
|
||||
private finishUnitAction(unit: UnitData, message: string, outcome: IntentCounterplayActionOutcome = {}) {
|
||||
const counterplayMessage = this.settleEnemyIntentCounterplay(unit, this.pendingMove, outcome);
|
||||
private async finishUnitAction(unit: UnitData, message: string, outcome: IntentCounterplayActionOutcome = {}) {
|
||||
const counterplaySettlement = this.settleEnemyIntentCounterplay(unit, this.pendingMove, outcome);
|
||||
const counterplayMessage = counterplaySettlement?.message;
|
||||
const resolvedMessage = counterplayMessage ? `${counterplayMessage}\n${message}` : message;
|
||||
this.actedUnitIds.add(unit.id);
|
||||
this.phase = 'idle';
|
||||
this.phase = 'animating';
|
||||
this.selectedUnit = undefined;
|
||||
this.pendingMove = undefined;
|
||||
this.targetingAction = undefined;
|
||||
@@ -15352,14 +15440,27 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.renderTacticalInitiativeChip();
|
||||
soundDirector.playSelect();
|
||||
this.pushBattleLog(resolvedMessage);
|
||||
this.checkBattleEvents();
|
||||
this.collectBattleEventsWithoutPresentation();
|
||||
this.updateObjectiveTracker();
|
||||
|
||||
if (this.resolveBattleOutcomeIfNeeded(counterplayMessage ? 820 : 0)) {
|
||||
await this.waitForBattleFeedbackReadability();
|
||||
this.showNextBattleEvent();
|
||||
await this.waitForBattleEventPresentation();
|
||||
if (!this.scene.isActive() || this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.phase = 'idle';
|
||||
if (this.resolveBattleOutcomeIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
await this.waitForBattleEventPresentation();
|
||||
if (!this.scene.isActive() || this.battleOutcome) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshEnemyIntentForecast();
|
||||
this.persistBattleAutosave('ally-action');
|
||||
|
||||
const remaining = this.remainingAllyCount();
|
||||
const turnHint =
|
||||
@@ -19906,6 +20007,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.phase === 'idle' &&
|
||||
this.activeFaction === 'ally' &&
|
||||
this.turnNumber === 1 &&
|
||||
!this.activeBattleEvent &&
|
||||
this.battleEventQueue.length === 0 &&
|
||||
this.battleEventObjects.length === 0 &&
|
||||
this.mapMenuObjects.length === 0 &&
|
||||
this.saveSlotPanelObjects.length === 0 &&
|
||||
@@ -21555,6 +21658,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
this.triggeredBattleEvents.add(groupedEvent.key);
|
||||
});
|
||||
this.resolveBattleEventCompletionWaiters();
|
||||
}
|
||||
|
||||
private clearBattleEvents() {
|
||||
@@ -21565,16 +21669,37 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.activeBattleEvent = undefined;
|
||||
this.battleEventQueue = [];
|
||||
this.deferBattleEventPresentation = false;
|
||||
this.resolveBattleEventCompletionWaiters();
|
||||
}
|
||||
|
||||
private waitForBattleEventPresentation() {
|
||||
if (!this.activeBattleEvent && this.battleEventObjects.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
this.battleEventCompletionWaiters.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private resolveBattleEventCompletionWaiters() {
|
||||
if (this.activeBattleEvent || this.battleEventObjects.length > 0) {
|
||||
return;
|
||||
}
|
||||
this.battleEventCompletionWaiters.splice(0).forEach((resolve) => resolve());
|
||||
}
|
||||
|
||||
private checkBattleEvents() {
|
||||
this.collectBattleEventsWithoutPresentation();
|
||||
this.showNextBattleEvent();
|
||||
}
|
||||
|
||||
private collectBattleEventsWithoutPresentation() {
|
||||
this.deferBattleEventPresentation = true;
|
||||
try {
|
||||
this.collectBattleEvents();
|
||||
} finally {
|
||||
this.deferBattleEventPresentation = false;
|
||||
}
|
||||
this.showNextBattleEvent();
|
||||
}
|
||||
|
||||
private collectBattleEvents() {
|
||||
@@ -22966,6 +23091,111 @@ export class BattleScene extends Phaser.Scene {
|
||||
return eventIds;
|
||||
}
|
||||
|
||||
private isSafeBattleAutosaveCheckpoint() {
|
||||
return (
|
||||
this.scene.isActive() &&
|
||||
this.battleAutosaveReady &&
|
||||
this.activeFaction === 'ally' &&
|
||||
this.phase === 'idle' &&
|
||||
!this.battleOutcome &&
|
||||
!this.resultNavigationPending &&
|
||||
!this.selectedUnit &&
|
||||
!this.pendingMove &&
|
||||
!this.targetingAction &&
|
||||
!this.selectedUsable
|
||||
);
|
||||
}
|
||||
|
||||
private persistBattleAutosave(reason: BattleAutosaveReason = 'debug') {
|
||||
this.battleAutosaveLastReason = reason;
|
||||
if (!this.isSafeBattleAutosaveCheckpoint()) {
|
||||
this.battleAutosaveSkipCount += 1;
|
||||
this.battleAutosaveLastResult = 'skipped';
|
||||
return false;
|
||||
}
|
||||
|
||||
let previousStorageValues: Array<{
|
||||
key: string;
|
||||
value: string | null;
|
||||
}> = [];
|
||||
let storageMutationStarted = false;
|
||||
try {
|
||||
const campaign = getCampaignState();
|
||||
const normalizedSlot = normalizeBattleSaveSlot(
|
||||
campaign.activeSaveSlot,
|
||||
campaignSaveSlotCount
|
||||
);
|
||||
const state = this.createBattleSaveState();
|
||||
const serializedState = JSON.stringify(state);
|
||||
const slotKey =
|
||||
this.battleSaveStorageKeyForSlot(normalizedSlot);
|
||||
const storageKeys = [
|
||||
...new Set(
|
||||
Object.keys(battleScenarios).flatMap((battleId) =>
|
||||
campaignBattleResumeStorageKeys(
|
||||
battleId,
|
||||
normalizedSlot,
|
||||
campaignSaveSlotCount
|
||||
)
|
||||
)
|
||||
)
|
||||
];
|
||||
previousStorageValues = storageKeys.map((key) => ({
|
||||
key,
|
||||
value: window.localStorage.getItem(key)
|
||||
}));
|
||||
storageMutationStarted = true;
|
||||
clearCampaignBattleSavesForSlot(normalizedSlot);
|
||||
window.localStorage.setItem(
|
||||
slotKey,
|
||||
serializedState
|
||||
);
|
||||
if (normalizedSlot === 1) {
|
||||
window.localStorage.setItem(
|
||||
battleSaveStorageKey,
|
||||
serializedState
|
||||
);
|
||||
window.localStorage.removeItem(
|
||||
legacyBattleSaveStorageKey
|
||||
);
|
||||
}
|
||||
saveCampaignState(campaign, normalizedSlot);
|
||||
this.battleAutosaveWriteCount += 1;
|
||||
this.battleAutosaveLastResult = 'saved';
|
||||
this.battleAutosaveLastSlot = normalizedSlot;
|
||||
this.battleAutosaveLastSavedAt = state.savedAt;
|
||||
return true;
|
||||
} catch {
|
||||
if (storageMutationStarted) {
|
||||
this.restoreBattleAutosaveStorage(
|
||||
previousStorageValues
|
||||
);
|
||||
}
|
||||
this.battleAutosaveFailureCount += 1;
|
||||
this.battleAutosaveLastResult = 'failed';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private restoreBattleAutosaveStorage(
|
||||
values: ReadonlyArray<{
|
||||
key: string;
|
||||
value: string | null;
|
||||
}>
|
||||
) {
|
||||
values.forEach(({ key, value }) => {
|
||||
try {
|
||||
if (value === null) {
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.localStorage.setItem(key, value);
|
||||
}
|
||||
} catch {
|
||||
// Best effort: write-denied storage normally also preserves the old value.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private saveBattleState(slot = 1) {
|
||||
try {
|
||||
const state = this.createBattleSaveState();
|
||||
@@ -22991,6 +23221,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
try {
|
||||
this.battleAutosaveReady = false;
|
||||
const campaign = loadCampaignState(slot);
|
||||
this.launchSortieUnitIds = this.normalizeLaunchSortieUnitIds(campaign.selectedSortieUnitIds);
|
||||
this.launchSortieFormationAssignments = normalizeSortieFormationAssignments(campaign.sortieFormationAssignments);
|
||||
@@ -23011,7 +23242,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
);
|
||||
this.launchSortieRecommendation = this.normalizeLaunchSortieRecommendation(state.sortieRecommendation);
|
||||
this.applyBattleSaveState(state);
|
||||
this.battleAutosaveReady = true;
|
||||
this.renderSituationPanel(`슬롯 ${slot}의 전투를 불러왔습니다.\n${this.formatSavedAt(state.savedAt)}`);
|
||||
this.scheduleFirstBattleTutorial();
|
||||
if (this.activeFaction === 'enemy' && !this.battleOutcome) {
|
||||
this.time.delayedCall(250, () => {
|
||||
if (this.activeFaction === 'enemy' && !this.battleOutcome) {
|
||||
@@ -27440,17 +27673,67 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private delay(ms: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.time.delayedCall(this.scaledBattleDuration(ms), () => resolve());
|
||||
});
|
||||
return this.waitSceneDuration(
|
||||
this.scaledBattleDuration(ms)
|
||||
);
|
||||
}
|
||||
|
||||
private waitSceneDuration(ms: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.time.delayedCall(ms, () => resolve());
|
||||
if (!this.scene.isActive()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let settled = false;
|
||||
const complete = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
this.events.off(
|
||||
Phaser.Scenes.Events.SHUTDOWN,
|
||||
complete
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
this.events.once(
|
||||
Phaser.Scenes.Events.SHUTDOWN,
|
||||
complete
|
||||
);
|
||||
this.time.delayedCall(ms, complete);
|
||||
});
|
||||
}
|
||||
|
||||
private extendBattleFeedbackReadableWindow(duration: number) {
|
||||
if (duration <= 0) {
|
||||
return;
|
||||
}
|
||||
this.battleFeedbackReadableUntil = Math.max(
|
||||
this.battleFeedbackReadableUntil,
|
||||
this.time.now + duration
|
||||
);
|
||||
}
|
||||
|
||||
private async waitForBattleFeedbackReadability() {
|
||||
while (this.scene.isActive()) {
|
||||
const remaining = this.battleFeedbackReadableUntil - this.time.now;
|
||||
const activeFeedbackObjects =
|
||||
this.mapResultPopupObjects.some(
|
||||
(object) => object.active
|
||||
) ||
|
||||
Boolean(
|
||||
this.battleSoundCaption?.active &&
|
||||
this.battleSoundCaption.visible
|
||||
);
|
||||
if (remaining <= 0 && !activeFeedbackObjects) {
|
||||
return;
|
||||
}
|
||||
await this.waitSceneDuration(
|
||||
remaining > 0 ? remaining : 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private moveUnitViewAsync(
|
||||
unit: UnitData,
|
||||
x: number,
|
||||
@@ -27588,6 +27871,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
.join('\n');
|
||||
this.renderRosterPanel('ally', turnMessage);
|
||||
this.refreshEnemyIntentForecast();
|
||||
this.persistBattleAutosave('ally-turn-start');
|
||||
|
||||
const firstUnit = this.firstActionableAlly();
|
||||
if (firstUnit) {
|
||||
@@ -29506,7 +29790,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
private showSupportMapResult(result: SupportResult) {
|
||||
if (result.usable.effect === 'heal') {
|
||||
this.showMapResultPopup(
|
||||
return this.showMapResultPopup(
|
||||
result.target,
|
||||
[
|
||||
`회복 +${result.healAmount}`,
|
||||
@@ -29524,10 +29808,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
? 42
|
||||
: 28
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMapResultPopup(
|
||||
return this.showMapResultPopup(
|
||||
result.target,
|
||||
[
|
||||
`강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴`,
|
||||
@@ -29597,6 +29880,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
const duration = this.scaledBattleDuration(680, 260);
|
||||
const delay = this.scaledBattleDuration(80, 30);
|
||||
this.extendBattleFeedbackReadableWindow(
|
||||
duration + delay + battleFeedbackSettlePaddingMs
|
||||
);
|
||||
const bounds = popup.getBounds();
|
||||
this.mapResultPopupLast = {
|
||||
unitId: unit.id,
|
||||
@@ -29762,6 +30048,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
if (!this.isTileVisible(anchor.x, anchor.y)) {
|
||||
this.showBattleSoundCaption('objective', lines.filter(Boolean).join(' · '), [anchor]);
|
||||
this.extendBattleFeedbackReadableWindow(
|
||||
2020 + battleFeedbackSettlePaddingMs
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29785,6 +30074,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (this.mapMask) {
|
||||
popup.setMask(this.mapMask);
|
||||
}
|
||||
this.mapResultPopupObjects =
|
||||
this.mapResultPopupObjects.filter(
|
||||
(object) => object.active
|
||||
);
|
||||
this.mapResultPopupObjects.push(popup);
|
||||
this.mapResultPopupPeakCount = Math.max(
|
||||
this.mapResultPopupPeakCount,
|
||||
this.mapResultPopupObjects.length
|
||||
);
|
||||
this.tweens.add({
|
||||
targets: popup,
|
||||
y: popup.y - 26,
|
||||
@@ -29792,8 +30090,20 @@ export class BattleScene extends Phaser.Scene {
|
||||
duration: 880,
|
||||
delay: 80,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => popup.destroy()
|
||||
onComplete: () => {
|
||||
this.mapResultPopupObjects =
|
||||
this.mapResultPopupObjects.filter(
|
||||
(object) =>
|
||||
object !== popup && object.active
|
||||
);
|
||||
if (popup.active) {
|
||||
popup.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.extendBattleFeedbackReadableWindow(
|
||||
960 + battleFeedbackSettlePaddingMs
|
||||
);
|
||||
}
|
||||
|
||||
private objectiveFeedbackAnchor(objective: BattleObjectiveDefinition, state: BattleObjectiveState) {
|
||||
@@ -34230,6 +34540,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
turnNumber: this.turnNumber,
|
||||
activeFaction: this.activeFaction,
|
||||
phase: this.phase,
|
||||
autosave: {
|
||||
ready: this.battleAutosaveReady,
|
||||
safeCheckpoint: this.isSafeBattleAutosaveCheckpoint(),
|
||||
writeCount: this.battleAutosaveWriteCount,
|
||||
skipCount: this.battleAutosaveSkipCount,
|
||||
failureCount: this.battleAutosaveFailureCount,
|
||||
lastResult: this.battleAutosaveLastResult ?? null,
|
||||
lastReason: this.battleAutosaveLastReason ?? null,
|
||||
lastSlot: this.battleAutosaveLastSlot ?? null,
|
||||
lastSavedAt: this.battleAutosaveLastSavedAt ?? null
|
||||
},
|
||||
deploymentKeyboard: this.deploymentKeyboardDebugState(),
|
||||
combatAssets: {
|
||||
status: this.scenarioCombatAssetStatus,
|
||||
@@ -34668,6 +34989,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
return false;
|
||||
}
|
||||
this.triggerFirstEngagementEvent(attacker, target);
|
||||
this.showNextBattleEvent();
|
||||
return this.isBattleEventKnown(firstBattleVolunteerPromiseEventKey);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user