Validate camp dialogue bond references

This commit is contained in:
2026-07-05 16:01:53 +09:00
parent 0c47bbd08e
commit 955a186568
3 changed files with 112 additions and 7 deletions

View File

@@ -19,11 +19,13 @@ try {
const campBattleIdEntries = collectCampBattleIdEntries(source, defaultBattleScenario.id);
const campBattleIdKeys = new Set(campBattleIdEntries.map((entry) => entry.key));
validateCampBattleIdEntries(campBattleIdEntries, battleScenarios);
const knownBondsById = collectKnownBondsById(battleScenarios);
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);
validateNumericProperties('gold', { min: 1, max: 20000 });
validateNumericProperties('bondExp', { min: 0, max: 200 });
validateNumericProperties('rewardExp', { min: 1, max: 200 });
@@ -33,7 +35,7 @@ try {
}
console.log(
`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.`
`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.`
);
} finally {
await server.close();
@@ -127,6 +129,18 @@ function validateCampBattleIdEntries(entries, battleScenarios) {
});
}
function collectKnownBondsById(battleScenarios) {
const bondsById = new Map();
Object.values(battleScenarios).forEach((scenario) => {
(scenario.bonds ?? []).forEach((bond) => {
if (!bondsById.has(bond.id)) {
bondsById.set(bond.id, bond);
}
});
});
return bondsById;
}
function collectCampEvents(text, arrayName) {
const { body, offset } = extractConstArray(text, arrayName);
return collectTopLevelObjects(body).map((event) => ({
@@ -204,6 +218,42 @@ function validateAvailableDuringSteps(event, context, isCampaignStep) {
return steps.length;
}
function validateCampDialogueBondReferences(events, knownBondsById) {
let bondReferenceCount = 0;
events.forEach((event, index) => {
const context = `campDialogues[${index}]`;
const bondIdEntries = collectStringProperties(event.body, 'bondId');
if (bondIdEntries.length === 0) {
return;
}
bondReferenceCount += bondIdEntries.length;
if (bondIdEntries.length > 1) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has multiple bondId properties`);
}
const unitIds = collectStringArrayProperty(event.body, 'unitIds', event.offset);
if (unitIds.length === 0) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has bondId but no unitIds`);
}
bondIdEntries.forEach((entry) => {
const bond = knownBondsById.get(entry.value);
if (!bond) {
errors.push(`${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} references unknown bondId "${entry.value}"`);
return;
}
if (!sameStringSet(unitIds.map((unit) => unit.value), bond.unitIds ?? [])) {
errors.push(
`${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} bondId "${entry.value}" units [${(
bond.unitIds ?? []
).join(', ')}] do not match dialogue unitIds [${unitIds.map((unit) => unit.value).join(', ')}]`
);
}
});
});
return bondReferenceCount;
}
function collectStringProperties(text, propertyName) {
const pattern = new RegExp(`\\b${propertyName}:\\s*'((?:\\\\'|[^'])*)'`, 'g');
return [...text.matchAll(pattern)].map((match) => ({
@@ -219,6 +269,25 @@ function collectStringLiterals(text, offset = 0) {
}));
}
function collectStringArrayProperty(text, propertyName, eventOffset = 0) {
const pattern = new RegExp(`\\b${propertyName}:\\s*\\[([\\s\\S]*?)\\]`);
const match = pattern.exec(text);
if (!match) {
return [];
}
const arrayBody = match[1];
const arrayOffset = eventOffset + (match.index ?? 0) + match[0].indexOf(arrayBody);
return collectStringLiterals(arrayBody, arrayOffset);
}
function sameStringSet(left, right) {
if (left.length !== right.length) {
return false;
}
const rightSet = new Set(right);
return left.every((value) => rightSet.has(value));
}
function parseRewardLabel(reward) {
const normalized = String(reward).replace(/\s+/g, ' ').trim();
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);