235 lines
10 KiB
JavaScript
235 lines
10 KiB
JavaScript
import { cleanText } from "./textUtils.js";
|
|
|
|
export function parseList(raw) {
|
|
if (!raw) return [];
|
|
const NUMBERED_ITEM_REGEX = /\d+[).]\s+([\s\S]+?)(?=\s*\d+[).]\s+|$)/g;
|
|
const items = Array.from(raw.matchAll(NUMBERED_ITEM_REGEX))
|
|
.map(match => match[1].trim())
|
|
.filter(Boolean)
|
|
.map(cleanText)
|
|
.filter(Boolean);
|
|
return items.length > 0
|
|
? items
|
|
: raw
|
|
.split(/\n?\d+[).]\s+/)
|
|
.map(line => cleanText(line))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function parseObjects(raw, type = "rooms") {
|
|
const cleanedRaw = raw.replace(/Intermediate Rooms:/i, "").replace(/Climax Room:/i, "").trim();
|
|
const mapper = (entry) => {
|
|
if (type === "encounters") {
|
|
const parts = entry.split(/:/);
|
|
if (parts.length >= 3) {
|
|
const name = parts[0].trim();
|
|
if (name.toLowerCase().includes('location name') || name.toLowerCase().includes('encounter name')) return null;
|
|
return { name, details: parts.slice(1).join(":").trim() };
|
|
}
|
|
if (parts.length === 2) {
|
|
const name = parts[0].trim();
|
|
if (name.toLowerCase().includes('location name') || name.toLowerCase().includes('encounter name')) return null;
|
|
return { name, details: parts[1].trim() };
|
|
}
|
|
return null;
|
|
}
|
|
if (type === "treasure") {
|
|
const parts = entry.split(/[—]/);
|
|
if (parts.length >= 2) {
|
|
const cleanName = parts[0].trim();
|
|
if (cleanName.toLowerCase().includes('treasure name') || cleanName.toLowerCase().includes('actual ')) return null;
|
|
const desc = parts.slice(1).join(' ').trim().replace(/^description\s*:?\s*/i, '').trim();
|
|
return { name: cleanName, description: desc };
|
|
}
|
|
}
|
|
const [name, ...descParts] = entry.split(/[-–—:]/);
|
|
const cleanName = name.trim();
|
|
if (cleanName.toLowerCase().includes('location name') ||
|
|
cleanName.toLowerCase().includes('npc name') ||
|
|
cleanName.toLowerCase().includes('treasure name') ||
|
|
cleanName.toLowerCase().includes('actual ')) return null;
|
|
const desc = type === "npcs"
|
|
? descParts.join(" ").trim().replace(/^description\s*:?\s*/i, '').trim()
|
|
: descParts.join(" ").trim();
|
|
const obj = { name: cleanName };
|
|
if (type === "rooms") return { ...obj, description: desc };
|
|
if (type === "npcs") return { ...obj, trait: desc };
|
|
if (type === "treasure") return { ...obj, description: desc };
|
|
return null;
|
|
};
|
|
return cleanedRaw.split(/\n?\d+[).]\s+/).map(cleanText).filter(Boolean).map(mapper).filter(Boolean);
|
|
}
|
|
|
|
export const parseEncounterText = (text, idx) => {
|
|
const encounterMatch = text.match(/Encounter\s+(\d+)\s+(.+?)\s+(?:Room Name|Location)\s+(.+?)\s+Details\s+(.+)/i);
|
|
if (encounterMatch) {
|
|
const [, , name, location, details] = encounterMatch;
|
|
return { name: name.trim(), details: `${location.trim()}: ${details.trim()}` };
|
|
}
|
|
const colonFormat = text.match(/Encounter\s+\d+\s+(.+?):\s*(.+?):\s*(.+)/i);
|
|
if (colonFormat) {
|
|
const [, name, location, details] = colonFormat;
|
|
return { name: name.trim(), details: `${location.trim()}: ${details.trim()}` };
|
|
}
|
|
const match = text.match(/^(\d+)\s+(.+?)(?::\s*(.+))?$/);
|
|
if (match) {
|
|
const [, , name, details] = match;
|
|
return name && details ? { name: name.trim(), details: details.trim() } : null;
|
|
}
|
|
const colonSplit = text.split(/[:]/);
|
|
if (colonSplit.length > 1) {
|
|
return {
|
|
name: colonSplit[0].replace(/^\d+\s+|Encounter\s+\d+\s+/i, "").trim(),
|
|
details: colonSplit.slice(1).join(":").trim()
|
|
};
|
|
}
|
|
const nameMatch = text.match(/^\d+\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/);
|
|
if (nameMatch) {
|
|
return {
|
|
name: nameMatch[1],
|
|
details: text.replace(/^\d+\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\s*/, "").trim()
|
|
};
|
|
}
|
|
return { name: `Encounter ${idx + 1}`, details: text.replace(/^\d+\s+|Encounter\s+\d+\s+/i, "").trim() };
|
|
};
|
|
|
|
export const splitCombinedEncounters = (encounters) => {
|
|
if (encounters.length === 0) return [];
|
|
const shouldSplit = encounters.length === 1 && (encounters[0].name === "1" || encounters[0].details?.match(/\d+\s+[A-Z]/) || encounters[0].details?.includes('Encounter'));
|
|
if (!shouldSplit) return encounters;
|
|
console.warn("Encounters appear combined, attempting to split...");
|
|
const combinedText = encounters[0].details || "";
|
|
const split = combinedText.split(/(?=Encounter\s+\d+|\d+\s+[A-Z][a-z])/i).filter(Boolean);
|
|
return (split.length > 1 || (split.length === 1 && combinedText.length > 100))
|
|
? split.map((text, idx) => parseEncounterText(text, idx)).filter(e => e?.name && e?.details?.length > 10)
|
|
: encounters;
|
|
};
|
|
|
|
function _splitCombinedNPCs(npcs) {
|
|
const shouldSplit = npcs.length === 1 && npcs[0].trait?.length > 80;
|
|
if (!shouldSplit) return npcs;
|
|
console.warn("NPCs appear combined, attempting to split...");
|
|
const split = npcs[0].trait.split(/(?=[A-Z][a-z]+\s+[A-Z][a-z]+\s*:)/).filter(Boolean);
|
|
return split.length > 1
|
|
? split.map(text => {
|
|
const [name, ...traitParts] = text.split(/[:]/);
|
|
return { name: name.trim(), trait: traitParts.join(":").trim() };
|
|
}).filter(n => n.name && n.trait?.length > 10)
|
|
: npcs;
|
|
}
|
|
|
|
function parseTreasureText(text, idx, splitTreasures) {
|
|
if (idx === splitTreasures.length - 1 && text.length < 40) {
|
|
return { name: splitTreasures[idx - 1]?.split(/\s+/).slice(-2).join(" ") || `Treasure ${idx}`, description: text };
|
|
}
|
|
const dashSplit = text.split(/[—]/);
|
|
if (dashSplit.length === 2) return { name: dashSplit[0].trim(), description: dashSplit[1].trim() };
|
|
if (text.length < 30 && /^[A-Z]/.test(text)) return { name: text.trim(), description: "" };
|
|
return null;
|
|
}
|
|
|
|
function _splitCombinedTreasures(treasure) {
|
|
const shouldSplit = treasure.length === 1 && treasure[0].description?.length > 60;
|
|
if (!shouldSplit) return treasure;
|
|
console.warn("Treasures appear combined, attempting to split...");
|
|
const split = treasure[0].description.split(/\s+—\s+/).filter(Boolean);
|
|
if (split.length <= 1) return treasure;
|
|
const parsed = split.map((text, idx) => parseTreasureText(text, idx, split)).filter(t => t?.name && t?.description);
|
|
if (parsed.length > 0) return parsed;
|
|
const nameDescPairs = treasure[0].description.match(/([A-Z][^—]+?)\s+—\s+([^—]+?)(?=\s+[A-Z][^—]+\s+—|$)/g);
|
|
return nameDescPairs
|
|
? nameDescPairs.map(pair => {
|
|
const match = pair.match(/([^—]+)\s+—\s+(.+)/);
|
|
return match ? { name: match[1].trim(), description: match[2].trim() } : null;
|
|
}).filter(t => t)
|
|
: treasure;
|
|
}
|
|
|
|
export function parseRandomEventsRaw(rawSection) {
|
|
const parsed = parseList(rawSection || "");
|
|
return parsed
|
|
.filter(e =>
|
|
e &&
|
|
e.toLowerCase() !== 'a random event occurs' &&
|
|
e.toLowerCase() !== 'a random event occurs.' &&
|
|
!e.toLowerCase().includes('placeholder') &&
|
|
e.length > 10
|
|
)
|
|
.map((e, index) => {
|
|
const cleaned = e.replace(/^(Event\s+\d+[:\s]+|Random\s+Event[:\s]+|Random\s+Events?[:\s]+)/i, '').trim();
|
|
const colonMatch = cleaned.match(/^([^:]+):\s*(.+)$/);
|
|
if (colonMatch) {
|
|
const name = colonMatch[1].trim();
|
|
const description = colonMatch[2].trim();
|
|
if (name.toLowerCase().includes('event name') || name.toLowerCase().includes('placeholder')) return null;
|
|
return { name, description };
|
|
}
|
|
const words = cleaned.split(/\s+/);
|
|
if (words.length > 3) {
|
|
return { name: words.slice(0, 2).join(' '), description: words.slice(2).join(' ') };
|
|
}
|
|
return { name: `Event ${index + 1}`, description: cleaned };
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function parseMainContentSections(mainContentRaw) {
|
|
const initialSplit = mainContentRaw.split(/Encounters:|NPCs:|Treasures?:|Random Events:/i);
|
|
const withRandom = (!initialSplit[4] && mainContentRaw.toLowerCase().includes('random'))
|
|
? (() => {
|
|
const randomMatch = mainContentRaw.match(/Random Events?[:\s]*\n?([^]*?)(?=Locations?:|Encounters?:|NPCs?:|Treasures?:|$)/i);
|
|
return randomMatch ? [...initialSplit.slice(0, 4), randomMatch[1]] : initialSplit;
|
|
})()
|
|
: initialSplit;
|
|
const withNpcs = (!withRandom[2] && mainContentRaw.toLowerCase().includes('npc'))
|
|
? (() => {
|
|
const npcMatch = mainContentRaw.match(/NPCs?[:\s]*\n?([^]*?)(?=Treasures?:|Random Events?:|Locations?:|Encounters?:|$)/i);
|
|
return npcMatch ? [withRandom[0], withRandom[1], npcMatch[1], withRandom[3], withRandom[4]] : withRandom;
|
|
})()
|
|
: withRandom;
|
|
|
|
const inter = withNpcs[0];
|
|
const enc = (withNpcs[1] || '').trim();
|
|
if (enc || !inter.includes('Encounter')) {
|
|
return {
|
|
intermediateRoomsSection: inter,
|
|
encountersSection: enc,
|
|
npcsSection: withNpcs[2],
|
|
treasureSection: withNpcs[3],
|
|
randomEventsSection: withNpcs[4],
|
|
};
|
|
}
|
|
const encounterMatches = inter.match(/Encounter\s+\d+[^]*?(?=Encounter\s+\d+|NPCs?:|Treasures?:|Random Events?:|Location \d+|$)/gi);
|
|
if (!encounterMatches || encounterMatches.length === 0) {
|
|
return {
|
|
intermediateRoomsSection: inter,
|
|
encountersSection: enc,
|
|
npcsSection: withNpcs[2],
|
|
treasureSection: withNpcs[3],
|
|
randomEventsSection: withNpcs[4],
|
|
};
|
|
}
|
|
const encountersSection = encounterMatches.map((m, i) => {
|
|
const match = m.match(/Encounter\s+(\d+)\s+(.+?)\s+(?:Room Name|Location)\s+(.+?)\s+Details\s+(.+)/i);
|
|
if (match) {
|
|
const [, num, name, location, details] = match;
|
|
return `${num}. ${name.trim()}: ${location.trim()}: ${details.trim().substring(0, 200)}`;
|
|
}
|
|
const simpleMatch = m.match(/Encounter\s+(\d+)\s+(.+?)\s+([A-Z][^:]+?)\s+Details\s+(.+)/i);
|
|
if (simpleMatch) {
|
|
const [, num, name, location, details] = simpleMatch;
|
|
return `${num}. ${name.trim()}: ${location.trim()}: ${details.trim().substring(0, 200)}`;
|
|
}
|
|
return `${i + 1}. ${m.trim()}`;
|
|
}).join('\n');
|
|
const intermediateRoomsSection = inter.replace(/Encounter\s+\d+[^]*?(?=Encounter\s+\d+|NPCs?:|Treasures?:|Random Events?:|Location \d+|$)/gi, '');
|
|
return {
|
|
intermediateRoomsSection,
|
|
encountersSection,
|
|
npcsSection: withNpcs[2],
|
|
treasureSection: withNpcs[3],
|
|
randomEventsSection: withNpcs[4],
|
|
};
|
|
}
|
|
|