feat: prioritize third-victory return dialogue

This commit is contained in:
2026-07-27 11:35:35 +09:00
parent f07a45c62b
commit b2e7c8b403
7 changed files with 2018 additions and 17 deletions

View File

@@ -172,9 +172,24 @@ RPG로 다듬는다.
- 낮 강변 환경음, 지면별 발소리, 목표 완료의 시각·음향 신호를 함께 - 낮 강변 환경음, 지면별 발소리, 목표 완료의 시각·음향 신호를 함께
사용해 소리를 듣지 못해도 진행 상태를 이해할 수 있게 한다. 사용해 소리를 듣지 못해도 진행 상태를 이해할 수 있게 한다.
다음 초반 묶음 세 번째 군영부터 반복되는 방문·대화·상점 흐름을 감사해 ### 완료한 8차 묶음: 세 번째 승리의 결과별 우선 귀환 대화
정보 수집, 장비 구매, 동료 대화 가운데 무엇을 먼저 택했는지가 출전
브리핑과 첫 턴의 전술 선택에 짧고 명확하게 되돌아오게 한다. - 세 번째 전투에서 실제로 강가 요새를 확보했는지, 30턴 안에 승리했는지,
몇 턴이 걸렸는지를 승리 기록에서 함께 읽는다.
- 요새를 놓쳤다면 유비·관우의 방어선 회고, 요새는 지켰지만 늦었다면
유비·장비의 추격 속도 회고, 둘 다 달성했다면 관우·장비의 본영 진입
대화를 `새 이야기 · 우선`으로 한 곳만 제시한다.
- 전투 결과와 반대되는 칭찬을 하지 않으며, 오래되거나 손상되어 목표와
턴을 신뢰할 수 없는 저장은 기존 정적 대사를 그대로 사용한다.
- 출진 준비의 `공명 대화` 권장은 우선 귀환 대화 한 건만 완료하면 충족된다.
나머지 두 대화와 공명 보상은 사라지지 않고 자유 선택으로 남는다.
- 우선 대사를 완료하면 대화 탭과 카드의 새 이야기 표식이 사라지고,
다시 군영에 들어와도 완료 상태를 유지한다.
- 네 가지 목표 조합과 Canvas·WebGL의 1920x1080 진행을 자동 검증한다.
다음 초반 묶음은 세 번째 군영에서 정보 수집, 장비 정비, 동료 대화 가운데
이번 출진의 준비 우선순위 하나를 직접 정하고, 그 선택이 네 번째 전투
브리핑과 첫 3턴 안에 짧고 명확하게 되돌아오게 한다.
## 장기 개선 순서 ## 장기 개선 순서

View File

@@ -42,6 +42,8 @@
"verify:first-pursuit-camp:browser": "node scripts/verify-first-pursuit-camp-exploration-browser.mjs", "verify:first-pursuit-camp:browser": "node scripts/verify-first-pursuit-camp-exploration-browser.mjs",
"verify:second-battle-relief": "node scripts/verify-second-battle-relief-exploration.mjs", "verify:second-battle-relief": "node scripts/verify-second-battle-relief-exploration.mjs",
"verify:second-battle-relief:browser": "node scripts/verify-second-battle-relief-exploration-browser.mjs", "verify:second-battle-relief:browser": "node scripts/verify-second-battle-relief-exploration-browser.mjs",
"verify:third-battle-return": "node scripts/verify-third-battle-return-dialogue.mjs",
"verify:third-battle-return:browser": "node scripts/verify-third-battle-priority-return-browser.mjs",
"verify:prologue-exploration-assets": "node scripts/verify-prologue-exploration-asset-data.mjs", "verify:prologue-exploration-assets": "node scripts/verify-prologue-exploration-asset-data.mjs",
"verify:exploration-backgrounds": "node scripts/verify-prologue-exploration-asset-data.mjs", "verify:exploration-backgrounds": "node scripts/verify-prologue-exploration-asset-data.mjs",
"verify:exploration-characters": "node scripts/verify-exploration-character-asset-data.mjs", "verify:exploration-characters": "node scripts/verify-exploration-character-asset-data.mjs",
@@ -73,7 +75,7 @@
"verify:interaction-ux": "node scripts/verify-interaction-ux.mjs", "verify:interaction-ux": "node scripts/verify-interaction-ux.mjs",
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs", "verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
"verify:release": "node scripts/verify-release-candidate.mjs", "verify:release": "node scripts/verify-release-candidate.mjs",
"verify:local-release": "pnpm run verify:static-data && pnpm run verify:unit-action-assets && pnpm run verify:enemy-intent && tsc --noEmit && pnpm run build && pnpm run verify:unit-action-assets:dist && pnpm run verify:loader-lifecycle && pnpm run verify:interaction-ux && pnpm run verify:city-stays:browser && pnpm run verify:prologue-village:browser && pnpm run verify:first-pursuit-camp:browser && pnpm run verify:second-battle-relief:browser && pnpm run verify:av-feedback:browser && pnpm run verify:release && pnpm run verify:performance", "verify:local-release": "pnpm run verify:static-data && pnpm run verify:unit-action-assets && pnpm run verify:enemy-intent && tsc --noEmit && pnpm run build && pnpm run verify:unit-action-assets:dist && pnpm run verify:loader-lifecycle && pnpm run verify:interaction-ux && pnpm run verify:city-stays:browser && pnpm run verify:prologue-village:browser && pnpm run verify:first-pursuit-camp:browser && pnpm run verify:second-battle-relief:browser && pnpm run verify:third-battle-return:browser && pnpm run verify:av-feedback:browser && pnpm run verify:release && pnpm run verify:performance",
"verify:1.0": "pnpm run verify:local-release && pnpm run verify:flow && pnpm run qa:campaign-complete && pnpm run qa:battle-maps:webgl", "verify:1.0": "pnpm run verify:local-release && pnpm run verify:flow && pnpm run qa:campaign-complete && pnpm run qa:battle-maps:webgl",
"verify:public-deploy": "node scripts/verify-public-deploy.mjs", "verify:public-deploy": "node scripts/verify-public-deploy.mjs",
"qa:representative": "node scripts/qa-representative-battles.mjs", "qa:representative": "node scripts/qa-representative-battles.mjs",

