feat: prioritize third-victory return dialogue
This commit is contained in:
@@ -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.`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user