From 0c47bbd08eb79a3995de155fa206004c0aa340da Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 15:52:34 +0900 Subject: [PATCH] Validate camp campaign step references --- scripts/verify-camp-reward-data.mjs | 40 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/scripts/verify-camp-reward-data.mjs b/scripts/verify-camp-reward-data.mjs index 0df0835..02cdb2a 100644 --- a/scripts/verify-camp-reward-data.mjs +++ b/scripts/verify-camp-reward-data.mjs @@ -13,6 +13,7 @@ const server = await createServer({ try { const { battleScenarios, defaultBattleScenario } = await server.ssrLoadModule('/src/game/data/battles.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); @@ -20,8 +21,9 @@ try { validateCampBattleIdEntries(campBattleIdEntries, battleScenarios); const dialogueEvents = collectCampEvents(source, 'campDialogues'); const visitEvents = collectCampEvents(source, 'campVisits'); - validateCampEventCollection(dialogueEvents, 'campDialogues', campBattleIdKeys); - validateCampEventCollection(visitEvents, 'campVisits', campBattleIdKeys); + const campaignStepReferences = + validateCampEventCollection(dialogueEvents, 'campDialogues', campBattleIdKeys, isCampaignStep) + + validateCampEventCollection(visitEvents, 'campVisits', campBattleIdKeys, isCampaignStep); validateNumericProperties('gold', { min: 1, max: 20000 }); validateNumericProperties('bondExp', { min: 0, max: 200 }); validateNumericProperties('rewardExp', { min: 1, max: 200 }); @@ -31,7 +33,7 @@ try { } console.log( - `Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${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, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.` ); } finally { await server.close(); @@ -133,8 +135,9 @@ function collectCampEvents(text, arrayName) { })); } -function validateCampEventCollection(events, collectionName, campBattleIdKeys) { +function validateCampEventCollection(events, collectionName, campBattleIdKeys, isCampaignStep) { const seenIds = new Map(); + let campaignStepReferenceCount = 0; if (events.length === 0) { errors.push(`${sourcePath}: ${collectionName} has no entries`); } @@ -176,7 +179,29 @@ function validateCampEventCollection(events, collectionName, campBattleIdKeys) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} references unknown campBattleIds.${reference[1]}`); } } + campaignStepReferenceCount += validateAvailableDuringSteps(event, context, isCampaignStep); }); + return campaignStepReferenceCount; +} + +function validateAvailableDuringSteps(event, context, isCampaignStep) { + const availableDuringMatch = /availableDuringSteps:\s*\[([\s\S]*?)\]/.exec(event.body); + if (!availableDuringMatch) { + return 0; + } + + const body = availableDuringMatch[1]; + const bodyOffset = event.offset + (availableDuringMatch.index ?? 0) + availableDuringMatch[0].indexOf(body); + const steps = collectStringLiterals(body, bodyOffset); + if (steps.length === 0) { + errors.push(`${sourcePath}:${lineNumber(bodyOffset)} ${context} has an empty availableDuringSteps list`); + } + steps.forEach((step) => { + if (!isCampaignStep(step.value)) { + errors.push(`${sourcePath}:${lineNumber(step.offset)} ${context} references unknown campaign step "${step.value}"`); + } + }); + return steps.length; } function collectStringProperties(text, propertyName) { @@ -187,6 +212,13 @@ function collectStringProperties(text, propertyName) { })); } +function collectStringLiterals(text, offset = 0) { + return [...text.matchAll(/'((?:\\'|[^'])*)'/g)].map((match) => ({ + value: match[1].replace(/\\'/g, "'"), + offset: offset + (match.index ?? 0) + })); +} + function parseRewardLabel(reward) { const normalized = String(reward).replace(/\s+/g, ' ').trim(); const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);