Files
scrollsmith/src/contentFixes.js
T
2026-02-21 22:19:21 -05:00

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;
}