View File

@@ -27,6 +27,7 @@ const checks = [
'scripts/verify-first-battle-camaraderie-memory.mjs', 'scripts/verify-first-battle-camaraderie-memory.mjs',
'scripts/verify-first-pursuit-scout-memory.mjs', 'scripts/verify-first-pursuit-scout-memory.mjs',
'scripts/verify-second-battle-relief-exploration.mjs', 'scripts/verify-second-battle-relief-exploration.mjs',
'scripts/verify-third-battle-return-dialogue.mjs',
'scripts/verify-prologue-exploration-asset-data.mjs', 'scripts/verify-prologue-exploration-asset-data.mjs',
'scripts/verify-exploration-character-asset-data.mjs', 'scripts/verify-exploration-character-asset-data.mjs',
'scripts/verify-prologue-dialogue-portrait-data.mjs', 'scripts/verify-prologue-dialogue-portrait-data.mjs',

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,422 @@
import assert from 'node:assert/strict';
import { createServer } from 'vite';
const server = await createServer({
logLevel: 'error',
server: { middlewareMode: true, hmr: false },
appType: 'custom'
});
try {
const dialogueModule = await server.ssrLoadModule(
'/src/game/data/thirdBattleReturnDialogue.ts'
);
const { battleScenarios } = await server.ssrLoadModule(
'/src/game/data/battles.ts'
);
verifyCanonicalBattleReferences(dialogueModule, battleScenarios);
verifyAllObjectiveCombinations(dialogueModule);
verifyTargetBattleIsolation(dialogueModule);
verifyLegacyReportFallback(dialogueModule);
verifyInvalidLegacyDataFallsBackToStaticDialogue(dialogueModule);
verifyReturnedDataIsCloned(dialogueModule);
console.log(
'Third-battle priority return dialogue verification passed (four objective combinations, result-specific priority targets, exact battle isolation, and static-dialogue fallback for invalid legacy data).'
);
} finally {
await server.close();
}
function verifyCanonicalBattleReferences(dialogueModule, battleScenarios) {
const scenario =
battleScenarios[dialogueModule.thirdBattleReturnSourceBattleId];
assert(scenario, 'The return dialogue must reference a canonical battle.');
assert.equal(scenario.id, 'third-battle-guangzong-road');
assert.equal(
scenario.quickVictoryTurnLimit,
dialogueModule.thirdBattleReturnQuickVictoryTurnLimit
);
const fortObjective = scenario.objectives.find(
({ id }) => id === dialogueModule.thirdBattleReturnObjectiveIds.fort
);
const quickObjective = scenario.objectives.find(
({ id }) => id === dialogueModule.thirdBattleReturnObjectiveIds.quick
);
assert.equal(fortObjective?.kind, 'secure-terrain');
assert.equal(fortObjective?.label, '강가 요새 확보');
assert.equal(quickObjective?.kind, 'quick-victory');
assert.equal(
quickObjective?.maxTurn,
dialogueModule.thirdBattleReturnQuickVictoryTurnLimit
);
assert.deepEqual(
dialogueModule.thirdBattleReturnTargetDialogueIds,
{
fortRecovery: 'liu-guan-after-guangzong-road',
tempoRecovery: 'liu-zhang-after-guangzong-road',
decisiveAdvance: 'guan-zhang-after-guangzong-road'
}
);
assert.equal(
dialogueModule.thirdBattleReturnPriorityLabel,
'새 이야기 · 우선'
);
}
function verifyAllObjectiveCombinations(dialogueModule) {
const testCases = [
{
fortAchieved: false,
quickAchieved: false,
turnNumber: 35,
expectedVariantId: 'fort-recovery',
expectedTargetDialogueId: 'liu-guan-after-guangzong-road',
expectedSummary: /강가 요새 미확보 · 35턴 · 30턴 제한 초과/,
expectedLine: /35턴 만에/,
forbiddenText: /강가 요새까지 확보했습니다/
},
{
fortAchieved: false,
quickAchieved: true,
turnNumber: 24,
expectedVariantId: 'fort-recovery',
expectedTargetDialogueId: 'liu-guan-after-guangzong-road',
expectedSummary: /강가 요새 미확보 · 24턴 신속 승리/,
expectedLine: /전령을 쫓는 속도는 늦지 않았지만/,
forbiddenText: /강가 요새까지 확보했습니다/
},
{
fortAchieved: true,
quickAchieved: false,
turnNumber: 34,
expectedVariantId: 'tempo-recovery',
expectedTargetDialogueId: 'liu-zhang-after-guangzong-road',
expectedSummary: /강가 요새 확보 · 34턴 · 30턴 제한 초과/,
expectedLine: /34턴이 걸려/,
forbiddenText: /신속 승리/
},
{
fortAchieved: true,
quickAchieved: true,
turnNumber: 22,
expectedVariantId: 'decisive-advance',
expectedTargetDialogueId: 'guan-zhang-after-guangzong-road',
expectedSummary: /강가 요새 확보 · 22턴 신속 승리/,
expectedLine: /22턴 만에/,
forbiddenText: /확보하지 못했습니다/
}
];
testCases.forEach((testCase) => {
const resolved =
dialogueModule.resolveThirdBattlePriorityReturnDialogue(
campaignWithHistory(dialogueModule, testCase)
);
assert(resolved, `Expected a dialogue for ${JSON.stringify(testCase)}.`);
assert.equal(resolved.reportSource, 'battle-history');
assert.equal(
resolved.sourceBattleId,
dialogueModule.thirdBattleReturnSourceBattleId
);
assert.equal(
resolved.targetDialogueId,
testCase.expectedTargetDialogueId
);
assert.equal(resolved.priorityLabel, '새 이야기 · 우선');
assert.equal(resolved.variantId, testCase.expectedVariantId);
assert.equal(resolved.turnNumber, testCase.turnNumber);
assert.equal(resolved.fortAchieved, testCase.fortAchieved);
assert.equal(resolved.quickAchieved, testCase.quickAchieved);
assert.equal(resolved.lines.length, 3);
assert.match(resolved.summary, testCase.expectedSummary);
assert.match(resolved.lines.join('\n'), testCase.expectedLine);
assert.doesNotMatch(
`${resolved.summary}\n${resolved.lines.join('\n')}`,
testCase.forbiddenText,
'A missed objective must never receive false praise.'
);
});
const failedStatusOverridesAchieved =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]: victoryReport(
dialogueModule,
{
fortAchieved: true,
quickAchieved: false,
turnNumber: 35,
fortStatus: 'failed'
}
)
}
});
assert.equal(
failedStatusOverridesAchieved?.fortAchieved,
false,
'A failed objective status must prevent phantom achievement praise.'
);
assert.equal(
failedStatusOverridesAchieved?.variantId,
'fort-recovery'
);
}
function verifyTargetBattleIsolation(dialogueModule) {
const targetVictory = victoryReport(dialogueModule, {
fortAchieved: true,
quickAchieved: true,
turnNumber: 20
});
const otherVictory = {
...targetVictory,
battleId: 'fourth-battle-guangzong-camp'
};
assert.equal(
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
'fourth-battle-guangzong-camp': otherVictory
},
firstBattleReport: otherVictory
}),
undefined,
'An unrelated victory must not create a third-camp priority dialogue.'
);
assert.equal(
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]: otherVictory
},
firstBattleReport: targetVictory
}),
undefined,
'A mismatched canonical history entry must block stale report fallback.'
);
assert.equal(
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]: {
...targetVictory,
outcome: 'defeat'
}
},
firstBattleReport: targetVictory
}),
undefined,
'A canonical defeat must not be replaced by a stale victory report.'
);
assert.equal(
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
'legacy-wrong-key': targetVictory
}
}),
undefined,
'A target settlement stored under another battle key must be ignored.'
);
const historyWins =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]: targetVictory
},
firstBattleReport: otherVictory
});
assert.equal(historyWins?.reportSource, 'battle-history');
assert.equal(historyWins?.variantId, 'decisive-advance');
}
function verifyLegacyReportFallback(dialogueModule) {
const legacyReport = victoryReport(dialogueModule, {
fortAchieved: true,
quickAchieved: true,
turnNumber: 19,
omitStatus: true
});
const fromMissingHistory =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
firstBattleReport: legacyReport
});
assert.equal(fromMissingHistory?.reportSource, 'legacy-report');
assert.equal(fromMissingHistory?.variantId, 'decisive-advance');
assert.equal(fromMissingHistory?.fortAchieved, true);
assert.equal(fromMissingHistory?.quickAchieved, true);
assert.match(fromMissingHistory?.lines.join('\n') ?? '', /19턴 만에/);
const fromEmptyHistory =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {},
firstBattleReport: legacyReport
});
assert.equal(fromEmptyHistory?.reportSource, 'legacy-report');
assert.equal(fromEmptyHistory?.variantId, 'decisive-advance');
const missingTurn =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
firstBattleReport: {
...legacyReport,
turnNumber: 'unknown'
}
});
assert.equal(
missingTurn,
undefined,
'A legacy report without a trustworthy turn must preserve the static dialogue.'
);
}
function verifyInvalidLegacyDataFallsBackToStaticDialogue(
dialogueModule
) {
const complete = victoryReport(dialogueModule, {
fortAchieved: true,
quickAchieved: true,
turnNumber: 21
});
const missingObjective =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
firstBattleReport: {
...complete,
objectives: complete.objectives.filter(
({ id }) => id !== dialogueModule.thirdBattleReturnObjectiveIds.quick
)
}
});
assertNoPriorityOverride(
missingObjective,
'A legacy report with a missing objective'
);
const duplicateObjective =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]: {
...complete,
objectives: [
...complete.objectives,
{
id: dialogueModule.thirdBattleReturnObjectiveIds.fort,
achieved: false,
status: 'failed'
}
]
}
}
});
assertNoPriorityOverride(
duplicateObjective,
'A report with duplicate objective ids'
);
const invalidObjective =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
firstBattleReport: {
...complete,
objectives: complete.objectives.map((objective) =>
objective.id === dialogueModule.thirdBattleReturnObjectiveIds.fort
? { ...objective, achieved: 'yes' }
: objective
)
}
});
assertNoPriorityOverride(
invalidObjective,
'A legacy report with a non-boolean objective result'
);
const inconsistentTurn =
dialogueModule.resolveThirdBattlePriorityReturnDialogue({
firstBattleReport: victoryReport(dialogueModule, {
fortAchieved: true,
quickAchieved: true,
turnNumber: 31
})
});
assertNoPriorityOverride(
inconsistentTurn,
'A quick-victory result that contradicts its turn number'
);
}
function verifyReturnedDataIsCloned(dialogueModule) {
const campaign = campaignWithHistory(dialogueModule, {
fortAchieved: true,
quickAchieved: true,
turnNumber: 18
});
const first =
dialogueModule.resolveThirdBattlePriorityReturnDialogue(campaign);
assert(first);
first.lines[0] = 'mutated';
const second =
dialogueModule.resolveThirdBattlePriorityReturnDialogue(campaign);
assert(second);
assert.notEqual(second.lines[0], 'mutated');
}
function campaignWithHistory(dialogueModule, options) {
return {
battleHistory: {
[dialogueModule.thirdBattleReturnSourceBattleId]:
victoryReport(dialogueModule, options)
},
firstBattleReport: {
...victoryReport(dialogueModule, options),
battleId: 'fourth-battle-guangzong-camp'
}
};
}
function victoryReport(
dialogueModule,
{
fortAchieved,
quickAchieved,
turnNumber,
fortStatus,
quickStatus,
omitStatus = false
}
) {
const objective = (id, achieved, status) => ({
id,
achieved,
...(omitStatus
? {}
: {
status:
status ??
(achieved ? 'done' : 'failed')
})
});
return {
battleId: dialogueModule.thirdBattleReturnSourceBattleId,
outcome: 'victory',
turnNumber,
objectives: [
objective(
dialogueModule.thirdBattleReturnObjectiveIds.fort,
fortAchieved,
fortStatus
),
objective(
dialogueModule.thirdBattleReturnObjectiveIds.quick,
quickAchieved,
quickStatus
)
]
};
}
function assertNoPriorityOverride(resolved, condition) {
assert.equal(
resolved,
undefined,
`${condition} must leave the existing static dialogue unchanged.`
);
}

