cleanup title and formatting
This commit is contained in:
@@ -12,7 +12,24 @@ function cleanText(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseList(raw) {
|
function parseList(raw) {
|
||||||
return raw
|
if (!raw) return [];
|
||||||
|
|
||||||
|
// Match all numbered items using a regex that captures the content
|
||||||
|
// This handles both "1. Title" and "1) Title" formats, and works even if multiple titles are on one line
|
||||||
|
// The regex captures everything after the number until the next number pattern or end of string
|
||||||
|
// Using [\s\S] to match any character including newlines, but stop at the next number pattern
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Fallback: if regex didn't work, try the old method
|
||||||
|
return items.length > 0
|
||||||
|
? items
|
||||||
|
: raw
|
||||||
.split(/\n?\d+[).]\s+/)
|
.split(/\n?\d+[).]\s+/)
|
||||||
.map(line => cleanText(line))
|
.map(line => cleanText(line))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -483,31 +500,40 @@ function validateAndFixContent(dungeonData) {
|
|||||||
|
|
||||||
export async function generateDungeon() {
|
export async function generateDungeon() {
|
||||||
// Step 1: Titles
|
// Step 1: Titles
|
||||||
const generatedTitlesRaw = await callOllama(
|
const generatedTitles = await callOllama(
|
||||||
`Generate 50 different and unique short, punchy dungeon titles (2-4 words maximum, single evocative name, no colons), numbered as a plain text list.
|
`Generate 50 different evocative dungeon titles for one-page dungeon adventures. Each title should be 2-4 words and feel like a complete, memorable location name.
|
||||||
Each title should come from a different style or theme. Make the set varied and evocative. For example:
|
|
||||||
|
|
||||||
- OSR / classic tabletop: gritty, mysterious, old-school
|
GOOD EXAMPLES (the kind of titles we want):
|
||||||
- Mörk Borg: dark, apocalyptic, foreboding
|
- "Shadows of Winterhalf"
|
||||||
- Pulpy fantasy: adventurous, dramatic, larger-than-life
|
- "Rust of a Forgotten Tomb"
|
||||||
- Mildly sci-fi: alien, technological, strange
|
- "Ruins of Alph"
|
||||||
- Weird fantasy: uncanny, surreal, unsettling
|
- "Wyrm's Maw"
|
||||||
- Whimsical: fun, quirky, playful
|
- "Echoes of Duskfall"
|
||||||
|
- "The Sunken Cathedral"
|
||||||
|
- "Bone Orchard"
|
||||||
|
- "Whispering Depths"
|
||||||
|
|
||||||
|
BAD EXAMPLES (avoid these - too generic/short):
|
||||||
|
- "Rust" (too short, not evocative)
|
||||||
|
- "Shadows" (single word, lacks context)
|
||||||
|
- "Ruin" (too vague)
|
||||||
|
|
||||||
|
Each title should:
|
||||||
|
- Be 2-4 words (preferably 3-4 words for better flavor)
|
||||||
|
- Sound like a complete location name that tells a story
|
||||||
|
- Use evocative, descriptive language
|
||||||
|
- Feel like it belongs in a classic tabletop RPG dungeon
|
||||||
|
- Be memorable and distinctive
|
||||||
|
|
||||||
|
Vary the styles: some can be "X of Y" format, some can be possessive ("Wyrm's Maw"), some can be descriptive phrases. Mix of dark fantasy, classic D&D, weird fantasy, and other styles.
|
||||||
|
|
||||||
CRITICAL: Ensure all spelling is correct. Double-check all words before outputting.
|
CRITICAL: Ensure all spelling is correct. Double-check all words before outputting.
|
||||||
Avoid repeating materials or adjectives. Absolutely do not use the words "Obsidian" or "Clockwork" in any title. Do not include explanations, markdown, or preambles. Do not include the style or theme in parenthesis. Only the 50 numbered titles.`,
|
Absolutely do not use the words "Obsidian" or "Clockwork" in any title. Do not include explanations, markdown, or preambles. Only output the 50 numbered titles, one per line.`,
|
||||||
undefined, 5, "Step 1: Titles"
|
undefined, 5, "Step 1: Titles"
|
||||||
);
|
);
|
||||||
const generatedTitles = parseList(generatedTitlesRaw);
|
|
||||||
console.log("Generated Titles:", generatedTitles);
|
console.log("Generated Titles:", generatedTitles);
|
||||||
let title = generatedTitles[Math.floor(Math.random() * generatedTitles.length)];
|
const titlesList = parseList(generatedTitles);
|
||||||
// Remove colons, dashes, and other separators, but keep the full title
|
const title = titlesList[Math.floor(Math.random() * titlesList.length)];
|
||||||
title = title.split(/[:—–-]/)[0].trim();
|
|
||||||
// Only limit to 4 words if it's clearly too long (more than 6 words)
|
|
||||||
const titleWords = title.split(/\s+/);
|
|
||||||
if (titleWords.length > 6) {
|
|
||||||
title = titleWords.slice(0, 4).join(' ');
|
|
||||||
}
|
|
||||||
console.log("Selected title:", title);
|
console.log("Selected title:", title);
|
||||||
|
|
||||||
// Step 2: Core Concepts
|
// Step 2: Core Concepts
|
||||||
@@ -669,13 +695,14 @@ EXAMPLE TREASURE:
|
|||||||
"Whispering Blade — This dagger amplifies the wielder's voice to a deafening roar when drawn. Found in the Guardian Golem's chamber, it was used to command the construct. The blade is cursed: each use permanently reduces the wielder's hearing. Can be used to shatter glass or stun enemies, but the curse cannot be removed."
|
"Whispering Blade — This dagger amplifies the wielder's voice to a deafening roar when drawn. Found in the Guardian Golem's chamber, it was used to command the construct. The blade is cursed: each use permanently reduces the wielder's hearing. Can be used to shatter glass or stun enemies, but the curse cannot be removed."
|
||||||
|
|
||||||
- **Strictly 1 Random Events Table:** A d6 table (EXACTLY 6 entries, no more, no less) with random events/wandering encounters. Each entry MUST:
|
- **Strictly 1 Random Events Table:** A d6 table (EXACTLY 6 entries, no more, no less) with random events/wandering encounters. Each entry MUST:
|
||||||
|
- Have a short, evocative event name (max 4 words)
|
||||||
- Provide interesting complications or opportunities (not just combat)
|
- Provide interesting complications or opportunities (not just combat)
|
||||||
- Tie to the core concepts and dynamic element
|
- Tie to the core concepts and dynamic element
|
||||||
- Add replayability and surprise
|
- Add replayability and surprise
|
||||||
- Be 15-20 words maximum
|
- Description should be 15-20 words maximum
|
||||||
- Be UNIQUE and DIFFERENT from each other (no duplicates or generic placeholders)
|
- Be UNIQUE and DIFFERENT from each other (no duplicates or generic placeholders)
|
||||||
- Be SPECIFIC to this dungeon's theme, conflict, and dynamic element
|
- Be SPECIFIC to this dungeon's theme, conflict, and dynamic element
|
||||||
Format as numbered 1-6 list under "Random Events:" label. Each event must be a complete, interesting sentence describing what happens.
|
Format as numbered 1-6 list under "Random Events:" label. Each event must be formatted as "Event Name: Description text" using colons, NOT em-dashes.
|
||||||
|
|
||||||
CRITICAL: Each item must be on its own numbered line. DO NOT combine multiple items into a single numbered entry.
|
CRITICAL: Each item must be on its own numbered line. DO NOT combine multiple items into a single numbered entry.
|
||||||
|
|
||||||
@@ -706,12 +733,12 @@ Treasures:
|
|||||||
4. Actual Item Name — Description text.
|
4. Actual Item Name — Description text.
|
||||||
|
|
||||||
Random Events:
|
Random Events:
|
||||||
1. Event description.
|
1. Event Name: Event description.
|
||||||
2. Event description.
|
2. Event Name: Event description.
|
||||||
3. Event description.
|
3. Event Name: Event description.
|
||||||
4. Event description.
|
4. Event Name: Event description.
|
||||||
5. Event description.
|
5. Event Name: Event description.
|
||||||
6. Event description.
|
6. Event Name: Event description.
|
||||||
|
|
||||||
CRITICAL: Every name must be unique and creative. Never use generic placeholders like "Location Name", "NPC Name", "Encounter Name", or "Treasure Name". Use actual descriptive names that fit the dungeon's theme.
|
CRITICAL: Every name must be unique and creative. Never use generic placeholders like "Location Name", "NPC Name", "Encounter Name", or "Treasure Name". Use actual descriptive names that fit the dungeon's theme.
|
||||||
|
|
||||||
@@ -773,7 +800,20 @@ Each section must start with its label on its own line, followed by numbered ite
|
|||||||
if (intermediateRooms.length > 3) {
|
if (intermediateRooms.length > 3) {
|
||||||
console.warn(`Expected exactly 3 intermediate locations but got ${intermediateRooms.length}, limiting to first 3`);
|
console.warn(`Expected exactly 3 intermediate locations but got ${intermediateRooms.length}, limiting to first 3`);
|
||||||
}
|
}
|
||||||
const rooms = [entranceRoom, ...limitedIntermediateRooms, climaxRoom];
|
|
||||||
|
// Deduplicate rooms by name (case-insensitive), keeping first occurrence
|
||||||
|
const allRooms = [entranceRoom, ...limitedIntermediateRooms, climaxRoom].filter(Boolean);
|
||||||
|
const seenNames = new Set();
|
||||||
|
const rooms = allRooms.filter(room => {
|
||||||
|
if (!room || !room.name) return false;
|
||||||
|
const nameLower = room.name.toLowerCase().trim();
|
||||||
|
if (seenNames.has(nameLower)) {
|
||||||
|
console.warn(`Duplicate room name detected: "${room.name}", skipping duplicate`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seenNames.add(nameLower);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
let encounters = splitCombinedEncounters(parseObjects(encountersSection || "", "encounters"));
|
let encounters = splitCombinedEncounters(parseObjects(encountersSection || "", "encounters"));
|
||||||
let npcs = parseObjects(npcsSection || "", "npcs");
|
let npcs = parseObjects(npcsSection || "", "npcs");
|
||||||
const treasure = parseObjects(treasureSection || "", "treasure");
|
const treasure = parseObjects(treasureSection || "", "treasure");
|
||||||
@@ -823,7 +863,7 @@ Each section must start with its label on its own line, followed by numbered ite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let randomEvents = parseList(randomEventsSection || "");
|
let randomEvents = parseList(randomEventsSection || "");
|
||||||
// Filter out placeholder events and strip any title prefixes
|
// Parse events into objects with name and description
|
||||||
randomEvents = randomEvents
|
randomEvents = randomEvents
|
||||||
.filter(e =>
|
.filter(e =>
|
||||||
e &&
|
e &&
|
||||||
@@ -832,28 +872,35 @@ Each section must start with its label on its own line, followed by numbered ite
|
|||||||
!e.toLowerCase().includes('placeholder') &&
|
!e.toLowerCase().includes('placeholder') &&
|
||||||
e.length > 10
|
e.length > 10
|
||||||
)
|
)
|
||||||
.map(e => {
|
.map((e, index) => {
|
||||||
// Strip title prefixes like "Event 1:", "Random Event:", "Guardian Golem:", "Location Name:", etc.
|
// Strip numbered prefixes like "Event 1:", "Random Event:", etc.
|
||||||
// Remove numbered prefixes, random event labels, and location/encounter name prefixes
|
|
||||||
let cleaned = e.replace(/^(Event\s+\d+[:\s]+|Random\s+Event[:\s]+|Random\s+Events?[:\s]+)/i, '').trim();
|
let cleaned = e.replace(/^(Event\s+\d+[:\s]+|Random\s+Event[:\s]+|Random\s+Events?[:\s]+)/i, '').trim();
|
||||||
// Remove location/encounter name prefixes (format: "Name: Location: description" or "Name: description")
|
|
||||||
// If it starts with a capitalized phrase followed by colon, it might be a title
|
// Parse "Event Name: Description" format
|
||||||
const titleMatch = cleaned.match(/^([A-Z][^:]{3,40}):\s*(.+)$/);
|
const colonMatch = cleaned.match(/^([^:]+):\s*(.+)$/);
|
||||||
if (titleMatch) {
|
if (colonMatch) {
|
||||||
const potentialTitle = titleMatch[1];
|
const name = colonMatch[1].trim();
|
||||||
const rest = titleMatch[2];
|
const description = colonMatch[2].trim();
|
||||||
// If the rest also has a colon, the first part is likely a title to remove
|
// Skip if name looks like a placeholder
|
||||||
if (rest.includes(':')) {
|
if (name.toLowerCase().includes('event name') || name.toLowerCase().includes('placeholder')) {
|
||||||
cleaned = rest;
|
return null;
|
||||||
} else {
|
|
||||||
// Check if it looks like a location name (common patterns)
|
|
||||||
if (potentialTitle.match(/\b(Chamber|Room|Tower|Hall|Temple|Shrine|Crypt|Tomb|Vault|Gate|Bridge|Path|Corridor|Labyrinth|Dungeon|Cavern|Cave|Grotto|Sanctum|Shrine|Altar|Pit|Well|Pool|Fountain|Garden|Courtyard|Plaza|Square|Arena|Colosseum|Theater|Library|Laboratory|Workshop|Forge|Smithy|Barracks|Armory|Treasury|Storehouse|Warehouse|Cellar|Basement|Attic|Loft|Garret|Turret|Spire|Keep|Fortress|Castle|Palace|Manor|Mansion|Estate|Villa|Cottage|Hut|Shack|Tent|Pavilion|Gazebo|Pergola|Arbor|Bower|Grotto|Grot|Den|Lair|Nest|Burrow|Hollow|Recess|Niche|Alcove|Cubby|Cubbyhole|Closet|Wardrobe|Cupboard|Cabinet|Drawer|Shelf|Rack|Peg|Hook|Nail|Screw|Bolt|Rivet|Pin|Clip|Clamp|Brace|Bracket|Support|Prop|Pillar|Column|Post|Pole|Rod|Stick|Staff|Wand|Scepter|Mace|Club|Hammer|Axe|Sword|Dagger|Knife|Blade|Edge|Point|Tip|End|Top|Bottom|Side|Front|Back|Left|Right|Center|Middle|Core|Heart|Soul|Spirit|Mind|Body|Head|Neck|Chest|Torso|Waist|Hip|Thigh|Knee|Leg|Foot|Toe|Heel|Ankle|Shin|Calf|Thigh|Buttock|Backside|Rear|Behind|Aft|Stern|Bow|Port|Starboard|Fore|Aft|Midship|Amidship|Amidships|Amidship|Amidships)\b/i)) {
|
|
||||||
cleaned = rest;
|
|
||||||
}
|
}
|
||||||
|
return { name, description };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: if no colon, use first few words as name
|
||||||
|
const words = cleaned.split(/\s+/);
|
||||||
|
if (words.length > 3) {
|
||||||
|
return {
|
||||||
|
name: words.slice(0, 2).join(' '),
|
||||||
|
description: words.slice(2).join(' ')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return cleaned.trim();
|
|
||||||
});
|
// Last resort: use as description with generic name
|
||||||
|
return { name: `Event ${index + 1}`, description: cleaned };
|
||||||
|
})
|
||||||
|
.filter(Boolean); // Remove null entries
|
||||||
randomEvents = randomEvents.slice(0, 6);
|
randomEvents = randomEvents.slice(0, 6);
|
||||||
|
|
||||||
// Generate context-aware fallbacks if needed (only if we have some events already)
|
// Generate context-aware fallbacks if needed (only if we have some events already)
|
||||||
@@ -861,12 +908,12 @@ Each section must start with its label on its own line, followed by numbered ite
|
|||||||
const dynamicElement = coreConcepts.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences';
|
const dynamicElement = coreConcepts.match(/Dynamic Element[:\s]+([^.]+)/i)?.[1]?.trim() || 'strange occurrences';
|
||||||
const conflict = coreConcepts.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a mysterious threat';
|
const conflict = coreConcepts.match(/Central Conflict[:\s]+([^.]+)/i)?.[1]?.trim() || 'a mysterious threat';
|
||||||
const fallbackEvents = [
|
const fallbackEvents = [
|
||||||
`The ${dynamicElement.toLowerCase()} causes unexpected changes in the environment.`,
|
{ name: 'Environmental Shift', description: `The ${dynamicElement.toLowerCase()} causes unexpected changes in the environment.` },
|
||||||
`A sign of ${conflict.toLowerCase()} appears, requiring immediate attention.`,
|
{ name: 'Conflict Manifestation', description: `A sign of ${conflict.toLowerCase()} appears, requiring immediate attention.` },
|
||||||
`The dungeon shifts, revealing a previously hidden passage or danger.`,
|
{ name: 'Dungeon Shift', description: `The dungeon shifts, revealing a previously hidden passage or danger.` },
|
||||||
`An NPC from the primary faction appears with urgent information.`,
|
{ name: 'Faction Messenger', description: `An NPC from the primary faction appears with urgent information.` },
|
||||||
`The power source fluctuates, creating temporary hazards or opportunities.`,
|
{ name: 'Power Fluctuation', description: `The power source fluctuates, creating temporary hazards or opportunities.` },
|
||||||
`Echoes of past events manifest, providing clues or complications.`
|
{ name: 'Echoes of the Past', description: `Echoes of past events manifest, providing clues or complications.` }
|
||||||
];
|
];
|
||||||
while (randomEvents.length < 6) {
|
while (randomEvents.length < 6) {
|
||||||
randomEvents.push(fallbackEvents[randomEvents.length % fallbackEvents.length]);
|
randomEvents.push(fallbackEvents[randomEvents.length % fallbackEvents.length]);
|
||||||
|
|||||||
@@ -129,19 +129,19 @@ export function dungeonTemplate(data) {
|
|||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
.room h3 {
|
.room h3 {
|
||||||
margin: 0.1em 0 0.05em;
|
margin: 0.08em 0 0.03em;
|
||||||
font-size: 0.95em;
|
font-size: 0.9em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
.room p {
|
.room p {
|
||||||
margin: 0 0 0.25em;
|
margin: 0 0 0.15em;
|
||||||
font-size: 0.85em;
|
font-size: 0.8em;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 1.3em;
|
line-height: 1.25em;
|
||||||
}
|
}
|
||||||
.encounter, .npc, .treasure, .plot-resolution {
|
.encounter, .npc, .treasure, .plot-resolution {
|
||||||
margin: 0 0 0.35em;
|
margin: 0 0 0.25em;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
@@ -252,14 +252,54 @@ export function dungeonTemplate(data) {
|
|||||||
${data.randomEvents && data.randomEvents.length > 0 ? `
|
${data.randomEvents && data.randomEvents.length > 0 ? `
|
||||||
<div class="section-block random-events">
|
<div class="section-block random-events">
|
||||||
<h2>Random Events (d6)</h2>
|
<h2>Random Events (d6)</h2>
|
||||||
<table>
|
<table class="encounters-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
${data.randomEvents.map((event, index) => `
|
${data.randomEvents.map((event, index) => {
|
||||||
|
// Handle both object format {name, description} and string format
|
||||||
|
let eventName = '';
|
||||||
|
let eventDesc = '';
|
||||||
|
if (typeof event === 'object' && event.name && event.description) {
|
||||||
|
eventName = event.name;
|
||||||
|
eventDesc = event.description;
|
||||||
|
} else if (typeof event === 'string') {
|
||||||
|
// Try to parse "Event Name: Description" format
|
||||||
|
const colonMatch = event.match(/^([^:]+):\s*(.+)$/);
|
||||||
|
if (colonMatch) {
|
||||||
|
eventName = colonMatch[1].trim();
|
||||||
|
eventDesc = colonMatch[2].trim();
|
||||||
|
} else {
|
||||||
|
// Fallback: use first few words as name, rest as description
|
||||||
|
const words = event.split(/\s+/);
|
||||||
|
if (words.length > 3) {
|
||||||
|
eventName = words.slice(0, 2).join(' ');
|
||||||
|
eventDesc = words.slice(2).join(' ');
|
||||||
|
} else {
|
||||||
|
eventName = `Event ${index + 1}`;
|
||||||
|
eventDesc = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eventName = `Event ${index + 1}`;
|
||||||
|
eventDesc = String(event || '');
|
||||||
|
}
|
||||||
|
// Truncate description to prevent overflow (similar to encounters)
|
||||||
|
if (eventDesc.length > 200) {
|
||||||
|
eventDesc = eventDesc.substring(0, 197).trim();
|
||||||
|
const lastPeriod = eventDesc.lastIndexOf('.');
|
||||||
|
if (lastPeriod > 150) {
|
||||||
|
eventDesc = eventDesc.substring(0, lastPeriod + 1);
|
||||||
|
} else {
|
||||||
|
eventDesc += '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${index + 1}</td>
|
<td>${index + 1}</td>
|
||||||
<td>${escapeHtml(event)}</td>
|
<td><strong>${escapeHtml(eventName)}</strong></td>
|
||||||
|
<td>${escapeHtml(eventDesc)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,16 +310,16 @@ export function dungeonTemplate(data) {
|
|||||||
<h2>Locations</h2>
|
<h2>Locations</h2>
|
||||||
${data.rooms.map(room => {
|
${data.rooms.map(room => {
|
||||||
let desc = room.description || '';
|
let desc = room.description || '';
|
||||||
// Truncate to 2 sentences max
|
// Truncate to 1 sentence max to prevent overflow
|
||||||
const sentences = desc.match(/[^.!?]+[.!?]+/g) || [desc];
|
const sentences = desc.match(/[^.!?]+[.!?]+/g) || [desc];
|
||||||
if (sentences.length > 2) {
|
if (sentences.length > 1) {
|
||||||
desc = sentences.slice(0, 2).join(' ').trim();
|
desc = sentences.slice(0, 1).join(' ').trim();
|
||||||
}
|
}
|
||||||
// Also limit by character count (~150 chars)
|
// Also limit by character count (~100 chars for tighter fit)
|
||||||
if (desc.length > 150) {
|
if (desc.length > 100) {
|
||||||
desc = desc.substring(0, 147).trim();
|
desc = desc.substring(0, 97).trim();
|
||||||
const lastPeriod = desc.lastIndexOf('.');
|
const lastPeriod = desc.lastIndexOf('.');
|
||||||
if (lastPeriod > 100) {
|
if (lastPeriod > 70) {
|
||||||
desc = desc.substring(0, lastPeriod + 1);
|
desc = desc.substring(0, lastPeriod + 1);
|
||||||
} else {
|
} else {
|
||||||
desc += '...';
|
desc += '...';
|
||||||
@@ -347,9 +387,7 @@ export function dungeonTemplate(data) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
${data.treasure && data.treasure.length > 0 ? `
|
${data.treasure && data.treasure.length > 0 ? `
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<h2>Treasure</h2>
|
<h2>Treasure</h2>
|
||||||
@@ -368,7 +406,9 @@ export function dungeonTemplate(data) {
|
|||||||
}).filter(Boolean).join('')}
|
}).filter(Boolean).join('')}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
${data.npcs && data.npcs.length > 0 ? `
|
${data.npcs && data.npcs.length > 0 ? `
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<h2>NPCs</h2>
|
<h2>NPCs</h2>
|
||||||
@@ -392,19 +432,19 @@ export function dungeonTemplate(data) {
|
|||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<h2>Plot Resolutions</h2>
|
<h2>Plot Resolutions</h2>
|
||||||
${data.plotResolutions.map(resolution => {
|
${data.plotResolutions.map(resolution => {
|
||||||
// Truncate to 2 sentences max to prevent overflow
|
// Truncate to 1 sentence max to prevent overflow (more aggressive)
|
||||||
let text = resolution || '';
|
let text = resolution || '';
|
||||||
// Split into sentences and keep only first 2
|
// Split into sentences and keep only first 1
|
||||||
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
||||||
if (sentences.length > 2) {
|
if (sentences.length > 1) {
|
||||||
text = sentences.slice(0, 2).join(' ').trim();
|
text = sentences.slice(0, 1).join(' ').trim();
|
||||||
}
|
}
|
||||||
// Also limit by character count as fallback (max ~150 chars)
|
// Also limit by character count as fallback (max ~120 chars for tighter fit)
|
||||||
if (text.length > 150) {
|
if (text.length > 120) {
|
||||||
text = text.substring(0, 147).trim();
|
text = text.substring(0, 117).trim();
|
||||||
// Try to end at a sentence boundary
|
// Try to end at a sentence boundary
|
||||||
const lastPeriod = text.lastIndexOf('.');
|
const lastPeriod = text.lastIndexOf('.');
|
||||||
if (lastPeriod > 100) {
|
if (lastPeriod > 90) {
|
||||||
text = text.substring(0, lastPeriod + 1);
|
text = text.substring(0, lastPeriod + 1);
|
||||||
} else {
|
} else {
|
||||||
text += '...';
|
text += '...';
|
||||||
|
|||||||
Reference in New Issue
Block a user