From 4f2ad79baed4172e281949988a3de53f15278d45 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 16:06:50 +0900 Subject: [PATCH] Validate camp visit bond references --- scripts/verify-camp-reward-data.mjs | 48 ++++++++++++++++++++++------- src/game/data/scenario.ts | 8 ----- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/scripts/verify-camp-reward-data.mjs b/scripts/verify-camp-reward-data.mjs index 46bee1e..ff4da0c 100644 --- a/scripts/verify-camp-reward-data.mjs +++ b/scripts/verify-camp-reward-data.mjs @@ -13,19 +13,22 @@ const server = await createServer({ try { const { battleScenarios, defaultBattleScenario } = await server.ssrLoadModule('/src/game/data/battles.ts'); + const scenarioData = await server.ssrLoadModule('/src/game/data/scenario.ts'); const { isCampaignStep } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const itemRewardArrays = collectItemRewardArrays(source); itemRewardArrays.forEach(({ body, index, offset }) => validateItemRewards(body, index, offset)); const campBattleIdEntries = collectCampBattleIdEntries(source, defaultBattleScenario.id); const campBattleIdKeys = new Set(campBattleIdEntries.map((entry) => entry.key)); validateCampBattleIdEntries(campBattleIdEntries, battleScenarios); - const knownBondsById = collectKnownBondsById(battleScenarios); + const knownBondsById = collectKnownBondsById(battleScenarios, scenarioData); const dialogueEvents = collectCampEvents(source, 'campDialogues'); const visitEvents = collectCampEvents(source, 'campVisits'); const campaignStepReferences = validateCampEventCollection(dialogueEvents, 'campDialogues', campBattleIdKeys, isCampaignStep) + validateCampEventCollection(visitEvents, 'campVisits', campBattleIdKeys, isCampaignStep); - const dialogueBondReferences = validateCampDialogueBondReferences(dialogueEvents, knownBondsById); + const campBondReferences = + validateCampBondReferences(dialogueEvents, 'campDialogues', knownBondsById, { matchUnitIds: true }) + + validateCampBondReferences(visitEvents, 'campVisits', knownBondsById); validateNumericProperties('gold', { min: 1, max: 20000 }); validateNumericProperties('bondExp', { min: 0, max: 200 }); validateNumericProperties('rewardExp', { min: 1, max: 200 }); @@ -35,7 +38,7 @@ try { } console.log( - `Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${dialogueBondReferences} camp dialogue bonds, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.` + `Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.` ); } finally { await server.close(); @@ -129,18 +132,41 @@ function validateCampBattleIdEntries(entries, battleScenarios) { }); } -function collectKnownBondsById(battleScenarios) { +function collectKnownBondsById(battleScenarios, scenarioData) { const bondsById = new Map(); - Object.values(battleScenarios).forEach((scenario) => { - (scenario.bonds ?? []).forEach((bond) => { + const addBonds = (bonds) => { + (bonds ?? []).forEach((bond) => { if (!bondsById.has(bond.id)) { bondsById.set(bond.id, bond); } }); + }; + Object.values(battleScenarios).forEach((scenario) => { + addBonds(scenario.bonds); + }); + Object.values(scenarioData).forEach((value) => { + if (isBattleBondArray(value)) { + addBonds(value); + } }); return bondsById; } +function isBattleBondArray(value) { + return Array.isArray(value) && value.length > 0 && value.every(isBattleBondEntry); +} + +function isBattleBondEntry(value) { + return ( + value && + typeof value.id === 'string' && + Array.isArray(value.unitIds) && + typeof value.title === 'string' && + Number.isFinite(value.level) && + Number.isFinite(value.exp) + ); +} + function collectCampEvents(text, arrayName) { const { body, offset } = extractConstArray(text, arrayName); return collectTopLevelObjects(body).map((event) => ({ @@ -218,10 +244,10 @@ function validateAvailableDuringSteps(event, context, isCampaignStep) { return steps.length; } -function validateCampDialogueBondReferences(events, knownBondsById) { +function validateCampBondReferences(events, collectionName, knownBondsById, options = {}) { let bondReferenceCount = 0; events.forEach((event, index) => { - const context = `campDialogues[${index}]`; + const context = `${collectionName}[${index}]`; const bondIdEntries = collectStringProperties(event.body, 'bondId'); if (bondIdEntries.length === 0) { return; @@ -231,8 +257,8 @@ function validateCampDialogueBondReferences(events, knownBondsById) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has multiple bondId properties`); } - const unitIds = collectStringArrayProperty(event.body, 'unitIds', event.offset); - if (unitIds.length === 0) { + const unitIds = options.matchUnitIds ? collectStringArrayProperty(event.body, 'unitIds', event.offset) : []; + if (options.matchUnitIds && unitIds.length === 0) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has bondId but no unitIds`); } @@ -242,7 +268,7 @@ function validateCampDialogueBondReferences(events, knownBondsById) { errors.push(`${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} references unknown bondId "${entry.value}"`); return; } - if (!sameStringSet(unitIds.map((unit) => unit.value), bond.unitIds ?? [])) { + if (options.matchUnitIds && !sameStringSet(unitIds.map((unit) => unit.value), bond.unitIds ?? [])) { errors.push( `${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} bondId "${entry.value}" units [${( bond.unitIds ?? [] diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 8acf295..4b15164 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -20324,14 +20324,6 @@ export const northernThirdBattleBonds: BattleBond[] = [ exp: 0, description: '황권의 보급 장부와 법정의 책략 판단이 합쳐지면 위군의 차단선을 피할 작은 틈을 찾습니다.' }, - { - id: 'ma-liang__jiang-wei', - unitIds: ['ma-liang', 'jiang-wei'], - title: '첫 길을 적는 손', - level: 44, - exp: 0, - description: '마량의 차분한 문서와 강유의 천수 길 판단이 맞물리면 새로 합류한 장수가 촉한군의 길을 빠르게 익힙니다.' - }, { id: 'zhao-yun__ma-dai_northern-third', unitIds: ['zhao-yun', 'ma-dai'],