View File

@@ -0,0 +1,304 @@
export const thirdBattleReturnSourceBattleId =
'third-battle-guangzong-road';
export const thirdBattleReturnCampStep = 'third-camp';
export const thirdBattleReturnPriorityLabel = '새 이야기 · 우선';
export const thirdBattleReturnQuickVictoryTurnLimit = 30;
export const thirdBattleReturnObjectiveIds = {
fort: 'fort',
quick: 'quick'
} as const;
export const thirdBattleReturnTargetDialogueIds = {
fortRecovery: 'liu-guan-after-guangzong-road',
tempoRecovery: 'liu-zhang-after-guangzong-road',
decisiveAdvance: 'guan-zhang-after-guangzong-road'
} as const;
export type ThirdBattleReturnDialogueVariantId =
| 'fort-recovery'
| 'tempo-recovery'
| 'decisive-advance';
export type ThirdBattleReturnTargetDialogueId =
(typeof thirdBattleReturnTargetDialogueIds)[
keyof typeof thirdBattleReturnTargetDialogueIds
];
export type ThirdBattleReturnReportSource =
| 'battle-history'
| 'legacy-report';
export type ThirdBattleReturnReport = {
battleId?: unknown;
outcome?: unknown;
turnNumber?: unknown;
objectives?: unknown;
};
export type ThirdBattleReturnCampaignState = {
battleHistory?: Readonly<
Record<string, ThirdBattleReturnReport | null | undefined>
>;
firstBattleReport?: ThirdBattleReturnReport | null;
};
export type ThirdBattlePriorityReturnDialogue = {
sourceBattleId: typeof thirdBattleReturnSourceBattleId;
campStep: typeof thirdBattleReturnCampStep;
targetDialogueId: ThirdBattleReturnTargetDialogueId;
priorityLabel: typeof thirdBattleReturnPriorityLabel;
reportSource: ThirdBattleReturnReportSource;
variantId: ThirdBattleReturnDialogueVariantId;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
title: string;
summary: string;
lines: [string, string, string];
};
type VictoryReportSelection = {
report: ThirdBattleReturnReport;
source: ThirdBattleReturnReportSource;
};
export function resolveThirdBattlePriorityReturnDialogue(
campaign?: ThirdBattleReturnCampaignState | null
): ThirdBattlePriorityReturnDialogue | undefined {
const selection = selectVictoryReport(campaign);
if (!selection) {
return undefined;
}
const turnNumber = positiveInteger(selection.report.turnNumber);
const fortAchieved = readObjective(
selection.report.objectives,
thirdBattleReturnObjectiveIds.fort
);
const quickAchieved = readObjective(
selection.report.objectives,
thirdBattleReturnObjectiveIds.quick
);
if (
turnNumber === undefined ||
fortAchieved === undefined ||
quickAchieved === undefined ||
quickAchieved !==
(turnNumber <= thirdBattleReturnQuickVictoryTurnLimit)
) {
return undefined;
}
return createResultDialogue({
reportSource: selection.source,
turnNumber,
fortAchieved,
quickAchieved
});
}
function selectVictoryReport(
campaign?: ThirdBattleReturnCampaignState | null
): VictoryReportSelection | undefined {
const history = campaign?.battleHistory;
if (
history &&
Object.prototype.hasOwnProperty.call(
history,
thirdBattleReturnSourceBattleId
)
) {
const report = history[thirdBattleReturnSourceBattleId];
return validThirdBattleVictory(report)
? { report, source: 'battle-history' }
: undefined;
}
const report = campaign?.firstBattleReport;
return validThirdBattleVictory(report)
? { report, source: 'legacy-report' }
: undefined;
}
function validThirdBattleVictory(
report?: ThirdBattleReturnReport | null
): report is ThirdBattleReturnReport {
return Boolean(
report &&
report.battleId === thirdBattleReturnSourceBattleId &&
report.outcome === 'victory'
);
}
function readObjective(
rawObjectives: unknown,
objectiveId: string
): boolean | undefined {
if (!Array.isArray(rawObjectives)) {
return undefined;
}
const matches = rawObjectives.filter(
(objective) =>
isRecord(objective) &&
objective.id === objectiveId
);
if (
matches.length !== 1 ||
typeof matches[0].achieved !== 'boolean'
) {
return undefined;
}
return (
matches[0].achieved === true &&
matches[0].status !== 'failed'
);
}
function createResultDialogue(options: {
reportSource: ThirdBattleReturnReportSource;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
}): ThirdBattlePriorityReturnDialogue {
if (!options.fortAchieved) {
return createFortRecoveryDialogue(options);
}
if (!options.quickAchieved) {
return createTempoRecoveryDialogue(options);
}
return createDecisiveAdvanceDialogue(options);
}
function createFortRecoveryDialogue(options: {
reportSource: ThirdBattleReturnReportSource;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
}): ThirdBattlePriorityReturnDialogue {
const pursuitLine = options.quickAchieved
? '유비: 전령을 쫓는 속도는 늦지 않았지만, 요새를 비워 둔 탓에 다음 길이 불안해졌소.'
: '유비: 전령을 쫓는 데도 시간이 걸렸고 요새까지 남겼소. 다음에는 길목부터 확실히 끊어야 하오.';
return baseDialogue({
...options,
targetDialogueId:
thirdBattleReturnTargetDialogueIds.fortRecovery,
variantId: 'fort-recovery',
title: '남겨 둔 강가 요새',
summary:
`강가 요새 미확보 · ${quickResultText(
options.quickAchieved,
options.turnNumber
)}`,
lines: [
`관우: ${options.turnNumber}턴 만에 전령 마원은 막았으나 강가 요새를 끝내 확보하지 못했습니다.`,
pursuitLine,
'관우: 다음 본영전에서는 먼저 물러설 자리를 지우고, 그 뒤에 적장을 몰아붙이겠습니다.'
]
});
}
function createTempoRecoveryDialogue(options: {
reportSource: ThirdBattleReturnReportSource;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
}): ThirdBattlePriorityReturnDialogue {
return baseDialogue({
...options,
targetDialogueId:
thirdBattleReturnTargetDialogueIds.tempoRecovery,
variantId: 'tempo-recovery',
title: '늦어진 전령 추격',
summary:
`강가 요새 확보 · ${quickResultText(
options.quickAchieved,
options.turnNumber
)}`,
lines: [
`장비: 강가 요새는 지켰지만 마원을 꺾는 데 ${options.turnNumber}턴이 걸려, 추격의 때를 놓쳤습니다.`,
'유비: 지킬 곳을 지킨 것은 옳소. 이제 전열을 세운 뒤 곧바로 익덕의 창이 이어지게 호흡을 줄이면 되오.',
'장비: 다음 본영전에서는 형님이 길을 잡는 순간 제가 바로 돌파하겠습니다.'
]
});
}
function createDecisiveAdvanceDialogue(options: {
reportSource: ThirdBattleReturnReportSource;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
}): ThirdBattlePriorityReturnDialogue {
return baseDialogue({
...options,
targetDialogueId:
thirdBattleReturnTargetDialogueIds.decisiveAdvance,
variantId: 'decisive-advance',
title: '본영으로 잇는 승리',
summary:
`강가 요새 확보 · ${quickResultText(
options.quickAchieved,
options.turnNumber
)}`,
lines: [
`관우: ${options.turnNumber}턴 만에 전령 마원을 꺾고 강가 요새까지 확보했습니다. 광종 본영으로 가는 길이 열렸습니다.`,
'장비: 지킬 곳도 지키고 쫓을 놈도 놓치지 않았으니, 이제 장각의 본영까지 이 기세를 잇읍시다.',
'관우: 좋다. 이번 성과를 자랑으로만 남기지 말고 본영 진입의 군율로 삼자.'
]
});
}
function baseDialogue(options: {
reportSource: ThirdBattleReturnReportSource;
targetDialogueId: ThirdBattleReturnTargetDialogueId;
variantId: ThirdBattleReturnDialogueVariantId;
turnNumber: number;
fortAchieved: boolean;
quickAchieved: boolean;
title: string;
summary: string;
lines: [string, string, string];
}): ThirdBattlePriorityReturnDialogue {
return {
sourceBattleId: thirdBattleReturnSourceBattleId,
campStep: thirdBattleReturnCampStep,
targetDialogueId: options.targetDialogueId,
priorityLabel: thirdBattleReturnPriorityLabel,
reportSource: options.reportSource,
variantId: options.variantId,
turnNumber: options.turnNumber,
fortAchieved: options.fortAchieved,
quickAchieved: options.quickAchieved,
title: options.title,
summary: options.summary,
lines: [...options.lines]
};
}
function quickResultText(
quickAchieved: boolean,
turnNumber: number
) {
return quickAchieved
? `${turnNumber}턴 신속 승리`
: `${turnNumber}턴 · ${thirdBattleReturnQuickVictoryTurnLimit}턴 제한 초과`;
}
function positiveInteger(value: unknown) {
return (
typeof value === 'number' &&
Number.isInteger(value) &&
value > 0 &&
value <= 9999
)
? value
: undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(
value &&
typeof value === 'object' &&
!Array.isArray(value)
);
}

