From 277a3ba7182632be3a401d2cfb96c25f9e3c3cb0 Mon Sep 17 00:00:00 2001 From: keligrubb Date: Mon, 8 Sep 2025 22:42:42 -0400 Subject: [PATCH] improve overall dungeon cohesiveness --- dungeonGenerator.js | 142 ++++++++++++++++++++++---------------------- imageGenerator.js | 9 +-- 2 files changed, 77 insertions(+), 74 deletions(-) diff --git a/dungeonGenerator.js b/dungeonGenerator.js index c898e13..88d3f47 100644 --- a/dungeonGenerator.js +++ b/dungeonGenerator.js @@ -3,10 +3,10 @@ import { callOllama } from "./ollamaClient.js"; // Utility: strip markdown artifacts function cleanText(str) { return str - .replace(/^#+\s*/gm, "") // remove headers - .replace(/\*\*(.*?)\*\*/g, "$1") // remove bold - .replace(/[*_`]/g, "") // remove stray formatting - .replace(/\s+/g, " ") // normalize whitespace + .replace(/^#+\s*/gm, "") + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/[*_`]/g, "") + .replace(/\s+/g, " ") .trim(); } @@ -53,85 +53,87 @@ Avoid repeating materials or adjectives. Absolutely do not use the words "Obsidi const title = generatedTitles[Math.floor(Math.random() * generatedTitles.length)]; console.log("Selected title:", title); - // Step 2: Flavor text - const flavorRaw = await callOllama( - `Write a single evocative paragraph describing the location titled "${title}". Absolutely do not use the words "Obsidian" or "Clockwork" anywhere in the paragraph. -Do not include hooks, NPCs, treasure, or instructions. Do not use bullet points or em-dashes. Output plain text only, one paragraph. Maximum 4 sentences.`, - undefined, 5, "Step 2: Flavor" + // Step 2: Core Concepts + const coreConceptsRaw = await callOllama( + `For a dungeon titled "${title}", generate three core concepts: a central conflict, a primary faction, and a major environmental hazard or dynamic element. +Output as a numbered list with bolded headings. Plain text only. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output. +Example: +1. **Central Conflict:** The dungeon's power source is failing, causing reality to warp. +2. **Primary Faction:** A group of rival cultists trying to seize control of the power source. +3. **Dynamic Element:** Zones of temporal distortion that cause random, brief time shifts.`, + undefined, 5, "Step 2: Core Concepts" ); - const flavor = flavorRaw; - console.log("Flavor text:", flavor); + const coreConcepts = coreConceptsRaw; + console.log("Core Concepts:", coreConcepts); - // Step 3: Hooks & Rumors - const hooksRumorsRaw = await callOllama( - `Based only on this location's flavor: - -${flavor} - -Generate 3 short adventure hooks or rumors (mix them naturally). -Output as a single numbered list, plain text only. Do not use em-dashes. -Maximum 2 sentences per item. No explanations or extra text.`, - undefined, 5, "Step 3: Hooks & Rumors" + // Step 3: Flavor Text & Hooks + const flavorHooksRaw = await callOllama( + `Based on the title "${title}" and these core concepts: +${coreConcepts} +Write a single evocative paragraph describing the location. Maximum 2 sentences. Maximum 100 words. Then, generate 3 short adventure hooks or rumors. +The hooks should reference the central conflict, faction, and dynamic element. +Output two sections labeled "Description:" and "Hooks & Rumors:". Use a numbered list for the hooks. Plain text only. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`, + undefined, 5, "Step 3: Flavor & Hooks" ); - const hooksRumors = parseList(hooksRumorsRaw); + const [flavorSection, hooksSection] = flavorHooksRaw.split(/Hooks & Rumors[:\n]/i); + const flavor = cleanText(flavorSection.replace(/Description[:\n]*/i, "")); + const hooksRumors = parseList(hooksSection || ""); + console.log("Flavor Text:", flavor); console.log("Hooks & Rumors:", hooksRumors); - // Step 4: Rooms & Encounters - const roomsEncountersRaw = await callOllama( - `Using the flavor and these hooks/rumors: - -Flavor: -${flavor} - -Hooks & Rumors: -${hooksRumors.join("\n")} - -Generate 5 rooms (name + short description) and 6 encounters (name + details). -Output two numbered lists, labeled "Rooms:" and "Encounters:". Plain text only. No extra explanation.`, - undefined, 5, "Step 4: Rooms & Encounters" + // Step 4: Key Rooms + const keyRoomsRaw = await callOllama( + `Based on the title "${title}", description "${flavor}", and these core concepts: +${coreConcepts} +Generate two key rooms that define the dungeon's narrative arc. +1. **The Entrance/Start Room:** The first room the adventurers will enter. Give it a name and a description that sets the tone and introduces the environmental hazard. +2. **The Climax/Final Room:** The final room where the central conflict will be resolved. Give it a name and a description that includes the primary faction and the central conflict. +Output two numbered lists, labeled "Entrance Room:" and "Climax Room:". Plain text only. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`, + undefined, 5, "Step 4: Key Rooms" ); - const [roomsSection, encountersSection] = roomsEncountersRaw.split(/Encounters[:\n]/i); - const rooms = parseObjects(roomsSection.replace(/Rooms[:\n]*/i, ""), "rooms", 120); - const encounters = parseObjects(encountersSection || "", "encounters", 120); + const [entranceSection, climaxSection] = keyRoomsRaw.split(/Climax Room[:\n]/i); + const entranceRoom = parseObjects(entranceSection.replace(/Entrance Room[:\n]*/i, ""), "rooms")[0]; + const climaxRoom = parseObjects(climaxSection || "", "rooms")[0]; + console.log("Entrance Room:", entranceRoom); + console.log("Climax Room:", climaxRoom); + + // Step 5: Main Content (Rooms, Encounters, NPCs, Treasures) + const mainContentRaw = await callOllama( + `Based on the following dungeon elements and the need for narrative flow: +Title: "${title}" +Description: "${flavor}" +Core Concepts: +${coreConcepts} +Entrance Room: ${JSON.stringify(entranceRoom)} +Climax Room: ${JSON.stringify(climaxRoom)} + +Generate the rest of the dungeon's content to fill the space between the entrance and the climax. +- **3 Intermediate Rooms:** Name and a description. Each description must be a maximum of 50 words and contain an environmental feature, a puzzle, or an element that connects to the core concepts or the final room. +- **4 Encounters:** Name and details. At least two encounters must be directly tied to the primary faction. +- **3 NPCs:** Proper name and a trait. One NPC should be a member of the primary faction, one should be a potential ally, and one should be a rival. +- **3 Treasures:** Name and a description that includes a danger or side-effect. Each treasure should be thematically tied to a specific encounter or room. +Output as four separate numbered lists, labeled "Intermediate Rooms:", "Encounters:", "NPCs:", and "Treasures:". Plain text only. Absolutely do not use the word "Obsidian" or "obsidian" anywhere in the output.`, + undefined, 5, "Step 5: Main Content" + ); + const [intermediateRoomsSection, encountersSection, npcsSection, treasureSection] = mainContentRaw.split(/Encounters[:\n]|NPCs[:\n]|Treasures?[:\n]/i); + const intermediateRooms = parseObjects(intermediateRoomsSection.replace(/Intermediate Rooms[:\n]*/i, ""), "rooms"); + const rooms = [entranceRoom, ...intermediateRooms, climaxRoom]; + const encounters = parseObjects(encountersSection || "", "encounters"); + const npcs = parseObjects(npcsSection || "", "npcs"); + const treasure = parseList(treasureSection || ""); console.log("Rooms:", rooms); console.log("Encounters:", encounters); - - // Step 5: Treasure & NPCs - const treasureNpcsRaw = await callOllama( - `Based only on these rooms and encounters: - -${JSON.stringify({ rooms, encounters }, null, 2)} - -Generate 3 treasures and 3 NPCs (name + trait, max 2 sentences each). -Each NPC has a proper name, not just a title. -Treasure should sometimes include a danger or side-effect. -Output numbered lists labeled "Treasure:" and "NPCs:". Plain text only, no extra text.`, - undefined, 5, "Step 5: Treasure & NPCs" - ); - const [treasureSection, npcsSection] = treasureNpcsRaw.split(/NPCs[:\n]/i); - const treasure = parseList(treasureSection.replace(/Treasures?[:\n]*/i, "")); - const npcs = parseObjects(npcsSection || "", "npcs", 120); - console.log("Treasure:", treasure); console.log("NPCs:", npcs); + console.log("Treasure:", treasure); - // Step 6: Plot Resolutions + // Step 6: Player Choices and Consequences const plotResolutionsRaw = await callOllama( - `Based on the following location's flavor and story hooks: + `Based on all of the following elements, suggest 3 possible, non-conflicting story climaxes or plot resolutions for adventurers exploring this location. Each resolution must provide a meaningful choice with a tangible consequence, directly related to the Central Conflict, the Primary Faction, or the NPCs. -Flavor: -${flavor} +Dungeon Elements: +${JSON.stringify({ title, flavor, hooksRumors, rooms, encounters, treasure, npcs, coreConcepts }, null, 2)} -Hooks & Rumors: -${hooksRumors.join("\n")} - -Major NPCs / Encounters: -${[...npcs.map(n => n.name), ...encounters.map(e => e.name)].join(", ")} - -Suggest 3 possible, non-conflicting story climaxes or plot resolutions for adventurers exploring this location. -These are prompts and ideas for brainstorming the story's ending, not fixed outcomes. Give the players meaningful choices and agency. -Start each item with phrases like "The adventurers could" or "The PCs might" to emphasize their hypothetical nature. -Deepen the narrative texture and allow roleplay and tactical creativity. -Keep each item short (max 2 sentences). Output as a numbered list, plain text only.`, +Start each item with phrases like "The adventurers could" or "The PCs might". Deepen the narrative texture and allow for roleplay and tactical creativity. Keep each item short (max 2 sentences). Output as a numbered list, plain text only. 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/imageGenerator.js b/imageGenerator.js index ed36991..4fc711a 100644 --- a/imageGenerator.js +++ b/imageGenerator.js @@ -14,7 +14,6 @@ async function upscaleImage(inputPath, outputPath, width, height) { try { await sharp(inputPath) .resize(width, height, { kernel: 'lanczos3' }) - .blur(0.3) .sharpen() .png({ compressionLevel: 9, @@ -38,13 +37,15 @@ async function generateVisualPrompt(flavor) { Your output must be a simple list of visual tags describing only the most essential elements of the scene. Focus on the core subject and mood. Rules: -- Describe a sparse scene with a single focal point or area. +- Describe a sparse scene with a single focal point or landscape. - Use only 3-5 key descriptive phrases or tags. - The entire output should be very short, 20-50 words maximum. - Do NOT repeat wording from the input. -- Focus only on visual content, not style, medium, or camera effects. +- Describe only the visual elements of the image. Focus on colors, shapes, textures, and spatial relationships. +- Exclude any references to style, medium, camera effects, sounds, hypothetical scenarios, or physical sensations. - Avoid describing fine details; focus on large forms and the overall impression. - Do NOT include phrases like “an image of” or “a scene showing”. +- Do NOT include the word "Obsidian" or "obsidian" at all. Input: ${flavor} @@ -177,7 +178,7 @@ async function downloadImage(filename, localFilename) { // 4c. Submit prompt and handle full image pipeline async function generateImageViaComfyUI(prompt, filename) { - const negativePrompt = `heavy shading, deep blacks, cross-hatching, dark, gritty, shadow-filled, chiaroscuro, scratchy lines, photorealism, hyper-realistic, high detail, 3D render, CGI, polished, smooth shading, detailed textures, noisy, cluttered, blurry, text, logo, signature, watermark, artist name, branding, ugly, deformed, unnatural patterns, perfect curves, repetitive textures`; + const negativePrompt = `heavy shading, deep blacks, dark, gritty, shadow-filled, chiaroscuro, scratchy lines, photorealism, hyper-realistic, high detail, 3D render, CGI, polished, smooth shading, detailed textures, noisy, cluttered, blurry, text, logo, signature, watermark, artist name, branding, ugly, deformed, unnatural patterns, perfect curves, repetitive textures`; const workflow = buildComfyWorkflow(prompt, negativePrompt); try {