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

@@ -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',

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.`
);
}