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