cleanup everything lol
This commit is contained in:
@@ -63,7 +63,7 @@ Optional: update the map path in `index.js` if you have a local dungeon map.
|
|||||||
* Three-column layout:
|
* Three-column layout:
|
||||||
|
|
||||||
* Column 1: Map, Adventure Hooks, Rumors
|
* Column 1: Map, Adventure Hooks, Rumors
|
||||||
* Column 2: Keyed Rooms
|
* Column 2: Rooms
|
||||||
* Column 3: Encounters, Treasure, NPCs
|
* Column 3: Encounters, Treasure, NPCs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -62,15 +62,15 @@ async function callOllama(prompt, model = "gemma3n:e4b", retries = 5, stepName =
|
|||||||
|
|
||||||
function parseList(raw) {
|
function parseList(raw) {
|
||||||
return raw
|
return raw
|
||||||
.split(/\n|(?=\d+[).]\s)/g)
|
.split(/\n?\d+[).]\s+/)
|
||||||
.map(line => cleanText(line.replace(/^\d+[).\s-]*/, "")))
|
.map(line => cleanText(line))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseObjects(raw, type = "rooms") {
|
function parseObjects(raw, type = "rooms") {
|
||||||
return raw
|
return raw
|
||||||
.split("\n")
|
.split(/\n?\d+[).]\s+/)
|
||||||
.map(line => cleanText(line.replace(/^\d+[).\s-]*/, "")))
|
.map(entry => cleanText(entry))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(entry => {
|
.map(entry => {
|
||||||
const [name, ...descParts] = entry.split(/[-–—:]/);
|
const [name, ...descParts] = entry.split(/[-–—:]/);
|
||||||
@@ -97,7 +97,7 @@ Each title should come from a different style or theme. Make the set varied and
|
|||||||
- Weird fantasy: uncanny, surreal, unsettling
|
- Weird fantasy: uncanny, surreal, unsettling
|
||||||
- Whimsical: fun, quirky, playful
|
- Whimsical: fun, quirky, playful
|
||||||
|
|
||||||
Avoid repeating materials or adjectives. Do not include explanations, markdown, or preambles. Only the 10 numbered titles.`,
|
Avoid repeating materials or adjectives. Avoid the words "obsidian" and "clockwork". Do not include explanations, markdown, or preambles. Only the 10 numbered titles.`,
|
||||||
undefined, 5, "Step 1: Titles"
|
undefined, 5, "Step 1: Titles"
|
||||||
);
|
);
|
||||||
const titles10 = parseList(titles10Raw, 30);
|
const titles10 = parseList(titles10Raw, 30);
|
||||||
@@ -108,7 +108,7 @@ Avoid repeating materials or adjectives. Do not include explanations, markdown,
|
|||||||
`Here are 10 dungeon titles:
|
`Here are 10 dungeon titles:
|
||||||
${titles10.join("\n")}
|
${titles10.join("\n")}
|
||||||
|
|
||||||
Select the 3 most interesting titles from the above list and create 2 additional unique titles.
|
Randomly select 3 of the titles from the above list and create 2 additional unique titles. Avoid the words "obsidian" and "clockwork".
|
||||||
Output exactly 5 titles as a numbered list, plain text only. No explanations.`,
|
Output exactly 5 titles as a numbered list, plain text only. No explanations.`,
|
||||||
undefined, 5, "Step 2: Narrow Titles"
|
undefined, 5, "Step 2: Narrow Titles"
|
||||||
);
|
);
|
||||||
@@ -117,7 +117,7 @@ Output exactly 5 titles as a numbered list, plain text only. No explanations.`,
|
|||||||
|
|
||||||
// --- Step 3: Final title ---
|
// --- Step 3: Final title ---
|
||||||
const bestTitleRaw = await callOllama(
|
const bestTitleRaw = await callOllama(
|
||||||
`From the following 5 dungeon titles, select the one that sounds the most fun to play.
|
`From the following 5 dungeon titles, randomly select only one of them.
|
||||||
Output only the title, no explanation, no numbering, no extra text:
|
Output only the title, no explanation, no numbering, no extra text:
|
||||||
|
|
||||||
${titles5.join("\n")}`,
|
${titles5.join("\n")}`,
|
||||||
@@ -129,7 +129,7 @@ ${titles5.join("\n")}`,
|
|||||||
// --- Step 4: Flavor text ---
|
// --- Step 4: Flavor text ---
|
||||||
const flavorRaw = await callOllama(
|
const flavorRaw = await callOllama(
|
||||||
`Write a single evocative paragraph describing the dungeon titled "${title}".
|
`Write a single evocative paragraph describing the dungeon titled "${title}".
|
||||||
Do not include hooks, NPCs, treasure, or instructions. Output plain text only, one paragraph.`,
|
Do not include hooks, NPCs, treasure, or instructions. Output plain text only, one paragraph. Maximum 4 sentences.`,
|
||||||
undefined, 5, "Step 4: Flavor"
|
undefined, 5, "Step 4: Flavor"
|
||||||
);
|
);
|
||||||
const flavor = flavorRaw;
|
const flavor = flavorRaw;
|
||||||
@@ -141,40 +141,23 @@ Do not include hooks, NPCs, treasure, or instructions. Output plain text only, o
|
|||||||
|
|
||||||
${flavor}
|
${flavor}
|
||||||
|
|
||||||
Generate 3 adventure hooks (one sentence each) and 3 rumors (one sentence each).
|
Generate 3 short adventure hooks or rumors (mix them naturally).
|
||||||
Output numbered lists only, plain text. Maximum 120 characters per item. No explanations or extra text.
|
Output as a single numbered list, plain text only.
|
||||||
Format as:
|
Maximum 2 sentences per item. No explanations or extra text.`,
|
||||||
|
|
||||||
Hooks:
|
|
||||||
1. ...
|
|
||||||
2. ...
|
|
||||||
3. ...
|
|
||||||
|
|
||||||
Rumors:
|
|
||||||
1. ...
|
|
||||||
2. ...
|
|
||||||
3. ...`
|
|
||||||
,
|
|
||||||
undefined, 5, "Step 5: Hooks & Rumors"
|
undefined, 5, "Step 5: Hooks & Rumors"
|
||||||
);
|
);
|
||||||
const [hooksSection, rumorsSection] = hooksRumorsRaw.split(/Rumors[:\n]/i);
|
const hooksRumors = parseList(hooksRumorsRaw, 120);
|
||||||
const hooks = parseList(hooksSection.replace(/Hooks[:\n]*/i, ""), 120);
|
console.log("🔹 Hooks & Rumors:", hooksRumors);
|
||||||
const rumors = parseList(rumorsSection || "", 120);
|
|
||||||
console.log("🔹 Hooks:", hooks);
|
|
||||||
console.log("🔹 Rumors:", rumors);
|
|
||||||
|
|
||||||
// --- Step 6: Rooms & Encounters ---
|
// --- Step 6: Rooms & Encounters ---
|
||||||
const roomsEncountersRaw = await callOllama(
|
const roomsEncountersRaw = await callOllama(
|
||||||
`Using the flavor, hooks, and rumors:
|
`Using the flavor and these hooks/rumors:
|
||||||
|
|
||||||
Flavor:
|
Flavor:
|
||||||
${flavor}
|
${flavor}
|
||||||
|
|
||||||
Hooks:
|
Hooks & Rumors:
|
||||||
${hooks.join("\n")}
|
${hooksRumors.join("\n")}
|
||||||
|
|
||||||
Rumors:
|
|
||||||
${rumors.join("\n")}
|
|
||||||
|
|
||||||
Generate 5 rooms (name + short description) and 3 encounters (name + details).
|
Generate 5 rooms (name + short description) and 3 encounters (name + details).
|
||||||
Output two numbered lists, labeled "Rooms:" and "Encounters:". Plain text only. No extra explanation.`,
|
Output two numbered lists, labeled "Rooms:" and "Encounters:". Plain text only. No extra explanation.`,
|
||||||
@@ -197,11 +180,33 @@ Output numbered lists labeled "Treasure:" and "NPCs:". Plain text only, no extra
|
|||||||
undefined, 5, "Step 7: Treasure & NPCs"
|
undefined, 5, "Step 7: Treasure & NPCs"
|
||||||
);
|
);
|
||||||
const [treasureSection, npcsSection] = treasureNpcsRaw.split(/NPCs[:\n]/i);
|
const [treasureSection, npcsSection] = treasureNpcsRaw.split(/NPCs[:\n]/i);
|
||||||
const treasure = parseList(treasureSection.replace(/Treasure[:\n]*/i, ""), 120);
|
const treasure = parseList(treasureSection.replace(/Treasures?[:\n]*/i, ""), 120);
|
||||||
const npcs = parseObjects(npcsSection || "", "npcs", 120);
|
const npcs = parseObjects(npcsSection || "", "npcs", 120);
|
||||||
console.log("🔹 Treasure:", treasure);
|
console.log("🔹 Treasure:", treasure);
|
||||||
console.log("🔹 NPCs:", npcs);
|
console.log("🔹 NPCs:", npcs);
|
||||||
|
|
||||||
|
// --- Step 8: Plot Resolutions ---
|
||||||
|
const plotResolutionsRaw = await callOllama(
|
||||||
|
`Based on the following dungeon flavor and story hooks:
|
||||||
|
|
||||||
|
Flavor:
|
||||||
|
${flavor}
|
||||||
|
|
||||||
|
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 dungeon.
|
||||||
|
These are prompts and ideas for brainstorming the dungeon's ending, not fixed outcomes.
|
||||||
|
Start each item with phrases like "The adventurers could..." or "The PCs might..." to emphasize their hypothetical nature.
|
||||||
|
Keep each item short (max 2 sentences). Output as a numbered list, plain text only.`,
|
||||||
|
undefined, 5, "Step 8: Plot Resolutions"
|
||||||
|
);
|
||||||
|
const plotResolutions = parseList(plotResolutionsRaw, 180);
|
||||||
|
console.log("🔹 Plot Resolutions:", plotResolutions);
|
||||||
|
|
||||||
console.log("\n🎉 Dungeon generation complete!");
|
console.log("\n🎉 Dungeon generation complete!");
|
||||||
return { title, flavor, map: "map.png", hooks, rumors, rooms, encounters, treasure, npcs };
|
return { title, flavor, map: "map.png", hooksRumors, rooms, encounters, treasure, npcs, plotResolutions };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,31 @@ function pickRandom(arr) {
|
|||||||
|
|
||||||
export function dungeonTemplate(data) {
|
export function dungeonTemplate(data) {
|
||||||
const bodyFonts = [
|
const bodyFonts = [
|
||||||
"'Libre Baskerville', serif",
|
"'Lora', serif",
|
||||||
"'Cardo', serif",
|
|
||||||
"'Merriweather', serif",
|
"'Merriweather', serif",
|
||||||
"'Fraunces', serif",
|
"'Libre Baskerville', serif",
|
||||||
"'Source Serif 4', serif",
|
"'Source Serif 4', serif"
|
||||||
"'Lora', serif"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const headingFonts = [
|
const headingFonts = [
|
||||||
"'Cinzel Decorative', cursive",
|
"'Cinzel Decorative', cursive",
|
||||||
"'MedievalSharp', cursive",
|
"'MedievalSharp', cursive",
|
||||||
"'Metamorphous', cursive",
|
"'Metamorphous', cursive",
|
||||||
"'Playfair Display', serif",
|
"'Playfair Display', serif"
|
||||||
"'Alegreya Sans SC', sans-serif"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableFonts = [
|
const tableFonts = [
|
||||||
"'Alegreya Sans', sans-serif",
|
"'Alegreya Sans', sans-serif",
|
||||||
"'Cabin', sans-serif",
|
"'Cabin', sans-serif",
|
||||||
"'IBM Plex Sans', sans-serif",
|
"'IBM Plex Sans', sans-serif",
|
||||||
"'Cormorant Garamond', serif",
|
"'Cormorant Garamond', serif"
|
||||||
"'Special Elite', monospace"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const quoteFonts = [
|
const quoteFonts = [
|
||||||
"'Walter Turncoat', cursive",
|
"'Walter Turncoat', cursive",
|
||||||
"'Uncial Antiqua', serif",
|
"'Uncial Antiqua', serif",
|
||||||
"'Beth Ellen', cursive",
|
"'Beth Ellen', cursive",
|
||||||
"'Pinyon Script', cursive",
|
"'Pinyon Script', cursive"
|
||||||
"'Dela Gothic One', sans-serif"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const bodyFont = pickRandom(bodyFonts);
|
const bodyFont = pickRandom(bodyFonts);
|
||||||
@@ -47,62 +42,69 @@ export function dungeonTemplate(data) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>${data.title}</title>
|
<title>${data.title}</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville&family=Lora&family=Cinzel+Decorative&family=MedievalSharp&family=Alegreya+Sans+SC&family=Alegreya+Sans&family=Cabin&family=Walter+Turncoat&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative&family=MedievalSharp&family=Metamorphous&family=Playfair+Display&family=Alegreya+Sans&family=Cabin&family=IBM+Plex+Sans&family=Cormorant+Garamond&family=Lora&family=Merriweather&family=Libre+Baskerville&family=Source+Serif+4&family=Walter+Turncoat&family=Uncial+Antiqua&family=Beth+Ellen&family=Pinyon+Script&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@page { size: A4 landscape; margin: 0; }
|
@page { size: A4 landscape; margin: 0; }
|
||||||
body {
|
body {
|
||||||
margin: 0; padding: 1cm;
|
margin: 0; padding: 1.5cm;
|
||||||
background: #d6c5a3;
|
background: #f5f5f5;
|
||||||
font-family: ${bodyFont};
|
font-family: ${bodyFont};
|
||||||
color: #2b2118;
|
color: #1a1a1a;
|
||||||
font-size: 0.8em;
|
font-size: 0.7em;
|
||||||
line-height: 1.3em;
|
line-height: 1.25em;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-family: ${headingFont};
|
font-family: ${headingFont};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin: 0.2em 0 0.3em;
|
margin: 0.2em 0 0.3em;
|
||||||
color: #3e1f0e;
|
color: #1a1a1a;
|
||||||
border-bottom: 2px solid #3e1f0e;
|
border-bottom: 2px solid #1a1a1a;
|
||||||
padding-bottom: 0.2em;
|
padding-bottom: 0.2em;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
.flavor {
|
.flavor {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-family: ${quoteFont};
|
font-family: ${quoteFont};
|
||||||
margin: 0.4em 0 1em;
|
margin: 0.4em 0 0.8em;
|
||||||
font-size: 0.95em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
.columns {
|
.columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 0.8cm;
|
gap: 0.5cm;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
.col {
|
.col {
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
gap: 0.4em;
|
gap: 0.15em;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-family: ${headingFont};
|
font-family: ${headingFont};
|
||||||
font-size: 1em;
|
font-size: 1.0em;
|
||||||
margin: 0.3em 0 0.1em;
|
margin: 0.3em 0 0.1em;
|
||||||
color: #3e1f0e;
|
color: #1a1a1a;
|
||||||
border-bottom: 1px solid #3e1f0e;
|
border-bottom: 1px solid #1a1a1a;
|
||||||
padding-bottom: 0.1em;
|
padding-bottom: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
.map img { max-width: 100%; border: 2px solid #3e1f0e; border-radius: 0.2cm; }
|
|
||||||
.room h3 { margin: 0.2em 0 0.05em; font-size: 0.95em; font-weight: bold; }
|
.room h3 { margin: 0.2em 0 0.05em; font-size: 0.95em; font-weight: bold; }
|
||||||
.room p { text-align: justify; word-wrap: break-word; margin: 0.1em 0 0.3em; }
|
.room p { text-align: justify; word-wrap: break-word; margin: 0.1em 0 0.3em; }
|
||||||
ul { padding-left: 1em; margin: 0.1em 0 0.4em; }
|
ul { padding-left: 1em; margin: 0.1em 0 0.3em; }
|
||||||
li { margin-bottom: 0.2em; }
|
li { margin-bottom: 0.2em; }
|
||||||
table { width: 100%; border-collapse: collapse; font-family: ${tableFont}; font-size: 0.8em; }
|
table { width: 100%; border-collapse: collapse; font-family: ${tableFont}; font-size: 0.8em; }
|
||||||
th, td { border: 1px solid #3e1f0e; padding: 0.2em; text-align: left; vertical-align: top; }
|
th, td { border: 1px solid #1a1a1a; padding: 0.2em; text-align: left; vertical-align: top; }
|
||||||
th { background: #d9c6a5; }
|
th { background: #e0e0e0; }
|
||||||
table tr:hover { background: rgba(62,31,14,0.05); }
|
table tr:hover { background: rgba(0, 0, 0, 0.05); }
|
||||||
|
.map-page {
|
||||||
|
page-break-before: always;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.map-page img { max-width: 100%; max-height: 27cm; border: 2px solid #1a1a1a; border-radius: 0.2cm; }
|
||||||
footer {
|
footer {
|
||||||
text-align: center; font-size: 0.7em; color: #5a4632; margin-top: 0.5em; font-style: italic;
|
text-align: center; font-size: 0.65em; color: #555; margin-top: 0.5em; font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -112,18 +114,10 @@ export function dungeonTemplate(data) {
|
|||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>Map</h2>
|
<h2>Adventure Hooks & Rumors</h2>
|
||||||
<div class="map"><img src="file://${data.map}" alt="Dungeon Map"></div>
|
<ul>${data.hooksRumors.map(item => `<li>${item}</li>`).join("")}</ul>
|
||||||
|
|
||||||
<h2>Adventure Hooks</h2>
|
<h2>Locations</h2>
|
||||||
<ul>${data.hooks.map(h => `<li>${h}</li>`).join("")}</ul>
|
|
||||||
|
|
||||||
<h2>Rumors</h2>
|
|
||||||
<ul>${data.rumors.map(r => `<li>${r}</li>`).join("")}</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
<h2>Keyed Rooms</h2>
|
|
||||||
${data.rooms.map((room, i) => `<div class="room"><h3>${i + 1}. ${room.name}</h3><p>${room.description}</p></div>`).join("")}
|
${data.rooms.map((room, i) => `<div class="room"><h3>${i + 1}. ${room.name}</h3><p>${room.description}</p></div>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,12 +129,22 @@ export function dungeonTemplate(data) {
|
|||||||
|
|
||||||
<h2>Treasure</h2>
|
<h2>Treasure</h2>
|
||||||
<ul>${data.treasure.map(t => `<li>${t}</li>`).join("")}</ul>
|
<ul>${data.treasure.map(t => `<li>${t}</li>`).join("")}</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
<h2>NPCs</h2>
|
<h2>NPCs</h2>
|
||||||
<ul>${data.npcs.map(n => `<li><b>${n.name}</b>: ${n.trait}</li>`).join("")}</ul>
|
<ul>${data.npcs.map(n => `<li><b>${n.name}</b>: ${n.trait}</li>`).join("")}</ul>
|
||||||
|
|
||||||
|
<h2>Plot Resolutions</h2>
|
||||||
|
<ul>${data.plotResolutions.map(p => `<li>${p}</li>`).join("")}</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="map-page">
|
||||||
|
<h2>Dungeon Map</h2>
|
||||||
|
<img src="file://${data.map}" alt="Dungeon Map">
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>Generated with Scrollsmith • © ${new Date().getFullYear()}</footer>
|
<footer>Generated with Scrollsmith • © ${new Date().getFullYear()}</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "auto-dm",
|
"name": "scrollsmith",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Reference in New Issue
Block a user