Validate inventory reward taxonomy
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"verify:camp-rewards": "node scripts/verify-camp-reward-data.mjs",
|
"verify:camp-rewards": "node scripts/verify-camp-reward-data.mjs",
|
||||||
"verify:campaign-recruits": "node scripts/verify-campaign-recruit-data.mjs",
|
"verify:campaign-recruits": "node scripts/verify-campaign-recruit-data.mjs",
|
||||||
"verify:equipment-catalog": "node scripts/verify-equipment-catalog-data.mjs",
|
"verify:equipment-catalog": "node scripts/verify-equipment-catalog-data.mjs",
|
||||||
|
"verify:inventory-rewards": "node scripts/verify-inventory-reward-data.mjs",
|
||||||
"verify:story-assets": "node scripts/verify-story-asset-data.mjs",
|
"verify:story-assets": "node scripts/verify-story-asset-data.mjs",
|
||||||
"verify:story-images": "node scripts/verify-story-image-asset-data.mjs",
|
"verify:story-images": "node scripts/verify-story-image-asset-data.mjs",
|
||||||
"verify:visual-assets": "node scripts/verify-visual-asset-data.mjs",
|
"verify:visual-assets": "node scripts/verify-visual-asset-data.mjs",
|
||||||
|
|||||||
202
scripts/verify-inventory-reward-data.mjs
Normal file
202
scripts/verify-inventory-reward-data.mjs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createServer } from 'vite';
|
||||||
|
|
||||||
|
const campScenePath = 'src/game/scenes/CampScene.ts';
|
||||||
|
const server = await createServer({
|
||||||
|
logLevel: 'error',
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
appType: 'custom'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||||
|
const { itemCatalog } = await server.ssrLoadModule('/src/game/data/battleItems.ts');
|
||||||
|
const campSceneSource = readFileSync(campScenePath, 'utf8');
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const scenarioEntries = Object.entries(battleScenarios);
|
||||||
|
const campSupplyLabels = new Set(collectCampSupplyLabels(campSceneSource));
|
||||||
|
const equipmentLabels = new Set(Object.values(itemCatalog).map((item) => item.name));
|
||||||
|
const campVisitRewardLists = collectCampVisitItemRewardLists(campSceneSource);
|
||||||
|
let checkedRewardCount = 0;
|
||||||
|
|
||||||
|
if (campSupplyLabels.size === 0) {
|
||||||
|
errors.push(`${campScenePath}: no camp supply labels found`);
|
||||||
|
}
|
||||||
|
if (equipmentLabels.size === 0) {
|
||||||
|
errors.push('src/game/data/battleItems.ts: no equipment labels found');
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarioEntries.forEach(([battleId, scenario]) => {
|
||||||
|
const context = `battleScenarios/${battleId}`;
|
||||||
|
checkedRewardCount += validateRewardList(errors, scenario.itemRewards, `${context}: itemRewards`, {
|
||||||
|
allowAnyInventoryCategory: true,
|
||||||
|
campSupplyLabels,
|
||||||
|
equipmentLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
const campaignReward = scenario.campaignReward;
|
||||||
|
if (!campaignReward) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkedRewardCount += validateRewardList(errors, campaignReward.supplies ?? [], `${context}: campaignReward.supplies`, {
|
||||||
|
expectedCategory: 'supply',
|
||||||
|
campSupplyLabels,
|
||||||
|
equipmentLabels
|
||||||
|
});
|
||||||
|
checkedRewardCount += validateRewardList(errors, campaignReward.equipment ?? [], `${context}: campaignReward.equipment`, {
|
||||||
|
disallowedCategories: ['supply'],
|
||||||
|
campSupplyLabels,
|
||||||
|
equipmentLabels
|
||||||
|
});
|
||||||
|
checkedRewardCount += validateRewardList(errors, campaignReward.reputation ?? [], `${context}: campaignReward.reputation`, {
|
||||||
|
expectedCategory: 'record',
|
||||||
|
allowSignedAmount: true,
|
||||||
|
campSupplyLabels,
|
||||||
|
equipmentLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
validateScenarioSupplySnapshot(errors, battleId, scenario, campSupplyLabels);
|
||||||
|
});
|
||||||
|
|
||||||
|
campVisitRewardLists.forEach(({ rewards, lineNumber }) => {
|
||||||
|
checkedRewardCount += validateRewardList(errors, rewards, `${campScenePath}:${lineNumber} camp visit itemRewards`, {
|
||||||
|
allowAnyInventoryCategory: true,
|
||||||
|
campSupplyLabels,
|
||||||
|
equipmentLabels
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
console.error(`Inventory reward data verification failed with ${errors.length} issue(s):`);
|
||||||
|
errors.slice(0, 80).forEach((error) => console.error(`- ${error}`));
|
||||||
|
if (errors.length > 80) {
|
||||||
|
console.error(`- ...and ${errors.length - 80} more`);
|
||||||
|
}
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Verified inventory reward taxonomy for ${checkedRewardCount} reward labels across ${scenarioEntries.length} battles and ${campVisitRewardLists.length} camp visit reward lists.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRewardList(errors, rewards, context, options) {
|
||||||
|
if (!Array.isArray(rewards)) {
|
||||||
|
errors.push(`${context} must be an array`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = new Set();
|
||||||
|
rewards.forEach((reward, index) => {
|
||||||
|
if (typeof reward !== 'string' || reward.trim().length === 0) {
|
||||||
|
errors.push(`${context}[${index}] must be a non-empty string`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseRewardLabel(reward);
|
||||||
|
const category = rewardCategory(parsed.label, options.campSupplyLabels, options.equipmentLabels);
|
||||||
|
if (!parsed.label) {
|
||||||
|
errors.push(`${context}[${index}] has an empty parsed label`);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(parsed.amount) || parsed.amount === 0 || (!options.allowSignedAmount && parsed.amount < 1)) {
|
||||||
|
errors.push(`${context}[${index}] has invalid amount ${parsed.amount}`);
|
||||||
|
}
|
||||||
|
if (labels.has(parsed.label)) {
|
||||||
|
errors.push(`${context} has duplicate parsed label "${parsed.label}"`);
|
||||||
|
}
|
||||||
|
if (options.expectedCategory && category !== options.expectedCategory) {
|
||||||
|
errors.push(`${context}[${index}] expected ${options.expectedCategory} reward but found ${category}: "${reward}"`);
|
||||||
|
}
|
||||||
|
if (options.disallowedCategories?.includes(category)) {
|
||||||
|
errors.push(`${context}[${index}] must not use ${category} reward label: "${reward}"`);
|
||||||
|
}
|
||||||
|
labels.add(parsed.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rewards.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateScenarioSupplySnapshot(errors, battleId, scenario, campSupplyLabels) {
|
||||||
|
const itemRewardSupplies = labelsByCategory(scenario.itemRewards ?? [], campSupplyLabels);
|
||||||
|
const campaignSupplies = labelsByCategory(scenario.campaignReward?.supplies ?? [], campSupplyLabels, { includeOnlySupplies: false });
|
||||||
|
|
||||||
|
if (!sameStringSet(itemRewardSupplies, campaignSupplies)) {
|
||||||
|
errors.push(
|
||||||
|
`battleScenarios/${battleId}: campaignReward.supplies does not match supply labels surfaced in itemRewards (${[
|
||||||
|
...itemRewardSupplies
|
||||||
|
].join(', ')} vs ${[...campaignSupplies].join(', ')})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelsByCategory(rewards, campSupplyLabels, options = {}) {
|
||||||
|
const includeOnlySupplies = options.includeOnlySupplies ?? true;
|
||||||
|
return new Set(
|
||||||
|
rewards
|
||||||
|
.map((reward) => parseRewardLabel(reward).label)
|
||||||
|
.filter((label) => (includeOnlySupplies ? campSupplyLabels.has(label) : true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewardCategory(label, campSupplyLabels, equipmentLabels) {
|
||||||
|
if (campSupplyLabels.has(label)) {
|
||||||
|
return 'supply';
|
||||||
|
}
|
||||||
|
if (equipmentLabels.has(label)) {
|
||||||
|
return 'equipment';
|
||||||
|
}
|
||||||
|
return 'record';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCampSupplyLabels(source) {
|
||||||
|
const match = source.match(/const\s+campSupplies[\s\S]*?=\s*\[([\s\S]*?)\];/);
|
||||||
|
if (!match) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return collectStringPropertyValues(match[1], 'label');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCampVisitItemRewardLists(source) {
|
||||||
|
return [...source.matchAll(/itemRewards:\s*\[([\s\S]*?)\]/g)].map((match) => ({
|
||||||
|
lineNumber: source.slice(0, match.index ?? 0).split(/\r?\n/).length,
|
||||||
|
rewards: collectSingleQuotedStrings(match[1])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectStringPropertyValues(source, propertyName) {
|
||||||
|
const pattern = new RegExp(`${propertyName}:\\s*'((?:\\\\'|[^'])*)'`, 'g');
|
||||||
|
return [...source.matchAll(pattern)].map((match) => unescapeSingleQuotedString(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSingleQuotedStrings(source) {
|
||||||
|
return [...source.matchAll(/'((?:\\'|[^'])*)'/g)].map((match) => unescapeSingleQuotedString(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function unescapeSingleQuotedString(value) {
|
||||||
|
return value.replace(/\\'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sign = match[2] === '-' ? -1 : 1;
|
||||||
|
return {
|
||||||
|
label: match[1].trim() || normalized,
|
||||||
|
amount: Number(match[3] ?? 1) * sign
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameStringSet(left, right) {
|
||||||
|
if (left.size !== right.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return [...left].every((value) => right.has(value));
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ const checks = [
|
|||||||
'scripts/verify-camp-reward-data.mjs',
|
'scripts/verify-camp-reward-data.mjs',
|
||||||
'scripts/verify-campaign-recruit-data.mjs',
|
'scripts/verify-campaign-recruit-data.mjs',
|
||||||
'scripts/verify-equipment-catalog-data.mjs',
|
'scripts/verify-equipment-catalog-data.mjs',
|
||||||
|
'scripts/verify-inventory-reward-data.mjs',
|
||||||
'scripts/verify-story-asset-data.mjs',
|
'scripts/verify-story-asset-data.mjs',
|
||||||
'scripts/verify-story-image-asset-data.mjs',
|
'scripts/verify-story-image-asset-data.mjs',
|
||||||
'scripts/verify-visual-asset-data.mjs',
|
'scripts/verify-visual-asset-data.mjs',
|
||||||
|
|||||||
Reference in New Issue
Block a user