View File

@@ -44,6 +44,11 @@ import {
secondBattleReliefSourceBattleId, secondBattleReliefSourceBattleId,
secondBattleReliefVisitId secondBattleReliefVisitId
} from '../data/secondBattleReliefExploration'; } from '../data/secondBattleReliefExploration';
import {
resolveThirdBattlePriorityReturnDialogue,
thirdBattleReturnCampStep,
thirdBattleReturnSourceBattleId
} from '../data/thirdBattleReturnDialogue';
import { import {
findCityStayAfterBattle, findCityStayAfterBattle,
type CityEquipmentOffer, type CityEquipmentOffer,
@@ -11643,7 +11648,18 @@ export class CampScene extends Phaser.Scene {
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds); this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments); this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments); this.sortieItemAssignments = this.normalizedSortieItemAssignments(this.campaign.sortieItemAssignments);
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id; const availableCampDialogues = this.availableCampDialogues();
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
const pendingPriorityReturnDialogueId =
priorityReturnDialogue &&
!this.completedCampDialogues().includes(priorityReturnDialogue.targetDialogueId) &&
availableCampDialogues.some((dialogue) => dialogue.id === priorityReturnDialogue.targetDialogueId)
? priorityReturnDialogue.targetDialogueId
: undefined;
this.selectedDialogueId =
pendingPriorityReturnDialogueId ??
availableCampDialogues[0]?.id ??
campDialogues[0].id;
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id; this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
const cityStay = this.currentCityStay(); const cityStay = this.currentCityStay();
if (cityStay) { if (cityStay) {
@@ -12883,6 +12899,18 @@ export class CampScene extends Phaser.Scene {
return resolveFirstBattleCamaraderieMemory(this.campaign); return resolveFirstBattleCamaraderieMemory(this.campaign);
} }
private thirdBattlePriorityReturnDialogue() {
if (
!this.campaign ||
this.retrySortieBattleId ||
this.campaign.step !== thirdBattleReturnCampStep ||
this.currentCampBattleId() !== thirdBattleReturnSourceBattleId
) {
return undefined;
}
return resolveThirdBattlePriorityReturnDialogue(this.campaign);
}
private firstPursuitScoutMemoryForNextSortie() { private firstPursuitScoutMemoryForNextSortie() {
const campaign = this.campaign; const campaign = this.campaign;
const scenario = this.nextSortieScenario(); const scenario = this.nextSortieScenario();
@@ -12917,6 +12945,18 @@ export class CampScene extends Phaser.Scene {
}; };
} }
private campDialogueWithThirdBattlePriorityReturn(dialogue: CampDialogue) {
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
if (!priorityReturnDialogue || dialogue.id !== priorityReturnDialogue.targetDialogueId) {
return dialogue;
}
return {
...dialogue,
title: priorityReturnDialogue.title,
lines: [...priorityReturnDialogue.lines]
};
}
private hasPendingFirstBattleCampFollowup() { private hasPendingFirstBattleCampFollowup() {
const followup = this.firstBattleCampFollowup(); const followup = this.firstBattleCampFollowup();
return Boolean( return Boolean(
@@ -12933,6 +12973,14 @@ export class CampScene extends Phaser.Scene {
); );
} }
private hasPendingThirdBattlePriorityReturn() {
const priorityReturnDialogue = this.thirdBattlePriorityReturnDialogue();
return Boolean(
priorityReturnDialogue &&
!this.completedCampDialogues().includes(priorityReturnDialogue.targetDialogueId)
);
}
private availableCampDialogues(): CampDialogue[] { private availableCampDialogues(): CampDialogue[] {
const battleId = this.currentCampBattleId(); const battleId = this.currentCampBattleId();
const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId)); const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId));
@@ -12940,7 +12988,9 @@ export class CampScene extends Phaser.Scene {
const available = dialogues.length > 0 const available = dialogues.length > 0
? dialogues ? dialogues
: campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id)); : campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(defaultBattleScenario.id));
const resolved = available.map((dialogue) => this.campDialogueWithFirstBattleFollowup(dialogue)); const resolved = available
.map((dialogue) => this.campDialogueWithFirstBattleFollowup(dialogue))
.map((dialogue) => this.campDialogueWithThirdBattlePriorityReturn(dialogue));
const camaraderieDialogue = this.firstBattleCamaraderieDialogue(); const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
return camaraderieDialogue && !resolved.some((dialogue) => dialogue.id === camaraderieDialogue.id) return camaraderieDialogue && !resolved.some((dialogue) => dialogue.id === camaraderieDialogue.id)
? [...resolved, camaraderieDialogue] ? [...resolved, camaraderieDialogue]
@@ -13936,6 +13986,7 @@ export class CampScene extends Phaser.Scene {
const active = this.activeTab === tab; const active = this.activeTab === tab;
const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup(); const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup();
const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory(); const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory();
const pendingThirdBattleReturn = tab === 'dialogue' && this.hasPendingThirdBattlePriorityReturn();
const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some( const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some(
(visit) => !this.completedCampVisits().includes(visit.id) (visit) => !this.completedCampVisits().includes(visit.id)
); );
@@ -13946,11 +13997,19 @@ export class CampScene extends Phaser.Scene {
indicator.setVisible(active || hovered); indicator.setVisible(active || hovered);
indicator.setAlpha(active ? 1 : 0.66); indicator.setAlpha(active ? 1 : 0.66);
if (tab === 'dialogue' && newBadge) { if (tab === 'dialogue' && newBadge) {
newBadge.setText(pendingFirstBattleFollowup ? '회고' : pendingCamaraderieMemory ? '전우' : '회고'); newBadge.setText(
pendingThirdBattleReturn
? '우선'
: pendingFirstBattleFollowup
? '회고'
: pendingCamaraderieMemory
? '전우'
: '회고'
);
} }
newBadge?.setVisible(Boolean( newBadge?.setVisible(Boolean(
(tab === 'city' && this.hasPendingCityStayActions()) || (tab === 'city' && this.hasPendingCityStayActions()) ||
(tab === 'dialogue' && (pendingFirstBattleFollowup || pendingCamaraderieMemory)) || (tab === 'dialogue' && (pendingThirdBattleReturn || pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
pendingCampVisit || pendingCampVisit ||
(tab === 'supplies' && pendingCategories.has('supplies')) || (tab === 'supplies' && pendingCategories.has('supplies')) ||
(tab === 'equipment' && pendingCategories.has('equipment')) (tab === 'equipment' && pendingCategories.has('equipment'))
@@ -19838,6 +19897,11 @@ export class CampScene extends Phaser.Scene {
bg.setStrokeStyle(1, palette.gold, 0.4); bg.setStrokeStyle(1, palette.gold, 0.4);
const availableDialogues = this.availableCampDialogues(); const availableDialogues = this.availableCampDialogues();
const completedDialogues = this.completedAvailableDialogues().length; const completedDialogues = this.completedAvailableDialogues().length;
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const priorityDialogueCompleted = Boolean(
thirdBattlePriorityReturn &&
this.completedCampDialogues().includes(thirdBattlePriorityReturn.targetDialogueId)
);
const availableVisits = this.availableCampVisits(); const availableVisits = this.availableCampVisits();
const completedVisits = this.completedAvailableVisits().length; const completedVisits = this.completedAvailableVisits().length;
const selectedUnits = this.selectedSortieUnits(); const selectedUnits = this.selectedSortieUnits();
@@ -19852,8 +19916,14 @@ export class CampScene extends Phaser.Scene {
const sortieNote = this.nextSortieRule().note; const sortieNote = this.nextSortieRule().note;
const recoveryHint = this.sortieRecoveryHint(); const recoveryHint = this.sortieRecoveryHint();
const hasBattle = Boolean(this.nextSortieScenario()); const hasBattle = Boolean(this.nextSortieScenario());
const dialogueProgress = thirdBattlePriorityReturn
? `대화 우선 ${priorityDialogueCompleted ? 1 : 0}/1 · 선택 ${Math.max(
0,
completedDialogues - Number(priorityDialogueCompleted)
)}/${Math.max(0, availableDialogues.length - 1)}`
: `대화 ${completedDialogues}/${availableDialogues.length}`;
const readyLine = [ const readyLine = [
`대화 ${completedDialogues}/${availableDialogues.length}`, dialogueProgress,
`방문 ${completedVisits}/${availableVisits.length}`, `방문 ${completedVisits}/${availableVisits.length}`,
`${hasBattle ? '출전' : '동행'} ${selectedSummary}`, `${hasBattle ? '출전' : '동행'} ${selectedSummary}`,
`보유 ${inventorySummary}` `보유 ${inventorySummary}`
@@ -20111,6 +20181,20 @@ export class CampScene extends Phaser.Scene {
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0); const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
const availableDialogues = this.availableCampDialogues(); const availableDialogues = this.availableCampDialogues();
const completedDialogues = this.completedAvailableDialogues().length; const completedDialogues = this.completedAvailableDialogues().length;
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const priorityDialogueCompleted = Boolean(
thirdBattlePriorityReturn &&
this.completedCampDialogues().includes(thirdBattlePriorityReturn.targetDialogueId)
);
const dialogueChecklistComplete = thirdBattlePriorityReturn
? priorityDialogueCompleted
: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length;
const dialogueChecklistDetail = thirdBattlePriorityReturn
? `우선 ${priorityDialogueCompleted ? 1 : 0}/1 · 선택 ${Math.max(
0,
completedDialogues - Number(priorityDialogueCompleted)
)}/${Math.max(0, availableDialogues.length - 1)}`
: `${completedDialogues}/${availableDialogues.length} 완료`;
const availableVisits = this.availableCampVisits(); const availableVisits = this.availableCampVisits();
const completedVisits = this.completedAvailableVisits().length; const completedVisits = this.completedAvailableVisits().length;
const selected = this.selectedSortieUnits(); const selected = this.selectedSortieUnits();
@@ -20176,8 +20260,8 @@ export class CampScene extends Phaser.Scene {
}, },
{ {
label: '공명 대화', label: '공명 대화',
complete: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length, complete: dialogueChecklistComplete,
detail: `${completedDialogues}/${availableDialogues.length} 완료`, detail: dialogueChecklistDetail,
priority: 'recommended' priority: 'recommended'
}, },
{ {
@@ -22740,6 +22824,7 @@ export class CampScene extends Phaser.Scene {
const dialogues = this.availableCampDialogues(); const dialogues = this.availableCampDialogues();
const firstBattleFollowup = this.firstBattleCampFollowup(); const firstBattleFollowup = this.firstBattleCampFollowup();
const camaraderieDialogue = this.firstBattleCamaraderieDialogue(); const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const compactDialogueList = dialogues.length > 3; const compactDialogueList = dialogues.length > 3;
const dialogueRowGap = compactDialogueList ? 35 : 64; const dialogueRowGap = compactDialogueList ? 35 : 64;
const dialogueRowHeight = compactDialogueList ? 30 : 48; const dialogueRowHeight = compactDialogueList ? 30 : 48;
@@ -22764,12 +22849,22 @@ export class CampScene extends Phaser.Scene {
}); });
const isFirstBattleFollowup = firstBattleFollowup?.targetDialogueId === dialogue.id; const isFirstBattleFollowup = firstBattleFollowup?.targetDialogueId === dialogue.id;
const isCamaraderieMemory = camaraderieDialogue?.id === dialogue.id; const isCamaraderieMemory = camaraderieDialogue?.id === dialogue.id;
const title = isFirstBattleFollowup || isCamaraderieMemory const isThirdBattlePriorityReturn =
thirdBattlePriorityReturn?.targetDialogueId === dialogue.id;
const pendingThirdBattlePriorityReturn =
isThirdBattlePriorityReturn && !completed;
const title = isFirstBattleFollowup || isCamaraderieMemory || isThirdBattlePriorityReturn
? this.compactText(dialogue.title, compactDialogueList ? 20 : 14) ? this.compactText(dialogue.title, compactDialogueList ? 20 : 14)
: dialogue.title; : dialogue.title;
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 3 : 8), title, this.textStyle(compactDialogueList ? 11 : 15, completed ? '#a8ffd0' : '#f2e3bf', true))); this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 3 : 8), title, this.textStyle(compactDialogueList ? 11 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
if (isFirstBattleFollowup || isCamaraderieMemory) { if (isFirstBattleFollowup || isCamaraderieMemory || pendingThirdBattlePriorityReturn) {
const badgeLabel = completed ? '완료' : isCamaraderieMemory ? '전우 기억' : '회고'; const badgeLabel = pendingThirdBattlePriorityReturn
? thirdBattlePriorityReturn?.priorityLabel ?? '새 이야기 · 우선'
: completed
? '완료'
: isCamaraderieMemory
? '전우 기억'
: '회고';
this.track( this.track(
this.add.text( this.add.text(
x + 320, x + 320,
@@ -22777,7 +22872,13 @@ export class CampScene extends Phaser.Scene {
badgeLabel, badgeLabel,
{ {
...this.textStyle(compactDialogueList ? 8 : 9, completed ? '#a8ffd0' : '#fff2b8', true), ...this.textStyle(compactDialogueList ? 8 : 9, completed ? '#a8ffd0' : '#fff2b8', true),
backgroundColor: completed ? '#21402d' : isCamaraderieMemory ? '#36507a' : '#8a3f25', backgroundColor: completed
? '#21402d'
: pendingThirdBattlePriorityReturn
? '#8a3f25'
: isCamaraderieMemory
? '#36507a'
: '#8a3f25',
padding: { x: 4, y: 2 } padding: { x: 4, y: 2 }
} }
) )
@@ -22803,12 +22904,25 @@ export class CampScene extends Phaser.Scene {
return; return;
} }
const completed = this.completedCampDialogues().includes(dialogue.id); const completed = this.completedCampDialogues().includes(dialogue.id);
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const isThirdBattlePriorityReturn =
thirdBattlePriorityReturn?.targetDialogueId === dialogue.id;
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92)); const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
bg.setOrigin(0); bg.setOrigin(0);
bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5); bg.setStrokeStyle(1, completed ? palette.green : palette.gold, 0.5);
this.track(this.add.text(x + 18, y + 16, dialogue.title, this.textStyle(20, '#f2e3bf', true))); this.track(this.add.text(x + 18, y + 16, dialogue.title, this.textStyle(20, '#f2e3bf', true)));
if (isThirdBattlePriorityReturn) {
this.track(
this.add.text(
x + 18,
y + 45,
`전투 기록 · ${thirdBattlePriorityReturn.summary}`,
this.textStyle(11, completed ? '#a8ffd0' : '#ffdf7b', true)
)
);
}
const dialogueBody = this.track( const dialogueBody = this.track(
this.add.text(x + 18, y + 52, dialogue.lines.join('\n'), { this.add.text(x + 18, y + (isThirdBattlePriorityReturn ? 70 : 52), dialogue.lines.join('\n'), {
...this.textStyle(14, '#d4dce6'), ...this.textStyle(14, '#d4dce6'),
wordWrap: { width: width - 36, useAdvancedWrap: true }, wordWrap: { width: width - 36, useAdvancedWrap: true },
lineSpacing: 3 lineSpacing: 3
@@ -24152,11 +24266,14 @@ export class CampScene extends Phaser.Scene {
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) { private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
const followup = this.firstBattleCampFollowup(); const followup = this.firstBattleCampFollowup();
const camaraderieMemory = this.firstBattleCamaraderieMemory(); const camaraderieMemory = this.firstBattleCamaraderieMemory();
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const preparationLine = camaraderieMemory?.dialogue.id === dialogue.id const preparationLine = camaraderieMemory?.dialogue.id === dialogue.id
? `\n다음 준비 · ${camaraderieMemory.unitNames.join('↔')}를 직접 핵심 공명조로 지정하면 추격 +${camaraderieMemory.bonusRate}%p · 자동 지정 없음` ? `\n다음 준비 · ${camaraderieMemory.unitNames.join('↔')}를 직접 핵심 공명조로 지정하면 추격 +${camaraderieMemory.bonusRate}%p · 자동 지정 없음`
: followup?.targetDialogueId === dialogue.id : followup?.targetDialogueId === dialogue.id
? `\n다음 준비 · ${followup.recommendationTitle}` ? `\n다음 준비 · ${followup.recommendationTitle}`
: ''; : thirdBattlePriorityReturn?.targetDialogueId === dialogue.id
? '\n귀환 회고 완료 · 나머지 동료 대화는 자유 선택'
: '';
this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}${preparationLine}`); this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}${preparationLine}`);
} }
@@ -25155,6 +25272,7 @@ export class CampScene extends Phaser.Scene {
const firstBattleCampFollowup = this.firstBattleCampFollowup(); const firstBattleCampFollowup = this.firstBattleCampFollowup();
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory(); const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue(); const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
const firstPursuitScoutVisit = this.availableCampVisits().find( const firstPursuitScoutVisit = this.availableCampVisits().find(
(visit) => visit.id === firstPursuitScoutVisitId (visit) => visit.id === firstPursuitScoutVisitId
); );
@@ -25326,6 +25444,32 @@ export class CampScene extends Phaser.Scene {
} }
} }
: null, : null,
thirdBattlePriorityReturn: thirdBattlePriorityReturn
? {
available: true,
sourceBattleId: thirdBattlePriorityReturn.sourceBattleId,
sourceTurn: thirdBattlePriorityReturn.turnNumber,
reportSource: thirdBattlePriorityReturn.reportSource,
fortAchieved: thirdBattlePriorityReturn.fortAchieved,
quickAchieved: thirdBattlePriorityReturn.quickAchieved,
variant: thirdBattlePriorityReturn.variantId,
targetDialogueId: thirdBattlePriorityReturn.targetDialogueId,
priorityLabel: thirdBattlePriorityReturn.priorityLabel,
summary: thirdBattlePriorityReturn.summary,
dialogueTitle: thirdBattlePriorityReturn.title,
dialogueLines: [...thirdBattlePriorityReturn.lines],
selected: this.selectedDialogueId === thirdBattlePriorityReturn.targetDialogueId,
completed: this.completedCampDialogues().includes(
thirdBattlePriorityReturn.targetDialogueId
),
pending: !this.completedCampDialogues().includes(
thirdBattlePriorityReturn.targetDialogueId
),
rowBounds: this.sortieObjectBoundsDebug(
this.campDialogueRowButtons[thirdBattlePriorityReturn.targetDialogueId]
)
}
: null,
firstPursuitScoutMemory: { firstPursuitScoutMemory: {
visitId: firstPursuitScoutVisitId, visitId: firstPursuitScoutVisitId,
available: Boolean(firstPursuitScoutVisit), available: Boolean(firstPursuitScoutVisit),
@@ -25396,6 +25540,8 @@ export class CampScene extends Phaser.Scene {
completed: this.completedCampDialogues().includes(selectedCampDialogue.id), completed: this.completedCampDialogues().includes(selectedCampDialogue.id),
firstVictoryFollowup: firstBattleCampFollowup?.targetDialogueId === selectedCampDialogue.id, firstVictoryFollowup: firstBattleCampFollowup?.targetDialogueId === selectedCampDialogue.id,
camaraderieMemory: firstBattleCamaraderieMemory?.dialogue.id === selectedCampDialogue.id, camaraderieMemory: firstBattleCamaraderieMemory?.dialogue.id === selectedCampDialogue.id,
thirdBattlePriorityReturn:
thirdBattlePriorityReturn?.targetDialogueId === selectedCampDialogue.id,
rowBounds: this.sortieObjectBoundsDebug(this.campDialogueRowButtons[selectedCampDialogue.id]), rowBounds: this.sortieObjectBoundsDebug(this.campDialogueRowButtons[selectedCampDialogue.id]),
bodyBounds: this.sortieTextBoundsDebug(this.campDialogueBodyText), bodyBounds: this.sortieTextBoundsDebug(this.campDialogueBodyText),
choices: selectedCampDialogue.choices.map((choice) => ({ choices: selectedCampDialogue.choices.map((choice) => ({