feat: carry camp promises into first victory
This commit is contained in:
@@ -27,6 +27,7 @@ const expectedCampMarkerIds = [
|
||||
'prologue-camp-review-scout-report',
|
||||
'prologue-camp-ready-vanguard',
|
||||
'prologue-camp-inspect-arms',
|
||||
'prologue-camp-reassure-volunteer',
|
||||
'prologue-camp-complete'
|
||||
];
|
||||
const expectedFirstCommandOrderIds = ['elite', 'mobile', 'siege'];
|
||||
@@ -40,6 +41,8 @@ const server = await createServer({
|
||||
try {
|
||||
const village = await server.ssrLoadModule('/src/game/data/prologueVillage.ts');
|
||||
const militiaCamp = await server.ssrLoadModule('/src/game/data/prologueMilitiaCamp.ts');
|
||||
const narrativeMemory = await server.ssrLoadModule('/src/game/data/firstBattleNarrativeMemory.ts');
|
||||
const scenario = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
||||
const sortieOrders = await server.ssrLoadModule('/src/game/data/sortieOrders.ts');
|
||||
const campaign = await server.ssrLoadModule('/src/game/state/campaignState.ts');
|
||||
const unitAssets = await server.ssrLoadModule('/src/game/data/unitAssets.ts');
|
||||
@@ -262,6 +265,14 @@ try {
|
||||
'militia camp should include at least one optional volunteer conversation',
|
||||
failures
|
||||
);
|
||||
const nervousVolunteer = campNpcs.find((npc) => npc.id === 'nervous-volunteer');
|
||||
assert(
|
||||
nervousVolunteer?.dialogue.length === 2 &&
|
||||
nervousVolunteer.repeatDialogue?.length === 1 &&
|
||||
dialogueText(nervousVolunteer).includes('보조를 맞추고'),
|
||||
'the nervous volunteer should make one optional promise and use a concise repeat line',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
!JSON.stringify(campNpcs).includes('피란'),
|
||||
'militia camp dialogue must use the established 피난 terminology consistently',
|
||||
@@ -302,6 +313,12 @@ try {
|
||||
'prologue-camp must be recognized by campaign progress summaries',
|
||||
failures
|
||||
);
|
||||
verifyFirstBattleNarrativeMemory(
|
||||
narrativeMemory,
|
||||
scenario.firstBattleVictoryPages,
|
||||
sortieOrders,
|
||||
failures
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(
|
||||
@@ -313,7 +330,8 @@ try {
|
||||
console.log(
|
||||
`Verified ${npcs.length} village NPCs, ${campNpcs.length} militia camp NPCs, ` +
|
||||
`${openingPages.length + brotherhoodPages.length + departurePages.length} prologue story pages, ` +
|
||||
`${firstCommandChoices.length} first-command choices, sequential meetings, camp preparation, and save markers.`
|
||||
`${firstCommandChoices.length} first-command choices, sequential meetings, camp preparation, ` +
|
||||
'optional promise memory, six victory-recall variants, and save markers.'
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
@@ -333,6 +351,169 @@ function assertArrayEquals(actual, expected, label, failures) {
|
||||
);
|
||||
}
|
||||
|
||||
function verifyFirstBattleNarrativeMemory(memory, sourcePages, sortieOrders, failures) {
|
||||
const battleId = 'first-battle-zhuo-commandery';
|
||||
const pageSnapshot = JSON.stringify(sourcePages);
|
||||
const frozenPages = deepFreeze(sourcePages.map((page) => ({
|
||||
...page,
|
||||
...(page.cutscene ? { cutscene: structuredClone(page.cutscene) } : {})
|
||||
})));
|
||||
const progressIdsByOrder = {
|
||||
elite: ['victory', 'elite-grade', 'elite-survival'],
|
||||
mobile: ['victory', 'mobile-quick', 'mobile-breakthrough'],
|
||||
siege: ['victory', 'siege-objective', 'siege-front', 'siege-support']
|
||||
};
|
||||
const missedIdByOrder = {
|
||||
elite: 'elite-survival',
|
||||
mobile: 'mobile-quick',
|
||||
siege: 'siege-support'
|
||||
};
|
||||
const missedLabelByOrder = {
|
||||
elite: '전원 생존',
|
||||
mobile: '제한 턴',
|
||||
siege: '후원 기여'
|
||||
};
|
||||
|
||||
for (const orderId of ['elite', 'mobile', 'siege']) {
|
||||
for (const achieved of [true, false]) {
|
||||
const failedProgressId = achieved ? null : missedIdByOrder[orderId];
|
||||
const report = {
|
||||
battleId,
|
||||
outcome: 'victory',
|
||||
turnNumber: 7,
|
||||
mvp: { unitId: 'liu-bei', name: ' 유비 ' },
|
||||
sortieOrder: {
|
||||
orderId,
|
||||
achieved,
|
||||
rewardGranted: false,
|
||||
progress: progressIdsByOrder[orderId].map((id) => ({
|
||||
id,
|
||||
achieved: id !== failedProgressId,
|
||||
value: id === failedProgressId ? 0 : 1,
|
||||
target: 1
|
||||
}))
|
||||
}
|
||||
};
|
||||
const resolved = memory.resolveFirstBattleVictoryNarrative(frozenPages, report);
|
||||
const guanYuPage = resolved.pages.find((page) => page.id === 'first-victory-guan-yu');
|
||||
const zhangFeiPage = resolved.pages.find((page) => page.id === 'first-victory-zhang-fei');
|
||||
const orderLabel = sortieOrders.sortieOrderDisplayLabel(battleId, orderId);
|
||||
const label = `${orderId}/${achieved ? 'achieved' : 'missed'}`;
|
||||
|
||||
assert(resolved.memory.source === 'campaign', `${label} must use campaign memory`, failures);
|
||||
assert(resolved.memory.orderId === orderId, `${label} must retain its order id`, failures);
|
||||
assert(resolved.memory.orderLabel === orderLabel, `${label} must use the contextual order label`, failures);
|
||||
assert(resolved.memory.achieved === achieved, `${label} must branch on achieved, not rewardGranted`, failures);
|
||||
assert(resolved.memory.turnNumber === 7, `${label} must retain the actual turn`, failures);
|
||||
assert(resolved.memory.mvp?.name === '유비', `${label} must normalize the MVP name`, failures);
|
||||
assert(
|
||||
guanYuPage?.text.includes(`${orderLabel} 군령`) &&
|
||||
zhangFeiPage?.text.includes(`${orderLabel} 군령`),
|
||||
`${label} dialogue must name the selected order`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
guanYuPage?.text !== zhangFeiPage?.text,
|
||||
`${label} must give Guan Yu and Zhang Fei distinct voices`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
(guanYuPage?.text.length ?? 999) <= 96 && (zhangFeiPage?.text.length ?? 999) <= 96,
|
||||
`${label} dialogue must fit the existing FHD story panel`,
|
||||
failures
|
||||
);
|
||||
if (achieved) {
|
||||
assert(
|
||||
resolved.memory.missedCriterionIds.length === 0 &&
|
||||
guanYuPage?.text.includes('모두 지켰') &&
|
||||
zhangFeiPage?.text.includes('완수'),
|
||||
`${label} must clearly remember command completion`,
|
||||
failures
|
||||
);
|
||||
} else {
|
||||
assert(
|
||||
resolved.memory.missedCriterionIds.includes(failedProgressId) &&
|
||||
guanYuPage?.text.includes(missedLabelByOrder[orderId]) &&
|
||||
zhangFeiPage?.text.includes(missedLabelByOrder[orderId]),
|
||||
`${label} must name the missed criterion`,
|
||||
failures
|
||||
);
|
||||
}
|
||||
if (orderId === 'mobile') {
|
||||
assert(
|
||||
guanYuPage?.text.includes('7턴') && zhangFeiPage?.text.includes('7턴'),
|
||||
`${label} must remember the actual battle turn`,
|
||||
failures
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
resolved.pages.every((page, index) => page !== frozenPages[index]),
|
||||
`${label} must return new page objects`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
resolved.pages.every((page, index) => (
|
||||
['first-victory-guan-yu', 'first-victory-zhang-fei'].includes(page.id) ||
|
||||
page.text === frozenPages[index].text
|
||||
)),
|
||||
`${label} must change only the two companion recall pages`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(frozenPages) === pageSnapshot &&
|
||||
JSON.stringify(sourcePages) === pageSnapshot,
|
||||
`${label} must not mutate shared story pages`,
|
||||
failures
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackReports = [
|
||||
[undefined, 'missing-report'],
|
||||
[{ battleId: 'other-battle', outcome: 'victory' }, 'battle-mismatch'],
|
||||
[{ battleId, outcome: 'defeat' }, 'non-victory'],
|
||||
[{ battleId, outcome: 'victory' }, 'missing-order'],
|
||||
[{
|
||||
battleId,
|
||||
outcome: 'victory',
|
||||
sortieOrder: { orderId: 'elite', achieved: true, progress: [] }
|
||||
}, 'invalid-order-progress']
|
||||
];
|
||||
fallbackReports.forEach(([report, reason]) => {
|
||||
const resolved = memory.resolveFirstBattleVictoryNarrative(frozenPages, report);
|
||||
assert(
|
||||
resolved.memory.source === 'fallback' &&
|
||||
resolved.memory.fallbackReason === reason &&
|
||||
JSON.stringify(resolved.pages) === pageSnapshot,
|
||||
`${reason} must preserve the authored victory dialogue exactly`,
|
||||
failures
|
||||
);
|
||||
});
|
||||
|
||||
assert(
|
||||
memory.volunteerPromiseLineForBattle(battleId, true) === memory.firstBattleVolunteerPromiseText &&
|
||||
memory.firstBattleVolunteerPromiseText.length <= 42,
|
||||
'the reassured volunteer should return in the existing three-line first-engagement banner',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
memory.volunteerPromiseLineForBattle(battleId, false) === undefined &&
|
||||
memory.volunteerPromiseLineForBattle('second-battle-yellow-turban-pursuit', true) === undefined,
|
||||
'the optional promise must not appear when skipped or leak into later battles',
|
||||
failures
|
||||
);
|
||||
}
|
||||
|
||||
function deepFreeze(value) {
|
||||
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
||||
return value;
|
||||
}
|
||||
Object.freeze(value);
|
||||
Object.values(value).forEach(deepFreeze);
|
||||
return value;
|
||||
}
|
||||
|
||||
function verifyNpcs(npcs, locationLabel, unitAssets, failures) {
|
||||
assert(
|
||||
new Set(npcs.map((npc) => npc.id)).size === npcs.length,
|
||||
|
||||
Reference in New Issue
Block a user