From df01615c5a6da792d9992e7473edb6a23394bad5 Mon Sep 17 00:00:00 2001 From: slaguru666 Date: Wed, 5 Aug 2026 08:48:17 +0100 Subject: [PATCH] Pivot: DELVE is an authoring system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four readings of generated output reached the same verdict, and the last one answered 'would you run this tomorrow' with No. Three renderer passes each fixed what the previous review named and each introduced a new fault: adding resolution blocks made it menu-like, and subtracting blocks buried the clue and the skip guidance. The density is not a rendering bug. Everything on the page is a vice at the table and a virtue at the desk. So one working file, two surfaces. The worksheet carries everything, labelled, with reroll commands beside each component and explicit prompts where only a human can write. The play sheet carries the GM's own prose, the leverage point, the decision on one line and the numbers — 61 lines against the worksheet's 137. The JSON is the document. An authored layer holds what the GM writes and the generator never touches it, so a reroll cannot destroy prose. Lock what is good, reroll what is not, write the read-aloud, export. Two bugs found while building it, both in reroll: the exclusion set omitted the area's own current value so a reroll could hand back what was already there, and when every option was in use — six areas against a six-item pool — the fallback did the same. A reroll that changes nothing looks broken. DELVE does not write the read-aloud and does not write the climax. It gets a GM to a strong first draft in a minute instead of an evening, then gets out of the way. Tests 28 -> 38. Co-Authored-By: Claude Opus 5 --- DESIGN.md | 31 ++- core/authoring.mjs | 150 +++++++++++ core/cli.mjs | 58 +++- core/render-authoring.mjs | 163 ++++++++++++ core/test.mjs | 43 +++ samples/finished-delve.json | 484 ++++++++++++++++++++++++++++++++++ samples/finished-play.md | 61 +++++ samples/finished-worksheet.md | 137 ++++++++++ 8 files changed, 1112 insertions(+), 15 deletions(-) create mode 100644 core/authoring.mjs create mode 100644 core/render-authoring.mjs create mode 100644 samples/finished-delve.json create mode 100644 samples/finished-play.md create mode 100644 samples/finished-worksheet.md diff --git a/DESIGN.md b/DESIGN.md index a380e6e..a1b92ab 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -38,11 +38,34 @@ GM-readable delve. Samples in [`samples/`](samples/). --- -## 2. What DELVE is +## 2. What DELVE is — an authoring system -The dungeon-scale layer over VANITY's Forge. It does not generate maps, monsters, encounters or -hoards — those exist and work. It generates **the fiction, the order, and the pressure**, and it -sequences the Forge without letting the Forge talk over it. +**This changed in draft 7, after four readings of generated output.** + +DELVE is the dungeon-scale layer over VANITY's Forge — it generates the fiction, the order and +the pressure, and sequences the Forge without letting it talk over DELVE. But it is **not** a tool +that hands a GM a delve to run. Four independent reviews converged: + +> *"The handwritten delve still tells the GM what matters faster."* +> *"Would you run this at a real table tomorrow, as-is?"* — **No.** + +Three renderer passes each fixed what the last review named and each introduced a new problem +(blocks → menu-like; subtraction → buried). The density is not a bug to render away: **everything +on the page is a vice at the table and a virtue at the desk.** + +So a delve has two surfaces and one working file: + +| | For | Carries | +|---|---|---| +| **Worksheet** | the desk | everything, labelled, reroll commands beside each component, explicit prompts where only a human can write | +| **Play sheet** | the table | the GM's own prose, the live leverage point, the decision on one line, the numbers. Nothing else | + +The JSON file is the document. An `authored` layer holds what the GM writes and the generator +never touches it, so **rerolling a component cannot destroy prose**. Lock what is good, reroll +what is not, write the read-aloud, export the play sheet. + +DELVE does not write the read-aloud and does not write the climax. It gets a GM to a strong first +draft in a minute instead of an evening, and then gets out of the way. --- diff --git a/core/authoring.mjs b/core/authoring.mjs new file mode 100644 index 0000000..9fdad03 --- /dev/null +++ b/core/authoring.mjs @@ -0,0 +1,150 @@ +/** + * DELVE as an authoring system. + * + * Four readings of generated output landed on the same verdict: the fiction and the structure are + * good, the table-readiness is not, and the honest product is a strong first draft a GM finishes. + * "The handwritten delve still tells the GM what matters faster." + * + * That reframes the density problem rather than solving it. Everything on the page is a vice at + * the table and a virtue at the desk. So a delve now has two surfaces: + * + * WORKSHEET — everything, labelled, with explicit prompts for what only a human can write. + * PLAY SHEET — only what survived, in the GM's own words, tight enough to scan. + * + * The delve JSON is the working file. It carries an `authored` layer that the generator never + * touches, so rerolling a component cannot destroy written prose. + */ + +import { Rng } from './rng.mjs'; +import { generateDelve } from './delve.mjs'; + +/** Fields a GM writes. Anything here, once written, outranks the generated text. */ +export const AUTHORED_FIELDS = ['readAloud', 'notes', 'nameOverride']; + +/** Components that can be individually rerolled. */ +export const REROLLABLE = ['situation', 'decision', 'temptation', 'feature', 'name']; + +/** A fresh working file: the generated delve plus an empty authored layer per area. */ +export function newWorkingFile(params) { + const delve = generateDelve(params); + delve.authored = { title: null, intro: null, areas: {} }; + for (const a of delve.areas) { + delve.authored.areas[a.index] = { readAloud: null, notes: null, nameOverride: null, locked: [] }; + } + delve.authored.ending = { readAloud: null, notes: null, locked: [] }; + return delve; +} + +const authoredFor = (d, i) => d.authored?.areas?.[i] ?? { locked: [] }; + +export const isLocked = (d, i, component) => (authoredFor(d, i).locked ?? []).includes(component); + +export function lock(d, i, component) { + const a = authoredFor(d, i); + if (!a.locked.includes(component)) a.locked.push(component); + return d; +} + +export function unlock(d, i, component) { + const a = authoredFor(d, i); + a.locked = (a.locked ?? []).filter(c => c !== component); + return d; +} + +export function setAuthored(d, i, field, value) { + if (!AUTHORED_FIELDS.includes(field)) throw new Error(`not an authored field: ${field}`); + if (i === 'ending') { d.authored.ending[field] = value; return d; } + authoredFor(d, i)[field] = value; + return d; +} + +/** + * Reroll one component of one area, keeping everything else — including anything written. + * + * Regeneration draws from a salted seed so a reroll gives something genuinely different rather + * than the same draw again, and the salt is recorded so the file stays reproducible. + */ +export function reroll(d, i, component, pack) { + if (!REROLLABLE.includes(component)) throw new Error(`not rerollable: ${component}`); + if (isLocked(d, i, component)) return d; + + d.rerolls = d.rerolls ?? {}; + const key = `${i}:${component}`; + d.rerolls[key] = (d.rerolls[key] ?? 0) + 1; + + const salt = `${d.seed}::reroll::${key}::${d.rerolls[key]}`; + const rng = new Rng(salt); + const motif = pack.motifs?.[d.skeleton.motif.id] ?? {}; + const area = d.areas.find(a => a.index === i); + if (!area) throw new Error(`no area ${i}`); + + /** + * Draw fresh, avoiding what the rest of the delve uses. When every option is already in play — + * six areas against a six-item pool — prefer duplicating another area over handing back what is + * already here. A reroll that changes nothing looks broken. + */ + const choose = (all, taken, keyOf, current) => { + const fresh = all.filter(x => !taken.has(keyOf(x))); + if (fresh.length) return rng.pick(fresh); + const notCurrent = all.filter(x => keyOf(x) !== current); + return notCurrent.length ? rng.pick(notCurrent) : null; + }; + + // Draw fresh, avoiding what the rest of the delve is already using. + const used = (get, current) => { + const taken = new Set(d.areas.filter(x => x.index !== i).map(get).filter(Boolean)); + if (current) taken.add(current); // never hand back what is already there + return taken; + }; + switch (component) { + case 'situation': { + const taken = used(a => a.situation?.occupant + a.situation?.doing, area.situation ? area.situation.occupant + area.situation.doing : null); + const pick = choose(motif.situations ?? [], taken, x => x.occupant + x.doing, + area.situation ? area.situation.occupant + area.situation.doing : null); + if (pick) area.situation = { occupant: pick.occupant, doing: pick.doing, onArrival: pick.onArrival, because: pick.because, offer: pick.offer }; + break; + } + case 'decision': { + const taken = used(a => a.decision?.cue, area.decision?.cue); + const pick = choose(motif.decisions ?? [], taken, x => x.cue, area.decision?.cue); + if (pick) area.decision = pick; + break; + } + case 'temptation': { + if (!area.hoard) break; + const taken = used(a => a.temptation?.id, area.temptation?.id); + const pick = choose(motif.temptations ?? [], taken, x => x.id, area.temptation?.id); + if (pick) area.temptation = pick; + break; + } + case 'feature': { + const taken = used(a => a.cueFragments?.[1], area.cueFragments?.[1]); + const pick = choose(motif.features ?? [], taken, x => x, area.cueFragments?.[1]); + if (pick) area.cueFragments = [area.cueFragments[0], pick].filter(Boolean); + break; + } + case 'name': { + const names = pack.areaNames?.[area.role] ?? []; + const taken = used(a => a.name, area.name); + const pick = choose(names, taken, x => x, area.name); + if (pick) area.name = pick; + break; + } + } + return d; +} + +/** What still needs a human. This is the worksheet's to-do list and the play sheet's gate. */ +export function outstanding(d) { + const todo = []; + if (!d.authored?.intro) todo.push({ where: 'intro', what: 'an opening paragraph for the delve' }); + for (const a of d.areas) { + const au = authoredFor(d, a.index); + if (!au.readAloud) todo.push({ where: `area ${a.index}`, what: 'read-aloud text' }); + } + if (!d.authored?.ending?.readAloud) todo.push({ where: 'ending', what: 'read-aloud text' }); + if (d.ending.authored && !d.authored?.ending?.notes) todo.push({ where: 'ending', what: 'the authored climax — DELVE deliberately leaves this to you' }); + return todo; +} + +export const readyToPlay = d => outstanding(d).length === 0; diff --git a/core/cli.mjs b/core/cli.mjs index 54d381c..dc16d46 100755 --- a/core/cli.mjs +++ b/core/cli.mjs @@ -1,20 +1,56 @@ #!/usr/bin/env node -/** DELVE CLI — map-free by necessity: forgeStage needs browser APIs (see spike report §3A). */ -import { readFileSync, writeFileSync } from 'fs'; +/** + * DELVE — an authoring system for VANITY delves. + * + * new node cli.mjs --new --seed=X --areas=6 --file=my-delve.json + * worksheet node cli.mjs --file=my-delve.json --worksheet + * reroll node cli.mjs --file=my-delve.json --reroll=3:situation + * lock node cli.mjs --file=my-delve.json --lock=3:decision + * write node cli.mjs --file=my-delve.json --write=3:readAloud --text="..." + * play node cli.mjs --file=my-delve.json --play + * + * The JSON file is the working document. Rerolling never touches written prose. + */ +import { readFileSync, writeFileSync, existsSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; -import { generateDelve } from './delve.mjs'; -import { renderMarkdown } from './render.mjs'; +import { newWorkingFile, reroll, lock, unlock, setAuthored, outstanding } from './authoring.mjs'; +import { renderWorksheet, renderPlay } from './render-authoring.mjs'; const here = dirname(fileURLToPath(import.meta.url)); const args = Object.fromEntries(process.argv.slice(2).map(a => { - const [k, v = true] = a.replace(/^--/, '').split('='); - return [k, v === true ? true : (/^\d+$/.test(v) ? Number(v) : v)]; + const [k, ...rest] = a.replace(/^--/, '').split('='); + const v = rest.join('='); + return [k, v === '' ? true : (/^\d+$/.test(v) ? Number(v) : v)]; })); -const theme = args.theme ?? 'barrow'; -const pack = JSON.parse(readFileSync(join(here, 'content', `${theme}.json`), 'utf8')); -const delve = generateDelve({ pack, ...args }); +const pack = JSON.parse(readFileSync(join(here, 'content', `${args.theme ?? 'barrow'}.json`), 'utf8')); +const file = args.file; +const load = () => JSON.parse(readFileSync(file, 'utf8')); +const save = d => writeFileSync(file, JSON.stringify(d, null, 2)); -if (args.json) { const out = JSON.stringify(delve, null, 2); args.out ? writeFileSync(args.out, out) : console.log(out); } -else { const md = renderMarkdown(delve); args.out ? writeFileSync(args.out, md) : console.log(md); } +let d; +if (args.new || !file || !existsSync(file)) { + d = newWorkingFile({ pack, ...args }); + if (file) { save(d); console.error(`created ${file} — seed ${d.seed}`); } +} else { + d = load(); +} + +const target = spec => { const [i, c] = String(spec).split(':'); return [i === 'ending' ? 'ending' : Number(i), c]; }; + +if (args.reroll) { const [i, c] = target(args.reroll); reroll(d, i, c, pack); save(d); console.error(`rerolled ${c} in area ${i}`); } +if (args.lock) { const [i, c] = target(args.lock); lock(d, i, c); save(d); console.error(`locked ${c} in area ${i}`); } +if (args.unlock) { const [i, c] = target(args.unlock); unlock(d, i, c); save(d); console.error(`unlocked ${c} in area ${i}`); } +if (args.write) { + const [i, f] = target(args.write); + setAuthored(d, i, f, args.text ?? ''); + save(d); console.error(`wrote ${f} for ${i}`); +} + +const out = args.play ? renderPlay(d) + : args.json ? JSON.stringify(d, null, 2) + : args.todo ? outstanding(d).map(t => `[ ] ${t.where} — ${t.what}`).join('\n') + : renderWorksheet(d); + +if (args.out) writeFileSync(args.out, out); else console.log(out); diff --git a/core/render-authoring.mjs b/core/render-authoring.mjs new file mode 100644 index 0000000..6cb97bb --- /dev/null +++ b/core/render-authoring.mjs @@ -0,0 +1,163 @@ +/** + * Two renders of one delve. + * + * WORKSHEET is for the desk: everything the generator knows, labelled, with the reroll commands + * beside each component and an explicit prompt where only a human can write. + * + * PLAY is for the table: the GM's own prose, the live leverage point, the decision on one line, + * the numbers. Nothing else. Four reviews said the same thing — "the handwritten delve tells the + * GM what matters faster" — and the only way to be fast is to carry less. + */ +import { outstanding, isLocked, REROLLABLE } from './authoring.mjs'; + +const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s); +const cost = c => { + if (!c) return 'free'; + const b = []; + if (c.bane) b.push(`+${c.bane} Bane${c.bane > 1 ? 's' : ''}`); + if (c.vice) b.push(`the Vice ${c.vice}`); + return b.length ? b.join(', ') : 'free'; +}; +const au = (d, i) => d.authored?.areas?.[i] ?? {}; + +/* ------------------------------------------------------------- worksheet */ + +export function renderWorksheet(d) { + const L = []; + const sk = d.skeleton, ap = sk.appeasement; + const todo = outstanding(d); + + L.push(`# WORKSHEET — ${d.authored?.title ?? sk.placeName}`); + L.push(''); + L.push(`\`seed ${d.seed} · ${d.params.theme} · ${d.params.areas} areas · depth ${d.params.depth} · generator ${d.generatorVersion}\``); + L.push(''); + L.push(todo.length + ? `**${todo.length} things still need you.** This is a first draft, not a play document. Finish it, then export the play sheet.` + : `**Finished.** Export the play sheet.`); + L.push(''); + if (todo.length) { for (const t of todo) L.push(`- [ ] **${t.where}** — ${t.what}`); L.push(''); } + L.push('---'); + L.push(''); + L.push('## The kernel'); + L.push(''); + L.push(`| | |`); + L.push(`|---|---|`); + L.push(`| **Transgression** | ${sk.knot.transgression} |`); + L.push(`| **Bottom problem** | ${sk.bottomProblem.label} — ${sk.bottomProblem.failureState} |`); + L.push(`| **Prize** | ${sk.prize.label} — ${sk.prize.function} |`); + L.push(`| **Motif / danger** | ${sk.motif.id} · ${sk.motif.danger} — ${sk.motif.dangerLine} |`); + L.push(`| **Appeasement** | ${ap.move} \`[${ap.attribute} ${ap.successes}]\` — +${ap.gain.vanity} Vanity, +${ap.cost.bane} Bane, repeatable |`); + L.push(`| **It asks** | ${d.ending.question} |`); + L.push(''); + L.push(`**WRITE ▸ an opening paragraph.** ${d.authored?.intro ? '✔ written' : '_(nothing yet — set `authored.intro`)_'}`); + if (d.authored?.intro) { L.push(''); L.push(`> ${d.authored.intro}`); } + L.push(''); + L.push('---'); + L.push(''); + + for (const a of d.areas) { + const w = au(d, a.index); + const locks = (w.locked ?? []); + const rerollable = REROLLABLE.filter(c => !locks.includes(c) && (c !== 'temptation' || a.hoard)); + L.push(`## Area ${a.index} — ${w.nameOverride ?? a.name}`); + L.push(`\`${a.role} · ${a.facet}${a.encounter ? ` · ${a.encounter.heat}` : ''}${a.hoard ? ` · ${a.hoard}` : ''}\``); + L.push(`*reroll:* ${rerollable.map(c => `\`--reroll=${a.index}:${c}\``).join(' ')}${locks.length ? ` · *locked:* ${locks.join(', ')}` : ''}`); + L.push(''); + L.push(`**IMAGE** ${a.cueFragments.join(' · ')}`); + if (a.situation) { + L.push(`**SITUATION** ${cap(a.situation.occupant)} — ${a.situation.doing}. On arrival, ${a.situation.onArrival}.`); + L.push(`**WHY** ${cap(a.situation.because)}. **OFFERS** ${a.situation.offer}.`); + } + L.push(`**DECISION** ${cap(a.decision.cue)} — *${a.decision.cost}*`); + const rv = a.decision.resolve ?? {}; + if (rv.roll) L.push(` · \`[${rv.roll}]\` → ${rv.success}`); + if (rv.failure) L.push(` · **miss** → ${rv.failure}`); + if (rv.orElse) L.push(` · **or** ${rv.orElse}`); + if (a.encounter?.roster) { + const R = a.encounter.roster; + L.push(`**ENCOUNTER** ${R.line} — ${R.foes.map(f => `${f.n}× ${f.name} atk${f.atk}/def${f.def}/G${f.grit}`).join(', ')}`); + L.push(` · harmed by ${R.harmedBy} · ${R.avoid}`); + } + if (a.temptation) { + const t = a.temptation; + L.push(`**TEMPTATION** ${cap(t.id)} — ${t.cue}: ${t.benefit}`); + L.push(` · take ${cost(t.acceptanceCost)} · use ${cost(t.useCost)} · standing: ${t.standingDrawback}`); + } + L.push(`**PRESSURE** trigger ${a.trigger}${a.baneBeat ? ` · bane beat ${a.baneBeat}` : ''} · fallback ${a.fallback.route}`); + L.push(''); + L.push(`**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above.`); + L.push(w.readAloud ? `> ${w.readAloud}` : `> _(nothing yet)_`); + if (w.notes) { L.push(''); L.push(`**Notes.** ${w.notes}`); } + L.push(''); + L.push('---'); + L.push(''); + } + + const we = d.authored?.ending ?? {}; + L.push(`## Ending — ${d.ending.name}`); + L.push(d.ending.authored ? '`AUTHORED SLOT — DELVE stops here on purpose`' : '`generated`'); + L.push(''); + L.push(`**IMAGE** ${d.ending.cueFragments.join(' · ')}`); + L.push(`**IT WANTS** ${sk.bottomProblem.wantNow}. **IT ASKS** ${d.ending.question}`); + L.push(`**APPEASED EVEN ONCE** it listens, and will trade ${sk.prize.label}.`); + L.push(''); + L.push('**WRITE ▸ read-aloud**'); + L.push(we.readAloud ? `> ${we.readAloud}` : `> _(nothing yet)_`); + L.push(''); + L.push('**WRITE ▸ the climax.** How it goes, what harms it, what it will take instead of a fight.'); + L.push(we.notes ? we.notes : `_(nothing yet — this is the seam; DELVE will not write it for you)_`); + L.push(''); + return L.join('\n'); +} + +/* ------------------------------------------------------------ play sheet */ + +export function renderPlay(d) { + const L = []; + const sk = d.skeleton, ap = sk.appeasement; + const todo = outstanding(d); + + L.push(`# ${d.authored?.title ?? sk.placeName}`); + L.push(''); + if (todo.length) { + L.push(`> ⚠ **${todo.length} unfinished** — ${todo.slice(0, 3).map(t => t.where).join(', ')}${todo.length > 3 ? '…' : ''}.`); + L.push('> Fragments are shown where nothing was written. Run the worksheet first.'); + L.push(''); + } + if (d.authored?.intro) { L.push(`> ${d.authored.intro}`); L.push(''); } + L.push(`**Appease:** ${ap.move} \`[${ap.attribute} ${ap.successes}]\` — +1 Vanity now, +1 Bane later. Repeatable, every area.`); + L.push(`**Clock:** 1d6 on a trigger. **1 — something comes.** **Tab:** ${d.pressure.target} by the end.`); + L.push(`**GM truth:** ${sk.knot.transgression} ${sk.bottomProblem.label} ${sk.bottomProblem.failureState}.`); + L.push(''); + L.push('---'); + L.push(''); + + for (const a of d.areas) { + const w = au(d, a.index); + const rv = a.decision.resolve ?? {}; + L.push(`### ${a.index} · ${w.nameOverride ?? a.name}`); + L.push(''); + L.push(w.readAloud ? `> ${w.readAloud}` : `> *(unwritten)* ${a.cueFragments.join('. ')}.`); + L.push(''); + if (a.situation) L.push(`**Here:** ${cap(a.situation.occupant)}, ${a.situation.doing} — ${a.situation.onArrival}. **They can:** ${a.situation.offer}.`); + L.push(`**${cap(a.decision.cue)}** — ${rv.roll ? `\`[${rv.roll}]\` ${rv.success}` : ''}${rv.failure ? ` · **miss** ${rv.failure}` : ''}${rv.orElse ? ` · **or** ${rv.orElse}` : ''}`); + if (a.encounter?.roster) { + const R = a.encounter.roster; + L.push(`**${cap(a.encounter.heat)}:** ${R.foes.map(f => `${f.n}× ${f.name} \`${f.atk}/${f.def}/${f.grit}\``).join(' · ')} — harmed by ${R.harmedBy}.`); + } + if (a.temptation) L.push(`**${cap(a.temptation.id)}:** ${a.temptation.benefit}. *Use: ${cost(a.temptation.useCost)}. ${a.temptation.standingDrawback}.*`); + if (w.notes) L.push(`**Note:** ${w.notes}`); + L.push(`\`${a.trigger}${a.baneBeat ? ` · ${a.baneBeat}` : ''} · skipped → move the image on\``); + L.push(''); + } + + const we = d.authored?.ending ?? {}; + L.push(`### ${d.ending.name}`); + L.push(''); + L.push(we.readAloud ? `> ${we.readAloud}` : `> *(unwritten)* ${d.ending.cueFragments.join('. ')}.`); + L.push(''); + L.push(`**It asks:** ${d.ending.question}`); + L.push(we.notes ? we.notes : `*(the climax is unwritten — this is the authored slot)*`); + L.push(''); + return L.join('\n'); +} diff --git a/core/test.mjs b/core/test.mjs index 003538c..9f55b3a 100644 --- a/core/test.mjs +++ b/core/test.mjs @@ -174,5 +174,48 @@ for (let i = 0; i < 30; i++) { } t('openers read as English', badOpener === 0, `${badOpener} malformed`); + +// ---- authoring layer ------------------------------------------------------- +import { newWorkingFile, reroll, lock, setAuthored, outstanding, readyToPlay } from './authoring.mjs'; +import { renderWorksheet, renderPlay } from './render-authoring.mjs'; + +{ + let changed = 0, lockHeld = 0, proseKept = 0; + for (let i = 0; i < 25; i++) { + const d = newWorkingFile({ pack, seed: `auth-${i}`, areas: 6 }); + const before = d.areas[2].decision.cue; + reroll(d, 3, 'decision', pack); + if (d.areas[2].decision.cue !== before) changed++; + + setAuthored(d, 3, 'readAloud', 'written by a human'); + lock(d, 3, 'situation'); + const sit = d.areas[2].situation.occupant; + reroll(d, 3, 'situation', pack); + if (d.areas[2].situation.occupant === sit) lockHeld++; + if (d.authored.areas[3].readAloud === 'written by a human') proseKept++; + } + t('a reroll always returns something different', changed === 25, `${changed}/25`); + t('a lock is respected', lockHeld === 25, `${lockHeld}/25`); + t('rerolling never destroys written prose', proseKept === 25, `${proseKept}/25`); + + const d = newWorkingFile({ pack, seed: 'auth-x', areas: 4 }); + t('a fresh delve is not ready to play', !readyToPlay(d), `${outstanding(d).length} outstanding`); + const ws = renderWorksheet(d); + t('the worksheet shows what still needs a human', ws.includes('WRITE ▸') && ws.includes('still need you')); + t('the worksheet offers reroll commands', ws.includes('--reroll=')); + const play = renderPlay(d); + t('an unfinished play sheet says so', play.includes('unfinished') && play.includes('(unwritten)')); + + setAuthored(d, 'ending', 'readAloud', 'x'); setAuthored(d, 'ending', 'notes', 'y'); + d.authored.intro = 'z'; + for (const a of d.areas) setAuthored(d, a.index, 'readAloud', `prose for ${a.index}`); + t('a finished delve reports ready', readyToPlay(d), `${outstanding(d).length} left`); + const done = renderPlay(d); + t('a finished play sheet uses the GM prose', done.includes('prose for 1') && !done.includes('(unwritten)')); + t('the play sheet is shorter than the worksheet', + done.split('\n').length < renderWorksheet(d).split('\n').length, + `play ${done.split('\n').length} vs worksheet ${renderWorksheet(d).split('\n').length} lines`); +} + console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0); diff --git a/samples/finished-delve.json b/samples/finished-delve.json new file mode 100644 index 0000000..f33552f --- /dev/null +++ b/samples/finished-delve.json @@ -0,0 +1,484 @@ +{ + "schema": 1, + "generatorVersion": "0.1.0", + "catalogVersion": "barrow@1", + "seed": "live-table-01", + "params": { + "areas": 5, + "depth": 2, + "party": 4, + "deadliness": "standard", + "density": "standard", + "greed": "standard", + "clock": "standard", + "ending": "authored", + "theme": "barrow" + }, + "skeleton": { + "placeName": "The Vault of the Patient Lord", + "knot": { + "appetiteId": "obeyed", + "accommodationId": "purchased", + "claimant": "Patient Lord", + "conscript": "a household", + "transgression": "The Patient Lord bought a household from those with nothing left, so that someone would always obey.", + "cruelty": 2 + }, + "motif": { + "id": "voices", + "danger": "obedience", + "dangerLine": "the dead here still take instruction, and give it", + "trigger": "loud" + }, + "prize": { + "kind": "seal", + "label": "the signet that is still obeyed", + "function": "the only thing the household answers to" + }, + "bottomProblem": { + "label": "The Patient Lord", + "wantNow": "to be obeyed without asking", + "failureState": "is disobeyed now, and cannot bear it", + "handledBy": "giving them what they want, at a price" + }, + "appeasement": { + "move": "do one small thing it asks, without arguing", + "attribute": "Poise", + "successes": 2, + "why": "it was obeyed", + "gain": { + "vanity": 1 + }, + "cost": { + "bane": 1 + }, + "repeatable": true, + "faction": "The Silent Host" + }, + "factions": [ + { + "id": "host", + "name": "The Silent Host", + "kind": "undead", + "essence": "still performing the ritual they were buried for", + "handledBy": "courtesy", + "conscriptedAs": "a household" + }, + { + "id": "scavengers", + "name": "things that moved in", + "kind": "beast", + "essence": "neither know nor care whose tomb this is", + "handledBy": "force or food" + } + ], + "foreshadow": [ + { + "index": 1, + "role": "approach", + "facet": "institution", + "fragment": "servants' bones arranged as though mid-errand" + }, + { + "index": 2, + "role": "complication", + "facet": "ritual", + "fragment": "orders scratched into the floor, the last one unfinished" + }, + { + "index": 3, + "role": "turn", + "facet": "demand", + "fragment": "a hall where your own voice arrives before you speak" + }, + { + "index": 4, + "role": "descent", + "facet": "wound", + "fragment": "an instruction repeated until the stone gave out" + }, + { + "index": 5, + "role": "threshold", + "facet": "anchor", + "fragment": "a key that fits nothing here" + }, + { + "index": 6, + "role": "ending", + "facet": "demand", + "fragment": "a seat that expects to be addressed" + } + ] + }, + "areas": [ + { + "index": 1, + "role": "approach", + "name": "The Weeping Stair", + "facet": "institution", + "cueFragments": [ + "servants' bones arranged as though mid-errand", + "someone has been talking here, and stopped when you arrived" + ], + "situation": { + "occupant": "three of the Host in servants' dress", + "doing": "carrying out an errand that has no destination left", + "onArrival": "they stop, and wait to be told what to do", + "because": "the order was never rescinded and nobody outranks it", + "offer": "tell them where to go, and they go" + }, + "truth": "This is how the place was ordered, and it still is", + "decision": { + "cue": "an order carved into the floor, still legible", + "cost": "obey it and the Host counts you as household; step over it and they do not", + "resolve": { + "roll": "Poise 2", + "success": "you obey exactly enough to be counted household — Skeletons stand down for you", + "failure": "you obey wrongly, which is worse than not obeying: +1 Bane", + "orElse": "step over it: no cost now, and the Host does not count you as theirs" + } + }, + "encounter": null, + "hoard": null, + "temptation": null, + "trigger": "seenTwice", + "baneBeat": "announcing", + "clue": { + "automatic": "servants' bones arranged as though mid-errand — and it was done deliberately", + "extra": "[VANITY: Observation — 1 success] buys who did it, and roughly when" + }, + "failureCase": "If they skip this, move its cue fragment into the next area so the motif still completes.", + "fallback": { + "route": "The Silent Host will receive anyone who behaves as a guest", + "cost": "one Bane on the tab, and they will expect it again" + } + }, + { + "index": 2, + "role": "complication", + "name": "The Lesser Vault", + "facet": "ritual", + "cueFragments": [ + "orders scratched into the floor, the last one unfinished", + "a door braced shut from within by furniture" + ], + "situation": { + "occupant": "a living grave-robber", + "doing": "repeating a phrase over and over, trying to get the intonation right", + "onArrival": "they go quiet and put their back to the wall", + "because": "they heard the password and cannot say it correctly", + "offer": "trade something for the phrase, badly remembered" + }, + "truth": "The office is still being performed, for someone who is no longer served by it", + "decision": { + "cue": "two doors, and behind one a voice giving instructions to nobody", + "cost": "the quiet door is longer; the voice would tell you exactly where to go", + "resolve": { + "roll": "Poise 2", + "success": "you answer the voice as a subordinate would — it tells you the route AND answers one question about what lies on it, without taking you into its service", + "failure": "you engage and it takes that as enlistment: it will give you orders for the rest of the delve, and the first is now", + "orElse": "take the quiet door and never speak to it: one extra Turn, no obligation" + } + }, + "encounter": { + "heat": "fight", + "kind": "undead", + "roster": { + "line": "1 Ghoul and 2 Skeletons", + "foes": [ + { + "n": 1, + "name": "Ghoul", + "atk": 4, + "def": 3, + "grit": 4, + "nerve": 5, + "note": "on a hit, a free Called Shot to freeze for a round" + }, + { + "n": 2, + "name": "Skeleton", + "atk": 3, + "def": 2, + "grit": 1, + "nerve": 5, + "note": "fearless" + } + ], + "harmedBy": "anything", + "avoid": "the Ghoul goes for court dress first — send someone plain" + } + }, + "hoard": "cache", + "temptation": { + "id": "tube", + "cue": "a short length of speaking-tube, cut and carried", + "benefit": "ask one question and get one true answer", + "useCost": { + "bane": 1 + }, + "standingDrawback": "whatever answers can now hear you" + }, + "trigger": "loud", + "baneBeat": "noticing", + "clue": { + "automatic": "orders scratched into the floor, the last one unfinished — and it was done deliberately", + "extra": "[VANITY: Observation — 1 success] buys who did it, and roughly when" + }, + "failureCase": "If they skip this, move its cue fragment into the next area so the motif still completes.", + "fallback": { + "route": "The Silent Host will receive anyone who behaves as a guest", + "cost": "one Bane on the tab, and they will expect it again" + } + }, + { + "index": 3, + "role": "turn", + "name": "The Crossing", + "facet": "demand", + "cueFragments": [ + "a hall where your own voice arrives before you speak", + "an order half-scratched out and rewritten by a different hand" + ], + "situation": { + "occupant": "the bells", + "doing": "ringing a summons room by room, coming closer", + "onArrival": "the bell in this room begins", + "because": "something has called for service and the house is obliging", + "offer": "answer the bell yourself before whatever is meant to arrives" + }, + "truth": "Here the want is explicit — the place asks something of whoever enters", + "decision": { + "cue": "a bell-pull that would summon someone", + "cost": "ring it and help comes, and it is not the kind you wanted", + "resolve": { + "roll": "Poise 2", + "success": "what comes is answerable to you for one scene", + "failure": "what comes is not the kind of help you wanted", + "orElse": "do not ring: the way on is longer by one Turn" + } + }, + "encounter": null, + "hoard": null, + "temptation": null, + "trigger": "loud", + "baneBeat": "noticing", + "clue": { + "automatic": "a hall where your own voice arrives before you speak — and it was done deliberately", + "extra": "[VANITY: Observation — 1 success] buys who did it, and roughly when" + }, + "failureCase": "If they skip this, move its cue fragment into the next area so the motif still completes.", + "fallback": { + "route": "The Silent Host will receive anyone who behaves as a guest", + "cost": "one Bane on the tab, and they will expect it again" + } + }, + { + "index": 4, + "role": "descent", + "name": "The Servants' Deep", + "facet": "wound", + "cueFragments": [ + "an instruction repeated until the stone gave out", + "boot-prints that stop mid-corridor and do not resume" + ], + "situation": { + "occupant": "a single figure at a speaking-tube", + "doing": "reporting, at length, to a listener who died first", + "onArrival": "it pauses mid-sentence and asks whether you are the reply", + "because": "it has never once been answered", + "offer": "answer as the listener would, and learn what it has been reporting" + }, + "truth": "This is the damage the want did to those who had to satisfy it", + "decision": { + "cue": "a door that opens to a spoken password nobody living knows", + "cost": "guessing costs a Bane per attempt; the long way costs two Turns", + "resolve": { + "roll": "Wits 2", + "success": "the word is already in your notes. It is in something you passed earlier — *someone has been talking here, and stopped when you arrived*. Say it and the door opens; if nobody wrote it down, the roll tells them now", + "failure": "you have the shape of it but not the word: one more attempt, or take the long way", + "orElse": "the long way: two Turns, no guessing, no Banes" + } + }, + "encounter": null, + "hoard": "pocket", + "temptation": { + "id": "bell", + "cue": "a small bell from a servant's board", + "benefit": "ring it and one order is obeyed, by anything", + "useCost": { + "bane": 2 + }, + "standingDrawback": "it rings faintly when someone gives you an order" + }, + "trigger": "disturbed", + "baneBeat": "relicUse", + "clue": { + "automatic": "an instruction repeated until the stone gave out — and it was done deliberately", + "extra": "[VANITY: Observation — 1 success] buys who did it, and roughly when" + }, + "failureCase": "If they skip this, move its cue fragment into the next area so the motif still completes.", + "fallback": { + "route": "The Silent Host will receive anyone who behaves as a guest", + "cost": "one Bane on the tab, and they will expect it again" + } + }, + { + "index": 5, + "role": "threshold", + "name": "The Last Door", + "facet": "anchor", + "cueFragments": [ + "a key that fits nothing here", + "wax jammed into a speaking-tube, still soft" + ], + "situation": { + "occupant": "one of the Host, kneeling", + "doing": "waiting for an instruction that was promised and never came", + "onArrival": "it addresses the first of you to speak as its superior", + "because": "it was told to await further orders and did", + "offer": "give it the instruction and it will carry it out to the letter, wherever that leads" + }, + "truth": "The thing the whole barrow is arranged around is close", + "decision": { + "cue": "an instruction you can obey, ignore, or countermand", + "cost": "countermanding it works once and marks you as someone who gives orders", + "resolve": { + "roll": "Poise 2", + "success": "you countermand it and the dead obey you instead — this marks you as someone who gives orders: from now on the Host brings you their disputes, and once per delve you may issue one order they must follow", + "failure": "the order stands and you have been seen to fail at giving one: +1 Bane, and the Host addresses someone else from now on", + "orElse": "obey it, or ignore it and go round: ignoring costs one Turn" + } + }, + "encounter": { + "heat": "battle", + "kind": "undead", + "roster": { + "line": "1 Wraith and 3 Skeletons", + "foes": [ + { + "n": 1, + "name": "Wraith", + "atk": 5, + "def": 3, + "grit": 10, + "nerve": 5, + "note": "INCORPOREAL — chill touch ignores armour and drains 1 max Grit" + }, + { + "n": 3, + "name": "Skeleton", + "atk": 3, + "def": 2, + "grit": 1, + "nerve": 5, + "note": "fearless" + } + ], + "harmedBy": "blessed, silvered or magical weapons ONLY", + "avoid": "it will take an appeasement over a fight every time" + } + }, + "hoard": "chest", + "temptation": { + "id": "staff", + "cue": "a staff of office, propped where it was set down", + "benefit": "give one order the dead must follow", + "acceptanceCost": { + "vice": "Pride" + }, + "useCost": { + "bane": 2 + }, + "standingDrawback": "you may not refuse an order given to you" + }, + "trigger": "disturbed", + "baneBeat": "refusal", + "clue": { + "automatic": "a key that fits nothing here — and it was done deliberately", + "extra": "[VANITY: Observation — 1 success] buys who did it, and roughly when" + }, + "failureCase": "If they skip this, the ending opens with them holding nothing the bottom problem wants.", + "fallback": { + "route": "The Silent Host will receive anyone who behaves as a guest", + "cost": "one Bane on the tab, and they will expect it again" + } + } + ], + "ending": { + "index": 6, + "role": "ending", + "mode": "authored", + "heat": null, + "hoard": null, + "name": "The Audience", + "cueFragments": [ + "a seat that expects to be addressed" + ], + "question": "\"Do as you are told.\"", + "authored": true + }, + "triggerGlossary": { + "announced": "entered somewhere grandly — the big doors, a shout, a herald", + "seenTwice": "reflected, echoed or witnessed twice over in one place", + "loud": "a raised voice, a broken door, steel on stone", + "lingering": "stayed in one place longer than it takes to search it", + "disturbed": "struck or moved something that had not moved in centuries" + }, + "budget": { + "plannedFights": 2, + "heatBase": "fight", + "wantFights": 2 + }, + "pressure": { + "turn": 0, + "clockRolls": 0, + "clockFired": 0, + "triggerNumber": 1, + "escalated": false, + "tab": 5, + "target": 5 + }, + "authored": { + "title": null, + "intro": "They bought a household from people with nothing left, so that someone would always obey. The household is still obeying.", + "areas": { + "1": { + "readAloud": "Forty steps down and the bones are still on their errand — a tray here, a folded cloth there, set down mid-stride three hundred years ago. Somebody was talking as you came through the arch, and stopped.", + "notes": null, + "nameOverride": null, + "locked": [] + }, + "2": { + "readAloud": "The floor is scratched over with orders, the last one unfinished. A door at the far end is braced shut from the inside with furniture, and behind you a living man is whispering the same phrase again and again, trying to get the sound of it right.", + "notes": null, + "nameOverride": null, + "locked": [] + }, + "3": { + "readAloud": "The hall answers before you do — your own voice reaches the far wall a half-beat early, saying what you have not said yet. Somewhere below a bell starts, and then the one in this room takes it up.", + "notes": null, + "nameOverride": null, + "locked": [] + }, + "4": { + "readAloud": "An instruction has been cut into the stone so many times the stone gave out. Bootprints cross the floor and simply stop. At the far wall a figure is talking steadily into a speaking-tube, reporting to somebody who died first.", + "notes": null, + "nameOverride": null, + "locked": [] + }, + "5": { + "readAloud": "Wax has been jammed into the tube here, still soft. A key that fits nothing hangs on a nail. One of them is kneeling at the last door, and has been for a very long time, waiting to be told what to do.", + "notes": null, + "nameOverride": null, + "locked": [] + } + }, + "ending": { + "readAloud": "The seat at the head of the hall is occupied by a shape that keeps the gown's outline and nothing else. It turns toward you and there is nothing in the collar to turn.", + "notes": "**The Patient Lord** — Wraith, Grit 6, atk 5, def 3, incorporeal. Blessed, silvered or magical weapons only; say so before initiative. It opens with \"Do as you are told.\" Obey one small instruction and it will trade the signet. Refuse and it is a fight it will win slowly.", + "locked": [] + } + } +} \ No newline at end of file diff --git a/samples/finished-play.md b/samples/finished-play.md new file mode 100644 index 0000000..f0657d7 --- /dev/null +++ b/samples/finished-play.md @@ -0,0 +1,61 @@ +# The Vault of the Patient Lord + +> They bought a household from people with nothing left, so that someone would always obey. The household is still obeying. + +**Appease:** do one small thing it asks, without arguing `[Poise 2]` — +1 Vanity now, +1 Bane later. Repeatable, every area. +**Clock:** 1d6 on a trigger. **1 — something comes.** **Tab:** 5 by the end. +**GM truth:** The Patient Lord bought a household from those with nothing left, so that someone would always obey. The Patient Lord is disobeyed now, and cannot bear it. + +--- + +### 1 · The Weeping Stair + +> Forty steps down and the bones are still on their errand — a tray here, a folded cloth there, set down mid-stride three hundred years ago. Somebody was talking as you came through the arch, and stopped. + +**Here:** Three of the Host in servants' dress, carrying out an errand that has no destination left — they stop, and wait to be told what to do. **They can:** tell them where to go, and they go. +**An order carved into the floor, still legible** — `[Poise 2]` you obey exactly enough to be counted household — Skeletons stand down for you · **miss** you obey wrongly, which is worse than not obeying: +1 Bane · **or** step over it: no cost now, and the Host does not count you as theirs +`seenTwice · announcing · skipped → move the image on` + +### 2 · The Lesser Vault + +> The floor is scratched over with orders, the last one unfinished. A door at the far end is braced shut from the inside with furniture, and behind you a living man is whispering the same phrase again and again, trying to get the sound of it right. + +**Here:** A living grave-robber, repeating a phrase over and over, trying to get the intonation right — they go quiet and put their back to the wall. **They can:** trade something for the phrase, badly remembered. +**Two doors, and behind one a voice giving instructions to nobody** — `[Poise 2]` you answer the voice as a subordinate would — it tells you the route AND answers one question about what lies on it, without taking you into its service · **miss** you engage and it takes that as enlistment: it will give you orders for the rest of the delve, and the first is now · **or** take the quiet door and never speak to it: one extra Turn, no obligation +**Fight:** 1× Ghoul `4/3/4` · 2× Skeleton `3/2/1` — harmed by anything. +**Tube:** ask one question and get one true answer. *Use: +1 Bane. whatever answers can now hear you.* +`loud · noticing · skipped → move the image on` + +### 3 · The Crossing + +> The hall answers before you do — your own voice reaches the far wall a half-beat early, saying what you have not said yet. Somewhere below a bell starts, and then the one in this room takes it up. + +**Here:** The bells, ringing a summons room by room, coming closer — the bell in this room begins. **They can:** answer the bell yourself before whatever is meant to arrives. +**A bell-pull that would summon someone** — `[Poise 2]` what comes is answerable to you for one scene · **miss** what comes is not the kind of help you wanted · **or** do not ring: the way on is longer by one Turn +`loud · noticing · skipped → move the image on` + +### 4 · The Servants' Deep + +> An instruction has been cut into the stone so many times the stone gave out. Bootprints cross the floor and simply stop. At the far wall a figure is talking steadily into a speaking-tube, reporting to somebody who died first. + +**Here:** A single figure at a speaking-tube, reporting, at length, to a listener who died first — it pauses mid-sentence and asks whether you are the reply. **They can:** answer as the listener would, and learn what it has been reporting. +**A door that opens to a spoken password nobody living knows** — `[Wits 2]` the word is already in your notes. It is in something you passed earlier — *someone has been talking here, and stopped when you arrived*. Say it and the door opens; if nobody wrote it down, the roll tells them now · **miss** you have the shape of it but not the word: one more attempt, or take the long way · **or** the long way: two Turns, no guessing, no Banes +**Bell:** ring it and one order is obeyed, by anything. *Use: +2 Banes. it rings faintly when someone gives you an order.* +`disturbed · relicUse · skipped → move the image on` + +### 5 · The Last Door + +> Wax has been jammed into the tube here, still soft. A key that fits nothing hangs on a nail. One of them is kneeling at the last door, and has been for a very long time, waiting to be told what to do. + +**Here:** One of the Host, kneeling, waiting for an instruction that was promised and never came — it addresses the first of you to speak as its superior. **They can:** give it the instruction and it will carry it out to the letter, wherever that leads. +**An instruction you can obey, ignore, or countermand** — `[Poise 2]` you countermand it and the dead obey you instead — this marks you as someone who gives orders: from now on the Host brings you their disputes, and once per delve you may issue one order they must follow · **miss** the order stands and you have been seen to fail at giving one: +1 Bane, and the Host addresses someone else from now on · **or** obey it, or ignore it and go round: ignoring costs one Turn +**Battle:** 1× Wraith `5/3/10` · 3× Skeleton `3/2/1` — harmed by blessed, silvered or magical weapons ONLY. +**Staff:** give one order the dead must follow. *Use: +2 Banes. you may not refuse an order given to you.* +`disturbed · refusal · skipped → move the image on` + +### The Audience + +> The seat at the head of the hall is occupied by a shape that keeps the gown's outline and nothing else. It turns toward you and there is nothing in the collar to turn. + +**It asks:** "Do as you are told." +**The Patient Lord** — Wraith, Grit 6, atk 5, def 3, incorporeal. Blessed, silvered or magical weapons only; say so before initiative. It opens with "Do as you are told." Obey one small instruction and it will trade the signet. Refuse and it is a fight it will win slowly. diff --git a/samples/finished-worksheet.md b/samples/finished-worksheet.md new file mode 100644 index 0000000..c61518f --- /dev/null +++ b/samples/finished-worksheet.md @@ -0,0 +1,137 @@ +# WORKSHEET — The Vault of the Patient Lord + +`seed live-table-01 · barrow · 5 areas · depth 2 · generator 0.1.0` + +**Finished.** Export the play sheet. + +--- + +## The kernel + +| | | +|---|---| +| **Transgression** | The Patient Lord bought a household from those with nothing left, so that someone would always obey. | +| **Bottom problem** | The Patient Lord — is disobeyed now, and cannot bear it | +| **Prize** | the signet that is still obeyed — the only thing the household answers to | +| **Motif / danger** | voices · obedience — the dead here still take instruction, and give it | +| **Appeasement** | do one small thing it asks, without arguing `[Poise 2]` — +1 Vanity, +1 Bane, repeatable | +| **It asks** | "Do as you are told." | + +**WRITE ▸ an opening paragraph.** ✔ written + +> They bought a household from people with nothing left, so that someone would always obey. The household is still obeying. + +--- + +## Area 1 — The Weeping Stair +`approach · institution` +*reroll:* `--reroll=1:situation` `--reroll=1:decision` `--reroll=1:feature` `--reroll=1:name` + +**IMAGE** servants' bones arranged as though mid-errand · someone has been talking here, and stopped when you arrived +**SITUATION** Three of the Host in servants' dress — carrying out an errand that has no destination left. On arrival, they stop, and wait to be told what to do. +**WHY** The order was never rescinded and nobody outranks it. **OFFERS** tell them where to go, and they go. +**DECISION** An order carved into the floor, still legible — *obey it and the Host counts you as household; step over it and they do not* + · `[Poise 2]` → you obey exactly enough to be counted household — Skeletons stand down for you + · **miss** → you obey wrongly, which is worse than not obeying: +1 Bane + · **or** step over it: no cost now, and the Host does not count you as theirs +**PRESSURE** trigger seenTwice · bane beat announcing · fallback The Silent Host will receive anyone who behaves as a guest + +**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above. +> Forty steps down and the bones are still on their errand — a tray here, a folded cloth there, set down mid-stride three hundred years ago. Somebody was talking as you came through the arch, and stopped. + +--- + +## Area 2 — The Lesser Vault +`complication · ritual · fight · cache` +*reroll:* `--reroll=2:situation` `--reroll=2:decision` `--reroll=2:temptation` `--reroll=2:feature` `--reroll=2:name` + +**IMAGE** orders scratched into the floor, the last one unfinished · a door braced shut from within by furniture +**SITUATION** A living grave-robber — repeating a phrase over and over, trying to get the intonation right. On arrival, they go quiet and put their back to the wall. +**WHY** They heard the password and cannot say it correctly. **OFFERS** trade something for the phrase, badly remembered. +**DECISION** Two doors, and behind one a voice giving instructions to nobody — *the quiet door is longer; the voice would tell you exactly where to go* + · `[Poise 2]` → you answer the voice as a subordinate would — it tells you the route AND answers one question about what lies on it, without taking you into its service + · **miss** → you engage and it takes that as enlistment: it will give you orders for the rest of the delve, and the first is now + · **or** take the quiet door and never speak to it: one extra Turn, no obligation +**ENCOUNTER** 1 Ghoul and 2 Skeletons — 1× Ghoul atk4/def3/G4, 2× Skeleton atk3/def2/G1 + · harmed by anything · the Ghoul goes for court dress first — send someone plain +**TEMPTATION** Tube — a short length of speaking-tube, cut and carried: ask one question and get one true answer + · take free · use +1 Bane · standing: whatever answers can now hear you +**PRESSURE** trigger loud · bane beat noticing · fallback The Silent Host will receive anyone who behaves as a guest + +**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above. +> The floor is scratched over with orders, the last one unfinished. A door at the far end is braced shut from the inside with furniture, and behind you a living man is whispering the same phrase again and again, trying to get the sound of it right. + +--- + +## Area 3 — The Crossing +`turn · demand` +*reroll:* `--reroll=3:situation` `--reroll=3:decision` `--reroll=3:feature` `--reroll=3:name` + +**IMAGE** a hall where your own voice arrives before you speak · an order half-scratched out and rewritten by a different hand +**SITUATION** The bells — ringing a summons room by room, coming closer. On arrival, the bell in this room begins. +**WHY** Something has called for service and the house is obliging. **OFFERS** answer the bell yourself before whatever is meant to arrives. +**DECISION** A bell-pull that would summon someone — *ring it and help comes, and it is not the kind you wanted* + · `[Poise 2]` → what comes is answerable to you for one scene + · **miss** → what comes is not the kind of help you wanted + · **or** do not ring: the way on is longer by one Turn +**PRESSURE** trigger loud · bane beat noticing · fallback The Silent Host will receive anyone who behaves as a guest + +**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above. +> The hall answers before you do — your own voice reaches the far wall a half-beat early, saying what you have not said yet. Somewhere below a bell starts, and then the one in this room takes it up. + +--- + +## Area 4 — The Servants' Deep +`descent · wound · pocket` +*reroll:* `--reroll=4:situation` `--reroll=4:decision` `--reroll=4:temptation` `--reroll=4:feature` `--reroll=4:name` + +**IMAGE** an instruction repeated until the stone gave out · boot-prints that stop mid-corridor and do not resume +**SITUATION** A single figure at a speaking-tube — reporting, at length, to a listener who died first. On arrival, it pauses mid-sentence and asks whether you are the reply. +**WHY** It has never once been answered. **OFFERS** answer as the listener would, and learn what it has been reporting. +**DECISION** A door that opens to a spoken password nobody living knows — *guessing costs a Bane per attempt; the long way costs two Turns* + · `[Wits 2]` → the word is already in your notes. It is in something you passed earlier — *someone has been talking here, and stopped when you arrived*. Say it and the door opens; if nobody wrote it down, the roll tells them now + · **miss** → you have the shape of it but not the word: one more attempt, or take the long way + · **or** the long way: two Turns, no guessing, no Banes +**TEMPTATION** Bell — a small bell from a servant's board: ring it and one order is obeyed, by anything + · take free · use +2 Banes · standing: it rings faintly when someone gives you an order +**PRESSURE** trigger disturbed · bane beat relicUse · fallback The Silent Host will receive anyone who behaves as a guest + +**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above. +> An instruction has been cut into the stone so many times the stone gave out. Bootprints cross the floor and simply stop. At the far wall a figure is talking steadily into a speaking-tube, reporting to somebody who died first. + +--- + +## Area 5 — The Last Door +`threshold · anchor · battle · chest` +*reroll:* `--reroll=5:situation` `--reroll=5:decision` `--reroll=5:temptation` `--reroll=5:feature` `--reroll=5:name` + +**IMAGE** a key that fits nothing here · wax jammed into a speaking-tube, still soft +**SITUATION** One of the Host, kneeling — waiting for an instruction that was promised and never came. On arrival, it addresses the first of you to speak as its superior. +**WHY** It was told to await further orders and did. **OFFERS** give it the instruction and it will carry it out to the letter, wherever that leads. +**DECISION** An instruction you can obey, ignore, or countermand — *countermanding it works once and marks you as someone who gives orders* + · `[Poise 2]` → you countermand it and the dead obey you instead — this marks you as someone who gives orders: from now on the Host brings you their disputes, and once per delve you may issue one order they must follow + · **miss** → the order stands and you have been seen to fail at giving one: +1 Bane, and the Host addresses someone else from now on + · **or** obey it, or ignore it and go round: ignoring costs one Turn +**ENCOUNTER** 1 Wraith and 3 Skeletons — 1× Wraith atk5/def3/G10, 3× Skeleton atk3/def2/G1 + · harmed by blessed, silvered or magical weapons ONLY · it will take an appeasement over a fight every time +**TEMPTATION** Staff — a staff of office, propped where it was set down: give one order the dead must follow + · take the Vice Pride · use +2 Banes · standing: you may not refuse an order given to you +**PRESSURE** trigger disturbed · bane beat refusal · fallback The Silent Host will receive anyone who behaves as a guest + +**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above. +> Wax has been jammed into the tube here, still soft. A key that fits nothing hangs on a nail. One of them is kneeling at the last door, and has been for a very long time, waiting to be told what to do. + +--- + +## Ending — The Audience +`AUTHORED SLOT — DELVE stops here on purpose` + +**IMAGE** a seat that expects to be addressed +**IT WANTS** to be obeyed without asking. **IT ASKS** "Do as you are told." +**APPEASED EVEN ONCE** it listens, and will trade the signet that is still obeyed. + +**WRITE ▸ read-aloud** +> The seat at the head of the hall is occupied by a shape that keeps the gown's outline and nothing else. It turns toward you and there is nothing in the collar to turn. + +**WRITE ▸ the climax.** How it goes, what harms it, what it will take instead of a fight. +**The Patient Lord** — Wraith, Grit 6, atk 5, def 3, incorporeal. Blessed, silvered or magical weapons only; say so before initiative. It opens with "Do as you are told." Obey one small instruction and it will trade the signet. Refuse and it is a fight it will win slowly.