improve overall dungeon cohesiveness
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user