423 lines
12 KiB
JavaScript
423 lines
12 KiB
JavaScript
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.`
|
|
);
|
|
}
|