feat: guide early stays and remember player choices
This commit is contained in:
353
src/game/data/xuzhouStayNarrativeMemory.ts
Normal file
353
src/game/data/xuzhouStayNarrativeMemory.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import type { StoryPage } from './scenario';
|
||||
import {
|
||||
normalizeXuzhouStayBattlePayoffSnapshot,
|
||||
resolveXuzhouStayBattlePayoff,
|
||||
xuzhouStayBattlePayoffTargetBattleId,
|
||||
xuzhouStayMiZhuUnitId,
|
||||
xuzhouStayShareStorehouseDutyChoiceId,
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId,
|
||||
type XuzhouStayBattlePayoffCampaignState,
|
||||
type XuzhouStayBattlePayoffChoiceId,
|
||||
type XuzhouStayBattlePayoffSnapshot
|
||||
} from './xuzhouStayBattlePayoff';
|
||||
|
||||
export const xuzhouStayNarrativeIntroPageId =
|
||||
'eighth-xuzhou-stay-memory-intro' as const;
|
||||
export const xuzhouStayNarrativeAftermathPageId =
|
||||
'eighth-xuzhou-stay-memory-aftermath' as const;
|
||||
|
||||
const xuzhouStayNarrativeIntroAnchorPageId =
|
||||
'eighth-xiaopei-road';
|
||||
const xuzhouStayNarrativeAftermathAnchorPageId =
|
||||
'eighth-victory-supply-road';
|
||||
|
||||
export type XuzhouStayNarrativeStage = 'story' | 'aftermath';
|
||||
export type XuzhouStayNarrativeMiZhuStatus =
|
||||
| 'deployed'
|
||||
| 'not-deployed'
|
||||
| 'retreated';
|
||||
export type XuzhouStayNarrativeSupportOutcome =
|
||||
| 'used'
|
||||
| 'unused'
|
||||
| 'unavailable';
|
||||
|
||||
export type XuzhouStayNarrativeHistoricalUnit = {
|
||||
unitId?: unknown;
|
||||
hp?: unknown;
|
||||
};
|
||||
|
||||
export type XuzhouStayNarrativeHistoricalReport = {
|
||||
battleId?: unknown;
|
||||
outcome?: unknown;
|
||||
units?: readonly XuzhouStayNarrativeHistoricalUnit[];
|
||||
xuzhouStayBattlePayoff?: unknown;
|
||||
};
|
||||
|
||||
export type XuzhouStayNarrativeMemory = {
|
||||
source: 'campaign' | 'historical-payoff';
|
||||
stage: XuzhouStayNarrativeStage;
|
||||
battleId: typeof xuzhouStayBattlePayoffTargetBattleId;
|
||||
informationCompleted: boolean;
|
||||
choiceId: XuzhouStayBattlePayoffChoiceId | null;
|
||||
informationCounterplays: number | null;
|
||||
miZhuStatus: XuzhouStayNarrativeMiZhuStatus | null;
|
||||
supportOutcome: XuzhouStayNarrativeSupportOutcome | null;
|
||||
bonusHealing: number;
|
||||
bonusBuffTurns: number;
|
||||
appliedPageIds: string[];
|
||||
};
|
||||
|
||||
export type XuzhouStayNarrativeResolution = {
|
||||
pages: StoryPage[];
|
||||
memory?: XuzhouStayNarrativeMemory;
|
||||
};
|
||||
|
||||
export type XuzhouStayNarrativeResolutionOptions = {
|
||||
battleId?: unknown;
|
||||
stage: XuzhouStayNarrativeStage;
|
||||
campaign?: XuzhouStayBattlePayoffCampaignState | null;
|
||||
historicalReport?: XuzhouStayNarrativeHistoricalReport | null;
|
||||
};
|
||||
|
||||
export function resolveXuzhouStayNarrativeMemory(
|
||||
sourcePages: readonly StoryPage[],
|
||||
options: XuzhouStayNarrativeResolutionOptions
|
||||
): XuzhouStayNarrativeResolution {
|
||||
const pages = sourcePages.map((page) => ({ ...page }));
|
||||
if (
|
||||
options.battleId !==
|
||||
xuzhouStayBattlePayoffTargetBattleId
|
||||
) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
return options.stage === 'story'
|
||||
? resolveIntroNarrative(pages, options.campaign)
|
||||
: resolveAftermathNarrative(
|
||||
pages,
|
||||
options.historicalReport
|
||||
);
|
||||
}
|
||||
|
||||
function resolveIntroNarrative(
|
||||
pages: StoryPage[],
|
||||
campaign?: XuzhouStayBattlePayoffCampaignState | null
|
||||
): XuzhouStayNarrativeResolution {
|
||||
const payoff = resolveXuzhouStayBattlePayoff(
|
||||
campaign,
|
||||
xuzhouStayBattlePayoffTargetBattleId
|
||||
);
|
||||
if (!payoff) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
const page = introPage(
|
||||
payoff.informationCompleted,
|
||||
payoff.choiceId
|
||||
);
|
||||
if (
|
||||
!insertAfter(
|
||||
pages,
|
||||
xuzhouStayNarrativeIntroAnchorPageId,
|
||||
page
|
||||
)
|
||||
) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
return {
|
||||
pages,
|
||||
memory: {
|
||||
source: 'campaign',
|
||||
stage: 'story',
|
||||
battleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationCompleted: payoff.informationCompleted,
|
||||
choiceId: payoff.choiceId ?? null,
|
||||
informationCounterplays: null,
|
||||
miZhuStatus: null,
|
||||
supportOutcome: null,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 0,
|
||||
appliedPageIds: [xuzhouStayNarrativeIntroPageId]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAftermathNarrative(
|
||||
pages: StoryPage[],
|
||||
report?: XuzhouStayNarrativeHistoricalReport | null
|
||||
): XuzhouStayNarrativeResolution {
|
||||
if (
|
||||
report?.battleId !==
|
||||
xuzhouStayBattlePayoffTargetBattleId ||
|
||||
report.outcome !== 'victory'
|
||||
) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
const payoff = normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
report.xuzhouStayBattlePayoff,
|
||||
{
|
||||
expectedBattleId:
|
||||
xuzhouStayBattlePayoffTargetBattleId
|
||||
}
|
||||
);
|
||||
if (!payoff) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
const miZhuStatus = historicalMiZhuStatus(payoff, report);
|
||||
const supportOutcome = historicalSupportOutcome(
|
||||
payoff,
|
||||
miZhuStatus
|
||||
);
|
||||
const page = aftermathPage(
|
||||
payoff,
|
||||
miZhuStatus,
|
||||
supportOutcome
|
||||
);
|
||||
if (
|
||||
!insertAfter(
|
||||
pages,
|
||||
xuzhouStayNarrativeAftermathAnchorPageId,
|
||||
page
|
||||
)
|
||||
) {
|
||||
return { pages };
|
||||
}
|
||||
|
||||
return {
|
||||
pages,
|
||||
memory: {
|
||||
source: 'historical-payoff',
|
||||
stage: 'aftermath',
|
||||
battleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationCompleted: payoff.informationCompleted,
|
||||
choiceId: payoff.choiceId ?? null,
|
||||
informationCounterplays:
|
||||
payoff.informationCounterplays,
|
||||
miZhuStatus: payoff.choiceId ? miZhuStatus : null,
|
||||
supportOutcome: payoff.choiceId
|
||||
? supportOutcome
|
||||
: null,
|
||||
bonusHealing: payoff.bonusHealing,
|
||||
bonusBuffTurns: payoff.bonusBuffTurns,
|
||||
appliedPageIds: [
|
||||
xuzhouStayNarrativeAftermathPageId
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function introPage(
|
||||
informationCompleted: boolean,
|
||||
choiceId?: XuzhouStayBattlePayoffChoiceId
|
||||
): StoryPage {
|
||||
const informationLine = informationCompleted
|
||||
? '서주에서 함께 살핀 장부 덕분에 적이 노릴 길목을 미리 짚을 수 있습니다.'
|
||||
: '';
|
||||
const choiceLine =
|
||||
choiceId === xuzhouStayShareStorehouseDutyChoiceId
|
||||
? '창고 당번을 함께 나누기로 한 뜻대로, 저도 전열에서 다친 이들을 먼저 돌보겠습니다.'
|
||||
: choiceId ===
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
? '장부를 제게 맡겨 주신 뜻대로, 전열이 흔들릴 때 병사들을 독려하겠습니다.'
|
||||
: '장부에서 확인한 움직임을 출진 장수들에게 빠짐없이 전하겠습니다.';
|
||||
|
||||
return {
|
||||
id: xuzhouStayNarrativeIntroPageId,
|
||||
bgm: 'battle-prep',
|
||||
chapter: '서주에서 세운 방침',
|
||||
background: 'story-xuzhou',
|
||||
speaker: '미축',
|
||||
portrait: 'mi-zhu',
|
||||
text: [informationLine, choiceLine]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
};
|
||||
}
|
||||
|
||||
function aftermathPage(
|
||||
payoff: XuzhouStayBattlePayoffSnapshot,
|
||||
miZhuStatus: XuzhouStayNarrativeMiZhuStatus,
|
||||
supportOutcome: XuzhouStayNarrativeSupportOutcome
|
||||
): StoryPage {
|
||||
const informationLine = payoff.informationCompleted
|
||||
? payoff.informationCounterplays > 0
|
||||
? `서주 장부에서 짚은 움직임 ${payoff.informationCounterplays}곳을 파훼해 보급로의 빈틈을 막았습니다.`
|
||||
: '서주 장부를 전장에 가져왔지만, 짚어 둔 움직임을 직접 파훼할 기회는 없었습니다.'
|
||||
: '';
|
||||
const supportLine = payoff.choiceId
|
||||
? supportResultLine(payoff, miZhuStatus, supportOutcome)
|
||||
: '';
|
||||
|
||||
return {
|
||||
id: xuzhouStayNarrativeAftermathPageId,
|
||||
bgm: 'militia-theme',
|
||||
chapter: '되살아난 서주의 약속',
|
||||
background: 'story-xuzhou',
|
||||
speaker: '미축',
|
||||
portrait: 'mi-zhu',
|
||||
text: [informationLine, supportLine]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
};
|
||||
}
|
||||
|
||||
function supportResultLine(
|
||||
payoff: XuzhouStayBattlePayoffSnapshot,
|
||||
miZhuStatus: XuzhouStayNarrativeMiZhuStatus,
|
||||
supportOutcome: XuzhouStayNarrativeSupportOutcome
|
||||
) {
|
||||
if (miZhuStatus === 'not-deployed') {
|
||||
return '저는 이번 출전에 함께하지 못해, 서주에서 정한 방침을 전장에 보태지 못했습니다.';
|
||||
}
|
||||
if (
|
||||
miZhuStatus === 'retreated' &&
|
||||
supportOutcome !== 'used'
|
||||
) {
|
||||
return '제가 전장에서 물러나는 바람에, 서주에서 정한 방침을 끝까지 펼치지 못했습니다.';
|
||||
}
|
||||
if (
|
||||
payoff.choiceId ===
|
||||
xuzhouStayShareStorehouseDutyChoiceId
|
||||
) {
|
||||
if (supportOutcome === 'used') {
|
||||
const retreatLine =
|
||||
miZhuStatus === 'retreated'
|
||||
? ' 비록 뒤에는 물러났지만,'
|
||||
: '';
|
||||
return `공동 구휼의 약속으로 응급 회복을 ${payoff.bonusHealing}만큼 더 보탰습니다.${retreatLine} 백성과 병사가 함께 창고를 지킨 뜻은 남았습니다.`;
|
||||
}
|
||||
return '공동 구휼의 방침을 쓸 기회는 없었지만, 다음 보급에도 백성과 병사의 몫을 함께 살피겠습니다.';
|
||||
}
|
||||
if (
|
||||
payoff.choiceId ===
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
) {
|
||||
if (supportOutcome === 'used') {
|
||||
const retreatLine =
|
||||
miZhuStatus === 'retreated'
|
||||
? ' 비록 뒤에는 물러났지만,'
|
||||
: '';
|
||||
return `맡겨 주신 장부로 격려의 기세를 ${payoff.bonusBuffTurns}턴 더 이어 냈습니다.${retreatLine} 믿고 맡긴 뜻에 보답할 수 있었습니다.`;
|
||||
}
|
||||
return '장부 위임의 방침을 쓸 기회는 없었지만, 다음에는 전열의 흔들림을 더 일찍 살피겠습니다.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function historicalMiZhuStatus(
|
||||
payoff: XuzhouStayBattlePayoffSnapshot,
|
||||
report: XuzhouStayNarrativeHistoricalReport
|
||||
): XuzhouStayNarrativeMiZhuStatus {
|
||||
if (!payoff.miZhuDeployed) {
|
||||
return 'not-deployed';
|
||||
}
|
||||
const unit = Array.isArray(report.units)
|
||||
? report.units.find(
|
||||
(candidate) =>
|
||||
candidate?.unitId === xuzhouStayMiZhuUnitId
|
||||
)
|
||||
: undefined;
|
||||
return unit && normalizedHp(unit.hp) <= 0
|
||||
? 'retreated'
|
||||
: 'deployed';
|
||||
}
|
||||
|
||||
function historicalSupportOutcome(
|
||||
payoff: XuzhouStayBattlePayoffSnapshot,
|
||||
miZhuStatus: XuzhouStayNarrativeMiZhuStatus
|
||||
): XuzhouStayNarrativeSupportOutcome {
|
||||
if (miZhuStatus === 'not-deployed') {
|
||||
return 'unavailable';
|
||||
}
|
||||
return payoff.supportUses > 0 ? 'used' : 'unused';
|
||||
}
|
||||
|
||||
function normalizedHp(value: unknown) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? Math.max(0, value)
|
||||
: 1;
|
||||
}
|
||||
|
||||
function insertAfter(
|
||||
pages: StoryPage[],
|
||||
anchorPageId: string,
|
||||
page: StoryPage
|
||||
) {
|
||||
if (pages.some((candidate) => candidate.id === page.id)) {
|
||||
return false;
|
||||
}
|
||||
const anchorIndex = pages.findIndex(
|
||||
(candidate) => candidate.id === anchorPageId
|
||||
);
|
||||
if (
|
||||
anchorIndex < 0 ||
|
||||
anchorIndex >= pages.length - 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
pages.splice(anchorIndex + 1, 0, page);
|
||||
return true;
|
||||
}
|
||||
@@ -1673,6 +1673,33 @@ type TurnEndPromptOptions = {
|
||||
secondaryLabel?: string;
|
||||
primaryAction?: () => void;
|
||||
secondaryAction?: () => void;
|
||||
initialFocus?: TurnPromptActionId;
|
||||
cancelActionId?: TurnPromptActionId;
|
||||
primaryMeaning?: TurnPromptActionMeaning;
|
||||
secondaryMeaning?: TurnPromptActionMeaning;
|
||||
primaryDangerous?: boolean;
|
||||
secondaryDangerous?: boolean;
|
||||
};
|
||||
|
||||
type TurnPromptActionId = 'primary' | 'secondary';
|
||||
|
||||
type TurnPromptActionMeaning =
|
||||
| 'end-turn'
|
||||
| 'review-battlefield'
|
||||
| 'continue-command'
|
||||
| 'abandon-unacted-and-end'
|
||||
| 'overwrite-save'
|
||||
| 'load-save'
|
||||
| 'cancel';
|
||||
|
||||
type TurnPromptButtonView = {
|
||||
id: TurnPromptActionId;
|
||||
label: string;
|
||||
meaning: TurnPromptActionMeaning;
|
||||
dangerous: boolean;
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
text: Phaser.GameObjects.Text;
|
||||
hint: Phaser.GameObjects.Text;
|
||||
};
|
||||
|
||||
type DeploymentSlot = {
|
||||
@@ -3857,6 +3884,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
private turnPromptMode?: TurnPromptMode;
|
||||
private turnPromptPrimaryAction?: () => void;
|
||||
private turnPromptSecondaryAction?: () => void;
|
||||
private turnPromptTitle?: string;
|
||||
private turnPromptBody?: string;
|
||||
private turnPromptFocusedAction?: TurnPromptActionId;
|
||||
private turnPromptCancelActionId?: TurnPromptActionId;
|
||||
private turnPromptButtonViews: TurnPromptButtonView[] = [];
|
||||
private mapResultPopupObjects: Phaser.GameObjects.Text[] = [];
|
||||
private mapResultPopupPeakCount = 0;
|
||||
private mapResultPopupLast?: {
|
||||
@@ -4051,6 +4083,11 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.turnPromptMode = undefined;
|
||||
this.turnPromptPrimaryAction = undefined;
|
||||
this.turnPromptSecondaryAction = undefined;
|
||||
this.turnPromptTitle = undefined;
|
||||
this.turnPromptBody = undefined;
|
||||
this.turnPromptFocusedAction = undefined;
|
||||
this.turnPromptCancelActionId = undefined;
|
||||
this.turnPromptButtonViews = [];
|
||||
this.turnText = undefined;
|
||||
this.battleTitleText = undefined;
|
||||
this.objectiveTrackerText = undefined;
|
||||
@@ -4315,7 +4352,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
if (this.turnPromptObjects.length > 0) {
|
||||
this.activateTurnPromptPrimaryAction();
|
||||
this.activateTurnPromptFocusedAction();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4351,7 +4388,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
if (this.turnPromptObjects.length > 0) {
|
||||
this.activateTurnPromptSecondaryAction();
|
||||
this.activateTurnPromptCancelAction();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4403,6 +4440,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (this.battleEventObjects.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (this.handleTurnPromptNavigationKey(event)) {
|
||||
return;
|
||||
}
|
||||
if (this.handleSaveSlotPanelKey(event)) {
|
||||
return;
|
||||
}
|
||||
@@ -10444,6 +10484,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (!this.enemyIntentForecastEnabled()) {
|
||||
return '아군 턴을 종료하시겠습니까?';
|
||||
}
|
||||
return `${this.enemyIntentTurnEndSummary()}\n이대로 턴을 종료할까요?`;
|
||||
}
|
||||
|
||||
private enemyIntentTurnEndSummary() {
|
||||
if (!this.enemyIntentForecastEnabled()) {
|
||||
return '적 예상 정보 없음';
|
||||
}
|
||||
const groups = new Map<string, { target: UnitData; immediate: number; approach: number }>();
|
||||
const plans = this.enemyIntentForecasts.size > 0
|
||||
? Array.from(this.enemyIntentForecasts.values())
|
||||
@@ -10460,13 +10507,16 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
groups.set(plan.target.id, group);
|
||||
});
|
||||
const summaries = Array.from(groups.values())
|
||||
.sort((left, right) => right.immediate - left.immediate || right.approach - left.approach)
|
||||
.slice(0, 2)
|
||||
.map((group) => `${group.target.name} 공${group.immediate}·추${group.approach}`);
|
||||
const sortedGroups = Array.from(groups.values())
|
||||
.sort((left, right) => right.immediate - left.immediate || right.approach - left.approach);
|
||||
const visibleGroups = sortedGroups.slice(0, 2);
|
||||
const omittedCount = Math.max(0, sortedGroups.length - visibleGroups.length);
|
||||
const summaries = visibleGroups.map(
|
||||
(group) => `${group.target.name} 공${group.immediate}·추${group.approach}`
|
||||
);
|
||||
return summaries.length > 0
|
||||
? `적 예상 · ${summaries.join(' · ')}\n이대로 턴을 종료할까요?`
|
||||
: '적 예상 · 즉시 공격 없음\n이대로 턴을 종료할까요?';
|
||||
? `적 예상 · ${summaries.join(' · ')}${omittedCount > 0 ? ` · 외 ${omittedCount}명` : ''}`
|
||||
: '적 예상 · 즉시 공격 없음';
|
||||
}
|
||||
|
||||
private enemyIntentTargetCounts(unitId: string) {
|
||||
@@ -20495,12 +20545,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
if (action === 'endTurn') {
|
||||
this.showTurnEndPrompt(undefined, {
|
||||
title: '턴 종료 확인',
|
||||
body: this.enemyIntentTurnEndBody(),
|
||||
primaryLabel: '턴 종료',
|
||||
secondaryLabel: '계속 지휘'
|
||||
});
|
||||
this.showManualTurnEndPrompt();
|
||||
return;
|
||||
}
|
||||
if (action === 'threat') {
|
||||
@@ -20544,6 +20589,38 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
private showManualTurnEndPrompt() {
|
||||
const unactedAllies = this.actionableAllies();
|
||||
if (unactedAllies.length === 0) {
|
||||
this.showTurnEndPrompt(undefined, {
|
||||
title: '턴 종료 확인',
|
||||
body: this.enemyIntentTurnEndBody(),
|
||||
primaryLabel: '턴 종료',
|
||||
secondaryLabel: '계속 지휘',
|
||||
primaryMeaning: 'end-turn',
|
||||
secondaryMeaning: 'continue-command'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleNames = unactedAllies.slice(0, 4).map((unit) => unit.name);
|
||||
const omittedNames = Math.max(0, unactedAllies.length - visibleNames.length);
|
||||
const unactedNames = `${visibleNames.join(' · ')}${omittedNames > 0 ? ` 외 ${omittedNames}명` : ''}`;
|
||||
this.showTurnEndPrompt(undefined, {
|
||||
title: `미행동 ${unactedAllies.length}명 · 턴 종료 확인`,
|
||||
body: `미행동 장수 · ${unactedNames}\n${this.enemyIntentTurnEndSummary()}\n행동을 포기하고 턴을 종료할까요?`,
|
||||
primaryLabel: '계속 지휘',
|
||||
secondaryLabel: '미행동 포기 후 종료',
|
||||
primaryAction: () => this.hideTurnEndPrompt(),
|
||||
secondaryAction: () => this.endAllyTurn(),
|
||||
initialFocus: 'primary',
|
||||
cancelActionId: 'primary',
|
||||
primaryMeaning: 'continue-command',
|
||||
secondaryMeaning: 'abandon-unacted-and-end',
|
||||
secondaryDangerous: true
|
||||
});
|
||||
}
|
||||
|
||||
private hideMapMenu() {
|
||||
this.mapMenuObjects.forEach((object) => object.destroy());
|
||||
this.mapMenuObjects = [];
|
||||
@@ -27760,6 +27837,32 @@ export class BattleScene extends Phaser.Scene {
|
||||
private showTurnEndPrompt(message?: string, options: TurnEndPromptOptions = {}) {
|
||||
this.hideTurnEndPrompt();
|
||||
this.turnPromptMode = options.mode ?? 'turn-end';
|
||||
const promptTitle = options.title ?? '모든 아군의 행동이 종료되었습니다.';
|
||||
const promptBody = options.body ?? this.enemyIntentTurnEndBody();
|
||||
const primaryLabel = options.primaryLabel ?? '턴 종료';
|
||||
const secondaryLabel = options.secondaryLabel ?? '전장 확인';
|
||||
const primaryAction = options.primaryAction ?? (() => this.endAllyTurn());
|
||||
const secondaryAction = options.secondaryAction ?? (() => this.hideTurnEndPrompt());
|
||||
const primaryMeaning = options.primaryMeaning ?? (
|
||||
this.turnPromptMode === 'save-overwrite-confirm'
|
||||
? 'overwrite-save'
|
||||
: this.turnPromptMode === 'load-confirm'
|
||||
? 'load-save'
|
||||
: 'end-turn'
|
||||
);
|
||||
const secondaryMeaning = options.secondaryMeaning ?? (
|
||||
this.turnPromptMode === 'turn-end' ? 'review-battlefield' : 'cancel'
|
||||
);
|
||||
this.turnPromptTitle = promptTitle;
|
||||
this.turnPromptBody = promptBody;
|
||||
this.turnPromptPrimaryAction = primaryAction;
|
||||
this.turnPromptSecondaryAction = secondaryAction;
|
||||
this.turnPromptFocusedAction = options.initialFocus ?? 'primary';
|
||||
this.turnPromptCancelActionId = options.cancelActionId ?? 'secondary';
|
||||
const expandedPrompt = promptBody.split('\n').length >= 3;
|
||||
const promptHeight = expandedPrompt ? 202 : 170;
|
||||
const buttonY = promptHeight - 46;
|
||||
const hintY = promptHeight - 19;
|
||||
|
||||
const shade = this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x03070b, 0.46);
|
||||
shade.setOrigin(0);
|
||||
@@ -27777,7 +27880,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.turnPromptObjects.push(shade);
|
||||
|
||||
const width = this.battleUiLength(388);
|
||||
const height = this.battleUiLength(162);
|
||||
const height = this.battleUiLength(promptHeight);
|
||||
const left = this.layout.mapX + this.layout.mapWidth / 2 - width / 2;
|
||||
const top = this.layout.mapY + this.layout.mapHeight / 2 - height / 2;
|
||||
const panel = this.add.rectangle(left, top, width, height, 0x101821, 0.96);
|
||||
@@ -27786,7 +27889,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
panel.setStrokeStyle(this.battleUiLength(2), palette.gold, 0.9);
|
||||
this.turnPromptObjects.push(panel);
|
||||
|
||||
const title = this.add.text(left + width / 2, top + this.battleUiLength(26), options.title ?? '모든 아군의 행동이 종료되었습니다.', {
|
||||
const title = this.add.text(left + width / 2, top + this.battleUiLength(26), promptTitle, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(20),
|
||||
color: '#f2e3bf',
|
||||
@@ -27796,79 +27899,136 @@ export class BattleScene extends Phaser.Scene {
|
||||
title.setDepth(41);
|
||||
this.turnPromptObjects.push(title);
|
||||
|
||||
const body = this.add.text(left + width / 2, top + this.battleUiLength(58), options.body ?? this.enemyIntentTurnEndBody(), {
|
||||
const body = this.add.text(left + width / 2, top + this.battleUiLength(52), promptBody, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(17),
|
||||
color: '#d4dce6',
|
||||
align: 'center',
|
||||
wordWrap: { width: width - this.battleUiLength(44), useAdvancedWrap: true }
|
||||
});
|
||||
body.setOrigin(0.5);
|
||||
body.setOrigin(0.5, 0);
|
||||
body.setDepth(41);
|
||||
this.turnPromptObjects.push(body);
|
||||
|
||||
const primaryLabel = options.primaryLabel ?? '턴 종료';
|
||||
const secondaryLabel = options.secondaryLabel ?? '전장 확인';
|
||||
const primaryAction = options.primaryAction ?? (() => this.endAllyTurn());
|
||||
const secondaryAction = options.secondaryAction ?? (() => this.hideTurnEndPrompt());
|
||||
this.turnPromptPrimaryAction = primaryAction;
|
||||
this.turnPromptSecondaryAction = secondaryAction;
|
||||
const buttonWidth = this.battleUiLength(136);
|
||||
const buttons = [
|
||||
{ label: primaryLabel, keyHint: 'Enter', x: left + this.battleUiLength(119), primary: true },
|
||||
{ label: secondaryLabel, keyHint: 'Esc', x: left + this.battleUiLength(269), primary: false }
|
||||
{
|
||||
id: 'primary' as const,
|
||||
label: primaryLabel,
|
||||
meaning: primaryMeaning,
|
||||
dangerous: options.primaryDangerous ?? false,
|
||||
x: left + this.battleUiLength(119)
|
||||
},
|
||||
{
|
||||
id: 'secondary' as const,
|
||||
label: secondaryLabel,
|
||||
meaning: secondaryMeaning,
|
||||
dangerous: options.secondaryDangerous ?? false,
|
||||
x: left + this.battleUiLength(269)
|
||||
}
|
||||
];
|
||||
|
||||
buttons.forEach(({ label, keyHint, x, primary }) => {
|
||||
const bg = this.add.rectangle(x, top + this.battleUiLength(116), buttonWidth, this.battleUiLength(36), 0x1a2630, 0.95);
|
||||
buttons.forEach(({ id, label, meaning, dangerous, x }) => {
|
||||
const bg = this.add.rectangle(x, top + this.battleUiLength(buttonY), buttonWidth, this.battleUiLength(36), 0x1a2630, 0.95);
|
||||
bg.setDepth(41);
|
||||
bg.setStrokeStyle(this.battleUiLength(1), primary ? palette.gold : palette.blue, 0.72);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.95));
|
||||
bg.on('pointerdown', () => (primary ? this.activateTurnPromptPrimaryAction(true) : this.activateTurnPromptSecondaryAction(true)));
|
||||
bg.on('pointerover', () => this.focusTurnPromptAction(id));
|
||||
bg.on('pointerdown', () => this.activateTurnPromptAction(id, true));
|
||||
this.turnPromptObjects.push(bg);
|
||||
|
||||
const text = this.add.text(x, top + this.battleUiLength(116), label, {
|
||||
const text = this.add.text(x, top + this.battleUiLength(buttonY), label, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(16),
|
||||
fontSize: this.battleUiFontSize(label.length > 9 ? 13 : 16),
|
||||
color: '#f2e3bf',
|
||||
fontStyle: '700'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(42);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
text.on('pointerdown', () => (primary ? this.activateTurnPromptPrimaryAction(true) : this.activateTurnPromptSecondaryAction(true)));
|
||||
text.on('pointerover', () => this.focusTurnPromptAction(id));
|
||||
text.on('pointerdown', () => this.activateTurnPromptAction(id, true));
|
||||
this.turnPromptObjects.push(text);
|
||||
|
||||
const hint = this.add.text(x, top + this.battleUiLength(143), keyHint, {
|
||||
const hint = this.add.text(x, top + this.battleUiLength(hintY), '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(10),
|
||||
color: primary ? '#cdbb8a' : '#91a7bd',
|
||||
color: '#91a7bd',
|
||||
fontStyle: '700'
|
||||
});
|
||||
hint.setOrigin(0.5);
|
||||
hint.setDepth(42);
|
||||
this.turnPromptObjects.push(hint);
|
||||
this.turnPromptButtonViews.push({ id, label, meaning, dangerous, background: bg, text, hint });
|
||||
});
|
||||
this.renderTurnPromptFocus();
|
||||
|
||||
void message;
|
||||
}
|
||||
|
||||
private activateTurnPromptPrimaryAction(fromPointer = false) {
|
||||
if (!this.turnPromptPrimaryAction) {
|
||||
return;
|
||||
private handleTurnPromptNavigationKey(event: KeyboardEvent) {
|
||||
if (this.turnPromptObjects.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const requestedFocus =
|
||||
event.code === 'ArrowLeft'
|
||||
? 'primary'
|
||||
: event.code === 'ArrowRight'
|
||||
? 'secondary'
|
||||
: event.code === 'Tab'
|
||||
? this.turnPromptFocusedAction === 'primary' && !event.shiftKey
|
||||
? 'secondary'
|
||||
: 'primary'
|
||||
: undefined;
|
||||
if (!requestedFocus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fromPointer) {
|
||||
this.suppressNextLeftClick = true;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.focusTurnPromptAction(requestedFocus);
|
||||
soundDirector.playSelect();
|
||||
this.turnPromptPrimaryAction();
|
||||
return true;
|
||||
}
|
||||
|
||||
private activateTurnPromptSecondaryAction(fromPointer = false) {
|
||||
const action = this.turnPromptSecondaryAction ?? (() => this.hideTurnEndPrompt());
|
||||
private focusTurnPromptAction(actionId: TurnPromptActionId) {
|
||||
if (this.turnPromptObjects.length === 0 || this.turnPromptFocusedAction === actionId) {
|
||||
return;
|
||||
}
|
||||
this.turnPromptFocusedAction = actionId;
|
||||
this.renderTurnPromptFocus();
|
||||
}
|
||||
|
||||
private renderTurnPromptFocus() {
|
||||
this.turnPromptButtonViews.forEach((view) => {
|
||||
const focused = this.turnPromptFocusedAction === view.id;
|
||||
const cancel = this.turnPromptCancelActionId === view.id;
|
||||
const tone = view.dangerous ? 0xd46a54 : view.id === 'primary' ? palette.gold : palette.blue;
|
||||
view.background.setFillStyle(
|
||||
focused ? (view.dangerous ? 0x4a2320 : 0x283947) : 0x1a2630,
|
||||
focused ? 0.99 : 0.95
|
||||
);
|
||||
view.background.setStrokeStyle(this.battleUiLength(focused ? 2 : 1), tone, focused ? 1 : 0.72);
|
||||
view.text.setColor(focused ? '#fff8df' : '#d4dce6');
|
||||
view.hint
|
||||
.setText(focused ? (cancel ? 'Enter · Esc' : 'Enter') : cancel ? 'Esc' : '←/→ · Tab')
|
||||
.setColor(view.dangerous ? '#ffb6a6' : cancel ? '#cdbb8a' : '#91a7bd');
|
||||
});
|
||||
}
|
||||
|
||||
private activateTurnPromptFocusedAction() {
|
||||
this.activateTurnPromptAction(this.turnPromptFocusedAction ?? 'primary');
|
||||
}
|
||||
|
||||
private activateTurnPromptCancelAction() {
|
||||
this.activateTurnPromptAction(this.turnPromptCancelActionId ?? 'secondary');
|
||||
}
|
||||
|
||||
private activateTurnPromptAction(actionId: TurnPromptActionId, fromPointer = false) {
|
||||
const action = actionId === 'primary'
|
||||
? this.turnPromptPrimaryAction
|
||||
: this.turnPromptSecondaryAction;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
if (fromPointer) {
|
||||
this.suppressNextLeftClick = true;
|
||||
}
|
||||
@@ -27876,12 +28036,25 @@ export class BattleScene extends Phaser.Scene {
|
||||
action();
|
||||
}
|
||||
|
||||
private activateTurnPromptPrimaryAction(fromPointer = false) {
|
||||
this.activateTurnPromptAction('primary', fromPointer);
|
||||
}
|
||||
|
||||
private activateTurnPromptSecondaryAction(fromPointer = false) {
|
||||
this.activateTurnPromptAction('secondary', fromPointer);
|
||||
}
|
||||
|
||||
private hideTurnEndPrompt() {
|
||||
this.turnPromptObjects.forEach((object) => object.destroy());
|
||||
this.turnPromptObjects = [];
|
||||
this.turnPromptMode = undefined;
|
||||
this.turnPromptPrimaryAction = undefined;
|
||||
this.turnPromptSecondaryAction = undefined;
|
||||
this.turnPromptTitle = undefined;
|
||||
this.turnPromptBody = undefined;
|
||||
this.turnPromptFocusedAction = undefined;
|
||||
this.turnPromptCancelActionId = undefined;
|
||||
this.turnPromptButtonViews = [];
|
||||
}
|
||||
|
||||
private pushBattleLog(message: string) {
|
||||
@@ -31821,6 +31994,23 @@ export class BattleScene extends Phaser.Scene {
|
||||
: null,
|
||||
turnPromptVisible: this.turnPromptObjects.length > 0,
|
||||
turnPromptMode: this.turnPromptMode ?? null,
|
||||
turnPrompt: this.turnPromptObjects.length > 0
|
||||
? {
|
||||
mode: this.turnPromptMode ?? null,
|
||||
title: this.turnPromptTitle ?? '',
|
||||
body: this.turnPromptBody ?? '',
|
||||
focusedAction: this.turnPromptFocusedAction ?? null,
|
||||
cancelAction: this.turnPromptCancelActionId ?? null,
|
||||
buttons: this.turnPromptButtonViews.map((view) => ({
|
||||
id: view.id,
|
||||
label: view.label,
|
||||
meaning: view.meaning,
|
||||
dangerous: view.dangerous,
|
||||
focused: this.turnPromptFocusedAction === view.id,
|
||||
escapeCancels: this.turnPromptCancelActionId === view.id
|
||||
}))
|
||||
}
|
||||
: null,
|
||||
mapResultFeedback: {
|
||||
activeCount: this.mapResultPopupObjects.filter((object) => object.active).length,
|
||||
peakCount: this.mapResultPopupPeakCount,
|
||||
|
||||
@@ -344,7 +344,7 @@ type CampTabButtonView = {
|
||||
|
||||
type VictoryRewardCardId = CampaignVictoryRewardCategoryId;
|
||||
|
||||
type VictoryRewardActionId = 'equipment' | 'supplies' | 'sortie' | 'close';
|
||||
type VictoryRewardActionId = 'equipment' | 'supplies' | 'guided' | 'sortie' | 'close';
|
||||
|
||||
type VictoryRewardIcon =
|
||||
| { kind: 'mark'; mark: string }
|
||||
@@ -368,6 +368,7 @@ type VictoryRewardActionView = {
|
||||
id: VictoryRewardActionId;
|
||||
label: string;
|
||||
button: Phaser.GameObjects.Rectangle;
|
||||
primary: boolean;
|
||||
};
|
||||
|
||||
type CampActionButtonView = {
|
||||
@@ -377,6 +378,23 @@ type CampActionButtonView = {
|
||||
hovered: boolean;
|
||||
};
|
||||
|
||||
type FirstCampGuidedAction =
|
||||
| {
|
||||
kind: 'visit';
|
||||
label: '북문 정찰막 둘러보기';
|
||||
targetId: typeof firstPursuitScoutVisitId;
|
||||
}
|
||||
| {
|
||||
kind: 'dialogue';
|
||||
label: string;
|
||||
targetId: string;
|
||||
}
|
||||
| {
|
||||
kind: 'sortie';
|
||||
label: '출진 준비';
|
||||
targetId: null;
|
||||
};
|
||||
|
||||
type EquipmentSwapRequest = {
|
||||
unitId: string;
|
||||
slot: EquipmentSlot;
|
||||
@@ -11517,6 +11535,9 @@ export class CampScene extends Phaser.Scene {
|
||||
private reportFormationHistoryUndoState?: ReportFormationHistoryUndoState;
|
||||
private tabButtons: CampTabButtonView[] = [];
|
||||
private sortieCommandButton?: Phaser.GameObjects.Rectangle;
|
||||
private sortieCommandLabel?: Phaser.GameObjects.Text;
|
||||
private sortieBypassButton?: Phaser.GameObjects.Rectangle;
|
||||
private sortieBypassLabel?: Phaser.GameObjects.Text;
|
||||
private sortiePrimaryActionButton?: CampActionButtonView;
|
||||
private sortieNextActionGuideBackground?: Phaser.GameObjects.Rectangle;
|
||||
private sortieNextActionGuideText?: Phaser.GameObjects.Text;
|
||||
@@ -11672,6 +11693,10 @@ export class CampScene extends Phaser.Scene {
|
||||
this.reportFormationHistoryView = undefined;
|
||||
this.reportFormationHistoryUndoState = undefined;
|
||||
this.tabButtons = [];
|
||||
this.sortieCommandButton = undefined;
|
||||
this.sortieCommandLabel = undefined;
|
||||
this.sortieBypassButton = undefined;
|
||||
this.sortieBypassLabel = undefined;
|
||||
this.sortiePrimaryActionButton = undefined;
|
||||
this.sortieNextActionGuideBackground = undefined;
|
||||
this.sortieNextActionGuideText = undefined;
|
||||
@@ -13025,6 +13050,85 @@ export class CampScene extends Phaser.Scene {
|
||||
);
|
||||
}
|
||||
|
||||
private firstCampGuidedAction(): FirstCampGuidedAction | undefined {
|
||||
if (
|
||||
!this.isFirstSortiePrepSlice() ||
|
||||
this.currentCampBattleId() !== campBattleIds.first
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.completedCampVisits().includes(firstPursuitScoutVisitId)) {
|
||||
return {
|
||||
kind: 'visit',
|
||||
label: '북문 정찰막 둘러보기',
|
||||
targetId: firstPursuitScoutVisitId
|
||||
};
|
||||
}
|
||||
const followup = this.firstBattleCampFollowup();
|
||||
if (
|
||||
followup &&
|
||||
!this.completedCampDialogues().includes(followup.targetDialogueId)
|
||||
) {
|
||||
return {
|
||||
kind: 'dialogue',
|
||||
label: `${followup.mentorName}와 핵심 회고`,
|
||||
targetId: followup.targetDialogueId
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'sortie',
|
||||
label: '출진 준비',
|
||||
targetId: null
|
||||
};
|
||||
}
|
||||
|
||||
private runFirstCampGuidedAction() {
|
||||
const action = this.firstCampGuidedAction();
|
||||
if (!action || action.kind === 'sortie') {
|
||||
this.showRequestedSortiePrep();
|
||||
return;
|
||||
}
|
||||
if (action.kind === 'visit') {
|
||||
this.activeTab = 'visit';
|
||||
this.selectedVisitId = action.targetId;
|
||||
this.render();
|
||||
this.startCampNavigation('CampVisitExplorationScene', { visitId: action.targetId });
|
||||
return;
|
||||
}
|
||||
this.activeTab = 'dialogue';
|
||||
this.selectedDialogueId = action.targetId;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private campPrimaryActionLabel(flow = this.currentSortieFlow()) {
|
||||
if (this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep) {
|
||||
return '엔딩 보기';
|
||||
}
|
||||
const guidedAction = this.firstCampGuidedAction();
|
||||
return guidedAction?.kind === 'visit'
|
||||
? '정찰막 둘러보기'
|
||||
: guidedAction?.label ?? '출진 준비';
|
||||
}
|
||||
|
||||
private refreshCampPrimaryActionPresentation() {
|
||||
const label = this.campPrimaryActionLabel();
|
||||
const guidedAction = this.firstCampGuidedAction();
|
||||
const bypassVisible = Boolean(guidedAction && guidedAction.kind !== 'sortie');
|
||||
this.sortieCommandLabel
|
||||
?.setText(label)
|
||||
.setFontSize(
|
||||
bypassVisible
|
||||
? Array.from(label).length > 7 ? 12 : 14
|
||||
: Array.from(label).length > 8 ? 13 : 16
|
||||
);
|
||||
[this.sortieBypassButton, this.sortieBypassLabel].forEach((object) => {
|
||||
object?.setVisible(bypassVisible);
|
||||
if (object?.input) {
|
||||
object.input.enabled = bypassVisible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private hasPendingFirstBattleCamaraderieMemory() {
|
||||
const dialogue = this.firstBattleCamaraderieDialogue();
|
||||
return Boolean(
|
||||
@@ -13102,16 +13206,36 @@ export class CampScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
this.showCampSaveSlotPanel();
|
||||
});
|
||||
const flow = this.currentSortieFlow();
|
||||
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '출진 준비';
|
||||
this.sortieCommandButton = this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
|
||||
const guidedAction = this.firstCampGuidedAction();
|
||||
const splitSortieCommand = Boolean(guidedAction && guidedAction.kind !== 'sortie');
|
||||
const sortieCommand = this.addCommandButton(
|
||||
this.campPrimaryActionLabel(),
|
||||
splitSortieCommand ? 1132 : 1152,
|
||||
38,
|
||||
splitSortieCommand ? 116 : 142,
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
const flow = this.currentSortieFlow();
|
||||
if (this.isFinalEpilogueFlow(flow)) {
|
||||
this.startVictoryStory();
|
||||
return;
|
||||
}
|
||||
this.showSortiePrep();
|
||||
}, true);
|
||||
this.runFirstCampGuidedAction();
|
||||
},
|
||||
true
|
||||
);
|
||||
this.sortieCommandButton = sortieCommand.background;
|
||||
this.sortieCommandLabel = sortieCommand.label;
|
||||
if (splitSortieCommand) {
|
||||
const sortieBypass = this.addCommandButton('바로 출진', 1230, 38, 72, () => {
|
||||
soundDirector.playSelect();
|
||||
this.showRequestedSortiePrep();
|
||||
});
|
||||
sortieBypass.label.setFontSize(13);
|
||||
this.sortieBypassButton = sortieBypass.background;
|
||||
this.sortieBypassLabel = sortieBypass.label;
|
||||
}
|
||||
this.refreshCampPrimaryActionPresentation();
|
||||
}
|
||||
|
||||
private showCampSaveSlotPanel() {
|
||||
@@ -13472,6 +13596,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const panelY = 90;
|
||||
const panelWidth = 884;
|
||||
const panelHeight = 546;
|
||||
const firstCampGuidedAction = report.battleId === campBattleIds.first
|
||||
? this.firstCampGuidedAction()
|
||||
: undefined;
|
||||
|
||||
const shade = this.trackVictoryReward(
|
||||
this.add.rectangle(0, 0, campLegacyCanvasWidth, campLegacyCanvasHeight, 0x020406, 0.78)
|
||||
@@ -13683,15 +13810,24 @@ export class CampScene extends Phaser.Scene {
|
||||
);
|
||||
noteText.setDepth(depth + 2);
|
||||
|
||||
const actionDefinitions: Array<{ id: VictoryRewardActionId; label: string; primary?: boolean }> = [
|
||||
{ id: 'equipment', label: '장비 확인' },
|
||||
{ id: 'supplies', label: '보급 확인' },
|
||||
{ id: 'sortie', label: '출진 준비', primary: true },
|
||||
{ id: 'close', label: '닫기' }
|
||||
];
|
||||
const buttonWidth = 158;
|
||||
const actionDefinitions: Array<{ id: VictoryRewardActionId; label: string; primary?: boolean }> =
|
||||
firstCampGuidedAction
|
||||
? [
|
||||
{ id: 'equipment', label: '장비 확인' },
|
||||
{ id: 'supplies', label: '보급 확인' },
|
||||
{ id: 'guided', label: firstCampGuidedAction.label, primary: true },
|
||||
{ id: 'sortie', label: '바로 출진' },
|
||||
{ id: 'close', label: '닫기' }
|
||||
]
|
||||
: [
|
||||
{ id: 'equipment', label: '장비 확인' },
|
||||
{ id: 'supplies', label: '보급 확인' },
|
||||
{ id: 'sortie', label: '출진 준비', primary: true },
|
||||
{ id: 'close', label: '닫기' }
|
||||
];
|
||||
const buttonWidth = firstCampGuidedAction ? 156 : 158;
|
||||
const buttonHeight = 48;
|
||||
const actionGap = 14;
|
||||
const actionGap = firstCampGuidedAction ? 8 : 14;
|
||||
const totalActionWidth = actionDefinitions.length * buttonWidth + (actionDefinitions.length - 1) * actionGap;
|
||||
const actionStartX = panelX + Math.floor((panelWidth - totalActionWidth) / 2);
|
||||
actionDefinitions.forEach((definition, index) => {
|
||||
@@ -13708,7 +13844,16 @@ export class CampScene extends Phaser.Scene {
|
||||
button.setInteractive({ useHandCursor: true });
|
||||
|
||||
const label = this.trackVictoryReward(
|
||||
this.add.text(buttonX + buttonWidth / 2, buttonY + buttonHeight / 2, definition.label, this.textStyle(14, definition.primary ? '#fff2b8' : '#f2e3bf', true))
|
||||
this.add.text(
|
||||
buttonX + buttonWidth / 2,
|
||||
buttonY + buttonHeight / 2,
|
||||
definition.label,
|
||||
this.textStyle(
|
||||
definition.id === 'guided' && Array.from(definition.label).length > 8 ? 12 : 14,
|
||||
definition.primary ? '#fff2b8' : '#f2e3bf',
|
||||
true
|
||||
)
|
||||
)
|
||||
);
|
||||
label.setOrigin(0.5);
|
||||
label.setDepth(depth + 3);
|
||||
@@ -13723,7 +13868,12 @@ export class CampScene extends Phaser.Scene {
|
||||
target.on('pointerout', () => button.setFillStyle(restingFill, 0.99));
|
||||
target.on('pointerdown', run);
|
||||
});
|
||||
this.victoryRewardActions.push({ id: definition.id, label: definition.label, button });
|
||||
this.victoryRewardActions.push({
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
button,
|
||||
primary: Boolean(definition.primary)
|
||||
});
|
||||
});
|
||||
soundDirector.playRewardRevealCue();
|
||||
}
|
||||
@@ -13759,6 +13909,10 @@ export class CampScene extends Phaser.Scene {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (action === 'guided') {
|
||||
this.runFirstCampGuidedAction();
|
||||
return;
|
||||
}
|
||||
if (action === 'sortie' || (action === 'close' && this.openSortiePrepOnCreate)) {
|
||||
this.showRequestedSortiePrep();
|
||||
}
|
||||
@@ -14110,7 +14264,7 @@ export class CampScene extends Phaser.Scene {
|
||||
target.on('pointerout', () => setHovered(false));
|
||||
target.on('pointerdown', action);
|
||||
});
|
||||
return bg;
|
||||
return { background: bg, label: text };
|
||||
}
|
||||
|
||||
private render() {
|
||||
@@ -14124,6 +14278,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.acknowledgePendingVictoryRewardCategories(['supplies']);
|
||||
}
|
||||
this.visitedTabs.add(this.activeTab);
|
||||
this.refreshCampPrimaryActionPresentation();
|
||||
this.updateTabButtons();
|
||||
this.renderUnitColumn();
|
||||
this.renderReportPanel();
|
||||
@@ -26042,6 +26197,7 @@ export class CampScene extends Phaser.Scene {
|
||||
: false;
|
||||
const cityEquipmentSortieStatus = this.cityEquipmentSortieStatus();
|
||||
const firstBattleCampFollowup = this.firstBattleCampFollowup();
|
||||
const firstCampGuidedAction = this.firstCampGuidedAction();
|
||||
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
|
||||
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||
@@ -26090,6 +26246,7 @@ export class CampScene extends Phaser.Scene {
|
||||
actions: this.victoryRewardActions.map((action) => ({
|
||||
id: action.id,
|
||||
label: action.label,
|
||||
primary: action.primary,
|
||||
bounds: this.sortieObjectBoundsDebug(action.button),
|
||||
interactive: Boolean(action.button.input?.enabled)
|
||||
}))
|
||||
@@ -26148,8 +26305,28 @@ export class CampScene extends Phaser.Scene {
|
||||
lineWidth: button.bg.lineWidth
|
||||
})),
|
||||
sortieCommand: {
|
||||
label: this.sortieCommandLabel?.text ?? null,
|
||||
guidedKind: firstCampGuidedAction?.kind ?? null,
|
||||
targetId: firstCampGuidedAction?.targetId ?? null,
|
||||
bounds: this.sortieObjectBoundsDebug(this.sortieCommandButton),
|
||||
interactive: Boolean(this.sortieCommandButton?.input?.enabled)
|
||||
interactive: Boolean(
|
||||
this.sortieCommandButton?.input?.enabled &&
|
||||
this.sortieCommandLabel?.input?.enabled
|
||||
),
|
||||
bypass:
|
||||
this.sortieBypassButton?.visible &&
|
||||
this.sortieBypassLabel?.visible
|
||||
? {
|
||||
label: this.sortieBypassLabel.text,
|
||||
bounds: this.sortieObjectBoundsDebug(
|
||||
this.sortieBypassButton
|
||||
),
|
||||
interactive: Boolean(
|
||||
this.sortieBypassButton.input?.enabled &&
|
||||
this.sortieBypassLabel.input?.enabled
|
||||
)
|
||||
}
|
||||
: null
|
||||
},
|
||||
firstVictoryFollowup: firstBattleCampFollowup
|
||||
? {
|
||||
|
||||
@@ -37,6 +37,10 @@ import {
|
||||
resolveFirstBattleVictoryNarrative,
|
||||
type FirstBattleVictoryNarrativeMemory
|
||||
} from '../data/firstBattleNarrativeMemory';
|
||||
import {
|
||||
resolveXuzhouStayNarrativeMemory,
|
||||
type XuzhouStayNarrativeMemory
|
||||
} from '../data/xuzhouStayNarrativeMemory';
|
||||
import {
|
||||
ensureUnitAnimations,
|
||||
loadUnitBaseSheets,
|
||||
@@ -53,7 +57,11 @@ import {
|
||||
} from '../data/storyCutscenes';
|
||||
import { prologueOpeningPages } from '../data/prologueVillage';
|
||||
import { type PortraitKey, type StoryPage } from '../data/scenario';
|
||||
import { campaignVictoryRewardPresentation, getFirstBattleReport } from '../state/campaignState';
|
||||
import {
|
||||
campaignVictoryRewardPresentation,
|
||||
getCampaignState,
|
||||
getFirstBattleReport
|
||||
} from '../state/campaignState';
|
||||
import { releaseTextureSource } from '../render/loaderLifecycle';
|
||||
import { isVisualMotionReduced } from '../settings/visualMotion';
|
||||
import { palette } from '../ui/palette';
|
||||
@@ -222,6 +230,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
private ownedStoryTextureKeys = new Set<string>();
|
||||
private rewardDisplay: StoryRewardDisplayModel = this.emptyRewardDisplay();
|
||||
private firstBattleNarrativeMemory?: FirstBattleVictoryNarrativeMemory;
|
||||
private xuzhouStayNarrativeMemory?: XuzhouStayNarrativeMemory;
|
||||
private visualMotionReduced = false;
|
||||
private storyEnvironmentProfile?: StoryEnvironmentProfile;
|
||||
private storyEnvironmentBackgroundKey?: string;
|
||||
@@ -248,8 +257,22 @@ export class StoryScene extends Phaser.Scene {
|
||||
)
|
||||
? resolveFirstBattleVictoryNarrative(sourcePages, getFirstBattleReport())
|
||||
: undefined;
|
||||
this.pages = firstBattleAftermath?.pages ?? sourcePages;
|
||||
const campaign = getCampaignState();
|
||||
const xuzhouStayNarrative = resolveXuzhouStayNarrativeMemory(
|
||||
firstBattleAftermath?.pages ?? sourcePages,
|
||||
{
|
||||
battleId: this.presentationBattleId,
|
||||
stage: this.presentationStage,
|
||||
campaign,
|
||||
historicalReport: this.presentationBattleId
|
||||
? campaign.battleHistory[this.presentationBattleId]
|
||||
: undefined
|
||||
}
|
||||
);
|
||||
this.pages = xuzhouStayNarrative.pages;
|
||||
this.firstBattleNarrativeMemory = firstBattleAftermath?.memory;
|
||||
this.xuzhouStayNarrativeMemory =
|
||||
xuzhouStayNarrative.memory;
|
||||
this.selectedStoryBackgroundKeys = storyBackgroundKeysForPages(this.pages);
|
||||
this.presentationWash = undefined;
|
||||
this.pageIndex = 0;
|
||||
@@ -349,6 +372,21 @@ export class StoryScene extends Phaser.Scene {
|
||||
)
|
||||
}
|
||||
: null,
|
||||
xuzhouStayNarrativeMemory:
|
||||
this.xuzhouStayNarrativeMemory
|
||||
? {
|
||||
...this.xuzhouStayNarrativeMemory,
|
||||
appliedPageIds: [
|
||||
...this.xuzhouStayNarrativeMemory.appliedPageIds
|
||||
],
|
||||
currentPageApplied: Boolean(
|
||||
page &&
|
||||
this.xuzhouStayNarrativeMemory.appliedPageIds.includes(
|
||||
page.id
|
||||
)
|
||||
)
|
||||
}
|
||||
: null,
|
||||
environment: this.storyEnvironmentDebugState(),
|
||||
presentation: this.storyPresentationDebugState(),
|
||||
assetStreaming: {
|
||||
|
||||
Reference in New Issue
Block a user