Validate battle reward data
This commit is contained in:
@@ -8,6 +8,7 @@ const server = await createServer({
|
||||
|
||||
try {
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
||||
const { unitClasses, terrainRules } = await server.ssrLoadModule('/src/game/data/battleRules.ts');
|
||||
const { itemCatalog, equipmentSlots } = await server.ssrLoadModule('/src/game/data/battleItems.ts');
|
||||
|
||||
@@ -16,6 +17,11 @@ try {
|
||||
const classKeys = new Set(Object.keys(unitClasses));
|
||||
const terrainKeys = new Set(Object.keys(terrainRules));
|
||||
const itemKeys = new Set(Object.keys(itemCatalog));
|
||||
const itemNames = new Set(Object.values(itemCatalog).map((item) => item.name));
|
||||
const knownRewardUnitIds = new Set([
|
||||
...scenarioEntries.flatMap(([, scenario]) => scenario.units.map((unit) => unit.id)),
|
||||
...(campaignRecruitUnits ?? []).map((unit) => unit.id)
|
||||
]);
|
||||
const errors = [];
|
||||
|
||||
scenarioEntries.forEach(([scenarioId, scenario]) => {
|
||||
@@ -31,7 +37,7 @@ try {
|
||||
validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context);
|
||||
validateObjectives(errors, scenario, context);
|
||||
validateSortie(errors, scenario, classKeys, context);
|
||||
validateRewards(errors, scenario, scenarioIds, context);
|
||||
validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context);
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
@@ -43,7 +49,13 @@ try {
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
const unitCount = scenarioEntries.reduce((total, [, scenario]) => total + scenario.units.length, 0);
|
||||
console.log(`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, objectives, sortie data, and equipment.`);
|
||||
const rewardCount = scenarioEntries.reduce(
|
||||
(total, [, scenario]) => total + scenario.itemRewards.length + campaignRewardLabelCount(scenario.campaignReward),
|
||||
0
|
||||
);
|
||||
console.log(
|
||||
`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, objectives, sortie data, equipment, and ${rewardCount} reward labels.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await server.close();
|
||||
@@ -233,17 +245,116 @@ function validateSortie(errors, scenario, classKeys, context) {
|
||||
});
|
||||
}
|
||||
|
||||
function validateRewards(errors, scenario, scenarioIds, context) {
|
||||
scenario.itemRewards.forEach((reward, index) => {
|
||||
assertNonEmptyString(errors, reward, `${context}: item reward ${index}`);
|
||||
});
|
||||
function validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context) {
|
||||
validateRewardLabels(errors, scenario.itemRewards, `${context}: itemRewards`);
|
||||
|
||||
const unlockBattleId = scenario.campaignReward?.unlockBattleId;
|
||||
const campaignReward = scenario.campaignReward;
|
||||
if (campaignReward) {
|
||||
validateRewardLabels(errors, campaignReward.supplies ?? [], `${context}: campaignReward.supplies`);
|
||||
validateRewardLabels(errors, campaignReward.equipment ?? [], `${context}: campaignReward.equipment`);
|
||||
validateRewardLabels(errors, campaignReward.reputation ?? [], `${context}: campaignReward.reputation`, { allowSignedAmount: true });
|
||||
validateRecruitRewards(errors, campaignReward.recruits ?? [], knownRewardUnitIds, context);
|
||||
validateEquipmentRewardLabels(errors, campaignReward.equipment ?? [], itemNames, context);
|
||||
if (campaignReward.unlockLabel !== undefined) {
|
||||
assertNonEmptyString(errors, campaignReward.unlockLabel, `${context}: campaignReward unlockLabel`);
|
||||
}
|
||||
if (campaignReward.note !== undefined) {
|
||||
assertNonEmptyString(errors, campaignReward.note, `${context}: campaignReward note`);
|
||||
}
|
||||
}
|
||||
|
||||
const unlockBattleId = campaignReward?.unlockBattleId;
|
||||
if (unlockBattleId && !scenarioIds.has(unlockBattleId)) {
|
||||
errors.push(`${context}: campaignReward unlockBattleId "${unlockBattleId}" is not a known battle`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRewardLabels(errors, rewards, context, options = {}) {
|
||||
if (!Array.isArray(rewards)) {
|
||||
errors.push(`${context} must be an array`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = new Set();
|
||||
rewards.forEach((reward, index) => {
|
||||
assertNonEmptyString(errors, reward, `${context}[${index}]`);
|
||||
if (typeof reward !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseRewardLabel(reward);
|
||||
if (!parsed.label) {
|
||||
errors.push(`${context}[${index}] has an empty parsed label`);
|
||||
}
|
||||
if (!Number.isInteger(parsed.amount) || parsed.amount < 1) {
|
||||
if (!options.allowSignedAmount || !Number.isInteger(parsed.amount) || parsed.amount === 0) {
|
||||
errors.push(`${context}[${index}] has invalid amount ${parsed.amount}`);
|
||||
}
|
||||
}
|
||||
if (labels.has(parsed.label)) {
|
||||
errors.push(`${context}: duplicate reward label "${parsed.label}"`);
|
||||
}
|
||||
labels.add(parsed.label);
|
||||
});
|
||||
}
|
||||
|
||||
function validateRecruitRewards(errors, recruits, knownRewardUnitIds, context) {
|
||||
const recruitIds = new Set();
|
||||
recruits.forEach((unitId, index) => {
|
||||
assertNonEmptyString(errors, unitId, `${context}: campaignReward.recruits[${index}]`);
|
||||
if (typeof unitId !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (!knownRewardUnitIds.has(unitId)) {
|
||||
errors.push(`${context}: campaignReward recruit "${unitId}" is not a known battle or campaign recruit unit`);
|
||||
}
|
||||
if (recruitIds.has(unitId)) {
|
||||
errors.push(`${context}: duplicate campaignReward recruit "${unitId}"`);
|
||||
}
|
||||
recruitIds.add(unitId);
|
||||
});
|
||||
}
|
||||
|
||||
function validateEquipmentRewardLabels(errors, equipmentRewards, itemNames, context) {
|
||||
equipmentRewards.forEach((reward) => {
|
||||
const { label } = parseRewardLabel(reward);
|
||||
if (itemNames.has(label) || looksLikeCampaignRecord(label)) {
|
||||
return;
|
||||
}
|
||||
errors.push(`${context}: campaignReward equipment "${reward}" is neither an equipment catalog item nor a campaign record/map label`);
|
||||
});
|
||||
}
|
||||
|
||||
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 looksLikeCampaignRecord(label) {
|
||||
return /기록|문서|장부|지도|표식|서찰|명단|명부|초안|부적|군령패|통행문서|방화선|말뚝|항복문|물길|병법서|봉화도|외곽도|수위표|성문도|정찰|진군|퇴각|회유|서약|고시문|길잡이|소문|풍문|논의|준비|명성|공적|민심|신뢰|명분|예우|공훈/.test(label);
|
||||
}
|
||||
|
||||
function campaignRewardLabelCount(campaignReward) {
|
||||
if (!campaignReward) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
(campaignReward.supplies?.length ?? 0) +
|
||||
(campaignReward.equipment?.length ?? 0) +
|
||||
(campaignReward.reputation?.length ?? 0) +
|
||||
(campaignReward.recruits?.length ?? 0) +
|
||||
(campaignReward.unlockBattleId ? 1 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
function isInBounds(point, map) {
|
||||
return (
|
||||
Number.isInteger(point.x) &&
|
||||
|
||||
Reference in New Issue
Block a user