291 lines
13 KiB
JavaScript
291 lines
13 KiB
JavaScript
import {
|
|
extractCanonicalNames,
|
|
validateContentCompleteness,
|
|
validateContentQuality,
|
|
validateContentStructure,
|
|
validateNarrativeCoherence,
|
|
} from "./validation.js";
|
|
|
|
export function validateNameConsistency(dungeonData) {
|
|
const canonicalNames = extractCanonicalNames(dungeonData);
|
|
const fixes = [];
|
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
canonicalNames.npcs.forEach(canonicalName => {
|
|
if (dungeonData.flavor) {
|
|
const original = dungeonData.flavor;
|
|
dungeonData.flavor = dungeonData.flavor.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName);
|
|
if (original !== dungeonData.flavor) fixes.push(`Fixed NPC name in flavor text: ${canonicalName}`);
|
|
}
|
|
if (dungeonData.hooksRumors) {
|
|
dungeonData.hooksRumors = dungeonData.hooksRumors.map(hook => {
|
|
const original = hook;
|
|
const fixed = hook.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName);
|
|
if (original !== fixed) fixes.push(`Fixed NPC name in hook: ${canonicalName}`);
|
|
return fixed;
|
|
});
|
|
}
|
|
if (dungeonData.encounters) {
|
|
dungeonData.encounters.forEach(encounter => {
|
|
if (encounter.details) {
|
|
const original = encounter.details;
|
|
encounter.details = encounter.details.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName);
|
|
if (original !== encounter.details) fixes.push(`Fixed NPC name in encounter: ${canonicalName}`);
|
|
}
|
|
});
|
|
}
|
|
if (dungeonData.plotResolutions) {
|
|
dungeonData.plotResolutions = dungeonData.plotResolutions.map(resolution => {
|
|
const original = resolution;
|
|
const fixed = resolution.replace(new RegExp(escapeRe(canonicalName), 'gi'), canonicalName);
|
|
if (original !== fixed) fixes.push(`Fixed NPC name in plot resolution: ${canonicalName}`);
|
|
return fixed;
|
|
});
|
|
}
|
|
});
|
|
|
|
canonicalNames.rooms.forEach(canonicalRoom => {
|
|
if (dungeonData.encounters) {
|
|
dungeonData.encounters.forEach(encounter => {
|
|
if (encounter.details) {
|
|
const original = encounter.details;
|
|
encounter.details = encounter.details.replace(new RegExp(escapeRe(canonicalRoom), 'gi'), canonicalRoom);
|
|
if (original !== encounter.details) fixes.push(`Fixed room name in encounter: ${canonicalRoom}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return fixes;
|
|
}
|
|
|
|
export function standardizeEncounterLocations(encounters, rooms) {
|
|
if (!encounters || !rooms) return { encounters, fixes: [] };
|
|
const roomNames = rooms.map(r => r.name.trim());
|
|
const fixes = [];
|
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const fixedEncounters = encounters.map(encounter => {
|
|
if (!encounter.details) return encounter;
|
|
const standardized = roomNames.reduce((details, roomName) => {
|
|
const roomNameRegex = new RegExp(`^${escapeRe(roomName)}\\s*:?\\s*`, 'i');
|
|
if (!roomNameRegex.test(details)) return details;
|
|
const hasColon = details.match(new RegExp(`^${escapeRe(roomName)}:`, 'i'));
|
|
if (hasColon) return details;
|
|
fixes.push(`Standardized location format for encounter: ${encounter.name}`);
|
|
return details.replace(roomNameRegex, `${roomName}: `);
|
|
}, encounter.details.trim());
|
|
return standardized !== encounter.details ? { ...encounter, details: standardized } : encounter;
|
|
});
|
|
return { encounters: fixedEncounters, fixes };
|
|
}
|
|
|
|
export function fixStructureIssues(dungeonData) {
|
|
const fixes = [];
|
|
if (dungeonData.rooms) {
|
|
dungeonData.rooms.forEach((room, i) => {
|
|
if (!room.name || !room.name.trim()) {
|
|
const desc = room.description || '';
|
|
const nameMatch = desc.match(/^([A-Z][^.!?]{5,30}?)(?:\s|\.|:)/);
|
|
if (nameMatch) {
|
|
room.name = nameMatch[1].trim();
|
|
fixes.push(`Extracted room name from description: "${room.name}"`);
|
|
} else {
|
|
room.name = `Room ${i + 1}`;
|
|
fixes.push(`Added default name for room ${i + 1}`);
|
|
}
|
|
}
|
|
const words = room.name.split(/\s+/);
|
|
if (words.length > 6) {
|
|
const original = room.name;
|
|
room.name = words.slice(0, 6).join(' ');
|
|
fixes.push(`Truncated room name: "${original}" -> "${room.name}"`);
|
|
}
|
|
});
|
|
}
|
|
if (dungeonData.encounters) {
|
|
dungeonData.encounters.forEach((encounter, i) => {
|
|
if (!encounter.name || !encounter.name.trim()) {
|
|
const details = encounter.details || '';
|
|
const nameMatch = details.match(/^([^:]+):\s*(.+)$/);
|
|
if (nameMatch) {
|
|
encounter.name = nameMatch[1].trim();
|
|
encounter.details = nameMatch[2].trim();
|
|
fixes.push(`Extracted encounter name from details: "${encounter.name}"`);
|
|
} else {
|
|
encounter.name = `Encounter ${i + 1}`;
|
|
fixes.push(`Added default name for encounter ${i + 1}`);
|
|
}
|
|
}
|
|
const words = encounter.name.split(/\s+/);
|
|
if (words.length > 6) {
|
|
const original = encounter.name;
|
|
encounter.name = words.slice(0, 6).join(' ');
|
|
fixes.push(`Truncated encounter name: "${original}" -> "${encounter.name}"`);
|
|
}
|
|
});
|
|
}
|
|
if (dungeonData.npcs) {
|
|
dungeonData.npcs.forEach((npc, i) => {
|
|
if (!npc.name || !npc.name.trim()) {
|
|
const trait = npc.trait || '';
|
|
const nameMatch = trait.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2})(?:\s|:)/);
|
|
if (nameMatch) {
|
|
npc.name = nameMatch[1].trim();
|
|
fixes.push(`Extracted NPC name from trait: "${npc.name}"`);
|
|
} else {
|
|
npc.name = `NPC ${i + 1}`;
|
|
fixes.push(`Added default name for NPC ${i + 1}`);
|
|
}
|
|
}
|
|
const words = npc.name.split(/\s+/);
|
|
if (words.length > 4) {
|
|
const original = npc.name;
|
|
npc.name = words.slice(0, 4).join(' ');
|
|
fixes.push(`Truncated NPC name: "${original}" -> "${npc.name}"`);
|
|
}
|
|
});
|
|
}
|
|
return fixes;
|
|
}
|
|
|
|
export function fixMissingContent(dungeonData) {
|
|
const fixes = [];
|
|
if (!dungeonData.npcs || dungeonData.npcs.length < 4) {
|
|
if (!dungeonData.npcs) dungeonData.npcs = [];
|
|
const factionName = dungeonData.coreConcepts?.match(/Primary Faction[:\s]+([^.]+)/i)?.[1]?.trim() || 'the primary faction';
|
|
while (dungeonData.npcs.length < 4) {
|
|
dungeonData.npcs.push({
|
|
name: `NPC ${dungeonData.npcs.length + 1}`,
|
|
trait: `A member of ${factionName.toLowerCase()} with unknown motives.`
|
|
});
|
|
fixes.push(`Added fallback NPC ${dungeonData.npcs.length}`);
|
|
}
|
|
}
|
|
if (!dungeonData.encounters || dungeonData.encounters.length < 6) {
|
|
if (!dungeonData.encounters) dungeonData.encounters = [];
|
|
if (dungeonData.encounters.length > 0 && dungeonData.rooms?.length > 0) {
|
|
const dynamicElement = dungeonData.coreConcepts?.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences';
|
|
const conflict = dungeonData.coreConcepts?.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a threat';
|
|
while (dungeonData.encounters.length < 6) {
|
|
const roomIndex = dungeonData.encounters.length % dungeonData.rooms.length;
|
|
const roomName = dungeonData.rooms[roomIndex]?.name || 'Unknown Location';
|
|
const fallbackNames = [
|
|
`${roomName} Guardian`, `${roomName} Threat`, `${roomName} Challenge`,
|
|
`${dynamicElement.split(' ')[0]} Manifestation`, `${conflict.split(' ')[0]} Encounter`, `${roomName} Hazard`
|
|
];
|
|
dungeonData.encounters.push({
|
|
name: fallbackNames[dungeonData.encounters.length % fallbackNames.length],
|
|
details: `${roomName}: An encounter related to ${dynamicElement.toLowerCase()} occurs here.`
|
|
});
|
|
fixes.push(`Added fallback encounter: "${dungeonData.encounters[dungeonData.encounters.length - 1].name}"`);
|
|
}
|
|
}
|
|
}
|
|
if (!dungeonData.treasure || dungeonData.treasure.length < 4) {
|
|
if (!dungeonData.treasure) dungeonData.treasure = [];
|
|
while (dungeonData.treasure.length < 4) {
|
|
dungeonData.treasure.push({
|
|
name: `Treasure ${dungeonData.treasure.length + 1}`,
|
|
description: `A mysterious item found in the dungeon.`
|
|
});
|
|
fixes.push(`Added fallback treasure ${dungeonData.treasure.length}`);
|
|
}
|
|
}
|
|
if (!dungeonData.randomEvents || dungeonData.randomEvents.length < 6) {
|
|
if (!dungeonData.randomEvents) dungeonData.randomEvents = [];
|
|
if (dungeonData.randomEvents.length > 0 && dungeonData.coreConcepts) {
|
|
const dynamicElement = dungeonData.coreConcepts.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences';
|
|
const conflict = dungeonData.coreConcepts.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a mysterious threat';
|
|
const fallbackEvents = [
|
|
{ name: 'Environmental Shift', description: `The ${dynamicElement.toLowerCase()} causes unexpected changes in the environment.` },
|
|
{ name: 'Conflict Manifestation', description: `A sign of ${conflict.toLowerCase()} appears, requiring immediate attention.` },
|
|
{ name: 'Dungeon Shift', description: `The dungeon shifts, revealing a previously hidden passage or danger.` },
|
|
{ name: 'Faction Messenger', description: `An NPC from the primary faction appears with urgent information.` },
|
|
{ name: 'Power Fluctuation', description: `The power source fluctuates, creating temporary hazards or opportunities.` },
|
|
{ name: 'Echoes of the Past', description: `Echoes of past events manifest, providing clues or complications.` }
|
|
];
|
|
while (dungeonData.randomEvents.length < 6) {
|
|
dungeonData.randomEvents.push(fallbackEvents[dungeonData.randomEvents.length % fallbackEvents.length]);
|
|
fixes.push(`Added fallback random event: "${dungeonData.randomEvents[dungeonData.randomEvents.length - 1].name}"`);
|
|
}
|
|
}
|
|
}
|
|
if (!dungeonData.plotResolutions || dungeonData.plotResolutions.length < 4) {
|
|
if (!dungeonData.plotResolutions) dungeonData.plotResolutions = [];
|
|
while (dungeonData.plotResolutions.length < 4) {
|
|
dungeonData.plotResolutions.push(`The adventurers could resolve the central conflict through decisive action.`);
|
|
fixes.push(`Added fallback plot resolution ${dungeonData.plotResolutions.length}`);
|
|
}
|
|
}
|
|
return fixes;
|
|
}
|
|
|
|
export function fixNarrativeCoherence(dungeonData) {
|
|
const fixes = [];
|
|
if (dungeonData.encounters && dungeonData.rooms) {
|
|
const roomNames = dungeonData.rooms.map(r => r.name.trim().toLowerCase());
|
|
dungeonData.encounters.forEach(encounter => {
|
|
if (!encounter.details) return;
|
|
const locationMatch = encounter.details.match(/^([^:]+):/);
|
|
if (locationMatch) {
|
|
const locName = locationMatch[1].trim().toLowerCase();
|
|
const matches = roomNames.some(rn =>
|
|
locName === rn || locName.includes(rn) || rn.includes(locName) ||
|
|
locName.split(/\s+/).some(word => rn.includes(word))
|
|
);
|
|
if (!matches && roomNames.length > 0) {
|
|
const roomIdx = Math.floor(Math.random() * roomNames.length);
|
|
const roomName = dungeonData.rooms[roomIdx].name;
|
|
encounter.details = encounter.details.replace(/^[^:]+:\s*/, `${roomName}: `);
|
|
fixes.push(`Fixed unknown location in encounter "${encounter.name}" to "${roomName}"`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return fixes;
|
|
}
|
|
|
|
export function validateAndFixContent(dungeonData) {
|
|
const allFixes = [];
|
|
const nameFixes = validateNameConsistency(dungeonData);
|
|
allFixes.push(...nameFixes);
|
|
const structureFixes = fixStructureIssues(dungeonData);
|
|
allFixes.push(...structureFixes);
|
|
if (dungeonData.encounters && dungeonData.rooms) {
|
|
const roomNames = dungeonData.rooms.map(r => r.name.trim());
|
|
dungeonData.encounters.forEach((encounter, idx) => {
|
|
if (!encounter.details) return;
|
|
if (!encounter.details.match(/^[^:]+:\s/)) {
|
|
const roomIdx = idx % roomNames.length;
|
|
const roomName = roomNames[roomIdx];
|
|
encounter.details = `${roomName}: ${encounter.details}`;
|
|
allFixes.push(`Added location "${roomName}" to encounter "${encounter.name}"`);
|
|
}
|
|
});
|
|
const locationResult = standardizeEncounterLocations(dungeonData.encounters, dungeonData.rooms);
|
|
dungeonData.encounters = locationResult.encounters;
|
|
allFixes.push(...locationResult.fixes);
|
|
}
|
|
const coherenceFixes = fixNarrativeCoherence(dungeonData);
|
|
allFixes.push(...coherenceFixes);
|
|
const contentFixes = fixMissingContent(dungeonData);
|
|
allFixes.push(...contentFixes);
|
|
|
|
const allIssues = [
|
|
...validateContentCompleteness(dungeonData),
|
|
...validateContentQuality(dungeonData),
|
|
...validateContentStructure(dungeonData),
|
|
...validateNarrativeCoherence(dungeonData),
|
|
];
|
|
if (allFixes.length > 0) {
|
|
console.log("\n[Validation] Applied fixes:");
|
|
allFixes.forEach(fix => console.log(` - ${fix}`));
|
|
}
|
|
if (allIssues.length > 0) {
|
|
console.log("\n[Validation] Content quality issues found (not auto-fixable):");
|
|
allIssues.forEach(issue => console.warn(` ⚠ ${issue}`));
|
|
} else {
|
|
console.log("\n[Validation] Content quality checks passed");
|
|
}
|
|
return dungeonData;
|
|
}
|