feat: improve early battle clarity and keyboard flow

This commit is contained in:
2026-07-28 03:15:40 +09:00
parent a41672bd0e
commit 1773bffbf2
9 changed files with 1945 additions and 477 deletions

View File

@@ -41,15 +41,22 @@ const server = await createServer({
let battleScenarios;
let scenarioModule;
let storyAssetModule;
let campaignFlowModule;
let prologueMilitiaCampModule;
try {
({ battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'));
scenarioModule = await server.ssrLoadModule('/src/game/data/scenario.ts');
storyAssetModule = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
campaignFlowModule = await server.ssrLoadModule('/src/game/data/campaignFlow.ts');
prologueMilitiaCampModule = await server.ssrLoadModule(
'/src/game/data/prologueMilitiaCamp.ts'
);
} finally {
await server.close();
}
validateEarlyHanSeokContinuity();
validateCriticalVictoryObjectives();
validateFinalStoryTransition();
validateWuzhangStoryBackgrounds();
@@ -66,11 +73,136 @@ if (failures.length > 0) {
const criticalObjectiveCount = Object.values(criticalSecureObjectiveIds)
.reduce((count, objectiveIds) => count + objectiveIds.length, 0);
console.log(
`Verified ${criticalObjectiveCount} narrative-critical secure objectives, ` +
`Verified the Han Seok first-battle retreat, second-battle pursuit, and final defeat, ` +
`${criticalObjectiveCount} narrative-critical secure objectives, ` +
'the Wuzhang transition and background family, the 55-66 timeline chapter, ' +
'and immediate unique camp reward continuity.'
);
function validateEarlyHanSeokContinuity() {
const firstBattleId = 'first-battle-zhuo-commandery';
const secondBattleId = 'second-battle-yellow-turban-pursuit';
const firstScenario = battleScenarios[firstBattleId];
const secondScenario = battleScenarios[secondBattleId];
assert(firstScenario, `Missing first Han Seok encounter: ${firstBattleId}`);
assert(secondScenario, `Missing Han Seok pursuit battle: ${secondBattleId}`);
if (!firstScenario || !secondScenario) {
return;
}
const firstLeaderObjective = firstScenario.objectives.find(
(objective) =>
objective.kind === 'defeat-leader' &&
objective.unitId === firstScenario.leaderUnitId
);
const firstBattleObjectiveCopy = [
firstScenario.victoryConditionLabel,
firstLeaderObjective?.label,
...firstScenario.openingObjectiveLines
].filter(Boolean).join(' ');
assert(
firstBattleObjectiveCopy.includes('한석') &&
firstBattleObjectiveCopy.includes('선봉') &&
/(격퇴|퇴각)/.test(firstBattleObjectiveCopy),
`${firstBattleId}: the first Han Seok encounter must be framed as routing his vanguard`
);
assert(
!/(?:한석|두령)[^.!?]{0,12}격파/.test(firstBattleObjectiveCopy),
`${firstBattleId}: first-battle objective copy must not imply Han Seok is finally defeated`
);
const commander = prologueMilitiaCampModule
.prologueMilitiaCampNpcDefinitions
.find((npc) => npc.id === 'zou-jing');
const commanderText = commander?.dialogue.map((line) => line.text).join(' ') ?? '';
assert(
commanderText.includes('한석') && commanderText.includes('선봉'),
'The pre-battle militia-camp command must introduce the first enemy as Han Seoks vanguard'
);
const departureText = scenarioModule.prologuePages
.filter((page) => ['first-sortie', 'battle-briefing'].includes(page.id))
.map((page) => page.text)
.join(' ');
assert(
departureText.includes('한석') && departureText.includes('선봉'),
'The playable prologue departure must preserve the Han Seok vanguard framing'
);
const firstVictoryText = firstScenario.victoryPages
.flatMap((page) => [
page.text,
page.cutscene?.title,
page.cutscene?.subtitle,
...(page.cutscene?.rewards?.map((reward) => reward.label) ?? [])
])
.filter(Boolean)
.join(' ');
assert(
firstVictoryText.includes('한석') &&
/(달아|퇴각|도주|빠져나)/.test(firstVictoryText) &&
/(북쪽|나루)/.test(firstVictoryText) &&
/(잔당|패잔병|남은 병력)/.test(firstVictoryText),
`${firstBattleId}: victory story must explicitly show Han Seok escaping north with the remnants`
);
const firstCampFlow = campaignFlowModule.getSortieFlow(firstBattleId);
const firstCampBridge = [
firstCampFlow?.title,
firstCampFlow?.description,
firstCampFlow?.rewardHint
].filter(Boolean).join(' ');
assert(
firstCampFlow?.nextBattleId === secondBattleId &&
firstCampBridge.includes('한석') &&
/(퇴각|달아|도주)/.test(firstCampBridge) &&
firstCampBridge.includes('추격'),
'The first-camp sortie flow must bridge Han Seoks retreat directly into the pursuit battle'
);
const secondIntroText = scenarioModule.secondBattleIntroPages
.flatMap((page) => [
page.text,
page.cutscene?.title,
page.cutscene?.subtitle,
...(page.cutscene?.rewards?.map((reward) => reward.label) ?? [])
])
.filter(Boolean)
.join(' ');
const secondBattleObjectiveCopy = [
secondScenario.victoryConditionLabel,
...secondScenario.openingObjectiveLines
].join(' ');
const secondVictoryText = secondScenario.victoryPages
.flatMap((page) => [
page.text,
page.cutscene?.title,
page.cutscene?.subtitle,
...(page.cutscene?.rewards?.map((reward) => reward.label) ?? [])
])
.filter(Boolean)
.join(' ');
assert(
secondIntroText.includes('탁현') &&
secondIntroText.includes('한석') &&
/(퇴각|달아|도주)/.test(secondIntroText) &&
secondIntroText.includes('추격'),
`${secondBattleId}: intro pages must identify the battle as pursuit of Han Seok after his Tak retreat`
);
assert(
secondBattleObjectiveCopy.includes('탁현') &&
secondBattleObjectiveCopy.includes('한석') &&
secondBattleObjectiveCopy.includes('퇴각') &&
secondScenario.victoryConditionLabel.includes('격파'),
`${secondBattleId}: battle objective must close the explicitly linked Han Seok pursuit`
);
assert(
secondVictoryText.includes('한석') && secondVictoryText.includes('격파'),
`${secondBattleId}: victory story must explicitly close the pursuit with Han Seoks final defeat`
);
}
function validateCriticalVictoryObjectives() {
for (const [battleId, objectiveIds] of Object.entries(criticalSecureObjectiveIds)) {
const scenario = battleScenarios[battleId];