feat: prioritize third-victory return dialogue
This commit is contained in:
@@ -172,9 +172,24 @@ RPG로 다듬는다.
|
||||
- 낮 강변 환경음, 지면별 발소리, 목표 완료의 시각·음향 신호를 함께
|
||||
사용해 소리를 듣지 못해도 진행 상태를 이해할 수 있게 한다.
|
||||
|
||||
다음 초반 묶음은 세 번째 군영부터 반복되는 방문·대화·상점 흐름을 감사해
|
||||
정보 수집, 장비 구매, 동료 대화 가운데 무엇을 먼저 택했는지가 출전
|
||||
브리핑과 첫 턴의 전술 선택에 짧고 명확하게 되돌아오게 한다.
|
||||
### 완료한 8차 묶음: 세 번째 승리의 결과별 우선 귀환 대화
|
||||
|
||||
- 세 번째 전투에서 실제로 강가 요새를 확보했는지, 30턴 안에 승리했는지,
|
||||
몇 턴이 걸렸는지를 승리 기록에서 함께 읽는다.
|
||||
- 요새를 놓쳤다면 유비·관우의 방어선 회고, 요새는 지켰지만 늦었다면
|
||||
유비·장비의 추격 속도 회고, 둘 다 달성했다면 관우·장비의 본영 진입
|
||||
대화를 `새 이야기 · 우선`으로 한 곳만 제시한다.
|
||||
- 전투 결과와 반대되는 칭찬을 하지 않으며, 오래되거나 손상되어 목표와
|
||||
턴을 신뢰할 수 없는 저장은 기존 정적 대사를 그대로 사용한다.
|
||||
- 출진 준비의 `공명 대화` 권장은 우선 귀환 대화 한 건만 완료하면 충족된다.
|
||||
나머지 두 대화와 공명 보상은 사라지지 않고 자유 선택으로 남는다.
|
||||
- 우선 대사를 완료하면 대화 탭과 카드의 새 이야기 표식이 사라지고,
|
||||
다시 군영에 들어와도 완료 상태를 유지한다.
|
||||
- 네 가지 목표 조합과 Canvas·WebGL의 1920x1080 진행을 자동 검증한다.
|
||||
|
||||
다음 초반 묶음은 세 번째 군영에서 정보 수집, 장비 정비, 동료 대화 가운데
|
||||
이번 출진의 준비 우선순위 하나를 직접 정하고, 그 선택이 네 번째 전투
|
||||
브리핑과 첫 3턴 안에 짧고 명확하게 되돌아오게 한다.
|
||||
|
||||
## 장기 개선 순서
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
"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: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:exploration-backgrounds": "node scripts/verify-prologue-exploration-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:save-flow": "node scripts/verify-save-retry-flow.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:public-deploy": "node scripts/verify-public-deploy.mjs",
|
||||
"qa:representative": "node scripts/qa-representative-battles.mjs",
|
||||
|
||||
@@ -27,6 +27,7 @@ const checks = [
|
||||
'scripts/verify-first-battle-camaraderie-memory.mjs',
|
||||
'scripts/verify-first-pursuit-scout-memory.mjs',
|
||||
'scripts/verify-second-battle-relief-exploration.mjs',
|
||||
'scripts/verify-third-battle-return-dialogue.mjs',
|
||||
'scripts/verify-prologue-exploration-asset-data.mjs',
|
||||
'scripts/verify-exploration-character-asset-data.mjs',
|
||||
'scripts/verify-prologue-dialogue-portrait-data.mjs',
|
||||
|
||||
1111
scripts/verify-third-battle-priority-return-browser.mjs
Normal file
1111
scripts/verify-third-battle-priority-return-browser.mjs
Normal file
File diff suppressed because it is too large
Load Diff
422
scripts/verify-third-battle-return-dialogue.mjs
Normal file
422
scripts/verify-third-battle-return-dialogue.mjs
Normal 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.`
|
||||
);
|
||||
}
|
||||
304
src/game/data/thirdBattleReturnDialogue.ts
Normal file
304
src/game/data/thirdBattleReturnDialogue.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,11 @@ import {
|
||||
secondBattleReliefSourceBattleId,
|
||||
secondBattleReliefVisitId
|
||||
} from '../data/secondBattleReliefExploration';
|
||||
import {
|
||||
resolveThirdBattlePriorityReturnDialogue,
|
||||
thirdBattleReturnCampStep,
|
||||
thirdBattleReturnSourceBattleId
|
||||
} from '../data/thirdBattleReturnDialogue';
|
||||
import {
|
||||
findCityStayAfterBattle,
|
||||
type CityEquipmentOffer,
|
||||
@@ -11643,7 +11648,18 @@ export class CampScene extends Phaser.Scene {
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
||||
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
|
||||
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;
|
||||
const cityStay = this.currentCityStay();
|
||||
if (cityStay) {
|
||||
@@ -12883,6 +12899,18 @@ export class CampScene extends Phaser.Scene {
|
||||
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() {
|
||||
const campaign = this.campaign;
|
||||
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() {
|
||||
const followup = this.firstBattleCampFollowup();
|
||||
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[] {
|
||||
const battleId = this.currentCampBattleId();
|
||||
const battleDialogues = campDialogues.filter((dialogue) => dialogue.availableAfterBattleIds.includes(battleId));
|
||||
@@ -12940,7 +12988,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const available = dialogues.length > 0
|
||||
? dialogues
|
||||
: 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();
|
||||
return camaraderieDialogue && !resolved.some((dialogue) => dialogue.id === camaraderieDialogue.id)
|
||||
? [...resolved, camaraderieDialogue]
|
||||
@@ -13936,6 +13986,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const active = this.activeTab === tab;
|
||||
const pendingFirstBattleFollowup = tab === 'dialogue' && this.hasPendingFirstBattleCampFollowup();
|
||||
const pendingCamaraderieMemory = tab === 'dialogue' && this.hasPendingFirstBattleCamaraderieMemory();
|
||||
const pendingThirdBattleReturn = tab === 'dialogue' && this.hasPendingThirdBattlePriorityReturn();
|
||||
const pendingCampVisit = tab === 'visit' && this.availableCampVisits().some(
|
||||
(visit) => !this.completedCampVisits().includes(visit.id)
|
||||
);
|
||||
@@ -13946,11 +13997,19 @@ export class CampScene extends Phaser.Scene {
|
||||
indicator.setVisible(active || hovered);
|
||||
indicator.setAlpha(active ? 1 : 0.66);
|
||||
if (tab === 'dialogue' && newBadge) {
|
||||
newBadge.setText(pendingFirstBattleFollowup ? '회고' : pendingCamaraderieMemory ? '전우' : '회고');
|
||||
newBadge.setText(
|
||||
pendingThirdBattleReturn
|
||||
? '우선'
|
||||
: pendingFirstBattleFollowup
|
||||
? '회고'
|
||||
: pendingCamaraderieMemory
|
||||
? '전우'
|
||||
: '회고'
|
||||
);
|
||||
}
|
||||
newBadge?.setVisible(Boolean(
|
||||
(tab === 'city' && this.hasPendingCityStayActions()) ||
|
||||
(tab === 'dialogue' && (pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
|
||||
(tab === 'dialogue' && (pendingThirdBattleReturn || pendingFirstBattleFollowup || pendingCamaraderieMemory)) ||
|
||||
pendingCampVisit ||
|
||||
(tab === 'supplies' && pendingCategories.has('supplies')) ||
|
||||
(tab === 'equipment' && pendingCategories.has('equipment'))
|
||||
@@ -19838,6 +19897,11 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setStrokeStyle(1, palette.gold, 0.4);
|
||||
const availableDialogues = this.availableCampDialogues();
|
||||
const completedDialogues = this.completedAvailableDialogues().length;
|
||||
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||
const priorityDialogueCompleted = Boolean(
|
||||
thirdBattlePriorityReturn &&
|
||||
this.completedCampDialogues().includes(thirdBattlePriorityReturn.targetDialogueId)
|
||||
);
|
||||
const availableVisits = this.availableCampVisits();
|
||||
const completedVisits = this.completedAvailableVisits().length;
|
||||
const selectedUnits = this.selectedSortieUnits();
|
||||
@@ -19852,8 +19916,14 @@ export class CampScene extends Phaser.Scene {
|
||||
const sortieNote = this.nextSortieRule().note;
|
||||
const recoveryHint = this.sortieRecoveryHint();
|
||||
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 = [
|
||||
`대화 ${completedDialogues}/${availableDialogues.length}`,
|
||||
dialogueProgress,
|
||||
`방문 ${completedVisits}/${availableVisits.length}`,
|
||||
`${hasBattle ? '출전' : '동행'} ${selectedSummary}`,
|
||||
`보유 ${inventorySummary}`
|
||||
@@ -20111,6 +20181,20 @@ export class CampScene extends Phaser.Scene {
|
||||
const assignedSupplyCount = campSupplies.reduce((total, supply) => total + this.totalAssignedSupplyCount(supply.usableId), 0);
|
||||
const availableDialogues = this.availableCampDialogues();
|
||||
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 completedVisits = this.completedAvailableVisits().length;
|
||||
const selected = this.selectedSortieUnits();
|
||||
@@ -20176,8 +20260,8 @@ export class CampScene extends Phaser.Scene {
|
||||
},
|
||||
{
|
||||
label: '공명 대화',
|
||||
complete: availableDialogues.length === 0 || completedDialogues >= availableDialogues.length,
|
||||
detail: `${completedDialogues}/${availableDialogues.length} 완료`,
|
||||
complete: dialogueChecklistComplete,
|
||||
detail: dialogueChecklistDetail,
|
||||
priority: 'recommended'
|
||||
},
|
||||
{
|
||||
@@ -22740,6 +22824,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const dialogues = this.availableCampDialogues();
|
||||
const firstBattleFollowup = this.firstBattleCampFollowup();
|
||||
const camaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||
const compactDialogueList = dialogues.length > 3;
|
||||
const dialogueRowGap = compactDialogueList ? 35 : 64;
|
||||
const dialogueRowHeight = compactDialogueList ? 30 : 48;
|
||||
@@ -22764,12 +22849,22 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
const isFirstBattleFollowup = firstBattleFollowup?.targetDialogueId === 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)
|
||||
: dialogue.title;
|
||||
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 3 : 8), title, this.textStyle(compactDialogueList ? 11 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
||||
if (isFirstBattleFollowup || isCamaraderieMemory) {
|
||||
const badgeLabel = completed ? '완료' : isCamaraderieMemory ? '전우 기억' : '회고';
|
||||
if (isFirstBattleFollowup || isCamaraderieMemory || pendingThirdBattlePriorityReturn) {
|
||||
const badgeLabel = pendingThirdBattlePriorityReturn
|
||||
? thirdBattlePriorityReturn?.priorityLabel ?? '새 이야기 · 우선'
|
||||
: completed
|
||||
? '완료'
|
||||
: isCamaraderieMemory
|
||||
? '전우 기억'
|
||||
: '회고';
|
||||
this.track(
|
||||
this.add.text(
|
||||
x + 320,
|
||||
@@ -22777,7 +22872,13 @@ export class CampScene extends Phaser.Scene {
|
||||
badgeLabel,
|
||||
{
|
||||
...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 }
|
||||
}
|
||||
)
|
||||
@@ -22803,12 +22904,25 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
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));
|
||||
bg.setOrigin(0);
|
||||
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)));
|
||||
if (isThirdBattlePriorityReturn) {
|
||||
this.track(
|
||||
this.add.text(
|
||||
x + 18,
|
||||
y + 45,
|
||||
`전투 기록 · ${thirdBattlePriorityReturn.summary}`,
|
||||
this.textStyle(11, completed ? '#a8ffd0' : '#ffdf7b', true)
|
||||
)
|
||||
);
|
||||
}
|
||||
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'),
|
||||
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
||||
lineSpacing: 3
|
||||
@@ -24152,11 +24266,14 @@ export class CampScene extends Phaser.Scene {
|
||||
private showDialogueReward(dialogue: CampDialogue, choice: CampDialogueChoice, rewardExp: number) {
|
||||
const followup = this.firstBattleCampFollowup();
|
||||
const camaraderieMemory = this.firstBattleCamaraderieMemory();
|
||||
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||
const preparationLine = camaraderieMemory?.dialogue.id === dialogue.id
|
||||
? `\n다음 준비 · ${camaraderieMemory.unitNames.join('↔')}를 직접 핵심 공명조로 지정하면 추격 +${camaraderieMemory.bonusRate}%p · 자동 지정 없음`
|
||||
: followup?.targetDialogueId === dialogue.id
|
||||
? `\n다음 준비 · ${followup.recommendationTitle}`
|
||||
: '';
|
||||
: thirdBattlePriorityReturn?.targetDialogueId === dialogue.id
|
||||
? '\n귀환 회고 완료 · 나머지 동료 대화는 자유 선택'
|
||||
: '';
|
||||
this.showCampNotice(`획득 내역 · ${choice.label} · 공명 +${rewardExp}\n응답 · ${choice.response}${preparationLine}`);
|
||||
}
|
||||
|
||||
@@ -25155,6 +25272,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const firstBattleCampFollowup = this.firstBattleCampFollowup();
|
||||
const firstBattleCamaraderieMemory = this.firstBattleCamaraderieMemory();
|
||||
const firstBattleCamaraderieDialogue = this.firstBattleCamaraderieDialogue();
|
||||
const thirdBattlePriorityReturn = this.thirdBattlePriorityReturnDialogue();
|
||||
const firstPursuitScoutVisit = this.availableCampVisits().find(
|
||||
(visit) => visit.id === firstPursuitScoutVisitId
|
||||
);
|
||||
@@ -25326,6 +25444,32 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
: 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: {
|
||||
visitId: firstPursuitScoutVisitId,
|
||||
available: Boolean(firstPursuitScoutVisit),
|
||||
@@ -25396,6 +25540,8 @@ export class CampScene extends Phaser.Scene {
|
||||
completed: this.completedCampDialogues().includes(selectedCampDialogue.id),
|
||||
firstVictoryFollowup: firstBattleCampFollowup?.targetDialogueId === selectedCampDialogue.id,
|
||||
camaraderieMemory: firstBattleCamaraderieMemory?.dialogue.id === selectedCampDialogue.id,
|
||||
thirdBattlePriorityReturn:
|
||||
thirdBattlePriorityReturn?.targetDialogueId === selectedCampDialogue.id,
|
||||
rowBounds: this.sortieObjectBoundsDebug(this.campDialogueRowButtons[selectedCampDialogue.id]),
|
||||
bodyBounds: this.sortieTextBoundsDebug(this.campDialogueBodyText),
|
||||
choices: selectedCampDialogue.choices.map((choice) => ({
|
||||
|
||||
Reference in New Issue
Block a user