feat: improve first-session combat and visual assets

This commit is contained in:
2026-07-18 22:11:50 +09:00
parent 4a2a0dfb19
commit f29956b90e
82 changed files with 3640 additions and 839 deletions

View File

@@ -44,6 +44,7 @@ try {
validateReachability(errors, recruitTemplates, recruitRewardRefs);
validateUniqueRecruitRewards(errors, recruitRewardRefs);
validateSortieRecruitAvailability(errors, scenarioEntries);
validateEarlyJianYongChoice(errors, battleScenarios);
if (errors.length) {
console.error(`Campaign recruit data verification failed with ${errors.length} issue(s):`);
@@ -214,6 +215,58 @@ function validateSortieRecruitAvailability(errors, scenarioEntries) {
});
}
function validateEarlyJianYongChoice(errors, battleScenarios) {
const firstBattleId = 'first-battle-zhuo-commandery';
const earlyChoiceBattleIds = [
'second-battle-yellow-turban-pursuit',
'third-battle-guangzong-road',
'fourth-battle-guangzong-camp',
'fifth-battle-sishui-vanguard',
'sixth-battle-jieqiao-relief',
'seventh-battle-xuzhou-rescue'
];
const foundingTrio = ['liu-bei', 'guan-yu', 'zhang-fei'];
const earlyRoster = [...foundingTrio, 'jian-yong'];
const firstScenario = battleScenarios[firstBattleId];
if (!firstScenario?.campaignReward?.recruits?.includes('jian-yong')) {
errors.push(`${firstBattleId}: first victory must recruit jian-yong`);
}
const firstVictoryCopy = firstScenario?.victoryPages?.map((page) => `${page.text ?? ''} ${page.cutscene?.title ?? ''} ${page.cutscene?.subtitle ?? ''}`).join(' ') ?? '';
if (!firstVictoryCopy.includes('간옹') || !firstVictoryCopy.includes('최대 3명')) {
errors.push(`${firstBattleId}: victory story must explain Jian Yong's arrival and the four-officer/three-slot choice`);
}
earlyChoiceBattleIds.forEach((battleId) => {
const scenario = battleScenarios[battleId];
if (!scenario) {
errors.push(`${battleId}: missing early-choice battle scenario`);
return;
}
const allyIds = new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id));
earlyRoster.forEach((unitId) => {
if (!allyIds.has(unitId)) {
errors.push(`${battleId}: early 4-of-3 roster is missing ${unitId}`);
}
});
if (scenario.sortie?.sortieLimit !== 3) {
errors.push(`${battleId}: early formation must keep sortieLimit 3, got ${scenario.sortie?.sortieLimit}`);
}
const recommendedIds = new Set((scenario.sortie?.recommendedUnits ?? []).map((entry) => entry.unitId));
if (recommendedIds.size !== foundingTrio.length || foundingTrio.some((unitId) => !recommendedIds.has(unitId))) {
errors.push(`${battleId}: recommended formation must remain the founding trio`);
}
if (!scenario.bonds.some((bond) => bond.id === 'liu-bei__jian-yong')) {
errors.push(`${battleId}: missing liu-bei__jian-yong resonance`);
}
});
const seventhRecruits = battleScenarios['seventh-battle-xuzhou-rescue']?.campaignReward?.recruits ?? [];
if (seventhRecruits.includes('jian-yong') || !seventhRecruits.includes('mi-zhu')) {
errors.push('seventh-battle-xuzhou-rescue: must recruit only the new Xuzhou officer Mi Zhu, not Jian Yong again');
}
}
function assertNonEmptyString(errors, value, context) {
if (typeof value !== 'string' || value.trim().length === 0) {
errors.push(`${context} must be a non-empty string`);