diff --git a/package.json b/package.json index b2920de..90a9080 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "measure:performance": "node scripts/measure-performance.mjs", "verify:battle-data": "node scripts/verify-battle-scenario-data.mjs", "verify:campaign-data": "node scripts/verify-campaign-flow-data.mjs", + "verify:camp-rewards": "node scripts/verify-camp-reward-data.mjs", "verify:story-assets": "node scripts/verify-story-asset-data.mjs", "verify:story-images": "node scripts/verify-story-image-asset-data.mjs", "verify:visual-assets": "node scripts/verify-visual-asset-data.mjs", diff --git a/scripts/verify-camp-reward-data.mjs b/scripts/verify-camp-reward-data.mjs new file mode 100644 index 0000000..248964b --- /dev/null +++ b/scripts/verify-camp-reward-data.mjs @@ -0,0 +1,74 @@ +import { readFileSync } from 'node:fs'; + +const sourcePath = 'src/game/scenes/CampScene.ts'; +const source = readFileSync(sourcePath, 'utf8'); +const errors = []; + +const itemRewardArrays = collectItemRewardArrays(source); +itemRewardArrays.forEach(({ body, index, offset }) => validateItemRewards(body, index, offset)); +validateNumericProperties('gold', { min: 1, max: 20000 }); +validateNumericProperties('bondExp', { min: 0, max: 200 }); +validateNumericProperties('rewardExp', { min: 1, max: 200 }); + +if (errors.length > 0) { + throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`); +} + +console.log(`Verified ${itemRewardArrays.length} camp item reward lists and camp reward numeric fields.`); + +function collectItemRewardArrays(text) { + return [...text.matchAll(/itemRewards:\s*\[([\s\S]*?)\]/g)].map((match, index) => ({ + body: match[1], + index, + offset: match.index ?? 0 + })); +} + +function validateItemRewards(body, index, offset) { + const entries = [...body.matchAll(/'((?:\\'|[^'])*)'/g)].map((match) => match[1].replace(/\\'/g, "'")); + if (entries.length === 0) { + return; + } + + const labels = new Set(); + entries.forEach((reward, rewardIndex) => { + const parsed = parseRewardLabel(reward); + if (!parsed.label) { + errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}][${rewardIndex}] has an empty parsed label`); + } + if (!Number.isInteger(parsed.amount) || parsed.amount < 1) { + errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}][${rewardIndex}] has invalid amount ${parsed.amount}`); + } + if (labels.has(parsed.label)) { + errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}] has duplicate label "${parsed.label}"`); + } + labels.add(parsed.label); + }); +} + +function validateNumericProperties(propertyName, { min, max }) { + const pattern = new RegExp(`${propertyName}:\\s*(-?\\d+)`, 'g'); + for (const match of source.matchAll(pattern)) { + const value = Number(match[1]); + if (!Number.isInteger(value) || value < min || value > max) { + errors.push(`${sourcePath}:${lineNumber(match.index ?? 0)} ${propertyName} has out-of-range value ${value}`); + } + } +} + +function parseRewardLabel(reward) { + const normalized = String(reward).replace(/\s+/g, ' ').trim(); + const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/); + if (!match) { + return { label: normalized, amount: 1 }; + } + + return { + label: match[1].trim() || normalized, + amount: Number(match[2] ?? 1) + }; +} + +function lineNumber(offset) { + return source.slice(0, offset).split(/\r?\n/).length; +} diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index c1d4d47..133568c 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -11,6 +11,7 @@ let browser; try { runCommand('node', ['scripts/verify-campaign-flow-data.mjs']); runCommand('node', ['scripts/verify-battle-scenario-data.mjs']); + runCommand('node', ['scripts/verify-camp-reward-data.mjs']); runCommand('node', ['scripts/verify-story-asset-data.mjs']); runCommand('node', ['scripts/verify-story-image-asset-data.mjs']); runCommand('node', ['scripts/verify-visual-asset-data.mjs']);