function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function escapeHtml(text) { if (!text) return ''; const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(text).replace(/[&<>"']/g, m => map[m]); } export function dungeonTemplate(data) { const bodyFonts = [ "'Lora', serif", "'Merriweather', serif", "'Libre Baskerville', serif", "'Source Serif 4', serif" ]; const headingFonts = [ "'New Rocker', system-ui", "'UnifrakturCook', cursive", "'IM Fell DW Pica', serif", "'Cinzel', serif", "'Cormorant Garamond', serif", "'Playfair Display', serif" ]; const quoteFonts = [ "'Playfair Display', serif", "'Libre Baskerville', serif", "'Merriweather', serif" ]; const bodyFont = pickRandom(bodyFonts); const headingFont = pickRandom(headingFonts); const quoteFont = pickRandom(quoteFonts); // Check if we have a map image to include const hasMap = data.map && typeof data.map === 'string' && data.map.startsWith('data:image/'); return ` ${data.title}

${escapeHtml(data.title)}

${data.flavor ? `

${escapeHtml(data.flavor)}

` : ''}
${data.hooksRumors && data.hooksRumors.length > 0 ? `

Hooks & Rumors

    ${data.hooksRumors.map(hook => `
  • ${escapeHtml(hook)}
  • `).join('')}
` : ''} ${data.randomEvents && data.randomEvents.length > 0 ? `

Random Events (d6)

${data.randomEvents.map((event, index) => ` `).join('')}
${index + 1} ${escapeHtml(event)}
` : ''} ${data.rooms && data.rooms.length > 0 ? `

Locations

${data.rooms.map(room => `

${escapeHtml(room.name)}

${escapeHtml(room.description)}

`).join('')}
` : ''}
${data.encounters && data.encounters.length > 0 ? `

Encounters (d6)

${data.encounters.map((encounter, index) => { // Truncate details to 4 sentences max to prevent overflow let details = encounter.details || ''; // Keep location prefix in details (format: "Location Name: details") // Split into sentences and keep only first 4 const sentences = details.match(/[^.!?]+[.!?]+/g) || [details]; if (sentences.length > 4) { details = sentences.slice(0, 4).join(' ').trim(); } // Also limit by character count as fallback (max ~350 chars) if (details.length > 350) { details = details.substring(0, 347).trim(); // Try to end at a sentence boundary const lastPeriod = details.lastIndexOf('.'); if (lastPeriod > 280) { details = details.substring(0, lastPeriod + 1); } else { details += '...'; } } return ` `; }).join('')}
${index + 1} ${escapeHtml(encounter.name)} ${escapeHtml(details)}
` : ''}
${data.treasure && data.treasure.length > 0 ? `

Treasure

${data.treasure.map(item => `
${typeof item === 'object' && item.name ? `${escapeHtml(item.name)} — ${escapeHtml(item.description)}` : escapeHtml(item)}
`).join('')}
` : ''} ${data.npcs && data.npcs.length > 0 ? `

NPCs

${data.npcs.map(npc => `
${escapeHtml(npc.name)}: ${escapeHtml(npc.trait)}
`).join('')}
` : ''} ${data.plotResolutions && data.plotResolutions.length > 0 ? `

Plot Resolutions

${data.plotResolutions.map(resolution => { // Truncate to 3 sentences max to prevent overflow let text = resolution || ''; // Split into sentences and keep only first 3 const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; if (sentences.length > 3) { text = sentences.slice(0, 3).join(' ').trim(); } // Also limit by character count as fallback (max ~200 chars) if (text.length > 200) { text = text.substring(0, 197).trim(); // Try to end at a sentence boundary const lastPeriod = text.lastIndexOf('.'); if (lastPeriod > 150) { text = text.substring(0, lastPeriod + 1); } else { text += '...'; } } return `
${escapeHtml(text)}
`; }).join('')}
` : ''}
${hasMap ? `
Dungeon Map
` : ''} `; }