Guard camp event ids

This commit is contained in:
2026-07-05 10:42:46 +09:00
parent f4ff01e917
commit 788e2ce21a

View File

@@ -6,6 +6,11 @@ const errors = [];
const itemRewardArrays = collectItemRewardArrays(source);
itemRewardArrays.forEach(({ body, index, offset }) => validateItemRewards(body, index, offset));
const campBattleIdKeys = collectCampBattleIdKeys(source);
const dialogueEvents = collectCampEvents(source, 'campDialogues');
const visitEvents = collectCampEvents(source, 'campVisits');
validateCampEventCollection(dialogueEvents, 'campDialogues');
validateCampEventCollection(visitEvents, 'campVisits');
validateNumericProperties('gold', { min: 1, max: 20000 });
validateNumericProperties('bondExp', { min: 0, max: 200 });
validateNumericProperties('rewardExp', { min: 1, max: 200 });
@@ -14,7 +19,9 @@ 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.`);
console.log(
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${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) => ({
@@ -56,6 +63,73 @@ function validateNumericProperties(propertyName, { min, max }) {
}
}
function collectCampBattleIdKeys(text) {
const objectSource = extractConstObject(text, 'campBattleIds');
return new Set([...objectSource.matchAll(/^\s*(\w+):/gm)].map((match) => match[1]));
}
function collectCampEvents(text, arrayName) {
const { body, offset } = extractConstArray(text, arrayName);
return collectTopLevelObjects(body).map((event) => ({
...event,
offset: offset + event.offset
}));
}
function validateCampEventCollection(events, collectionName) {
const seenIds = new Map();
if (events.length === 0) {
errors.push(`${sourcePath}: ${collectionName} has no entries`);
}
events.forEach((event, index) => {
const context = `${collectionName}[${index}]`;
const idEntries = collectStringProperties(event.body, 'id');
const eventId = idEntries[0]?.value ?? '';
if (!eventId.trim()) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no id`);
} else if (seenIds.has(eventId)) {
errors.push(
`${sourcePath}:${lineNumber(event.offset)} ${context} duplicates id "${eventId}" first used at ${sourcePath}:${seenIds.get(
eventId
)}`
);
} else {
seenIds.set(eventId, lineNumber(event.offset));
}
const choiceIds = idEntries.slice(1).map((entry) => entry.value);
const seenChoiceIds = new Set();
choiceIds.forEach((choiceId, choiceIndex) => {
if (!choiceId.trim()) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context}.choices[${choiceIndex}] has an empty id`);
}
if (seenChoiceIds.has(choiceId)) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has duplicate choice id "${choiceId}"`);
}
seenChoiceIds.add(choiceId);
});
const availableMatch = /availableAfterBattleIds:\s*\[([\s\S]*?)\]/.exec(event.body);
if (!availableMatch || availableMatch[1].trim().length === 0) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no availableAfterBattleIds`);
}
for (const reference of event.body.matchAll(/campBattleIds\.(\w+)/g)) {
if (!campBattleIdKeys.has(reference[1])) {
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} references unknown campBattleIds.${reference[1]}`);
}
}
});
}
function collectStringProperties(text, propertyName) {
const pattern = new RegExp(`\\b${propertyName}:\\s*'((?:\\\\'|[^'])*)'`, 'g');
return [...text.matchAll(pattern)].map((match) => ({
value: match[1].replace(/\\'/g, "'"),
offset: match.index ?? 0
}));
}
function parseRewardLabel(reward) {
const normalized = String(reward).replace(/\s+/g, ' ').trim();
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);
@@ -72,3 +146,102 @@ function parseRewardLabel(reward) {
function lineNumber(offset) {
return source.slice(0, offset).split(/\r?\n/).length;
}
function extractConstObject(text, objectName) {
const startPattern = new RegExp(`const\\s+${objectName}\\b[^=]*=\\s*\\{`);
const match = startPattern.exec(text);
if (!match) {
throw new Error(`Cannot find ${objectName}.`);
}
const start = match.index + match[0].length - 1;
const end = findMatchingDelimiter(text, start, '{', '}');
return text.slice(start, end + 1);
}
function extractConstArray(text, arrayName) {
const startPattern = new RegExp(`const\\s+${arrayName}\\b[\\s\\S]*?=\\s*\\[`);
const match = startPattern.exec(text);
if (!match) {
throw new Error(`Cannot find ${arrayName}.`);
}
const start = match.index + match[0].length - 1;
const end = findMatchingDelimiter(text, start, '[', ']');
return {
body: text.slice(start + 1, end),
offset: start + 1
};
}
function collectTopLevelObjects(text) {
const objects = [];
let depth = 0;
let start = -1;
let quote = '';
let escaped = false;
for (let index = 0; index < text.length; index += 1) {
const char = text[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
quote = '';
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '{') {
if (depth === 0) {
start = index;
}
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0 && start >= 0) {
objects.push({ body: text.slice(start, index + 1), offset: start });
start = -1;
}
}
}
return objects;
}
function findMatchingDelimiter(text, start, openChar, closeChar) {
let depth = 0;
let quote = '';
let escaped = false;
for (let index = start; index < text.length; index += 1) {
const char = text[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
quote = '';
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === openChar) {
depth += 1;
} else if (char === closeChar) {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
throw new Error(`Cannot find matching ${closeChar} from offset ${start}.`);
}