121 lines
4.7 KiB
JavaScript
121 lines
4.7 KiB
JavaScript
export function extractCanonicalNames(dungeonData) {
|
|
const names = { npcs: [], rooms: [], factions: [] };
|
|
if (dungeonData.npcs) {
|
|
dungeonData.npcs.forEach(npc => { if (npc.name) names.npcs.push(npc.name.trim()); });
|
|
}
|
|
if (dungeonData.rooms) {
|
|
dungeonData.rooms.forEach(room => { if (room.name) names.rooms.push(room.name.trim()); });
|
|
}
|
|
if (dungeonData.coreConcepts) {
|
|
const factionMatch = dungeonData.coreConcepts.match(/Primary Faction[:\s]+([^.]+)/i);
|
|
if (factionMatch) names.factions.push(factionMatch[1].trim());
|
|
}
|
|
return names;
|
|
}
|
|
|
|
export function validateContentCompleteness(dungeonData) {
|
|
const issues = [];
|
|
const checks = [
|
|
['title', 0, 'Missing title'],
|
|
['flavor', 20, 'Flavor text too short'],
|
|
['hooksRumors', 4, 'Expected at least 4 hooks'],
|
|
['rooms', 5, 'Expected at least 5 rooms'],
|
|
['encounters', 6, 'Expected at least 6 encounters'],
|
|
['npcs', 4, 'Expected at least 4 NPCs'],
|
|
['treasure', 4, 'Expected at least 4 treasures'],
|
|
['randomEvents', 6, 'Expected 6 random events'],
|
|
['plotResolutions', 4, 'Expected at least 4 plot resolutions']
|
|
];
|
|
checks.forEach(([key, min, msg]) => {
|
|
const val = dungeonData[key];
|
|
if (!val || (Array.isArray(val) ? val.length < min : val.trim().length < min)) {
|
|
issues.push(`${msg}${Array.isArray(val) ? `, got ${val?.length || 0}` : ''}`);
|
|
}
|
|
});
|
|
dungeonData.rooms?.forEach((r, i) => {
|
|
if (!r.description || r.description.trim().length < 20) {
|
|
issues.push(`Room ${i + 1} (${r.name}) description too short`);
|
|
}
|
|
});
|
|
dungeonData.encounters?.forEach((e, i) => {
|
|
if (!e.details || e.details.trim().length < 30) {
|
|
issues.push(`Encounter ${i + 1} (${e.name}) details too short`);
|
|
}
|
|
});
|
|
dungeonData.npcs?.forEach((n, i) => {
|
|
if (!n.trait || n.trait.trim().length < 30) {
|
|
issues.push(`NPC ${i + 1} (${n.name}) description too short`);
|
|
}
|
|
});
|
|
return issues;
|
|
}
|
|
|
|
export function validateContentQuality(dungeonData) {
|
|
const issues = [];
|
|
const vagueWords = /\b(some|various|several|many|few|things|stuff|items|objects)\b/gi;
|
|
const checkVague = (text, ctx) => {
|
|
if (!text) return;
|
|
const matches = text.match(vagueWords);
|
|
if (matches?.length > 2) {
|
|
issues.push(`${ctx} contains vague language: "${matches.slice(0, 3).join('", "')}"`);
|
|
}
|
|
};
|
|
checkVague(dungeonData.flavor, 'Flavor text');
|
|
dungeonData.rooms?.forEach(r => checkVague(r.description, `Room "${r.name}"`));
|
|
dungeonData.encounters?.forEach(e => checkVague(e.details, `Encounter "${e.name}"`));
|
|
dungeonData.npcs?.forEach(n => checkVague(n.trait, `NPC "${n.name}"`));
|
|
dungeonData.rooms?.forEach(r => {
|
|
if (r.description?.length < 50) {
|
|
issues.push(`Room "${r.name}" description too short`);
|
|
}
|
|
});
|
|
return issues;
|
|
}
|
|
|
|
export function validateContentStructure(dungeonData) {
|
|
const issues = [];
|
|
dungeonData.rooms?.forEach((r, i) => {
|
|
if (!r.name?.trim()) issues.push(`Room ${i + 1} missing name`);
|
|
if (r.name?.split(/\s+/).length > 6) issues.push(`Room "${r.name}" name too long`);
|
|
});
|
|
dungeonData.encounters?.forEach((e, i) => {
|
|
if (!e.name?.trim()) issues.push(`Encounter ${i + 1} missing name`);
|
|
if (e.name?.split(/\s+/).length > 6) issues.push(`Encounter "${e.name}" name too long`);
|
|
if (e.details && !e.details.match(/^[^:]+:\s/)) {
|
|
issues.push(`Encounter "${e.name}" details missing location prefix`);
|
|
}
|
|
});
|
|
dungeonData.npcs?.forEach((n, i) => {
|
|
if (!n.name?.trim()) issues.push(`NPC ${i + 1} missing name`);
|
|
if (n.name?.split(/\s+/).length > 4) issues.push(`NPC "${n.name}" name too long`);
|
|
});
|
|
return issues;
|
|
}
|
|
|
|
export function validateNarrativeCoherence(dungeonData) {
|
|
const issues = [];
|
|
const factionMatch = dungeonData.coreConcepts?.match(/Primary Faction[:\s]+([^.]+)/i);
|
|
const factionName = factionMatch?.[1]?.trim();
|
|
if (dungeonData.encounters && dungeonData.rooms) {
|
|
const roomNames = dungeonData.rooms.map(r => r.name.trim().toLowerCase());
|
|
dungeonData.encounters.forEach(e => {
|
|
const locMatch = e.details?.match(/^([^:]+):/);
|
|
if (locMatch) {
|
|
const locName = locMatch[1].trim().toLowerCase();
|
|
if (!roomNames.some(rn => locName.includes(rn) || rn.includes(locName))) {
|
|
issues.push(`Encounter "${e.name}" references unknown location "${locMatch[1]}"`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (factionName) {
|
|
const factionLower = factionName.toLowerCase();
|
|
const refs = (dungeonData.npcs?.filter(n => n.trait?.toLowerCase().includes(factionLower)).length ?? 0)
|
|
+ (dungeonData.encounters?.filter(e => e.details?.toLowerCase().includes(factionLower)).length ?? 0);
|
|
if (refs < 2) {
|
|
issues.push(`Faction "${factionName}" poorly integrated (${refs} references)`);
|
|
}
|
|
}
|
|
return issues;
|
|
}
|