diff --git a/dungeonGenerator.js b/dungeonGenerator.js
index 8849715..509fb7d 100644
--- a/dungeonGenerator.js
+++ b/dungeonGenerator.js
@@ -21,11 +21,29 @@ function parseList(raw) {
function parseObjects(raw, type = "rooms") {
const cleanedRaw = raw.replace(/Intermediate Rooms:/i, "").replace(/Climax Room:/i, "").trim();
const mapper = (entry) => {
+ if (type === "encounters") {
+ // For encounters, format is "Encounter Name: Location Name: details"
+ // We want to keep the encounter name separate and preserve the location:details structure
+ const parts = entry.split(/:/);
+ if (parts.length >= 3) {
+ // Format: "Encounter Name: Location Name: details"
+ return {
+ name: parts[0].trim(),
+ details: parts.slice(1).join(":").trim() // Keep "Location Name: details"
+ };
+ } else if (parts.length === 2) {
+ // Format: "Encounter Name: details" (fallback)
+ return {
+ name: parts[0].trim(),
+ details: parts[1].trim()
+ };
+ }
+ }
+ // For other types, use original logic
const [name, ...descParts] = entry.split(/[-–—:]/);
const desc = descParts.join(" ").trim();
const obj = { name: name.trim() };
if (type === "rooms") return { ...obj, description: desc };
- if (type === "encounters") return { ...obj, details: desc };
if (type === "npcs") return { ...obj, trait: desc };
if (type === "treasure") return { ...obj, description: desc };
return entry;
@@ -373,7 +391,7 @@ Generate the rest of the dungeon's content to fill the space between the entranc
- Hidden aspects discoverable through interaction or investigation
Format as "Name: description" using colons, NOT em-dashes.
-- **Strictly 6 Encounters:** Numbered 1-6 (for d6 rolling). Name (max 6 words) and details (90-120 words per encounter). Each encounter must:
+- **Strictly 6 Encounters:** Numbered 1-6 (for d6 rolling). Name (max 6 words) and details (2-3 sentences MAX, approximately 30-50 words). Each encounter must:
- Start with the room/location name followed by a colon, then the details (e.g., "Location Name: Details text")
- The location name must match one of the actual room names from this dungeon
- Include environmental hazards/opportunities (cover, elevation, traps, interactable objects, terrain features)
@@ -491,7 +509,7 @@ Each resolution should:
- Integrate NPCs, faction dynamics, and player actions
- Include failure states or unexpected outcomes as options
- Reflect different approaches players might take
-Keep each item to 50-60 words (2-3 sentences). Output as a numbered list, plain text only. Do not use em-dashes (—) anywhere in the output. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`,
+Keep each item to 1-2 sentences MAX (approximately 20-30 words). Be extremely concise. Output as a numbered list, plain text only. Do not use em-dashes (—) anywhere in the output. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`,
undefined, 5, "Step 6: Plot Resolutions"
);
const plotResolutions = parseList(plotResolutionsRaw);
diff --git a/dungeonTemplate.js b/dungeonTemplate.js
index b571022..6959a91 100644
--- a/dungeonTemplate.js
+++ b/dungeonTemplate.js
@@ -169,6 +169,8 @@ export function dungeonTemplate(data) {
vertical-align: top;
line-height: 1.3em;
border-bottom: 1px solid #1a1a1a;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
}
table tr:last-child td {
border-bottom: 1px solid #1a1a1a;
@@ -181,12 +183,14 @@ export function dungeonTemplate(data) {
}
.encounters-table td:nth-child(2) {
font-weight: bold;
- width: 30%;
+ width: 25%;
padding-right: 0.5em;
border-right: 1px solid #1a1a1a;
}
.encounters-table td:nth-child(3) {
width: auto;
+ font-size: 0.75em;
+ line-height: 1.2em;
}
.map-page {
display: flex;
@@ -265,13 +269,35 @@ export function dungeonTemplate(data) {
Encounters (d6)
- ${data.encounters.map((encounter, index) => `
+ ${data.encounters.map((encounter, index) => {
+ // Truncate details to 2-3 sentences max to prevent overflow
+ let details = encounter.details || '';
+ // Remove location prefix from details if present (format: "Location Name: details")
+ details = details.replace(/^[^:]+:\s*/, '');
+ // Split into sentences and keep only first 3
+ const sentences = details.match(/[^.!?]+[.!?]+/g) || [details];
+ if (sentences.length > 3) {
+ details = sentences.slice(0, 3).join(' ').trim();
+ }
+ // Also limit by character count as fallback (max ~250 chars)
+ if (details.length > 250) {
+ details = details.substring(0, 247).trim();
+ // Try to end at a sentence boundary
+ const lastPeriod = details.lastIndexOf('.');
+ if (lastPeriod > 200) {
+ details = details.substring(0, lastPeriod + 1);
+ } else {
+ details += '...';
+ }
+ }
+ return `
| ${index + 1} |
${encounter.name} |
- ${encounter.details} |
+ ${details} |
- `).join('')}
+ `;
+ }).join('')}
@@ -304,11 +330,31 @@ export function dungeonTemplate(data) {
${data.plotResolutions && data.plotResolutions.length > 0 ? `
Plot Resolutions
- ${data.plotResolutions.map(resolution => `
+ ${data.plotResolutions.map(resolution => {
+ // Truncate to 1-2 sentences max to prevent overflow
+ let text = resolution || '';
+ // Split into sentences and keep only first 2
+ const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
+ if (sentences.length > 2) {
+ text = sentences.slice(0, 2).join(' ').trim();
+ }
+ // Also limit by character count as fallback (max ~150 chars)
+ if (text.length > 150) {
+ text = text.substring(0, 147).trim();
+ // Try to end at a sentence boundary
+ const lastPeriod = text.lastIndexOf('.');
+ if (lastPeriod > 100) {
+ text = text.substring(0, lastPeriod + 1);
+ } else {
+ text += '...';
+ }
+ }
+ return `
- ${resolution}
+ ${text}
- `).join('')}
+ `;
+ }).join('')}
` : ''}
diff --git a/index.js b/index.js
index bcdd36e..9082d2b 100644
--- a/index.js
+++ b/index.js
@@ -2,7 +2,7 @@ import "dotenv/config";
import { generateDungeon } from "./dungeonGenerator.js";
import { generateDungeonImages } from "./imageGenerator.js";
import { generatePDF } from "./generatePDF.js";
-import { OLLAMA_MODEL, listOpenWebUIModels } from "./ollamaClient.js";
+import { OLLAMA_MODEL } from "./ollamaClient.js";
// Utility to create a filesystem-safe filename from the dungeon title
function slugify(text) {
@@ -20,11 +20,6 @@ function slugify(text) {
console.log("Using Ollama API URL:", process.env.OLLAMA_API_URL);
console.log("Using Ollama model:", OLLAMA_MODEL);
- // Try to list available models if using Open WebUI
- if (process.env.OLLAMA_API_URL?.includes("/api/chat/completions")) {
- await listOpenWebUIModels();
- }
-
// Generate the dungeon data
const dungeonData = await generateDungeon();
diff --git a/ollamaClient.js b/ollamaClient.js
index 0d42e42..4251956 100644
--- a/ollamaClient.js
+++ b/ollamaClient.js
@@ -47,13 +47,6 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
? { model, messages: [{ role: "user", content: prompt }] }
: { model, prompt, stream: false };
- // Debug logging for Open WebUI
- if (isUsingOpenWebUI) {
- console.log(`[${stepName}] Using Open WebUI API`);
- console.log(`[${stepName}] Model name: "${model}"`);
- console.log(`[${stepName}] API URL: ${OLLAMA_API_URL}`);
- }
-
const response = await fetch(OLLAMA_API_URL, {
method: "POST",
headers,
@@ -68,14 +61,9 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
} catch {
// Ignore errors reading error response
}
- const errorMsg = `Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`;
- if (isUsingOpenWebUI) {
- console.error(`[${stepName}] Request details:`);
- console.error(` URL: ${OLLAMA_API_URL}`);
- console.error(` Model: "${model}"`);
- console.error(` Body: ${JSON.stringify(body, null, 2)}`);
- }
- throw new Error(errorMsg);
+ throw new Error(
+ `Ollama request failed: ${response.status} ${response.statusText}${errorDetails}`,
+ );
}
const data = await response.json();
@@ -122,40 +110,3 @@ export async function callOllamaExplicit(
) {
return callOllamaBase(prompt, model, retries, stepName, apiType);
}
-
-// Helper function to list available models from Open WebUI
-export async function listOpenWebUIModels() {
- if (!OLLAMA_API_URL || !OLLAMA_API_URL.includes("/api/chat/completions")) {
- console.log("Not using Open WebUI API, skipping model list");
- return null;
- }
-
- try {
- const headers = { "Content-Type": "application/json" };
- if (OLLAMA_API_KEY) {
- headers["Authorization"] = `Bearer ${OLLAMA_API_KEY}`;
- }
-
- // Try to get models from Open WebUI's models endpoint
- // Open WebUI might have a /api/v1/models endpoint
- const baseUrl = OLLAMA_API_URL.replace("/api/chat/completions", "");
- const modelsUrl = `${baseUrl}/api/v1/models`;
-
- const response = await fetch(modelsUrl, {
- method: "GET",
- headers,
- });
-
- if (response.ok) {
- const data = await response.json();
- console.log("Available models from Open WebUI:", JSON.stringify(data, null, 2));
- return data;
- } else {
- console.log(`Could not list models: ${response.status} ${response.statusText}`);
- return null;
- }
- } catch (err) {
- console.log(`Error listing models: ${err.message}`);
- return null;
- }
-}