Compare commits

...

2 Commits

Author SHA1 Message Date
af315783e0 cleanup everything lol
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/cron/ci Pipeline was successful
2025-09-01 16:53:37 -04:00
15ce02eec1 revamp release uploads 2025-09-01 15:38:55 -04:00
5 changed files with 113 additions and 88 deletions

View File

@@ -45,16 +45,32 @@ steps:
commands:
- pdf=$(ls *.pdf | head -n1)
- tag=$(date +%F)
# Create the release (ignore error if it already exists)
- |
curl -s -o /dev/null -w "%{http_code}" -X POST \
echo "Creating release for tag $tag..."
create_resp=$(curl -s -w "%{http_code}" -o /tmp/create.json -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$tag\", \"name\": \"$tag\", \"draft\": false, \"prerelease\": false}" \
https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases || true
# Upload the PDF as a release asset
- |
curl -X POST \
-d "{\"tag_name\":\"$tag\",\"name\":\"$tag\"}" \
https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases)
echo "Create release HTTP status: $create_resp"
echo "Fetching release ID..."
release_id=$(curl -s \
-H "Authorization: token $GITEA_TOKEN" \
https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases/tags/$tag |
awk -F: '/"id"[ ]*:/ {gsub(/[^0-9]/,"",$2); print $2; exit}')
echo "Release ID = $release_id"
echo "Checking if asset $pdf already exists..."
assets=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases/$release_id/assets)
echo "Assets response: $assets"
if echo "$assets" | grep -q "\"name\":\"$pdf\""; then
echo "Asset $pdf already uploaded, skipping."
exit 0
fi
echo "Uploading $pdf to release $release_id..."
upload_resp=$(curl -s -w "%{http_code}" -o /tmp/upload.json -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$pdf" \
"https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases/$tag/assets"
https://git.keligrubb.com/api/v1/repos/keligrubb/scrollsmith/releases/$release_id/assets)
echo "Upload HTTP status: $upload_resp"
echo "Upload response: $(cat /tmp/upload.json)"

View File

@@ -63,7 +63,7 @@ Optional: update the map path in `index.js` if you have a local dungeon map.
* Three-column layout:
* Column 1: Map, Adventure Hooks, Rumors
* Column 2: Keyed Rooms
* Column 2: Rooms
* Column 3: Encounters, Treasure, NPCs
---

View File

@@ -62,15 +62,15 @@ async function callOllama(prompt, model = "gemma3n:e4b", retries = 5, stepName =
function parseList(raw) {
return raw
.split(/\n|(?=\d+[).]\s)/g)
.map(line => cleanText(line.replace(/^\d+[).\s-]*/, "")))
.split(/\n?\d+[).]\s+/)
.map(line => cleanText(line))
.filter(Boolean);
}
function parseObjects(raw, type = "rooms") {
return raw
.split("\n")
.map(line => cleanText(line.replace(/^\d+[).\s-]*/, "")))
.split(/\n?\d+[).]\s+/)
.map(entry => cleanText(entry))
.filter(Boolean)
.map(entry => {
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
- 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"
);
const titles10 = parseList(titles10Raw, 30);
@@ -108,7 +108,7 @@ Avoid repeating materials or adjectives. Do not include explanations, markdown,
`Here are 10 dungeon titles:
${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.`,
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 ---
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:
${titles5.join("\n")}`,
@@ -129,7 +129,7 @@ ${titles5.join("\n")}`,
// --- Step 4: Flavor text ---
const flavorRaw = await callOllama(
`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"
);
const flavor = flavorRaw;
@@ -141,40 +141,23 @@ Do not include hooks, NPCs, treasure, or instructions. Output plain text only, o
${flavor}
Generate 3 adventure hooks (one sentence each) and 3 rumors (one sentence each).
Output numbered lists only, plain text. Maximum 120 characters per item. No explanations or extra text.
Format as:
Hooks:
1. ...
2. ...
3. ...
Rumors:
1. ...
2. ...
3. ...`
,
Generate 3 short adventure hooks or rumors (mix them naturally).
Output as a single numbered list, plain text only.
Maximum 2 sentences per item. No explanations or extra text.`,
undefined, 5, "Step 5: Hooks & Rumors"
);
const [hooksSection, rumorsSection] = hooksRumorsRaw.split(/Rumors[:\n]/i);
const hooks = parseList(hooksSection.replace(/Hooks[:\n]*/i, ""), 120);
const rumors = parseList(rumorsSection || "", 120);
console.log("🔹 Hooks:", hooks);
console.log("🔹 Rumors:", rumors);
const hooksRumors = parseList(hooksRumorsRaw, 120);
console.log("🔹 Hooks & Rumors:", hooksRumors);
// --- Step 6: Rooms & Encounters ---
const roomsEncountersRaw = await callOllama(
`Using the flavor, hooks, and rumors:
`Using the flavor and these hooks/rumors:
Flavor:
${flavor}
Hooks:
${hooks.join("\n")}
Rumors:
${rumors.join("\n")}
Hooks & Rumors:
${hooksRumors.join("\n")}
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.`,
@@ -197,11 +180,33 @@ Output numbered lists labeled "Treasure:" and "NPCs:". Plain text only, no extra
undefined, 5, "Step 7: Treasure & NPCs"
);
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);
console.log("🔹 Treasure:", treasure);
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!");
return { title, flavor, map: "map.png", hooks, rumors, rooms, encounters, treasure, npcs };
return { title, flavor, map: "map.png", hooksRumors, rooms, encounters, treasure, npcs, plotResolutions };
}

View File

@@ -4,36 +4,31 @@ function pickRandom(arr) {
export function dungeonTemplate(data) {
const bodyFonts = [
"'Libre Baskerville', serif",
"'Cardo', serif",
"'Lora', serif",
"'Merriweather', serif",
"'Fraunces', serif",
"'Source Serif 4', serif",
"'Lora', serif"
"'Libre Baskerville', serif",
"'Source Serif 4', serif"
];
const headingFonts = [
"'Cinzel Decorative', cursive",
"'MedievalSharp', cursive",
"'Metamorphous', cursive",
"'Playfair Display', serif",
"'Alegreya Sans SC', sans-serif"
"'Playfair Display', serif"
];
const tableFonts = [
"'Alegreya Sans', sans-serif",
"'Cabin', sans-serif",
"'IBM Plex Sans', sans-serif",
"'Cormorant Garamond', serif",
"'Special Elite', monospace"
"'Cormorant Garamond', serif"
];
const quoteFonts = [
"'Walter Turncoat', cursive",
"'Uncial Antiqua', serif",
"'Beth Ellen', cursive",
"'Pinyon Script', cursive",
"'Dela Gothic One', sans-serif"
"'Pinyon Script', cursive"
];
const bodyFont = pickRandom(bodyFonts);
@@ -47,62 +42,69 @@ export function dungeonTemplate(data) {
<head>
<meta charset="UTF-8">
<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>
@page { size: A4 landscape; margin: 0; }
body {
margin: 0; padding: 1cm;
background: #d6c5a3;
margin: 0; padding: 1.5cm;
background: #f5f5f5;
font-family: ${bodyFont};
color: #2b2118;
font-size: 0.8em;
line-height: 1.3em;
color: #1a1a1a;
font-size: 0.7em;
line-height: 1.25em;
}
h1 {
font-family: ${headingFont};
text-align: center;
font-size: 2em;
margin: 0.2em 0 0.3em;
color: #3e1f0e;
border-bottom: 2px solid #3e1f0e;
color: #1a1a1a;
border-bottom: 2px solid #1a1a1a;
padding-bottom: 0.2em;
letter-spacing: 0.1em;
}
.flavor {
text-align: center;
font-style: italic;
font-family: ${quoteFont};
margin: 0.4em 0 1em;
font-size: 0.95em;
margin: 0.4em 0 0.8em;
font-size: 0.9em;
}
.columns {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.8cm;
gap: 0.5cm;
align-items: start;
}
.col {
display: flex; flex-direction: column;
gap: 0.4em;
gap: 0.15em;
}
h2 {
font-family: ${headingFont};
font-size: 1em;
font-size: 1.0em;
margin: 0.3em 0 0.1em;
color: #3e1f0e;
border-bottom: 1px solid #3e1f0e;
color: #1a1a1a;
border-bottom: 1px solid #1a1a1a;
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 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; }
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 { background: #d9c6a5; }
table tr:hover { background: rgba(62,31,14,0.05); }
th, td { border: 1px solid #1a1a1a; padding: 0.2em; text-align: left; vertical-align: top; }
th { background: #e0e0e0; }
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 {
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>
</head>
@@ -112,18 +114,10 @@ export function dungeonTemplate(data) {
<div class="columns">
<div class="col">
<h2>Map</h2>
<div class="map"><img src="file://${data.map}" alt="Dungeon Map"></div>
<h2>Adventure Hooks & Rumors</h2>
<ul>${data.hooksRumors.map(item => `<li>${item}</li>`).join("")}</ul>
<h2>Adventure Hooks</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>
<h2>Locations</h2>
${data.rooms.map((room, i) => `<div class="room"><h3>${i + 1}. ${room.name}</h3><p>${room.description}</p></div>`).join("")}
</div>
@@ -135,12 +129,22 @@ export function dungeonTemplate(data) {
<h2>Treasure</h2>
<ul>${data.treasure.map(t => `<li>${t}</li>`).join("")}</ul>
</div>
<div class="col">
<h2>NPCs</h2>
<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 class="map-page">
<h2>Dungeon Map</h2>
<img src="file://${data.map}" alt="Dungeon Map">
</div>
<footer>Generated with Scrollsmith • © ${new Date().getFullYear()}</footer>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"name": "auto-dm",
"name": "scrollsmith",
"version": "1.0.0",
"main": "index.js",
"type": "module",