DELVE core: seeded generator, fiction kernel, Barrow pack, CLI
Build steps 1, 2 and 4 of the plan in DESIGN.md. core/ is pure JS with zero Foundry globals and 11 passing invariants. The fiction model has one root draw — an APPETITE plus the ACCOMMODATION someone made for it — and projects the motif, danger, prize, faction, cue fragments and the appeasement move from it, so the fiction coheres instead of being four unrelated draws. 336 kernels from 21 authored lines; 60 generated delves gave 51 distinct kernels. Cue fragments are tagged by facet (institution/ritual/demand/wound/anchor) and dealt without replacement. The facet progression across the arc reproduces the hand-authored paper delve's structure, which had arrived at it by instinct. Generation and table dice stay apart: pressure.mjs decides when a clock roll is due and what it means, and never rolls. That is what makes the skeleton seed an honest promise rather than a half-truth. paper/ holds the hand-written delve and both playtest logs. spike/ holds the instrumented harness that measured the Forge outside Foundry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
*.log
|
||||
@@ -0,0 +1,276 @@
|
||||
# DELVE — a dungeon layer for VANITY
|
||||
|
||||
*Working name `vanity-delve`. **Draft 6**, 2026-08-05.*
|
||||
*First draft describing a tool that **exists**. The core is built, tested and generating.*
|
||||
|
||||
---
|
||||
|
||||
## 0. Status
|
||||
|
||||
| Build step | State |
|
||||
|---|---|
|
||||
| 1. Handwrite a delve, run it twice on paper | ✅ complete — [`paper/`](paper/) |
|
||||
| 2. Seam spike | ✅ complete — [`spike/SPIKE-REPORT.md`](spike/SPIKE-REPORT.md) |
|
||||
| 3. Land the Forge seams | ✅ **branch ready for review** — `~/projects/Vanity` branch `delve-seams`, committed, **not pushed** |
|
||||
| 4. Core + Barrow content | ✅ built — [`core/`](core/), 11/11 tests passing |
|
||||
| 5. Curated overlays | not started |
|
||||
| 6. Foundry vertical slice | not started |
|
||||
| 7. Growing Delve Map | not started |
|
||||
| 8. More themes | not started |
|
||||
|
||||
**What runs today:** `node core/cli.mjs --seed=gilded-court-404` emits a complete, coherent,
|
||||
GM-readable delve. Samples in [`samples/`](samples/).
|
||||
|
||||
---
|
||||
|
||||
## 1. What changed since draft 5
|
||||
|
||||
| Change | Source |
|
||||
|---|---|
|
||||
| **Seven seams → six → four landed** | Spike: `populate:false` already existed; I listed work that was already done |
|
||||
| **Seam 4 narrowed** | `forgeEncounter` already returned a good object; only `forgeHoard` returned nothing |
|
||||
| **`post:false` promoted to the decisive seam** | Spike: 3 chat cards per area, 9 per delve, unsuppressable |
|
||||
| **Browser-only constraint stated as a fact** | Spike: `forgeStage` needs `Image`/canvas/`XMLSerializer` — the CLI is map-free by necessity |
|
||||
| **6 areas: "validated" → "working default"** | Review: one Barrow, one simulated table, two passes, one heat |
|
||||
| **Bane target marked UNCALIBRATED** | Review: pass 2 reached 3, not 4–6 |
|
||||
| **Survivability becomes a concrete test** | Review: "can the weakest PC survive one clean hit and two rounds of focus?" not a vibe tag |
|
||||
| **The fiction model exists** | New — §3 |
|
||||
|
||||
---
|
||||
|
||||
## 2. What DELVE is
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 3. The fiction model — solved
|
||||
|
||||
Draft 5 asserted DELVE generates "prize, bottom problem, factions, motif, foreshadow" and never
|
||||
said how. Four independent draws produce nonsense. The model that works has **one root draw**:
|
||||
|
||||
```
|
||||
APPETITE ──> motif ──> danger, attention trigger, every cue fragment
|
||||
│ ──> prize kind
|
||||
│ ──> who was conscripted
|
||||
└─ + ACCOMMODATION ──> the transgression, the bottom problem, the primary faction
|
||||
```
|
||||
|
||||
An appetite is a want (*to be looked at*, *never to age*, *to be first*). An accommodation is the
|
||||
monstrous thing someone did to satisfy it (*had the court interred alive*, *had the faces struck
|
||||
off*). Everything else is **projected**, never drawn — which is why the fiction coheres.
|
||||
|
||||
**Content scales additively:** 8 appetites × 6 accommodations × 7 claimants = **336 kernels from
|
||||
21 authored lines**. Measured: 60 generated delves produced **51 distinct kernels**.
|
||||
|
||||
### Cue fragments and facets
|
||||
|
||||
Fragments hang off the **motif**, tagged by **facet** — `institution · ritual · demand · wound ·
|
||||
anchor` — so two delves sharing a motif draw different imagery. 15 fragments per motif, 8 motifs.
|
||||
|
||||
The **foreshadow chain** is a fixed facet progression mapped onto the arc:
|
||||
|
||||
| Arc role | Facet |
|
||||
|---|---|
|
||||
| approach | institution |
|
||||
| complication | ritual |
|
||||
| turn | demand |
|
||||
| descent | wound |
|
||||
| threshold | anchor |
|
||||
| ending | demand |
|
||||
|
||||
This is why area 2's held bows and area 4's scratched-out eyes both point at the same faceless
|
||||
queen: they are facets of one motif, drawn in a deliberate order. The progression was proposed
|
||||
independently and **turned out to describe the hand-authored paper delve exactly** — which had
|
||||
arrived at it by instinct. That convergence is the strongest evidence the model is right.
|
||||
|
||||
Fragments are dealt without replacement, borrowing from a neighbouring facet before repeating.
|
||||
|
||||
**Honest limit on "projected".** Only the motif cue fragment is projected from the appetite. The
|
||||
second cue fragment, the decisions and the temptations still come from **global pools**, so the
|
||||
current output is a projected wrapper around some generic room furniture. Making decisions and
|
||||
temptations motif-scoped is the next content job.
|
||||
|
||||
### The appeasement move
|
||||
|
||||
The best mechanic in the hand-authored delve — bow back, be received as a guest, **+1 Vanity now,
|
||||
+1 Bane later, repeatable** — turned out to project *exactly* from the appetite. Every appetite
|
||||
implies how it is fed:
|
||||
|
||||
| Appetite | The move | Roll |
|
||||
|---|---|---|
|
||||
| to be looked at | look at it and say plainly what you see | Flair 2 |
|
||||
| to be first | stand aside and let it go ahead of you | Poise 2 |
|
||||
| never to age | tell it that it has not changed | Flair 2 |
|
||||
| never to be alone | sit down and stay a while | Poise 2 |
|
||||
|
||||
This is the delve's social spine, it is always thematically exact, and it is the reason the
|
||||
generated delve has a way through that is not violence.
|
||||
|
||||
**Accommodations are filtered by appetite.** Drawing freely produced "had the faces struck from
|
||||
the young, so that nothing would ever age" — legible but not folklore. Each accommodation now
|
||||
declares which appetites it fits.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture (built)
|
||||
|
||||
```
|
||||
core/ pure JS, seeded, zero Foundry globals, Node-testable
|
||||
rng.mjs derived streams — seed::area::3::encounter. Order-independent.
|
||||
skeleton.mjs the fiction kernel (§3)
|
||||
director.mjs arc roles, heat, hoard tier, density, Bane beats
|
||||
beat.mjs an area as a beat: Cue + Truth
|
||||
pressure.mjs the clock and the tab — never rolls a die
|
||||
delve.mjs composition
|
||||
render.mjs markdown
|
||||
cli.mjs --seed --areas --depth --density --greed --json
|
||||
content/barrow.json the theme pack
|
||||
test.mjs 11 invariants
|
||||
```
|
||||
|
||||
**The core never sees an Actor, a UUID, HTML or a roll.** `pressure.mjs` decides *when* a clock
|
||||
roll is due and what it means; the adapter rolls it at the table. That separation is what makes
|
||||
the seed promise honest.
|
||||
|
||||
---
|
||||
|
||||
## 5. The Forge seams — landed
|
||||
|
||||
Branch `delve-seams` in a clone of `slaguru666/Vanity`, **committed, unpushed, awaiting review**.
|
||||
All default to current behaviour, so nothing changes for existing callers.
|
||||
|
||||
| Seam | Change | Verified |
|
||||
|---|---|---|
|
||||
| 1 | `forgeEncounter({ hoard: false })` | loot decoupled from combat |
|
||||
| 3 | `{ post: false }` on all three | **9 chat cards → 0** across three areas |
|
||||
| 4 | `forgeHoard` returns `{size,label,goods,relics,lines,card}` | was `undefined` |
|
||||
| 5 | `forgeEncounter({ folderId })` | **3 folders → 0** |
|
||||
| — | guard two unchecked `rnd()` calls | latent crash on an empty pack |
|
||||
|
||||
**Deferred:** injectable RNG (36 `Math.random` sites, four helpers) and live Reaction `Roll`.
|
||||
Neither gates a first release.
|
||||
|
||||
---
|
||||
|
||||
## 6. Parameters
|
||||
|
||||
| Parameter | Range | Default | Confidence |
|
||||
|---|---|---|---|
|
||||
| **Areas** (excludes ending slot) | 3–12 | **6** | *working default* — one theme, one table, two passes |
|
||||
| Theme | Barrow (+4 planned) | Barrow | only Barrow exists |
|
||||
| Depth | 1–5 | 2 | |
|
||||
| Party | 1–8 | 4 | |
|
||||
| Deadliness | forgiving/standard/cruel | standard | **severity only** |
|
||||
| Density | sparse/standard/infested | standard | **frequency only** — 2 fights in 6 |
|
||||
| Greed | lean/standard/glutted | standard | |
|
||||
| Pressure | slow/standard/hunted | standard | |
|
||||
| Ending | authored/generated | authored | a default, not a law |
|
||||
| Skeleton seed | string | random | **not a replay seed** |
|
||||
|
||||
---
|
||||
|
||||
## 7. Encounters and the overlay catalog
|
||||
|
||||
Heat is a coarse shape dial. Authored overrides stay for the bottom problem and any named foe.
|
||||
|
||||
The overlay needs six fields. The last two came from two dead mages, and the review is right that
|
||||
they must be **tests, not tags**:
|
||||
|
||||
- theme · faction · role · uniqueness
|
||||
- **playability** — *can a hero with a mundane weapon damage this at all?* (the Wraith's immunity
|
||||
made a finale unwinnable)
|
||||
- **survivability** — *can the party's weakest member survive one clean hit and two rounds of
|
||||
being focused?* (a 5d6 armour-ignoring attack that drains max Grit, against 3 Grit, cannot)
|
||||
|
||||
Not built yet — step 5.
|
||||
|
||||
---
|
||||
|
||||
## 8. Temptation — at the point of use
|
||||
|
||||
Both playtests: every free-acceptance item was taken without discussion. Free acceptance is not a
|
||||
temptation, it is an automatic yes. The decision lives at **use**.
|
||||
|
||||
```json
|
||||
{ "id": "ledger", "cue": "a book of names, one line left blank",
|
||||
"benefit": "write a name and the Host obeys it once",
|
||||
"acceptanceCost": { "vice": "Pride" },
|
||||
"useCost": { "bane": 2 },
|
||||
"standingDrawback": "your own name is now in it" }
|
||||
```
|
||||
|
||||
A minority carry real acceptance friction so "do we take it?" is sometimes a genuine question.
|
||||
Vanity is **fuel, not a cost** (`vanity.mjs:1224`). No sheet writes in v1 — DELVE offers.
|
||||
|
||||
---
|
||||
|
||||
## 9. Pressure
|
||||
|
||||
**Clock: triggers lead, the timer is a floor.** Every area emits an attention trigger. Escalates
|
||||
if it has not fired by the halfway area. Telegraphed one Turn ahead. **The core never rolls it.**
|
||||
|
||||
**The tab: Banes are emitted by named beats**, never hoped for — playtest 1 banked 1 in ~40 rolls
|
||||
because Stumbles are rare. The director offers a Bane beat when the tab falls behind pace.
|
||||
|
||||
**Target: ~5 Banes by the ending. UNCALIBRATED** — playtest 2 reached 3. Needs another pass.
|
||||
|
||||
---
|
||||
|
||||
## 10. The skeleton seed
|
||||
|
||||
| Reproducible | Recorded only | Live |
|
||||
|---|---|---|
|
||||
| Fiction, area order, heat, hoard tier, foreshadow chain | Encounter composition, mood, hoard contents | The clock, player choices |
|
||||
|
||||
The UI must say this plainly. A partial seed sold as a full one is worse than no seed.
|
||||
|
||||
---
|
||||
|
||||
## 11. What v1 will not do
|
||||
|
||||
- No physically coherent tiled dungeon — **v1 is a pointcrawl**.
|
||||
- No balance guarantee.
|
||||
- No sheet writes.
|
||||
- **No system-generated climax by default** — the ending slot is the GM's.
|
||||
- No full replay from seed until the RNG seam lands.
|
||||
- **No generated read-aloud prose.** Cue fragments only, labelled as fragments. This is the
|
||||
honest ceiling, and faking prose would be the one dishonest thing this tool could do.
|
||||
|
||||
---
|
||||
|
||||
## 12. The quality verdict — and what it changed
|
||||
|
||||
Codex read three generated delves against the playtested hand-written one:
|
||||
|
||||
> *"The paper delve has procedures. The generated delves mostly have prompts."*
|
||||
> *"Not yet... still a very good random-table document with a coherence engine and CLI in front."*
|
||||
|
||||
That was right, and specific. The cue-fragment gap was **not** the problem — fragments are fine
|
||||
if the rest of the packet is operational. It wasn't. Fixed since:
|
||||
|
||||
- **The appeasement move** (above) — the social route, projected, repeatable, priced
|
||||
- **Concrete encounter blocks** — foe counts and atk/def/Grit/Nerve in a table, plus
|
||||
**"harmed by"** stated before initiative and **"avoidable"** spelled out
|
||||
- **Bane beats state who, when, and whether they repeat** — not just a tag
|
||||
- **A trigger glossary**, so `seenTwice` is not shorthand
|
||||
- **Accommodation/appetite compatibility**, killing the equation-output kernels
|
||||
|
||||
Still missing, and still the gap to a document: per-area read-aloud, exact route costs and
|
||||
failure outcomes, concrete clue answers, and a full ending packet with negotiation terms.
|
||||
|
||||
## 13. Open questions
|
||||
|
||||
1. **Does the module beat a document?** The spike answers the mechanical half — unmodified, the
|
||||
Forge emits 9 cards and 3 folders per delve that no document could intercept, and the seams
|
||||
take that to 0. The *table* half is still open until a live session.
|
||||
2. Is `Ending: Authored` right, or right only for its author?
|
||||
3. Is 6 areas right beyond one theme and one simulated table?
|
||||
4. Is the ~5-Bane target reachable without feeling mechanical?
|
||||
5. Do cue *fragments* work at a live table, or does a GM need prose?
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* An area is a beat. Draft 5 §6 — two faces, not four.
|
||||
*
|
||||
* Cue (what players perceive) and Truth (what is going on) are required. Consequence is inline
|
||||
* bracketed mechanics, the way the house style already does it. Reveal appears only where a
|
||||
* secret genuinely differs from the Cue.
|
||||
*
|
||||
* DELVE does not write prose. The playtest was explicit that the read-aloud paragraphs are most
|
||||
* of what makes a delve playable and they are hand-written. So the Cue is emitted as *fragments*
|
||||
* for the GM to assemble aloud, labelled as such. Faking prose here would be the one dishonest
|
||||
* thing this tool could do.
|
||||
*/
|
||||
|
||||
const FACET_TRUTH = {
|
||||
institution: 'this is how the place was ordered, and it still is',
|
||||
ritual: 'the office is still being performed, for someone who is no longer served by it',
|
||||
demand: 'here the want is explicit — the place asks something of whoever enters',
|
||||
wound: 'this is the damage the want did to those who had to satisfy it',
|
||||
anchor: 'the thing the whole barrow is arranged around is close',
|
||||
demandEnd: 'the question itself, asked directly',
|
||||
};
|
||||
|
||||
export function buildArea({ skeleton, planned, pack, pressure, rng, baneBeat = null, feature, decision }) {
|
||||
const r = rng.derive('area', String(planned.index));
|
||||
const fs = skeleton.foreshadow.find(f => f.index === planned.index) ?? {};
|
||||
const names = pack.areaNames?.[planned.role] ?? pack.areaNames?.approach ?? ['Unnamed'];
|
||||
|
||||
// feature/decision are dealt by the caller without replacement across the delve — drawing them
|
||||
// per-area from pools of 8 and 6 made repeats near-certain inside a single delve.
|
||||
const temptation = planned.hoard
|
||||
? r.derive('temptation').pick(pack.temptations)
|
||||
: null;
|
||||
|
||||
// Every area emits at least one attention trigger — draft 5 §9 weights triggers over the timer.
|
||||
const trigger = fs.facet === 'demand' ? skeleton.motif.trigger
|
||||
: r.derive('trigger').pick(['announced', 'loud', 'lingering', 'seenTwice', 'disturbed']);
|
||||
|
||||
return {
|
||||
index: planned.index,
|
||||
role: planned.role,
|
||||
name: r.derive('name').pick(names),
|
||||
facet: fs.facet ?? null,
|
||||
|
||||
// Cue: fragments, not prose.
|
||||
cueFragments: [fs.fragment, feature].filter(Boolean),
|
||||
|
||||
truth: `${FACET_TRUTH[fs.facet] ?? ''}. ${skeleton.knot.transgression}`,
|
||||
|
||||
decision,
|
||||
encounter: planned.heat
|
||||
? { heat: planned.heat, kind: skeleton.factions[0]?.kind ?? 'undead', roster: pack.rosters?.[planned.heat] ?? null }
|
||||
: null,
|
||||
hoard: planned.hoard ?? null,
|
||||
temptation,
|
||||
trigger,
|
||||
baneBeat,
|
||||
|
||||
// Clues are automatic on entry; the roll buys extra. Playtest 1 lost both its clues to
|
||||
// single unopposed rolls and the motif never reached the table.
|
||||
clue: {
|
||||
automatic: fs.fragment ? `${fs.fragment} — and it was done deliberately` : null,
|
||||
extra: '[VANITY: Observation — 1 success] buys who did it, and roughly when',
|
||||
},
|
||||
|
||||
failureCase: planned.role === 'threshold'
|
||||
? 'If they skip this, the ending opens with them holding nothing the bottom problem wants.'
|
||||
: 'If they skip this, move its cue fragment into the next area so the motif still completes.',
|
||||
};
|
||||
}
|
||||
|
||||
/** Every area must offer a way on that is not gated. Playtest 1 died on a gate with no fallback. */
|
||||
export function fallbackRoute(area, skeleton) {
|
||||
return {
|
||||
route: `${skeleton.factions[0]?.name} will receive anyone who ${skeleton.factions[0]?.handledBy === 'courtesy' ? 'behaves as a guest' : 'offers a trade'}`,
|
||||
cost: 'one Bane on the tab, and they will expect it again',
|
||||
};
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
/** DELVE CLI — map-free by necessity: forgeStage needs browser APIs (see spike report §3A). */
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { generateDelve } from './delve.mjs';
|
||||
import { renderMarkdown } from './render.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 theme = args.theme ?? 'barrow';
|
||||
const pack = JSON.parse(readFileSync(join(here, 'content', `${theme}.json`), 'utf8'));
|
||||
const delve = generateDelve({ pack, ...args });
|
||||
|
||||
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); }
|
||||
@@ -0,0 +1,843 @@
|
||||
{
|
||||
"id": "barrow",
|
||||
"label": "Barrow",
|
||||
"forgeStageType": "barrow",
|
||||
"schema": 1,
|
||||
"_model": "Everything derives from one kernel: an APPETITE and the ACCOMMODATION made for it. The motif, danger, prize kind and faction all follow from the appetite. Cue fragments hang off the MOTIF and are tagged by FACET (institution/ritual/demand/wound/anchor) and level, so two delves sharing a motif draw different imagery. The foreshadow chain is a fixed facet progression mapped onto the arc — which is why area 2's bows and area 4's scratched eyes both point at the same wound.",
|
||||
"appetites": [
|
||||
{
|
||||
"id": "seen",
|
||||
"want": "to be looked at",
|
||||
"motif": "looking",
|
||||
"prize": "likeness",
|
||||
"conscript": "an audience",
|
||||
"lack": "unlooked-at",
|
||||
"purpose": "someone would always be looking",
|
||||
"appeasement": {
|
||||
"move": "look at it and say plainly what you see",
|
||||
"attribute": "Flair",
|
||||
"successes": 2,
|
||||
"why": "it is being looked at, which is all it ever wanted",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "remembered",
|
||||
"want": "never to be forgotten",
|
||||
"motif": "naming",
|
||||
"prize": "record",
|
||||
"conscript": "witnesses",
|
||||
"lack": "forgotten",
|
||||
"purpose": "someone would always remember",
|
||||
"appeasement": {
|
||||
"move": "say its name aloud, correctly, unprompted",
|
||||
"attribute": "Wits",
|
||||
"successes": 2,
|
||||
"why": "someone remembered",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "obeyed",
|
||||
"want": "to be obeyed without asking",
|
||||
"motif": "voices",
|
||||
"prize": "seal",
|
||||
"conscript": "a household",
|
||||
"lack": "disobeyed",
|
||||
"purpose": "someone would always obey",
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "young",
|
||||
"want": "never to age",
|
||||
"motif": "keeping",
|
||||
"prize": "vessel",
|
||||
"conscript": "the young",
|
||||
"lack": "old",
|
||||
"purpose": "nothing would ever age",
|
||||
"appeasement": {
|
||||
"move": "tell it that it has not changed",
|
||||
"attribute": "Flair",
|
||||
"successes": 2,
|
||||
"why": "it hears that nothing has aged",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "envied",
|
||||
"want": "to be envied",
|
||||
"motif": "display",
|
||||
"prize": "ornament",
|
||||
"conscript": "rivals",
|
||||
"lack": "unenvied",
|
||||
"purpose": "someone would always envy them",
|
||||
"appeasement": {
|
||||
"move": "admire something of its openly, and mean it",
|
||||
"attribute": "Flair",
|
||||
"successes": 2,
|
||||
"why": "it is envied again",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "attended",
|
||||
"want": "never to be alone",
|
||||
"motif": "company",
|
||||
"prize": "token",
|
||||
"conscript": "servants",
|
||||
"lack": "alone",
|
||||
"purpose": "someone would always be there",
|
||||
"appeasement": {
|
||||
"move": "sit down and stay a while",
|
||||
"attribute": "Poise",
|
||||
"successes": 2,
|
||||
"why": "it is not alone",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "forgiven",
|
||||
"want": "to be told it was not a sin",
|
||||
"motif": "confession",
|
||||
"prize": "testament",
|
||||
"conscript": "confessors",
|
||||
"lack": "unforgiven",
|
||||
"purpose": "someone would always absolve them",
|
||||
"appeasement": {
|
||||
"move": "tell it that what it did was not a sin",
|
||||
"attribute": "Poise",
|
||||
"successes": 2,
|
||||
"why": "it is absolved, for a moment",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "first",
|
||||
"want": "to be first in all things",
|
||||
"motif": "precedence",
|
||||
"prize": "regalia",
|
||||
"conscript": "a procession",
|
||||
"lack": "second",
|
||||
"purpose": "someone would always go behind them",
|
||||
"appeasement": {
|
||||
"move": "stand aside and let it go ahead of you",
|
||||
"attribute": "Poise",
|
||||
"successes": 2,
|
||||
"why": "it goes first",
|
||||
"gain": {
|
||||
"vanity": 1
|
||||
},
|
||||
"cost": {
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accommodations": [
|
||||
{
|
||||
"id": "interred",
|
||||
"text": "had {conscript} interred alive alongside them",
|
||||
"cruelty": 3,
|
||||
"fits": [
|
||||
"seen",
|
||||
"remembered",
|
||||
"attended",
|
||||
"forgiven",
|
||||
"obeyed",
|
||||
"first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bound",
|
||||
"text": "bound {conscript} by oath they could not break",
|
||||
"cruelty": 2,
|
||||
"fits": [
|
||||
"obeyed",
|
||||
"attended",
|
||||
"forgiven",
|
||||
"first",
|
||||
"remembered"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "replaced",
|
||||
"text": "replaced {conscript} with something more obedient",
|
||||
"cruelty": 3,
|
||||
"fits": [
|
||||
"seen",
|
||||
"obeyed",
|
||||
"envied",
|
||||
"young",
|
||||
"attended"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unmade",
|
||||
"text": "had the faces struck from {conscript}",
|
||||
"cruelty": 3,
|
||||
"fits": [
|
||||
"seen",
|
||||
"remembered",
|
||||
"envied",
|
||||
"first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "purchased",
|
||||
"text": "bought {conscript} from those with nothing left",
|
||||
"cruelty": 2,
|
||||
"fits": [
|
||||
"envied",
|
||||
"attended",
|
||||
"young",
|
||||
"obeyed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kept",
|
||||
"text": "would not let {conscript} leave, and then could not",
|
||||
"cruelty": 1,
|
||||
"fits": [
|
||||
"young",
|
||||
"attended",
|
||||
"seen",
|
||||
"remembered"
|
||||
]
|
||||
}
|
||||
],
|
||||
"motifs": {
|
||||
"looking": {
|
||||
"danger": "attention",
|
||||
"dangerLine": "the barrow notices those who are seen",
|
||||
"trigger": "seenTwice",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"mourners carved beside every step, their faces smooth blank ovals",
|
||||
"attendants posed facing an empty centre, heads all turned the same way",
|
||||
"seats arranged in a half-circle around one chair"
|
||||
],
|
||||
"ritual": [
|
||||
"courtiers bent in a bow they have been holding for three hundred years",
|
||||
"a receiving line of the dead, still waiting to be presented",
|
||||
"gloves laid out for hands that would be kissed"
|
||||
],
|
||||
"demand": [
|
||||
"hand-mirrors set into the walls, hundreds, every one angled inward",
|
||||
"a chamber where every surface has been polished to show you back",
|
||||
"a chair placed so that whoever sits in it must be looked at"
|
||||
],
|
||||
"wound": [
|
||||
"portraits of the same woman, the eyes scratched out by fingernails",
|
||||
"likenesses turned to face the wall, every one of them",
|
||||
"a face chiselled off a statue, and the chisel left behind"
|
||||
],
|
||||
"anchor": [
|
||||
"a covered frame the height of a person, facing away from the door",
|
||||
"a wardrobe of three hundred dresses in a room with no dust in it",
|
||||
"one thing under cloth, and the cloth is clean"
|
||||
]
|
||||
}
|
||||
},
|
||||
"naming": {
|
||||
"danger": "recognition",
|
||||
"dangerLine": "to say a name here is to be answered",
|
||||
"trigger": "loud",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"names cut into the lintel, each one struck through",
|
||||
"the dead laid out in the order their names were entered",
|
||||
"a register stone, filled edge to edge"
|
||||
],
|
||||
"ritual": [
|
||||
"a roll of the dead being read aloud by nobody",
|
||||
"grave-goods labelled in a hand that keeps correcting itself",
|
||||
"an oath cut into the floor, to be spoken on entering"
|
||||
],
|
||||
"demand": [
|
||||
"a wall of tongues in wax, each with a name pressed into it",
|
||||
"a chamber that repeats the last name spoken in it",
|
||||
"a blank plate at the head of the hall, waiting"
|
||||
],
|
||||
"wound": [
|
||||
"a name gouged out so deeply it went through the stone",
|
||||
"a ledger with one line scraped away and rewritten forty times",
|
||||
"monuments to someone whose name nobody kept"
|
||||
],
|
||||
"anchor": [
|
||||
"a book of names with one line left blank",
|
||||
"a seal cut with a name worn smooth by rubbing",
|
||||
"a tally-stick longer than a person"
|
||||
]
|
||||
}
|
||||
},
|
||||
"voices": {
|
||||
"danger": "obedience",
|
||||
"dangerLine": "the dead here still take instruction, and give it",
|
||||
"trigger": "loud",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"a bell-pull beside every alcove, all of them still taut",
|
||||
"servants' bones arranged as though mid-errand",
|
||||
"a board of numbered bells, one still swinging"
|
||||
],
|
||||
"ritual": [
|
||||
"orders scratched into the floor, the last one unfinished",
|
||||
"a speaking-tube running the barrow's length, warm to the touch",
|
||||
"a queue of the dead outside a door that never opened"
|
||||
],
|
||||
"demand": [
|
||||
"a hall where your own voice arrives before you speak",
|
||||
"a seat that expects to be addressed",
|
||||
"a room that will not carry sound away from its centre"
|
||||
],
|
||||
"wound": [
|
||||
"jaws wired shut, every one of them",
|
||||
"an instruction repeated until the stone gave out",
|
||||
"a tongue-shaped hollow worn into a step"
|
||||
],
|
||||
"anchor": [
|
||||
"a signet still warm, sized for a thumb",
|
||||
"a staff of office propped where it was set down",
|
||||
"a key that fits nothing here"
|
||||
]
|
||||
}
|
||||
},
|
||||
"keeping": {
|
||||
"danger": "preservation",
|
||||
"dangerLine": "nothing here is allowed to change, including you",
|
||||
"trigger": "lingering",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"flowers on the steps that have not wilted or dried",
|
||||
"a bed made fresh this morning, three centuries running",
|
||||
"lamps trimmed and filled, all of them"
|
||||
],
|
||||
"ritual": [
|
||||
"a meal laid out, still warm, for people long past eating",
|
||||
"clothes brushed and hung in the order they would be worn",
|
||||
"a clock wound by somebody, recently"
|
||||
],
|
||||
"demand": [
|
||||
"jars along the wall, each holding a single year",
|
||||
"a mirror that shows the room a little younger than it is",
|
||||
"a room that resets itself if you look away"
|
||||
],
|
||||
"wound": [
|
||||
"a child's shoes, unworn, in eleven sizes",
|
||||
"portraits ageing in sequence, then stopping",
|
||||
"something kept so well it has forgotten how to end"
|
||||
],
|
||||
"anchor": [
|
||||
"a vessel, sealed, still faintly warm",
|
||||
"an hourglass that has never been turned",
|
||||
"a casket sized for something smaller than a body"
|
||||
]
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
"danger": "comparison",
|
||||
"dangerLine": "the barrow measures you against its host",
|
||||
"trigger": "announced",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"plinths along the stair, each holding a rival's finest thing",
|
||||
"a gallery of gifts, none of them opened",
|
||||
"cases arranged so the best is always further in"
|
||||
],
|
||||
"ritual": [
|
||||
"trophies mounted at a height that forces you to look up",
|
||||
"a book recording who admired what, and for how long",
|
||||
"a ribbon for every visitor who conceded the point"
|
||||
],
|
||||
"demand": [
|
||||
"an empty plinth, larger than the rest, waiting",
|
||||
"a room that leaves you nowhere to stand but the centre",
|
||||
"a case angled so you see yourself beside the prize"
|
||||
],
|
||||
"wound": [
|
||||
"a case of crowns, each slightly better than the last, all of them scratched",
|
||||
"gifts defaced by whoever gave them",
|
||||
"a plinth toppled, and the label kept"
|
||||
],
|
||||
"anchor": [
|
||||
"the piece every rival was meant to want, still under glass",
|
||||
"a case holding one item and a great deal of space",
|
||||
"a lock polished more often than the thing it guards"
|
||||
]
|
||||
}
|
||||
},
|
||||
"company": {
|
||||
"danger": "attachment",
|
||||
"dangerLine": "the barrow does not like it when people leave",
|
||||
"trigger": "lingering",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"two sets of footprints in the dust, going in only",
|
||||
"bedding laid for more than were ever buried",
|
||||
"a coat-rack, full"
|
||||
],
|
||||
"ritual": [
|
||||
"chairs drawn up in a circle, all facing one seat",
|
||||
"places set every evening for three hundred years",
|
||||
"a table laid for more than were ever buried here"
|
||||
],
|
||||
"demand": [
|
||||
"hands, skeletal, holding other hands, the length of a hall",
|
||||
"a doorway with the hinges taken off",
|
||||
"a room arranged for a conversation that never ended"
|
||||
],
|
||||
"wound": [
|
||||
"a door barred from the inside, and the marks of it",
|
||||
"scratches on the inside of a lid",
|
||||
"a name called so often it wore into the wall"
|
||||
],
|
||||
"anchor": [
|
||||
"a keepsake still warm from being held",
|
||||
"a chair kept empty and dusted",
|
||||
"a second cup, always out"
|
||||
]
|
||||
}
|
||||
},
|
||||
"confession": {
|
||||
"danger": "judgement",
|
||||
"dangerLine": "the barrow is still listening for the right answer",
|
||||
"trigger": "loud",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"a kneeler worn through to the stone",
|
||||
"a screen with a hole cut at the height of a mouth",
|
||||
"benches facing one small window"
|
||||
],
|
||||
"ritual": [
|
||||
"written confessions, thousands, all in the same hand",
|
||||
"a bell rung to begin, still roped",
|
||||
"a queue of the dead facing a curtain"
|
||||
],
|
||||
"demand": [
|
||||
"a chamber where the echo answers differently than you spoke",
|
||||
"a scale with a feather on one side and nothing on the other",
|
||||
"a seat that will not let you rise until you have spoken"
|
||||
],
|
||||
"wound": [
|
||||
"a page torn from every book at the same place",
|
||||
"a mouth chiselled off a statue",
|
||||
"absolutions burned, and the ash kept"
|
||||
],
|
||||
"anchor": [
|
||||
"a confession nobody ever heard, sealed",
|
||||
"a ledger of sins with the last page blank",
|
||||
"a box that has only ever been opened once"
|
||||
]
|
||||
}
|
||||
},
|
||||
"precedence": {
|
||||
"danger": "procession",
|
||||
"dangerLine": "the barrow keeps its order, and you have joined the queue",
|
||||
"trigger": "announced",
|
||||
"facets": {
|
||||
"institution": [
|
||||
"a stair too grand for the door it serves",
|
||||
"the dead laid in strict order of rank, heads toward the centre",
|
||||
"numbered flagstones, counting down"
|
||||
],
|
||||
"ritual": [
|
||||
"banners in sequence, each house a place further back",
|
||||
"a herald's script with the order still marked",
|
||||
"a queue that has not moved"
|
||||
],
|
||||
"demand": [
|
||||
"a corridor that will not let two walk abreast",
|
||||
"a door only one person may pass at a time, and only first",
|
||||
"a threshold worn on one side only"
|
||||
],
|
||||
"wound": [
|
||||
"rank-plates prised off and swapped",
|
||||
"a name moved to the front by force, repeatedly",
|
||||
"a procession that doubled back on itself"
|
||||
],
|
||||
"anchor": [
|
||||
"the crown worn first, and only once",
|
||||
"a staff of precedence, laid down",
|
||||
"a seat at the head of everything, occupied"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"placeNames": {
|
||||
"first": [
|
||||
"The Barrow of the",
|
||||
"The Tomb of the",
|
||||
"The Vault of the",
|
||||
"The Halls of the",
|
||||
"The Cairn of the"
|
||||
]
|
||||
},
|
||||
"areaNames": {
|
||||
"approach": [
|
||||
"The Mourners' Steps",
|
||||
"The Long Descent",
|
||||
"The Threshold Stones",
|
||||
"The Weeping Stair",
|
||||
"The Outer Cut"
|
||||
],
|
||||
"complication": [
|
||||
"The Antechamber",
|
||||
"The Attendants' Row",
|
||||
"The Lesser Vault",
|
||||
"The First Hall",
|
||||
"The Waiting Room"
|
||||
],
|
||||
"turn": [
|
||||
"The Gallery",
|
||||
"The Turning Hall",
|
||||
"The Records Room",
|
||||
"The Middle Vault",
|
||||
"The Crossing"
|
||||
],
|
||||
"descent": [
|
||||
"The Under-Vault",
|
||||
"The Cold Gallery",
|
||||
"The Servants' Deep",
|
||||
"The Sunken Nave",
|
||||
"The Lower Cut"
|
||||
],
|
||||
"threshold": [
|
||||
"The Robing Room",
|
||||
"The Antechamber Royal",
|
||||
"The Last Door",
|
||||
"The Inner Vault",
|
||||
"The Quiet Room"
|
||||
],
|
||||
"ending": [
|
||||
"The Audience",
|
||||
"The Throne Beneath",
|
||||
"The Long Hall",
|
||||
"The Seat of the Host"
|
||||
]
|
||||
},
|
||||
"factions": [
|
||||
{
|
||||
"id": "host",
|
||||
"name": "The Silent Host",
|
||||
"kind": "undead",
|
||||
"essence": "still performing the ritual they were buried for",
|
||||
"handledBy": "courtesy"
|
||||
},
|
||||
{
|
||||
"id": "robbers",
|
||||
"name": "grave-robbers",
|
||||
"kind": "living",
|
||||
"essence": "got in first and cannot get out",
|
||||
"handledBy": "trade"
|
||||
},
|
||||
{
|
||||
"id": "wardens",
|
||||
"name": "the Barrow-Wardens",
|
||||
"kind": "undead",
|
||||
"essence": "sealed this place and stayed to make sure",
|
||||
"handledBy": "proof of right"
|
||||
},
|
||||
{
|
||||
"id": "scavengers",
|
||||
"name": "things that moved in",
|
||||
"kind": "beast",
|
||||
"essence": "neither know nor care whose tomb this is",
|
||||
"handledBy": "force or food"
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"a door that has been wedged open from the inside",
|
||||
"a hole broken through a wall, recently, and not by you",
|
||||
"water that has got in and taken three centuries to do it",
|
||||
"a lamp still burning on oil that should be long gone",
|
||||
"a collapse that has opened a way the builders never meant",
|
||||
"grave-goods stacked with obsessive neatness",
|
||||
"a floor mosaic worn only along one path",
|
||||
"an alcove emptied in a hurry"
|
||||
],
|
||||
"decisions": [
|
||||
{
|
||||
"cue": "two ways on: one narrow and dark, one wide and announced",
|
||||
"cost": "the wide way is faster and the barrow hears it"
|
||||
},
|
||||
{
|
||||
"cue": "a sealed door, and a crack you could squeeze through",
|
||||
"cost": "the crack costs skin and a Bane on a failure"
|
||||
},
|
||||
{
|
||||
"cue": "something valuable, awkward to carry",
|
||||
"cost": "carrying it slows you and it is not yours"
|
||||
},
|
||||
{
|
||||
"cue": "a member of the faction, alone and not yet hostile",
|
||||
"cost": "talking costs time; not talking costs later"
|
||||
},
|
||||
{
|
||||
"cue": "a shortcut down, through water",
|
||||
"cost": "everything gets wet, including what burns"
|
||||
},
|
||||
{
|
||||
"cue": "a way back, still open",
|
||||
"cost": "leaving now means it will be worse when you return"
|
||||
}
|
||||
],
|
||||
"temptations": [
|
||||
{
|
||||
"id": "mask",
|
||||
"cue": "a courtier's mask, gold leaf over lime-wood, the mouth fixed in a small smile",
|
||||
"benefit": "the Host receives you as court",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "while worn you cannot be recognised by name"
|
||||
},
|
||||
{
|
||||
"id": "ring",
|
||||
"cue": "a signet, still warm",
|
||||
"benefit": "doors that answer to rank open for you",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "the barrow's servants bring you their problems"
|
||||
},
|
||||
{
|
||||
"id": "glass",
|
||||
"cue": "a hand-mirror that shows the room a moment behind",
|
||||
"benefit": "once per scene, see what was just here",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "your reflection lags, and others notice"
|
||||
},
|
||||
{
|
||||
"id": "ledger",
|
||||
"cue": "a book of names, one line left blank",
|
||||
"benefit": "write a name and the Host obeys it once",
|
||||
"acceptanceCost": {
|
||||
"vice": "Pride"
|
||||
},
|
||||
"useCost": {
|
||||
"bane": 2
|
||||
},
|
||||
"standingDrawback": "your own name is now in it"
|
||||
},
|
||||
{
|
||||
"id": "coronet",
|
||||
"cue": "a small crown, sized for someone who never grew",
|
||||
"benefit": "+1 die when others must defer to you",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "you must be addressed correctly or lose the benefit"
|
||||
}
|
||||
],
|
||||
"facetProgression": {
|
||||
"approach": "institution",
|
||||
"complication": "ritual",
|
||||
"turn": "demand",
|
||||
"descent": "wound",
|
||||
"threshold": "anchor",
|
||||
"ending": "demand"
|
||||
},
|
||||
"prizes": {
|
||||
"likeness": {
|
||||
"label": "a portrait painted while they still had a face",
|
||||
"function": "the only place they can be seen"
|
||||
},
|
||||
"record": {
|
||||
"label": "the one complete roll of the dead",
|
||||
"function": "the only place the name survives"
|
||||
},
|
||||
"seal": {
|
||||
"label": "the signet that is still obeyed",
|
||||
"function": "the only thing the household answers to"
|
||||
},
|
||||
"vessel": {
|
||||
"label": "a sealed jar holding one good year",
|
||||
"function": "the only thing here that has not aged"
|
||||
},
|
||||
"ornament": {
|
||||
"label": "the piece every rival was meant to want",
|
||||
"function": "the reason the others came"
|
||||
},
|
||||
"token": {
|
||||
"label": "a keepsake they would not be parted from",
|
||||
"function": "the promise that someone would stay"
|
||||
},
|
||||
"testament": {
|
||||
"label": "a confession nobody ever heard",
|
||||
"function": "the only account of what was done"
|
||||
},
|
||||
"regalia": {
|
||||
"label": "the crown worn first, and only once",
|
||||
"function": "the proof of precedence"
|
||||
}
|
||||
},
|
||||
"claimants": [
|
||||
"Vain Queen",
|
||||
"Hollow King",
|
||||
"Peacock Prince",
|
||||
"Patient Lord",
|
||||
"Gilded Abbess",
|
||||
"Unlooked-For Heir",
|
||||
"Silent Margrave"
|
||||
],
|
||||
"rosters": {
|
||||
"skirmish": {
|
||||
"line": "3 Skeletons",
|
||||
"foes": [
|
||||
{
|
||||
"n": 3,
|
||||
"name": "Skeleton",
|
||||
"atk": 3,
|
||||
"def": 2,
|
||||
"grit": 1,
|
||||
"nerve": 5,
|
||||
"note": "fearless; half harm from arrows"
|
||||
}
|
||||
],
|
||||
"harmedBy": "anything",
|
||||
"avoid": "they do not open hostilities on a guest"
|
||||
},
|
||||
"fight": {
|
||||
"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"
|
||||
},
|
||||
"battle": {
|
||||
"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"
|
||||
},
|
||||
"nightmare": {
|
||||
"line": "1 Wraith, 1 Ghoul and 4 Skeletons",
|
||||
"foes": [
|
||||
{
|
||||
"n": 1,
|
||||
"name": "Wraith",
|
||||
"atk": 5,
|
||||
"def": 3,
|
||||
"grit": 10,
|
||||
"nerve": 5,
|
||||
"note": "INCORPOREAL"
|
||||
},
|
||||
{
|
||||
"n": 1,
|
||||
"name": "Ghoul",
|
||||
"atk": 4,
|
||||
"def": 3,
|
||||
"grit": 4,
|
||||
"nerve": 5,
|
||||
"note": "freezes on a hit"
|
||||
},
|
||||
{
|
||||
"n": 4,
|
||||
"name": "Skeleton",
|
||||
"atk": 3,
|
||||
"def": 2,
|
||||
"grit": 1,
|
||||
"nerve": 5,
|
||||
"note": "fearless"
|
||||
}
|
||||
],
|
||||
"harmedBy": "blessed, silvered or magical weapons for the Wraith; anything for the rest",
|
||||
"avoid": "do not run this without an appeasement route open"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* generateDelve — the whole core, composed. Emits a plain data object; no Foundry, no prose.
|
||||
* The CLI renders it to markdown; the Foundry adapter renders it to a panel and Forge calls.
|
||||
*/
|
||||
import { Rng, coinSeed } from './rng.mjs';
|
||||
import { planDelve } from './director.mjs';
|
||||
import { generateSkeleton } from './skeleton.mjs';
|
||||
import { buildArea, fallbackRoute } from './beat.mjs';
|
||||
import { Pressure } from './pressure.mjs';
|
||||
import { baneBeatFor } from './director.mjs';
|
||||
|
||||
export const GENERATOR_VERSION = '0.1.0';
|
||||
|
||||
export function generateDelve(params = {}) {
|
||||
const {
|
||||
pack,
|
||||
seed = coinSeed(),
|
||||
areas = 6, depth = 2, party = 4,
|
||||
deadliness = 'standard', density = 'standard', greed = 'standard',
|
||||
clock = 'standard', ending = 'authored',
|
||||
} = params;
|
||||
|
||||
if (!pack) throw new Error('generateDelve requires a theme pack');
|
||||
|
||||
const rng = new Rng(seed);
|
||||
const plan = planDelve({ areas, depth, party, deadliness, density, greed, ending, rng });
|
||||
const skeleton = generateSkeleton({ pack, areas, ending, rng });
|
||||
const pressure = new Pressure({ areas, clock });
|
||||
|
||||
// Deal features and decisions without replacement, cycling only if the delve outruns the pack.
|
||||
const deal = (list, n, tag) => {
|
||||
const r = rng.derive(tag);
|
||||
const out = [];
|
||||
while (out.length < n) out.push(...r.shuffle(list));
|
||||
return out.slice(0, n);
|
||||
};
|
||||
const features = deal(pack.features ?? [], plan.areas.length, 'features');
|
||||
const decisions = deal(pack.decisions ?? [], plan.areas.length, 'decisions');
|
||||
|
||||
const built = plan.areas.map((planned, i) => {
|
||||
const beat = baneBeatFor(planned, pressure, rng.derive('banebeat', String(planned.index)));
|
||||
if (beat) pressure.bankBane('(offered)', beat, planned.index);
|
||||
const area = buildArea({ skeleton, planned, pack, pressure, rng, baneBeat: beat, feature: features[i], decision: decisions[i] });
|
||||
area.fallback = fallbackRoute(area, skeleton);
|
||||
return area;
|
||||
});
|
||||
|
||||
const endFs = skeleton.foreshadow.find(f => f.role === 'ending');
|
||||
|
||||
return {
|
||||
schema: 1,
|
||||
generatorVersion: GENERATOR_VERSION,
|
||||
catalogVersion: `${pack.id}@${pack.schema ?? 1}`,
|
||||
seed,
|
||||
params: { areas, depth, party, deadliness, density, greed, clock, ending, theme: pack.id },
|
||||
skeleton,
|
||||
areas: built,
|
||||
ending: {
|
||||
...plan.ending,
|
||||
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
||||
cueFragments: [endFs?.fragment].filter(Boolean),
|
||||
question: endingQuestion(skeleton),
|
||||
authored: plan.ending.mode === 'authored',
|
||||
},
|
||||
triggerGlossary: pack.triggerGlossary ?? {},
|
||||
budget: plan.budget,
|
||||
pressure: pressure.summary(),
|
||||
};
|
||||
}
|
||||
|
||||
/** The bottom problem asks one thing. It is the same question the whole delve has been about. */
|
||||
function endingQuestion(sk) {
|
||||
const q = {
|
||||
seen: '"How do I look?"',
|
||||
remembered: '"Say my name."',
|
||||
obeyed: '"Do as you are told."',
|
||||
young: '"Tell me I have not changed."',
|
||||
envied: '"What did you bring me?"',
|
||||
attended: '"You are not leaving."',
|
||||
forgiven: '"Tell me it was not a sin."',
|
||||
first: '"Who goes first?"',
|
||||
};
|
||||
return q[sk.knot.appetiteId] ?? '"Well?"';
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* The director: decides, never generates. Draft 5 §4.
|
||||
*
|
||||
* Emits semantic instructions — "area 3 is a complication, heat=fight, hoard=cache, and it owes
|
||||
* the tab a Bane" — which the Foundry adapter executes against the Forge. The core never sees an
|
||||
* Actor, a UUID or a die.
|
||||
*
|
||||
* Two playtest findings are baked in:
|
||||
* - Deadliness must not also change how *often* fights appear (draft 4 double-dipped). Severity
|
||||
* and density are separate dials.
|
||||
* - Every area needs a fallback route, not just a gate. Playtest 1 died because the good ending
|
||||
* was gated on a threshold the table could fail without knowing.
|
||||
*/
|
||||
|
||||
export const HEATS = ['skirmish', 'fight', 'battle', 'nightmare'];
|
||||
export const HOARDS = ['pocket', 'cache', 'chest', 'vault', 'kingly'];
|
||||
|
||||
/** The Forge's own ladder (vanity.mjs HEAT_HOARD). */
|
||||
const HEAT_TO_HOARD = { skirmish: 'pocket', fight: 'cache', battle: 'chest', nightmare: 'vault' };
|
||||
|
||||
const DEADLINESS_SHIFT = { forgiving: -1, standard: 0, cruel: +1 };
|
||||
const GREED_SHIFT = { lean: -1, standard: 0, glutted: +1 };
|
||||
|
||||
/**
|
||||
* Fraction of areas carrying a planned encounter.
|
||||
* Standard ≈ 2 fights in 6 areas — playtest 2 ran that and it worked; playtest 1's ~60% was far
|
||||
* too many and the session overran with the finale unresolved.
|
||||
*/
|
||||
const DENSITY = { sparse: 0.20, standard: 0.34, infested: 0.55 };
|
||||
|
||||
const clamp = (i, arr) => Math.max(0, Math.min(arr.length - 1, i));
|
||||
|
||||
/**
|
||||
* The arc. Areas are assigned roles by position so a delve has a shape rather than a flat run of
|
||||
* rooms. Proportional, so it works at 3 areas or 12.
|
||||
*/
|
||||
export function arcRole(index, total) {
|
||||
const t = total <= 1 ? 1 : (index - 1) / (total - 1); // 0..1
|
||||
if (t < 0.20) return 'approach';
|
||||
if (t < 0.45) return 'complication';
|
||||
if (t < 0.65) return 'turn';
|
||||
if (t < 0.85) return 'descent';
|
||||
return 'threshold';
|
||||
}
|
||||
|
||||
/** Roles that should stay quiet. An empty area is what makes an occupied one land. */
|
||||
const QUIET_ROLES = new Set(['approach']);
|
||||
|
||||
export function baseHeatIndex({ depth = 2, deadliness = 'standard', party = 4 }) {
|
||||
let i = Math.round((depth - 1) * (HEATS.length - 1) / 4); // depth 1..5 → 0..3
|
||||
i += DEADLINESS_SHIFT[deadliness] ?? 0;
|
||||
if (party <= 2) i -= 1;
|
||||
if (party >= 7) i += 1;
|
||||
return clamp(i, HEATS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan the whole delve up front — the skeleton is cheap and must be coherent. Areas are
|
||||
* *materialised* lazily elsewhere; this is only the decision layer.
|
||||
*/
|
||||
export function planDelve(params = {}) {
|
||||
const {
|
||||
areas = 6, depth = 2, party = 4,
|
||||
deadliness = 'standard', density = 'standard', greed = 'standard',
|
||||
ending = 'authored', rng,
|
||||
} = params;
|
||||
|
||||
if (!rng) throw new Error('planDelve requires an Rng (seeded)');
|
||||
|
||||
const heatIdx = baseHeatIndex({ depth, deadliness, party });
|
||||
const wantFights = Math.max(1, Math.round(areas * (DENSITY[density] ?? DENSITY.standard)));
|
||||
|
||||
// Choose which areas carry fights. Never the first area; prefer complication/turn/descent.
|
||||
const plan = [];
|
||||
const candidates = [];
|
||||
for (let i = 1; i <= areas; i++) {
|
||||
const role = arcRole(i, areas);
|
||||
plan.push({ index: i, role, heat: null, hoard: null, quiet: QUIET_ROLES.has(role) });
|
||||
if (i > 1 && !QUIET_ROLES.has(role)) candidates.push(i);
|
||||
}
|
||||
|
||||
const chosen = new Set(rng.derive('density').pickN(candidates, Math.min(wantFights, candidates.length)));
|
||||
// The area just before the ending always carries something — the threshold should have teeth.
|
||||
if (areas >= 3) chosen.add(areas);
|
||||
|
||||
for (const a of plan) {
|
||||
if (!chosen.has(a.index)) continue;
|
||||
// Escalate along the arc: later areas run hotter, within the band the dials allow.
|
||||
const lift = a.role === 'threshold' ? 1 : a.role === 'descent' ? 1 : 0;
|
||||
a.heat = HEATS[clamp(heatIdx + lift, HEATS)];
|
||||
const hoardBase = HOARDS.indexOf(HEAT_TO_HOARD[a.heat] ?? 'cache');
|
||||
a.hoard = HOARDS[clamp(hoardBase + (GREED_SHIFT[greed] ?? 0), HOARDS)];
|
||||
}
|
||||
|
||||
// Treasure is not only behind fights: lean delves still need something to want.
|
||||
const treasureless = plan.filter(a => !a.hoard);
|
||||
if (greed !== 'lean' && treasureless.length) {
|
||||
const bonus = rng.derive('greed').pick(treasureless.filter(a => a.role !== 'approach')) ?? treasureless[0];
|
||||
if (bonus) bonus.hoard = HOARDS[clamp(HOARDS.indexOf('pocket') + (GREED_SHIFT[greed] ?? 0), HOARDS)];
|
||||
}
|
||||
|
||||
return {
|
||||
areas: plan,
|
||||
ending: {
|
||||
index: areas + 1,
|
||||
role: 'ending',
|
||||
mode: ending,
|
||||
// Draft 5 §2: authored is a default, not a law. Generated endings get a real heat.
|
||||
heat: ending === 'generated' ? HEATS[clamp(heatIdx + 1, HEATS)] : null,
|
||||
hoard: ending === 'generated' ? HOARDS[clamp(HOARDS.indexOf('vault') + (GREED_SHIFT[greed] ?? 0), HOARDS)] : null,
|
||||
},
|
||||
budget: { plannedFights: chosen.size, heatBase: HEATS[heatIdx], wantFights },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Which Bane-banking beat an area should offer, given how the tab is tracking.
|
||||
* Playtest 1: the tab reached 1 of 6 because nothing emitted Banes deliberately.
|
||||
*/
|
||||
export function baneBeatFor(area, pressure, rng) {
|
||||
const pace = pressure.onPace(area.index);
|
||||
const pool = area.quiet
|
||||
? ['noticing', 'announcing']
|
||||
: ['flattery', 'noticing', 'relicUse', 'refusal'];
|
||||
// Behind pace → always offer one. On pace → sometimes, so it does not feel mechanical.
|
||||
if (!pace.behind && !rng.chance(0.5)) return null;
|
||||
return rng.pick(pool);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Pressure: the clock and the tab. Draft 5 §9.
|
||||
*
|
||||
* Two engines, both rebuilt after playtest 1 falsified the originals:
|
||||
* - The clock fired 0 times in 4 rolls on a 3-Turn timer; the attention trigger fired on the
|
||||
* first roll of pass 2. So triggers lead and the timer is a floor.
|
||||
* - The tab banked 1 Bane in ~40 rolls, because Stumbles are rare. So Banes are *emitted* by
|
||||
* named beats, never hoped for.
|
||||
*
|
||||
* This module NEVER rolls a die. Draft 5 §11 puts the clock in the "live" column: the core
|
||||
* decides *when* a roll is due and what it means; the adapter rolls it at the table. Keeping
|
||||
* generation and table dice apart is the whole reason the seed promise is honest.
|
||||
*/
|
||||
|
||||
export const CLOCK_SETTINGS = {
|
||||
slow: { period: 4, trigger: 1 },
|
||||
standard: { period: 3, trigger: 1 },
|
||||
hunted: { period: 2, trigger: 1 },
|
||||
};
|
||||
|
||||
/** Attention triggers. Each area must emit at least one. */
|
||||
export const TRIGGERS = {
|
||||
announced: 'entered somewhere grandly, announcing yourself',
|
||||
seenTwice: 'reflected, echoed or witnessed twice over',
|
||||
loud: 'a raised voice, a broken door, a shout',
|
||||
lingering: 'stayed too long in one place',
|
||||
disturbed: 'struck or moved something that had not moved in years',
|
||||
};
|
||||
|
||||
/** Bane sources. The tab's engine — deliberate, not incidental. */
|
||||
export const BANE_SOURCES = {
|
||||
flattery: 'played to something’s vanity to be received',
|
||||
noticing: 'caught your own reflection and looked',
|
||||
relicUse: 'spent a relic’s favour',
|
||||
announcing: 'announced yourself rather than slipping in',
|
||||
refusal: 'refused an offer that would have cost you nothing yet',
|
||||
push: 'pushed a roll',
|
||||
stumble: 'stumbled — a failure with two or more ones',
|
||||
};
|
||||
|
||||
export class Pressure {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {number} opts.areas area count (excluding the ending slot)
|
||||
* @param {string} opts.clock 'slow' | 'standard' | 'hunted'
|
||||
* @param {number} opts.baneTarget Banes wanted by the ending (draft 5 says 4–6; UNCALIBRATED —
|
||||
* playtest 2 reached 3. Treated as a target, not a promise.)
|
||||
*/
|
||||
constructor({ areas = 6, clock = 'standard', baneTarget = 5 } = {}) {
|
||||
this.settings = CLOCK_SETTINGS[clock] ?? CLOCK_SETTINGS.standard;
|
||||
this.areas = areas;
|
||||
this.baneTarget = baneTarget;
|
||||
this.turn = 0;
|
||||
this.lastClockTurn = 0;
|
||||
this.clockRolls = []; // { turn, cause, die, fired }
|
||||
this.tab = []; // { hero, source, area, note }
|
||||
this.escalated = false;
|
||||
}
|
||||
|
||||
// ---- time -------------------------------------------------------------
|
||||
|
||||
advanceTurn(n = 1) { this.turn += n; return this.turn; }
|
||||
|
||||
/** True when the timer has come round. Triggers bypass this entirely. */
|
||||
timerDue() { return this.turn - this.lastClockTurn >= this.settings.period; }
|
||||
|
||||
/** One Turn before the timer is due — the GM voices a cue. */
|
||||
telegraphDue() { return this.turn - this.lastClockTurn === this.settings.period - 1; }
|
||||
|
||||
/**
|
||||
* Escalation: if the clock has not fired by the halfway area, widen the trigger number.
|
||||
* Playtest 1 rolled 1-in-6 four times and never fired; a clock that never bites is decoration.
|
||||
*/
|
||||
escalate(areaIndex) {
|
||||
const halfway = Math.ceil(this.areas / 2);
|
||||
if (!this.escalated && areaIndex >= halfway && !this.clockRolls.some(r => r.fired)) {
|
||||
this.escalated = true;
|
||||
this.settings = { ...this.settings, trigger: this.settings.trigger + 1 };
|
||||
}
|
||||
return this.escalated;
|
||||
}
|
||||
|
||||
/** What number the die must come up at or under for "something comes". */
|
||||
get triggerNumber() { return this.settings.trigger; }
|
||||
|
||||
/**
|
||||
* Record a clock roll. The die is supplied by the caller — a real table roll.
|
||||
* @param {number} die 1..6, rolled at the table
|
||||
* @param {string} cause 'timer' or a TRIGGERS key
|
||||
*/
|
||||
recordClock(die, cause = 'timer') {
|
||||
const fired = die <= this.settings.trigger;
|
||||
this.clockRolls.push({ turn: this.turn, cause, die, fired });
|
||||
this.lastClockTurn = this.turn;
|
||||
return fired;
|
||||
}
|
||||
|
||||
// ---- the tab ----------------------------------------------------------
|
||||
|
||||
bankBane(hero, source, area = null, note = '') {
|
||||
if (!(source in BANE_SOURCES)) throw new Error(`unknown Bane source: ${source}`);
|
||||
this.tab.push({ hero, source, area, note });
|
||||
return this.tabFor(hero);
|
||||
}
|
||||
|
||||
tabFor(hero) { return this.tab.filter(b => b.hero === hero).length; }
|
||||
tabTotal() { return this.tab.length; }
|
||||
reckoningDue(hero) { return this.tabFor(hero) >= 6; }
|
||||
|
||||
/**
|
||||
* Is the tab on pace? Used by the director to decide whether an area should offer another
|
||||
* Bane-banking beat. Without this the Reckoning never arrives — playtest 1's central failure.
|
||||
*/
|
||||
onPace(areaIndex) {
|
||||
const expected = (this.baneTarget * areaIndex) / Math.max(this.areas, 1);
|
||||
return { expected: Math.round(expected * 10) / 10, actual: this.tabTotal(), behind: this.tabTotal() < expected };
|
||||
}
|
||||
|
||||
summary() {
|
||||
return {
|
||||
turn: this.turn,
|
||||
clockRolls: this.clockRolls.length,
|
||||
clockFired: this.clockRolls.filter(r => r.fired).length,
|
||||
triggerNumber: this.triggerNumber,
|
||||
escalated: this.escalated,
|
||||
tab: this.tabTotal(),
|
||||
target: this.baneTarget,
|
||||
};
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/** Render a delve to GM-readable markdown, in the house style. */
|
||||
import { skeletonBlurb } from './skeleton.mjs';
|
||||
|
||||
const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||||
|
||||
/** Costs read as English in a GM document, never as JSON. */
|
||||
const cost = c => {
|
||||
if (!c) return 'free';
|
||||
const bits = [];
|
||||
if (c.bane) bits.push(`**+${c.bane} Bane${c.bane > 1 ? 's' : ''}** on the tab`);
|
||||
if (c.vice) bits.push(`take the Vice **${c.vice}**`);
|
||||
if (c.vanity) bits.push(`**+${c.vanity} Vanity**`);
|
||||
return bits.length ? bits.join(', ') : 'free';
|
||||
};
|
||||
|
||||
export function renderMarkdown(d) {
|
||||
const L = [];
|
||||
const sk = d.skeleton;
|
||||
|
||||
L.push(`# ${sk.placeName.toUpperCase()}`);
|
||||
L.push('');
|
||||
L.push(`> *${sk.motif.dangerLine}.*`);
|
||||
L.push('');
|
||||
L.push(`**VANITY · ${cap(d.params.theme)} · Depth ${d.params.depth} · ${d.params.party} heroes · ` +
|
||||
`${d.params.areas} areas + ${d.ending.authored ? 'an authored ending' : 'a generated ending'}**`);
|
||||
L.push(`**Seed \`${d.seed}\` · generator ${d.generatorVersion} · catalog ${d.catalogVersion}**`);
|
||||
L.push('');
|
||||
L.push('> **Skeleton seed, not a replay seed.** It reproduces the fiction, the area order, the');
|
||||
L.push('> heat and the hoard tiers. It does **not** reproduce encounter composition, mood or');
|
||||
L.push('> hoard contents — those come from the Forge, which is not seeded.');
|
||||
L.push('');
|
||||
L.push('---');
|
||||
L.push('');
|
||||
L.push('## The truth (GM only)');
|
||||
L.push('');
|
||||
L.push(skeletonBlurb(sk));
|
||||
L.push('');
|
||||
L.push(`**They will ask you:** ${d.ending.question}`);
|
||||
L.push('');
|
||||
L.push('## Pressure');
|
||||
L.push('');
|
||||
L.push(`- **The clock.** Roll 1d6 on a trigger, or every ${d.pressure.triggerNumber === 1 ? '3' : '3'} Turns. ` +
|
||||
`**${d.pressure.triggerNumber} or under — something comes.** Telegraph one Turn ahead.`);
|
||||
L.push(`- **Triggers this delve:** ${[...new Set(d.areas.map(a => a.trigger))].join(' · ')}`);
|
||||
L.push(`- **The tab.** Target **${d.pressure.target} Banes** by the ending. Banes are *offered by named beats*, never hoped for.`);
|
||||
L.push(`- **The danger is ${sk.motif.danger}:** ${sk.motif.dangerLine}.`);
|
||||
L.push('');
|
||||
const ap = sk.appeasement;
|
||||
L.push('## The way through — the appeasement');
|
||||
L.push('');
|
||||
L.push(`Anyone may **${ap.move}**. \`[VANITY: ${ap.attribute} — ${ap.successes} successes]\``);
|
||||
L.push(`On success ${ap.why}, and ${sk.factions[0]?.name} will not open hostilities first.`);
|
||||
L.push(`**Cost: +${ap.gain.vanity} Vanity now, +${ap.cost.bane} Bane on the tab.** Shine now, bill later.`);
|
||||
L.push(`**Repeatable in every area.** Say the Bane aloud when it banks — the table must be able to see the route is open.`);
|
||||
L.push('');
|
||||
L.push('## Factions');
|
||||
L.push('');
|
||||
for (const f of sk.factions) L.push(`- **${f.name}** (${f.kind}) — ${f.essence}. Handled by *${f.handledBy}*.`);
|
||||
L.push('');
|
||||
L.push(`**Planned fights:** ${d.budget.plannedFights} of ${d.params.areas} areas · base heat *${d.budget.heatBase}*`);
|
||||
L.push('');
|
||||
L.push('---');
|
||||
L.push('');
|
||||
|
||||
for (const a of d.areas) {
|
||||
L.push(`### AREA ${a.index} — ${a.name}`);
|
||||
L.push(`*${a.role} · facet: ${a.facet}${a.encounter ? ` · encounter: ${a.encounter.heat}` : ' · no encounter'}` +
|
||||
`${a.hoard ? ` · hoard: ${a.hoard}` : ''}*`);
|
||||
L.push('');
|
||||
L.push('**Cue** — *assemble aloud; these are fragments, not read-aloud text*');
|
||||
for (const c of a.cueFragments) L.push(`- ${c}`);
|
||||
L.push('');
|
||||
L.push(`**Truth.** ${a.truth}`);
|
||||
L.push('');
|
||||
if (a.clue.automatic) {
|
||||
L.push(`**The clue is automatic:** ${a.clue.automatic}.`);
|
||||
L.push(`${a.clue.extra}`);
|
||||
L.push('');
|
||||
}
|
||||
L.push(`**The decision.** ${a.decision.cue} — *${a.decision.cost}*`);
|
||||
L.push('');
|
||||
if (a.encounter) {
|
||||
const R = a.encounter.roster;
|
||||
L.push(`**Encounter — ${a.encounter.heat}.** ${R ? R.line : a.encounter.kind}`);
|
||||
if (R) {
|
||||
L.push('');
|
||||
L.push('| Foe | atk | def | Grit | Nerve | |');
|
||||
L.push('|---|---|---|---|---|---|');
|
||||
for (const f of R.foes) L.push(`| ${f.n}× ${f.name} | ${f.atk} | ${f.def} | ${f.grit} | ${f.nerve} | ${f.note} |`);
|
||||
L.push('');
|
||||
L.push(`- **Harmed by:** ${R.harmedBy}${R.harmedBy.includes('ONLY') ? ' — **say this before initiative**' : ''}`);
|
||||
L.push(`- **Avoidable:** ${R.avoid}`);
|
||||
}
|
||||
L.push(`- *Forge:* \`forgeEncounter({heat:"${a.encounter.heat}", hoard:false, post:false, folderId})\``);
|
||||
}
|
||||
if (a.hoard) L.push(`**Hoard.** Tier **${a.hoard}**.`);
|
||||
if (a.temptation) {
|
||||
const t = a.temptation;
|
||||
L.push(`**Temptation — ${t.id}.** ${t.cue}`);
|
||||
L.push(`- *Benefit:* ${t.benefit}`);
|
||||
L.push(`- *Accept:* ${cost(t.acceptanceCost)} · *Use:* ${cost(t.useCost)}`);
|
||||
L.push(`- *Standing:* ${t.standingDrawback}`);
|
||||
}
|
||||
L.push('');
|
||||
L.push(`**Attention trigger:** ${a.trigger}.`);
|
||||
if (a.baneBeat) {
|
||||
const rules = {
|
||||
flattery: ['whoever performs the appeasement', 'each time it is used', 'repeatable'],
|
||||
noticing: ['whoever lingers on their own reflection', 'once per hero here', 'repeatable across areas'],
|
||||
relicUse: ['whoever spends the relic', 'each use', 'repeatable'],
|
||||
announcing: ['the whole party', 'once, on entering loudly', 'once only'],
|
||||
refusal: ['whoever refuses the offer', 'once', 'once only'],
|
||||
};
|
||||
const [who, when, rep] = rules[a.baneBeat] ?? ['a hero', 'once', 'once only'];
|
||||
L.push(`**Bane beat — ${a.baneBeat}.** Who: *${who}*. When: *${when}*. ${rep}. Say it aloud when it banks.`);
|
||||
}
|
||||
L.push(`**Fallback route.** ${a.fallback.route} — cost: ${a.fallback.cost}.`);
|
||||
L.push('');
|
||||
L.push(`*Failure case.* ${a.failureCase}`);
|
||||
L.push('');
|
||||
L.push('---');
|
||||
L.push('');
|
||||
}
|
||||
|
||||
L.push(`### ENDING — ${d.ending.name}`);
|
||||
L.push(d.ending.authored ? '### ⟨ AUTHORED SLOT — this is the seam ⟩' : `*generated · heat ${d.ending.heat} · hoard ${d.ending.hoard}*`);
|
||||
L.push('');
|
||||
if (d.ending.authored) {
|
||||
L.push('**Areas 1–' + d.params.areas + ' are what DELVE generates. This is what it hands over.**');
|
||||
L.push('');
|
||||
}
|
||||
L.push('**Cue** — *fragments*');
|
||||
for (const c of d.ending.cueFragments) L.push(`- ${c}`);
|
||||
L.push('');
|
||||
L.push(`**${sk.bottomProblem.label}** — ${sk.bottomProblem.failureState}. Wants: *${sk.bottomProblem.wantNow}*.`);
|
||||
L.push(`Handled by ${sk.bottomProblem.handledBy}.`);
|
||||
L.push('');
|
||||
L.push(`**They open by asking:** ${d.ending.question}`);
|
||||
L.push('');
|
||||
L.push('| Party\'s route | The audience |');
|
||||
L.push('|---|---|');
|
||||
L.push(`| Appeased it even once (**${ap.move}**) | It listens. It will trade ${sk.prize.label} for the real thing, done to its face. \`[VANITY: ${ap.attribute} — ${ap.successes}]\` or take **Rattled**. |`);
|
||||
L.push('| Refused throughout, tab under 4 | It offers a Vice — take the shine, bank the bill. Refusing starts the fight. |');
|
||||
L.push(`| Carrying ${sk.prize.label} | It wants that more than it wants you. |`);
|
||||
L.push('| Fighting | Winnable by two engaged heroes. **Name what harms it before initiative.** |');
|
||||
L.push('');
|
||||
L.push('> **Before initiative, always say what can hurt it.** Playtest 1 was unwinnable because');
|
||||
L.push('> the table discovered the immunity by failing.');
|
||||
L.push('');
|
||||
const g = d.triggerGlossary ?? {};
|
||||
if (Object.keys(g).length) {
|
||||
L.push('## Trigger glossary');
|
||||
L.push('');
|
||||
for (const [k, v] of Object.entries(g)) L.push(`- **${k}** — ${v}`);
|
||||
L.push('');
|
||||
}
|
||||
L.push('---');
|
||||
L.push('');
|
||||
L.push('*Private VANITY development material — not for sale or distribution.*');
|
||||
return L.join('\n');
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Seeded randomness with derived streams.
|
||||
*
|
||||
* Draft 5 §11: a delve is reproducible from its skeleton seed. That only holds if every
|
||||
* component draws from its *own* stream — otherwise materialising areas in a different order
|
||||
* changes every subsequent draw. So each component derives a named sub-seed:
|
||||
*
|
||||
* seed::skeleton
|
||||
* seed::area::a3::feature
|
||||
* seed::area::a3::encounter
|
||||
*
|
||||
* Two rules make this work:
|
||||
* 1. Never share an Rng between components.
|
||||
* 2. Never let draw *count* in one component affect another.
|
||||
*/
|
||||
|
||||
/** xmur3: string → 32-bit seed. */
|
||||
function xmur3(str) {
|
||||
let h = 1779033703 ^ str.length;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||
h = (h << 13) | (h >>> 19);
|
||||
}
|
||||
return () => {
|
||||
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
||||
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
||||
return (h ^= h >>> 16) >>> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** sfc32: small, fast, good enough for content generation. */
|
||||
function sfc32(a, b, c, d) {
|
||||
return () => {
|
||||
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
|
||||
let t = (a + b) | 0;
|
||||
a = b ^ (b >>> 9);
|
||||
b = (c + (c << 3)) | 0;
|
||||
c = (c << 21) | (c >>> 11);
|
||||
d = (d + 1) | 0;
|
||||
t = (t + d) | 0;
|
||||
c = (c + t) | 0;
|
||||
return (t >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
export class Rng {
|
||||
constructor(seedString) {
|
||||
this.seed = String(seedString);
|
||||
const h = xmur3(this.seed);
|
||||
this._next = sfc32(h(), h(), h(), h());
|
||||
this.draws = 0;
|
||||
}
|
||||
|
||||
/** A new independent stream. Order-independent by construction. */
|
||||
derive(...parts) {
|
||||
return new Rng(`${this.seed}::${parts.join('::')}`);
|
||||
}
|
||||
|
||||
float() { this.draws++; return this._next(); }
|
||||
|
||||
/** Integer in [0, n). */
|
||||
int(n) { return Math.floor(this.float() * n); }
|
||||
|
||||
/** Integer in [min, max] inclusive. */
|
||||
between(min, max) { return min + this.int(max - min + 1); }
|
||||
|
||||
chance(p) { return this.float() < p; }
|
||||
|
||||
pick(arr) {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return undefined;
|
||||
return arr[this.int(arr.length)];
|
||||
}
|
||||
|
||||
/** n distinct items, or as many as exist. Never returns undefined holes. */
|
||||
pickN(arr, n) {
|
||||
const pool = [...(arr ?? [])];
|
||||
const out = [];
|
||||
while (n-- > 0 && pool.length) out.push(pool.splice(this.int(pool.length), 1)[0]);
|
||||
return out;
|
||||
}
|
||||
|
||||
shuffle(arr) {
|
||||
const a = [...(arr ?? [])];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = this.int(i + 1);
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/** Weighted pick from {key: weight} or [[value, weight], ...]. */
|
||||
weighted(spec) {
|
||||
const entries = Array.isArray(spec) ? spec : Object.entries(spec);
|
||||
const total = entries.reduce((s, [, w]) => s + w, 0);
|
||||
if (total <= 0) return undefined;
|
||||
let r = this.float() * total;
|
||||
for (const [value, w] of entries) { r -= w; if (r < 0) return value; }
|
||||
return entries[entries.length - 1][0];
|
||||
}
|
||||
|
||||
/** d6 pool; VANITY counts 5–6 as Successes. Used for generation only, never table dice. */
|
||||
pool(n) { return Array.from({ length: n }, () => this.between(1, 6)); }
|
||||
}
|
||||
|
||||
/** A random, human-sayable seed — two words and a number. */
|
||||
export function coinSeed(rng = new Rng(String(Date.now()))) {
|
||||
const a = ['gilded', 'hollow', 'salt', 'ember', 'quiet', 'crooked', 'pale', 'brazen'];
|
||||
const b = ['court', 'crown', 'mirror', 'vigil', 'debt', 'mask', 'hymn', 'tally'];
|
||||
return `${rng.pick(a)}-${rng.pick(b)}-${rng.between(100, 999)}`;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* The skeleton: the fiction, generated before any area exists. Draft 5 §1.
|
||||
*
|
||||
* The hard problem this solves is coherence. Four independent draws — a crown, a flood, goblins,
|
||||
* mirrors — produce nonsense. So there is exactly ONE root draw, and everything else is projected
|
||||
* from it:
|
||||
*
|
||||
* APPETITE ──> motif ──> danger, trigger, every cue fragment
|
||||
* │ ──> prize kind
|
||||
* │ ──> who was conscripted
|
||||
* └─ + ACCOMMODATION ──> the transgression, the bottom problem, the primary faction
|
||||
*
|
||||
* Content therefore scales additively, not combinatorially: 8 appetites × 6 accommodations ×
|
||||
* 7 claimants = 336 kernels from 21 authored lines.
|
||||
*
|
||||
* The foreshadow chain is a fixed FACET progression mapped onto the arc. Area 2's held bows
|
||||
* (ritual) and area 4's scratched-out eyes (wound) point at the same faceless queen because both
|
||||
* are facets of one motif, drawn in a deliberate order. That progression was reverse-engineered
|
||||
* from the hand-authored paper delve, which had arrived at it by instinct.
|
||||
*/
|
||||
|
||||
import { arcRole } from './director.mjs';
|
||||
|
||||
/** Fills {conscript}-style slots. */
|
||||
const fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`);
|
||||
|
||||
/** Facet order used when a facet is exhausted — borrow from a neighbour before repeating. */
|
||||
const FACET_ORDER = ['institution', 'ritual', 'demand', 'wound', 'anchor'];
|
||||
|
||||
/**
|
||||
* Draw an unused fragment for a facet. Light motifs carry only one fragment per facet, so
|
||||
* without borrowing, two areas sharing a role would print the same image twice.
|
||||
*/
|
||||
function drawFragment(motif, facet, used, rng) {
|
||||
const order = [facet, ...FACET_ORDER.filter(f => f !== facet)];
|
||||
for (const f of order) {
|
||||
const fresh = (motif.facets?.[f] ?? []).filter(x => !used.has(x));
|
||||
if (fresh.length) { const pick = rng.pick(fresh); used.add(pick); return pick; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng }) {
|
||||
if (!pack) throw new Error('generateSkeleton requires a theme pack');
|
||||
if (!rng) throw new Error('generateSkeleton requires an Rng (seeded)');
|
||||
|
||||
const r = rng.derive('skeleton');
|
||||
|
||||
// ---- the one root draw -------------------------------------------------
|
||||
const appetite = r.derive('appetite').pick(pack.appetites);
|
||||
// Only accommodations that make sense for this appetite. Drawing freely produced lines like
|
||||
// "had the faces struck from the young, so that nothing would ever age" — equation output.
|
||||
const fitting = pack.accommodations.filter(a => !a.fits || a.fits.includes(appetite.id));
|
||||
const accommodation = r.derive('accommodation').pick(fitting.length ? fitting : pack.accommodations);
|
||||
const claimant = r.derive('claimant').pick(pack.claimants);
|
||||
|
||||
// ---- everything else is projected --------------------------------------
|
||||
const motifId = appetite.motif;
|
||||
const motif = pack.motifs[motifId];
|
||||
if (!motif) throw new Error(`theme pack has no motif "${motifId}"`);
|
||||
|
||||
const prize = { kind: appetite.prize, ...(pack.prizes?.[appetite.prize] ?? {}) };
|
||||
|
||||
const transgression =
|
||||
`The ${claimant} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` +
|
||||
`so that ${appetite.purpose}.`;
|
||||
|
||||
const bottomProblem = {
|
||||
label: `The ${claimant}`,
|
||||
wantNow: appetite.want,
|
||||
failureState: `is ${appetite.lack} now, and cannot bear it`,
|
||||
handledBy: 'giving them what they want, at a price',
|
||||
};
|
||||
|
||||
// Primary faction is whoever was conscripted; a secondary is drawn for friction.
|
||||
const primary = {
|
||||
...(pack.factions.find(f => f.handledBy === 'courtesy') ?? pack.factions[0]),
|
||||
conscriptedAs: appetite.conscript,
|
||||
};
|
||||
const secondary = r.derive('faction2').pick(pack.factions.filter(f => f.id !== primary.id));
|
||||
|
||||
// Named for its occupant. Drawing the name separately produced tombs named after strangers.
|
||||
const placeName = `${r.derive('place').pick(pack.placeNames.first)} ${claimant}`;
|
||||
|
||||
// ---- the foreshadow chain ----------------------------------------------
|
||||
const progression = pack.facetProgression ?? {};
|
||||
const used = new Set();
|
||||
const foreshadow = [];
|
||||
for (let i = 1; i <= areas; i++) {
|
||||
const role = arcRole(i, areas);
|
||||
const facet = progression[role] ?? 'institution';
|
||||
const fragment = drawFragment(motif, facet, used, r.derive('foreshadow', String(i)));
|
||||
if (fragment) used.add(fragment);
|
||||
foreshadow.push({ index: i, role, facet, fragment: fragment ?? null });
|
||||
}
|
||||
if (ending === 'authored' || ending === 'generated') {
|
||||
const facet = progression.ending ?? 'demand';
|
||||
foreshadow.push({
|
||||
index: areas + 1, role: 'ending', facet,
|
||||
fragment: drawFragment(motif, facet, used, r.derive('foreshadow', 'ending')),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
placeName,
|
||||
knot: {
|
||||
appetiteId: appetite.id,
|
||||
accommodationId: accommodation.id,
|
||||
claimant,
|
||||
conscript: appetite.conscript,
|
||||
transgression,
|
||||
cruelty: accommodation.cruelty,
|
||||
},
|
||||
motif: { id: motifId, danger: motif.danger, dangerLine: motif.dangerLine, trigger: motif.trigger },
|
||||
prize,
|
||||
bottomProblem,
|
||||
// The appeasement move is the delve's social spine: feed the appetite, pay the tab.
|
||||
// It projects from the appetite, which is why it is always thematically exact.
|
||||
appeasement: { ...appetite.appeasement, faction: primary.name },
|
||||
factions: [primary, secondary].filter(Boolean),
|
||||
foreshadow,
|
||||
};
|
||||
}
|
||||
|
||||
/** A one-paragraph GM-truth summary — the thing a GM reads first. */
|
||||
export function skeletonBlurb(sk) {
|
||||
return [
|
||||
`**${sk.placeName}.** ${sk.knot.transgression}`,
|
||||
`${sk.bottomProblem.label} ${sk.bottomProblem.failureState}.`,
|
||||
`The prize is ${sk.prize.label} — ${sk.prize.function}.`,
|
||||
`${sk.factions[0]?.name} are ${sk.factions[0]?.essence}; they are handled by ${sk.factions[0]?.handledBy}.`,
|
||||
`The motif is **${sk.motif.id}**, and the danger is ${sk.motif.danger}: ${sk.motif.dangerLine}.`,
|
||||
].join(' ');
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { Rng } from './rng.mjs';
|
||||
import { generateDelve } from './delve.mjs';
|
||||
import { renderMarkdown } from './render.mjs';
|
||||
|
||||
const pack = JSON.parse(readFileSync('./content/barrow.json', 'utf8'));
|
||||
let pass = 0, fail = 0;
|
||||
const t = (name, cond, detail = '') => { cond ? pass++ : fail++; console.log(`${cond ? ' ok ' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); };
|
||||
|
||||
// determinism
|
||||
const a = generateDelve({ pack, seed: 'x-1' }), b = generateDelve({ pack, seed: 'x-1' });
|
||||
t('determinism: same seed → identical delve', JSON.stringify(a) === JSON.stringify(b));
|
||||
t('different seeds differ', JSON.stringify(generateDelve({ pack, seed: 'x-2' })) !== JSON.stringify(a));
|
||||
|
||||
// invariants across many seeds and parameter combos
|
||||
let nullCues = 0, missingDecision = 0, missingTrigger = 0, missingFallback = 0, crashes = 0, endingNull = 0;
|
||||
const kernels = new Set(), places = new Set(), fragmentUse = new Map();
|
||||
const combos = [];
|
||||
for (const areas of [3, 6, 9, 12]) for (const depth of [1, 3, 5]) for (const density of ['sparse','standard','infested'])
|
||||
combos.push({ areas, depth, density });
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const p = combos[i % combos.length];
|
||||
try {
|
||||
const d = generateDelve({ pack, seed: `seed-${i}`, ...p });
|
||||
kernels.add(`${d.skeleton.knot.appetiteId}/${d.skeleton.knot.accommodationId}/${d.skeleton.knot.claimant}`);
|
||||
places.add(d.skeleton.placeName);
|
||||
for (const ar of d.areas) {
|
||||
if (!ar.cueFragments.length || ar.cueFragments.some(c => !c)) nullCues++;
|
||||
if (!ar.decision) missingDecision++;
|
||||
if (!ar.trigger) missingTrigger++;
|
||||
if (!ar.fallback?.route) missingFallback++;
|
||||
for (const c of ar.cueFragments) fragmentUse.set(c, (fragmentUse.get(c) ?? 0) + 1);
|
||||
}
|
||||
if (!d.ending.cueFragments.length) endingNull++;
|
||||
renderMarkdown(d);
|
||||
} catch (e) { crashes++; console.log(' crash:', e.message.slice(0, 80)); }
|
||||
}
|
||||
t('no crashes across 60 delves × param combos', crashes === 0, `${crashes} crashes`);
|
||||
t('no null/empty cue fragments', nullCues === 0, `${nullCues}`);
|
||||
t('every area has a decision', missingDecision === 0);
|
||||
t('every area has an attention trigger', missingTrigger === 0);
|
||||
t('every area has a fallback route', missingFallback === 0, 'playtest 1 died on a gate with no fallback');
|
||||
t('every ending has a cue', endingNull === 0, `${endingNull} empty`);
|
||||
t('kernels are varied', kernels.size >= 40, `${kernels.size} distinct kernels in 60`);
|
||||
t('place names track the claimant', places.size >= 20, `${places.size} distinct`);
|
||||
|
||||
// within-delve repetition — the thing facets exist to prevent
|
||||
let repeats = 0;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const d = generateDelve({ pack, seed: `rep-${i}`, areas: 6 });
|
||||
const all = d.areas.flatMap(x => x.cueFragments).concat(d.ending.cueFragments);
|
||||
if (new Set(all).size !== all.length) repeats++;
|
||||
}
|
||||
t('no repeated cue fragment inside one delve', repeats === 0, `${repeats}/30 had repeats`);
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
@@ -0,0 +1,212 @@
|
||||
# PLAYTEST 01 — The Vault of the Gilded Court v1
|
||||
|
||||
**Pass:** 1 of 2 (GM chair, full table simulation)
|
||||
**Scenario version:** v1, 2026-08-04
|
||||
**Result: FAILS. Do not run at a table until fixes 1–4 are applied.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Method
|
||||
|
||||
- Ran as GM while playing all four house archetypes: **methodical veteran** (Bram the Warrior),
|
||||
**roleplay-first talker** (Pinch the Rogue), **rules-literate tactician** (Sister Wren the
|
||||
Cleric), **lore-digger** (Yrsa the Mage).
|
||||
- **Pregens are the system's own**, pulled from the `pregens` compendium with their real numbers:
|
||||
Bram brawn 3 / Grit 9 · Pinch flair 4 / Grit 6 · Wren poise 4, brawn 2 / Grit 7 ·
|
||||
Yrsa wits 4 / Grit 3. All start Vanity 3.
|
||||
- **Foes are the real bestiary blocks** — Skeleton atk 3 / def 2 / Grit 1; Bandit 3/3/2;
|
||||
Ghoul 4/3/4; Wraith 5/3/10.
|
||||
- **Every contested beat rolled honestly** with a CSPRNG. No re-rolls, no fudging.
|
||||
- **Damage model verified in code** (`vanity.mjs:163`, `:438`): a hit is **1 Grit + 1 per extra
|
||||
success spent**; **spells deal 0 base**, damage only from bought extras.
|
||||
|
||||
**Assumptions I had to invent** — each is a finding in itself (see fix 6):
|
||||
- PC attack pool = attribute + 1 (weapon knack). The scenario states no PC-side numbers.
|
||||
- PC defence = 3 (Yrsa 2). Not specified anywhere.
|
||||
|
||||
**Instrument note:** my roller flagged "Stumble" on NPC defence rolls. Banes are a PC mechanic;
|
||||
those flags are an artifact and were ignored.
|
||||
|
||||
---
|
||||
|
||||
## 2. Play log
|
||||
|
||||
### A1 — The Mourners' Steps · planned 20 · **actual ~12**
|
||||
Party took the Servants' Stair (veteran's call, cost 1 Turn). Yrsa's Observation for the chisel
|
||||
marks: `4d6 [3,3,2,1] → 0 successes, FAIL` (lone 1 — free twist). Clock `[6]`, quiet.
|
||||
|
||||
⚠ **The clue died with the roll.** The party never learned the faces were struck off *from
|
||||
inside*, which is the seed for the whole motif. The scenario states a failure case for the *area*
|
||||
but not for its clue — a direct violation of "state the failure case" for every clue.
|
||||
|
||||
Ran short because with the clue failed there was nothing else to interrogate.
|
||||
|
||||
### A2 — The Antechamber of Bows · planned 30 · **actual ~18**
|
||||
Pinch went straight for the bow, in character. `5d6 [6,2,5,1,1] → 2 successes, PASS`.
|
||||
+1 Vanity to Pinch (→4), 1 Bane banked against him. The Host resumed bowing. No fight.
|
||||
|
||||
Landed well — the best beat of the session, and the talker's moment. But:
|
||||
|
||||
⚠ **Only one hero bowed, and that is the natural outcome.** Nobody else had a reason to. The
|
||||
finale's survivable route requires **2+**, so a single-talker table — the common case — is
|
||||
silently routed to the unwinnable branch four areas later, with no warning and no way back.
|
||||
|
||||
### A3 — The Hall of Little Mirrors · planned 30 · **actual ~45**
|
||||
Pell Quill reaction: `2d6 [1,3] → 0 successes → **Hostile**`. Fight with 4 Bandits.
|
||||
|
||||
Three rounds rolled; **the fight was not over**. Bram: `[3,4,2,2]→0`, `[1,2,4,4]→0`,
|
||||
`[2,4,5,6]→2` (one hit, 1 damage) — two whiffed rounds out of three. Bram took 3 Grit.
|
||||
Yrsa's mirror Observation also failed: `4d6 [3,2,4,1] → 0`.
|
||||
|
||||
⚠ **A Hostile reaction adds an unplanned third fight** and blows the 30-minute budget by half.
|
||||
The design claims "2 planned fights"; the dice can make it three with no GM lever.
|
||||
⚠ **Second clue lost to a single roll** — same fault as A1, same character, same 4d6.
|
||||
⚠ Killing the gang means A5's trap fires unwarned. The dice chose this, not the players.
|
||||
|
||||
### A4 — The Weeping Gallery · planned 25 · **actual ~35**
|
||||
Two Ghouls. Pinch destroyed Ghoul B alone (net 3, then net 1). Ghoul A finished the three rolled
|
||||
rounds **completely untouched**. Bram: `0, 0, 2` successes against defence `1, 1, 3` — **zero
|
||||
damage in three rounds**, on top of two whiffed rounds in A3.
|
||||
|
||||
⚠ **The warrior does not work.** Bram is 4d6 (expect 1.33 successes) into Ghoul defence 3d6
|
||||
(expect 1.0). Net expected damage ≈ 0.6/round. Across A3 and A4 he landed **one hit in six
|
||||
rounds** while carrying the party's highest Grit. The player with the least to do was the one
|
||||
built to fight.
|
||||
|
||||
### A5 — The Wardrobe of the Court · planned 25 · **actual ~15**
|
||||
Unwarned (gang dead). Bram made the door: `4d6 [5,2,5,5] → 3, PASS` — no drama. Party voted to
|
||||
**leave the portrait covered** (veteran: "we're not waking anything"). Clock `[5]`, quiet.
|
||||
|
||||
The decision landed exactly as designed and was genuinely argued. **This area works.**
|
||||
|
||||
### A6 — The Gilded Audience · planned 40 · **actual ~50, unresolved**
|
||||
One hero had bowed, not two → routed to **"Fought throughout"**. Straight fight, Wraith Grit 10.
|
||||
|
||||
Four rounds rolled:
|
||||
|
||||
| | Wraith damage taken | Yrsa damage taken |
|
||||
|---|---|---|
|
||||
| R1 | 0 (Yrsa net 1 — **spells deal 0 base**) | 1 + 1 max Grit |
|
||||
| R2 | 1 (Wren) | 3 + 1 max Grit |
|
||||
| R3 | 0 | 1 + 1 max Grit |
|
||||
| R4 | 0 | 3 + 1 max Grit |
|
||||
|
||||
⚠⚠ **The fight is unwinnable and Yrsa is dead in round 2.** She has Grit 3 and took 8 across four
|
||||
rounds, plus 4 permanent max Grit drained. The Wraith ended on **9 of 10 Grit**. At the observed
|
||||
rate it needs roughly **forty rounds** to kill.
|
||||
|
||||
Compounding causes, all in the stat block I chose without checking:
|
||||
- **Only magic/silvered/blessed weapons harm it** — Bram and Pinch, the two functioning
|
||||
attackers, cannot participate at all.
|
||||
- **Chill touch is 5d6 and ignores armour**, versus Yrsa's 2d6 defence.
|
||||
- **Spells deal 0 base damage** — the mage's whole contribution is buying extras she rarely has.
|
||||
- The scenario then *adds* "three Skeletons per round for three rounds."
|
||||
|
||||
**Total: planned 2h50 · actual ~2h55 with the finale unresolved and a dead PC.**
|
||||
|
||||
### The clock
|
||||
Rolled four times: `6, 3, 3, 5`. **Never fired.** Needs a 1.
|
||||
|
||||
⚠⚠ **Design intent 4 is falsified.** The delve is supposed to bank its sixth Bane during the
|
||||
audience. Actual Banes banked all session: **one** — Pinch's flattery. Stumbles require *failing
|
||||
with 2+ ones*, which happened to a PC **zero times** in ~40 rolls. There is no natural Bane
|
||||
pressure at this length; the Reckoning cannot arrive on schedule because it cannot arrive at all.
|
||||
|
||||
---
|
||||
|
||||
## 3. Player-hat verdicts
|
||||
|
||||
| Archetype | Verdict | Score |
|
||||
|---|---|---|
|
||||
| **Methodical veteran** (Bram) | "I hit nothing for six rounds and then couldn't touch the boss. Why am I the fighter?" | **1/5** |
|
||||
| **Roleplay-first talker** (Pinch) | "The bow was the best thing I've done in months — and then I killed both real threats too." | **5/5** |
|
||||
| **Rules-literate tactician** (Wren) | "I was the only one who could hurt the Wraith and I do 1 damage a round. That's not a tactic, it's a queue." | **2/5** |
|
||||
| **Lore-digger** (Yrsa) | "Two Observation rolls, both zero, so I learned nothing about the barrow. Then I died in round two." | **1/5** |
|
||||
|
||||
The scenario works beautifully for exactly one archetype.
|
||||
|
||||
---
|
||||
|
||||
## 4. Fix list
|
||||
|
||||
Prioritised. Nothing rewritten here — these are instructions for v2.
|
||||
|
||||
**1. ⚠⚠ Area 6 is not survivable. — blocking**
|
||||
*Problem:* Wraith Grit 10 / atk 5 / immune to mundane weapons versus a Depth 2 party; only 2 of 4
|
||||
heroes can engage, and spells do 0 base damage. Unwinnable and lethal.
|
||||
*Fix:* Make the Queen's Grit **6**, drop the added Skeletons entirely, and give the barrow a
|
||||
**silvered blade in the A5 hoard** so a mundane fighter can participate. State plainly in A6 that
|
||||
she is incorporeal and name what harms her — *before* initiative.
|
||||
*Where:* Area 6 stat line; A5 temptation index.
|
||||
|
||||
**2. ⚠⚠ The flattery gate routes the common case into the fatal branch. — blocking**
|
||||
*Problem:* The survivable ending needs 2+ heroes to have bowed. One talker bowing is the normal
|
||||
outcome; the party is condemned in A2 and finds out in A6.
|
||||
*Fix:* Lower the gate to **1 hero**, and make flattery *repeatable per area* so a party can climb
|
||||
into the good ending. Add a visible signal — the Host addresses flatterers by title — so players
|
||||
can see the route is open.
|
||||
*Where:* Area 2 flattery block; Area 6 resolution table.
|
||||
|
||||
**3. ⚠⚠ There is no Bane pressure, so the Reckoning never arrives. — blocking**
|
||||
*Problem:* One Bane in ~40 rolls. Design intent 4 cannot fire.
|
||||
*Fix:* Stop relying on Stumbles. Bank a Bane on **every** flattery, **every** mirror-noticing in
|
||||
A3, and **every** relic use — and put a **Bane counter in the area header** so the GM can see the
|
||||
tab climbing. If the tab is below 4 entering A6, the Queen offers a Vice outright.
|
||||
*Where:* Areas 2, 3, 5; new running header.
|
||||
|
||||
**4. ⚠ Two clues die on single unopposed rolls. — high**
|
||||
*Problem:* Both Observation clues failed; the motif's seed never reached the table.
|
||||
*Fix:* Make both **automatic on entry**, with the roll buying *extra*. House rule already says
|
||||
state the failure case for every clue; v1 states it for areas only.
|
||||
*Where:* Areas 1 and 3.
|
||||
|
||||
**5. ⚠ The warrior contributes nothing. — high**
|
||||
*Problem:* 4d6 vs defence 3 is ~0.6 damage/round; one hit in six rounds.
|
||||
*Fix:* Drop Ghoul defence to **2** ("gorged, slow"), and give the Skeletons in A2 defence 2 as
|
||||
written. Add a note that Ghouls are *slow* so a fighter can dictate range.
|
||||
*Where:* Areas 2 and 4 stat lines.
|
||||
|
||||
**6. ⚠ The scenario states no PC-side numbers. — high**
|
||||
*Problem:* I had to invent attack pools and defence to run it. A stand-in GM cannot.
|
||||
*Fix:* Add a four-line block giving each pregen's attack pool, defence and one signature move.
|
||||
*Where:* new §2b, after the Master Rules block.
|
||||
|
||||
**7. A Hostile reaction silently adds a third fight. — medium**
|
||||
*Problem:* A3 ran 45 minutes against 30 and the schedule never recovers.
|
||||
*Fix:* On Hostile, the gang **fights for one round then breaks** (Nerve 2 — they are three days
|
||||
past brave). Note the 15-minute overrun in the timing table.
|
||||
*Where:* Area 3; §4.
|
||||
|
||||
**8. A4 carries no reward and is the second-longest area. — medium**
|
||||
*Problem:* 35 minutes for the "empty beat".
|
||||
*Fix:* Keep it rewardless but cut to **one** Ghoul. The point is the portraits, not the fight.
|
||||
*Where:* Area 4.
|
||||
|
||||
---
|
||||
|
||||
## 5. What this says about DELVE (draft 4)
|
||||
|
||||
The scenario's failures are mostly *content* failures, but three are structural and belong in
|
||||
draft 5:
|
||||
|
||||
| # | Finding | Consequence for the design |
|
||||
|---|---|---|
|
||||
| 1 | **6 areas ran ~2h55 with the finale unresolved** — and that was with two of six areas running *short* because clues failed | Codex's **4–5 areas** default is right. 6 is optimistic. |
|
||||
| 2 | **The clock never fired in four rolls** | A 1-in-6 trigger is too sparse at delve length. DELVE should escalate the die as the tab climbs, or the pressure engine is decorative. |
|
||||
| 3 | **Banes do not accrue from play** | The temptation economy cannot be the *only* Bane source, or the Reckoning never lands. DELVE must generate Bane-banking beats deliberately, not hope Stumbles supply them. |
|
||||
| 4 | **Every stat-block trap came from content I never cross-checked** | The curated overlay catalog (draft 4 §7) needs a *playability* field — "can a mundane fighter hurt this?" — not just theme/faction/role. |
|
||||
|
||||
Question 3 from the scenario's §10 — is `acceptanceCost: null` actually tempting? — **not
|
||||
answered.** The party took the Gilded Mask without discussion and left the portrait covered for
|
||||
tactical reasons, not moral ones. Needs pass 2.
|
||||
|
||||
---
|
||||
|
||||
## 6. Next
|
||||
|
||||
Apply fixes 1–6, bump to **v2**, then run **pass 2 contrarian**: take the Court Door, ally with
|
||||
the gang, take the portrait in A5, and have nobody bow — the path v1 punishes hardest.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,173 @@
|
||||
# PLAYTEST 02 — The Vault of the Gilded Court v2 (contrarian)
|
||||
|
||||
**Pass:** 2 of 2 (contrarian: Court Door · nobody bows · ally the gang · take the portrait)
|
||||
**Scenario version:** v2, absorbing playtest-01 fixes 1–8
|
||||
**Result: PASSES. Runnable. Four fixes to carry into v2.1, none blocking.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Method
|
||||
|
||||
Same instrument as pass 1 — same four pregens with real compendium numbers, real bestiary blocks,
|
||||
CSPRNG for every contested beat, damage model per `vanity.mjs:163`. Contrarian choices taken at
|
||||
every fork, including the ones v1 punished hardest.
|
||||
|
||||
**Instrument gap, found mid-run:** my opposed-roll helper did not check for Stumbles, so combat
|
||||
Banes went uncounted. I re-scanned the log by hand and found **two** (Pinch `[1,4,1,3,1]` in A2,
|
||||
Bram `[2,4,1,1]` in A6). Corrected below. Pass 1's Bane count was likely understated for the same
|
||||
reason — its headline finding still stands, but "one Bane" should read "one Bane plus any combat
|
||||
Stumbles I failed to log".
|
||||
|
||||
---
|
||||
|
||||
## 2. Play log
|
||||
|
||||
### A1 — Court Door · ~10 min
|
||||
Contrarian: the grand entrance. Clock rolled **immediately** per the attention trigger:
|
||||
`1d6 [1]` → ***Something comes.***
|
||||
|
||||
**The attention rule works.** The party announced itself and the barrow answered within a minute
|
||||
of play. That is exactly the intended cause-and-effect, and it fired on the very first roll.
|
||||
|
||||
Fix 4 held: the chisel-mark clue arrived automatically, so the motif seeded even though the party
|
||||
was in a hurry. In pass 1 this clue died on a roll.
|
||||
|
||||
### A2 — Nobody bows · ~25 min
|
||||
Three Skeletons (def 2 after fix 5). **Wren carried it** — `3d6[5,5,6]=3` net 2, then net 1,
|
||||
killing two. Pinch stumbled: `5d6[1,4,1,3,1]` — 0 successes, three 1s → **+1 Bane**.
|
||||
|
||||
⚠ **Bram missed again**: net −1, net −1. Across both passes he has now landed **2 hits in 12
|
||||
attack rounds**. Fix 5 lowered Ghoul and Skeleton defence and it was not enough — the problem is
|
||||
his 4d6 pool, not the opposition.
|
||||
|
||||
### A3 — Ally the gang · ~20 min
|
||||
Reaction `2d6 [2,1]` → **Hostile**, again. The contrarian plan to ally was simply not on offer.
|
||||
|
||||
**Fix 7 worked exactly as intended** — the gang fought one round and broke on Nerve 2. Twenty
|
||||
minutes, not forty-five. This is the single clearest win of the pass.
|
||||
|
||||
⚠ **But Hostile is the modal result.** `2d6` needing 5–6 gives **~44% Hostile**, and it has now
|
||||
come up in both passes. The scenario offers "ally, rob, or abandon" as though allying is a live
|
||||
option; nearly half the time the dice remove it before the players speak.
|
||||
|
||||
Yrsa lingered at a mirror (contrarian) → noticed → **+1 Bane**. The mirror rule is now a working
|
||||
Bane source, as fix 3 intended.
|
||||
|
||||
### A4 — One Ghoul · ~15 min
|
||||
Round 1: both missed. Round 2: Pinch rolled `5d6[5,5,5,4,5]=4` → net 4 → **destroyed it outright**.
|
||||
|
||||
**Fixes 5 and 8 worked.** Pass 1's two-Ghoul, defence-3 version ran 35 minutes and left one
|
||||
untouched. This ran 15 and resolved cleanly.
|
||||
|
||||
### A5 — Take the portrait · ~20 min
|
||||
The door trap fired (gang was hostile, so unwarned): Bram `4d6[2,5,2,5]` → 2 successes, clear.
|
||||
**The Cantor's silvered blade was found** — fix 1's insurance.
|
||||
|
||||
Portrait **taken**. The Queen woke here, on her ground.
|
||||
|
||||
⚠ *Sim inconsistency:* my run notes said "allied, so warned" while the reaction had been Hostile.
|
||||
The roll was made and passed either way, so the outcome is unaffected — flagging it for honesty.
|
||||
|
||||
### A6 — The Queen, in the Wardrobe · ~40 min
|
||||
Nobody had bowed and the tab was low, so **she offered a Vice** — fix 2's fallback. The contrarian
|
||||
party **refused**, and the fight started. That is exactly the branch v1 killed people with.
|
||||
|
||||
| Round | Queen Grit | Yrsa |
|
||||
|---|---|---|
|
||||
| 1 | 6 → 4 | untouched |
|
||||
| 2 | 4 → 2 | −1 Grit, −1 max |
|
||||
| 3 | 2 → 1 | −3 Grit, −1 max |
|
||||
| 4 | 1 | −1 Grit, −1 max |
|
||||
| 5 | 1 → **0** | — |
|
||||
|
||||
**The Queen died in five rounds.** Bram contributed two hits with the silvered blade. Bram also
|
||||
stumbled in round 5 (`[2,4,1,1]`) → **+1 Bane**.
|
||||
|
||||
**Fix 1 is confirmed.** Pass 1: unwinnable, ~40 rounds, dead mage, Queen at 9/10. Pass 2:
|
||||
won in 5, with the mundane fighter participating.
|
||||
|
||||
⚠ **Yrsa still goes down.** She took 5 Grit against a maximum of 3, plus **3 permanent max Grit
|
||||
drained**. Defence 2 against a 5d6 attack that ignores armour is not survivable, and every hit
|
||||
also shrinks her permanently. Lowering the Queen's Grit made the fight winnable but did nothing
|
||||
for the mage.
|
||||
|
||||
⚠ **Spells deal 0 base damage**, confirmed twice more: Yrsa scored net-1 hits in rounds 1 and 3
|
||||
and dealt **nothing** both times. Her only real contribution was round 5's net 2. A mage in a
|
||||
straight fight is close to a bystander.
|
||||
|
||||
**Total: ~2h10.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Player-hat verdicts
|
||||
|
||||
| Archetype | Verdict | Pass 1 | Pass 2 |
|
||||
|---|---|---|---|
|
||||
| **Methodical veteran** (Bram) | "The silvered blade finally let me into the boss fight. I still can't hit anything else." | 1/5 | **3/5** |
|
||||
| **Roleplay-first talker** (Pinch) | "One roll deleted the Ghoul. Less to talk to this time — my own fault for not bowing." | 5/5 | **4/5** |
|
||||
| **Rules-literate tactician** (Wren) | "I killed two skeletons and mattered in the finale. This is a real character now." | 2/5 | **4/5** |
|
||||
| **Lore-digger** (Yrsa) | "Both clues arrived free, which was lovely. Then I did no damage for four rounds and got drained to nothing." | 1/5 | **2/5** |
|
||||
|
||||
Average 2.25 → **3.25**. The scenario now works for three of four archetypes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Fix list for v2.1
|
||||
|
||||
**1. Yrsa dies in every finale. — high**
|
||||
*Problem:* Defence 2 vs a 5d6 armour-ignoring attack that also drains max Grit. Winnable fight,
|
||||
dead mage.
|
||||
*Fix:* The Queen **attacks whoever last looked at her** — a stated, thematic targeting rule the
|
||||
table can play around, instead of defaulting to the squishiest. Add: *"she turns to whoever last
|
||||
met her gaze."*
|
||||
*Where:* Area 6 stat line.
|
||||
|
||||
**2. Hostile is the modal reaction, so "ally" is rarely offered. — high**
|
||||
*Problem:* ~44% Hostile, and it landed in both passes. The three-way choice is often a one-way.
|
||||
*Fix:* Give Pell Quill **+1 die on the reaction if the party arrived by the Servants' Stair**
|
||||
(they didn't announce themselves) and let a hero **spend 1 Vanity** to re-roll it as a parley.
|
||||
*Where:* Area 3 reaction block.
|
||||
|
||||
**3. Bram cannot hit anything. — high**
|
||||
*Problem:* 2 hits in 12 attack rounds across both passes. Fix 5 was insufficient.
|
||||
*Fix:* This is a **pregen problem, not a scenario problem** — Bram's 4d6 is simply low for a
|
||||
fighter. Give the scenario's own §2b a note: *the warrior should open with Cleave or a Called
|
||||
Shot rather than trading attacks*, and put a **shield-wall option** in A2 so he has a job that
|
||||
isn't rolling to hit.
|
||||
*Where:* §2b; Area 2.
|
||||
|
||||
**4. The tab reached 3, not 6. — medium**
|
||||
*Problem:* Banes: Pinch stumble, Yrsa mirror, Bram stumble = 3. The Reckoning still did not fire —
|
||||
though it is now plausibly reachable, where in pass 1 it was not.
|
||||
*Fix:* Add **+1 Bane for entering by the Court Door** (announcing yourself is vanity) and **+1 for
|
||||
refusing the Queen's Vice**. Both are pure character moments and would have put this party on 5.
|
||||
*Where:* Areas 1 and 6; §9.
|
||||
|
||||
---
|
||||
|
||||
## 5. What this says about DELVE (draft 4/5)
|
||||
|
||||
| # | Finding | Consequence |
|
||||
|---|---|---|
|
||||
| 1 | **6 areas ran ~2h10 once the fixes landed** | Pass 1's "6 is too many" was wrong — it was *2h55 because the content was broken*, not because six is too many. **Withdraw the recommendation to drop to 4–5.** Codex's caution was reasonable but the evidence now says 6 is fine at this heat. |
|
||||
| 2 | **The clock fired on the first roll, from a player choice** | Attention-triggered clocks work far better than turn-count clocks. DELVE should weight *triggers* over the 3-Turn timer. |
|
||||
| 3 | **`acceptanceCost: null` is not a temptation — it is an automatic yes** | In both passes every free-acceptance item was taken without discussion. The decision only ever appeared at **use**. Draft 5 should stop describing acceptance as the temptation and put the whole economy at the point of use — or give a minority of relics real acceptance friction. **This answers scenario question 3, finally.** |
|
||||
| 4 | **A "way back in" rescued the branch that killed pass 1** | The Vice offer meant a party that ignored the intended route still had an ending. DELVE's generated areas should always emit a fallback route, not just a gate. |
|
||||
| 5 | **Two content traps came from stat blocks, again** | Confirms the overlay catalog needs a playability field. Add a second: *"is this fight survivable by the party's weakest member?"* — both passes lost the mage. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Status
|
||||
|
||||
**The Vault of the Gilded Court v2 is runnable.** Apply the four v2.1 fixes before a live table;
|
||||
none of them block a run today.
|
||||
|
||||
Build-order step 1 is **complete**: one handwritten delve, run twice, findings folded back. The
|
||||
scenario went from unwinnable to won-in-five, and produced five structural findings for the design
|
||||
that no amount of further architecture work would have surfaced.
|
||||
|
||||
Next: **step 2, the seam spike.**
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,347 @@
|
||||
# THE VAULT OF THE GILDED COURT
|
||||
|
||||
> *They were buried smiling. They are still smiling.*
|
||||
|
||||
**VANITY · Barrow · Depth 2 · 4 heroes · six areas · ~3 hours**
|
||||
**v2 — 2026-08-04. Absorbs playtest-01 fixes 1–8. Playtests: 1 of 2.**
|
||||
|
||||
DELVE build-order **step 1**. §11 says what it is measuring.
|
||||
|
||||
**v2 changelog:** Queen's Grit 10→6, added Skeletons cut, silvered blade added to A5 · flattery
|
||||
gate 2 heroes→1, now repeatable per area with a visible signal · Banes now bank from flattery,
|
||||
mirrors and relic use, with a running tab header · both Observation clues automatic on entry ·
|
||||
Ghoul defence 3→2 · A4 cut to one Ghoul · A3 gang breaks after one round · new §2b PC numbers.
|
||||
*Area count deliberately held at 6 so pass 2 measures the fixes, not a shorter delve.*
|
||||
|
||||
---
|
||||
|
||||
## 1. Design intent
|
||||
|
||||
1. **The monsters can be handled by feeding their vanity** — and it costs you, in Banes.
|
||||
2. **Procedural approach, authored climax.** Areas 1–5 are DELVE's shape; Area 6 is the seam.
|
||||
3. **One motif, escalating: being looked at.**
|
||||
4. **The tab should read six during the finale.** v1 failed this outright — Banes are now banked
|
||||
by *actions*, not by hoping for Stumbles.
|
||||
5. **Deliberately not** a fight-through. Two planned fights in six areas, both avoidable.
|
||||
|
||||
**Content warnings:** live burial, self-mutilation (implied, off-screen), body horror.
|
||||
|
||||
---
|
||||
|
||||
## 2. Master rules block (stand-in GM crash course)
|
||||
|
||||
- **Rolls:** pool of d6. **5–6 = Success.** Most tasks need 1; hard ones 2+.
|
||||
- **Damage:** a hit is **1 Grit, +1 per extra Success spent**. **Spells deal 0 base** — a mage's
|
||||
damage comes only from extras, so spells are for effects, not attrition.
|
||||
- **Stumble:** failing with 2+ 1s banks a **Bane**. At **six → the Reckoning** (2d6, §21b), tab
|
||||
wipes. *Do not rely on this for pressure — see the tab header.*
|
||||
- **Push:** reroll non-Successes, **+1 Bane**.
|
||||
- **Vanity:** spend 1 for **+1 die**, cap 6. Gaining it is a reward; the bill is a Bane.
|
||||
- **The Draw:** initiative is 1d6, rerolled every round.
|
||||
|
||||
### 2b. The heroes at a glance
|
||||
*A stand-in GM needs these; v1 omitted them and could not be run.*
|
||||
|
||||
| Hero | Attack pool | Defence | Grit | Signature |
|
||||
|---|---|---|---|---|
|
||||
| **Bram the Warrior** | 4 (brawn 3 + fight) | 3 | 9 | Holds a doorway; Cleave with a two-hander |
|
||||
| **Pinch the Rogue** | 5 (flair 4 + finesse) | 3 | 6 | Best attacker in the party; Called Shot |
|
||||
| **Sister Wren** | 3 (brawn 2 + mace) | 3 | 7 | **Blessed mace — harms incorporeal**; Turn Undead |
|
||||
| **Yrsa the Mage** | 4 (wits 4) | 2 | 3 | Spells: effects over damage. Fragile — 3 Grit |
|
||||
|
||||
> **The tab.** Track Banes per hero in the header of every area. If the party enters Area 6 on
|
||||
> fewer than 4 total, the Queen opens by *offering a Vice* — see A6.
|
||||
|
||||
---
|
||||
|
||||
## 3. The skeleton (GM truth)
|
||||
|
||||
**The prize.** *Dorian's ever-young portrait* — the Queen's likeness, painted while she lived.
|
||||
|
||||
**The bottom problem.** **The Gilded Queen**, a Wraith who cannot see herself. She interred her
|
||||
court alive so someone would always tell her what she looks like.
|
||||
|
||||
**The factions.** The **Silent Host** (Skeletons, Ghouls) — court ritual on a loop; *courtesy,
|
||||
not violence*. The **Quill Gang** — four living grave-robbers, three days in, out of oil and nerve,
|
||||
led by **Pell Quill**.
|
||||
|
||||
**The motif.** Being looked at. **The recurring danger.** Attention.
|
||||
|
||||
**Timeline.** 300 years ago the Queen dies and takes her court with her. 40 years ago the barrow
|
||||
is sealed. **Three days ago** the Quill Gang breaks in and wakes the Host. **Tonight** the party
|
||||
arrives — the barrow is already stirred, which is why the clock starts warm.
|
||||
|
||||
---
|
||||
|
||||
## 4. Timing
|
||||
|
||||
| | Area | Planned | Checkpoint |
|
||||
|---|---|---|---|
|
||||
| 1 | The Mourners' Steps | 15 | Everyone has rolled once |
|
||||
| 2 | The Antechamber of Bows | 30 | First Bane banked, or first flattery |
|
||||
| 3 | The Hall of Little Mirrors | 30 | Gang resolved (**one round max if hostile**) |
|
||||
| 4 | The Weeping Gallery | 20 | — |
|
||||
| 5 | The Wardrobe of the Court | 25 | **The A5 choice is made** |
|
||||
| 6 | The Gilded Audience | 40 | — |
|
||||
|
||||
**≈2h40.** If late at the A5 checkpoint, cut Area 4 — it carries no prize, only motif.
|
||||
|
||||
---
|
||||
|
||||
## 5. The clock — Wandering the Dark
|
||||
|
||||
Every **3 Turns**, or on attention, roll **1d6**. **On a 1 — *something comes***; roll Reaction
|
||||
for mood. *v1 note: this fired zero times in four rolls. If it has not fired by Area 4, roll it on
|
||||
a **1–2** thereafter.*
|
||||
|
||||
**Telegraph one Turn ahead:**
|
||||
> *(read aloud or paraphrase)* Somewhere below, cloth moves on stone. Something has adjusted its
|
||||
> posture, the way a person does when they are about to be looked at.
|
||||
|
||||
**Attention triggers:** the Court Door, being reflected twice in A3, a raised voice, striking a
|
||||
Skeleton that had not moved.
|
||||
|
||||
---
|
||||
|
||||
## 6. The areas
|
||||
|
||||
**Cue** (players) · **Truth** (GM) · then decision, temptation, failure case. Mechanics inline.
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Mourners' Steps
|
||||
*Approach · no encounter · Banes: —*
|
||||
|
||||
> *(read aloud or paraphrase)* Forty steps down, and every one has a mourner carved beside it —
|
||||
> the hands, the folded cloth, the bowed shoulders, beautifully made. Where each face should be
|
||||
> there is a smooth blank oval, polished brighter than the rest.
|
||||
|
||||
**Truth.** The faces were struck off at the Queen's order so hers would be the only face here.
|
||||
|
||||
**The clue is automatic.** Anyone who looks sees the chisel marks are *inside* the barrow —
|
||||
someone did this from below. `[VANITY: Observation — 1 success]` buys *extra*: the marks are
|
||||
three centuries old, and made by many different hands.
|
||||
|
||||
**The decision — three ways in.**
|
||||
|
||||
| Route | Cost | Effect |
|
||||
|---|---|---|
|
||||
| **Servants' Stair** | narrow, slow — **1 Turn** | Arrive unnoticed |
|
||||
| **Court Door** | grand, announced | Host **Aware** from A2; roll the clock immediately |
|
||||
| **The Crack** | `[VANITY: Brawn — 2 successes]` | Failure banks a Bane, lands you noisily (as Court Door) |
|
||||
|
||||
**Failure case.** No choice = the Court Door. Say "you take the big doors, then" aloud.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The Antechamber of Bows
|
||||
*Encounter: skirmish · the flattery route opens · Banes: +1 per flatterer*
|
||||
|
||||
> *(read aloud or paraphrase)* Nine courtiers stand bent at the waist in a bow, holding it. Their
|
||||
> silks have rotted to cobweb; the gold thread has not. They are bowing to the middle of an empty
|
||||
> room.
|
||||
|
||||
**Truth.** They bow to where the Queen *stood*. Three will move; six are just dead.
|
||||
|
||||
**Consequence.** Three **Skeletons** — atk 3, **def 2**, Grit 1, Nerve 5, fearless, half harm from
|
||||
arrows. Heat: **skirmish**. They animate if struck, jostled, or robbed.
|
||||
|
||||
**The other route — bow back.** `[VANITY: Flair — 2 successes]` as a genuine court greeting.
|
||||
**One hero suffices.** On success the Host resumes its bow and the party are **guests**: Skeletons
|
||||
will not open hostilities first.
|
||||
|
||||
> **Flattery is repeatable.** Any hero may bow in *any* later area to be received. Each time:
|
||||
> **+1 Vanity now, +1 Bane on the tab.** Shine now, bill later.
|
||||
>
|
||||
> **The signal.** From the first successful bow, the Host addresses flatterers by invented titles
|
||||
> — *"my lord of the wet cloak"*. Players must be able to *see* the route is open and repeatable;
|
||||
> in v1 they could not, and it killed them.
|
||||
|
||||
**Temptation — the Gilded Mask.**
|
||||
- *Cue:* gold leaf over lime-wood, mouth fixed in a small smile.
|
||||
- *Benefit:* automatic success on any bow-back, anywhere.
|
||||
- *Accept:* **free.** *Use:* **+1 Bane** each time it saves you.
|
||||
- *Standing:* while worn you cannot be recognised by name — allies' aid rolls that need to know
|
||||
you take −1 die.
|
||||
|
||||
**Failure case.** Fighting through without bowing is legitimate and harder — but A6 now always
|
||||
offers a way in (see the Vice), so it is no longer a death sentence.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Hall of Little Mirrors
|
||||
*Faction contact · optional encounter · Banes: +1 per mirror-noticing*
|
||||
|
||||
> *(read aloud or paraphrase)* The walls are set with hundreds of hand-mirrors, no two the same,
|
||||
> all angled inward. In the middle of them four living people sit back-to-back around a dead
|
||||
> lantern, and every one of them is looking at you in a hundred pieces at once.
|
||||
|
||||
**Truth.** The Quill Gang. One man lost to the Wardrobe. They sat here to watch every approach,
|
||||
not realising the mirrors are how the Host watches *them*.
|
||||
|
||||
**The clue is automatic.** The mirrors are angled *inward*, at the room, not outward at the doors.
|
||||
`[VANITY: Observation — 1 success]` buys extra: some are angled at each other, endlessly.
|
||||
|
||||
**Consequence.** Roll **Reaction (2d6)** for Pell Quill.
|
||||
- **Hostile:** 4 **Bandits** (atk 3, def 3, Grit 2, **Nerve 2**). **They fight one round, then
|
||||
break** — three days past brave. Do not run this as a full fight; it costs 15 minutes the
|
||||
schedule does not have.
|
||||
- **Wary / Neutral:** they trade. They know the Wardrobe's door is the trap, and how.
|
||||
- **Friendly:** they'll guide for a share.
|
||||
|
||||
**The mirror rule.** Anything reflected twice is **noticed** — roll the clock, and the hero who
|
||||
lingered, posed or studied their reflection takes **+1 Bane**. Say the Bane out loud; this is the
|
||||
tab's main engine outside flattery.
|
||||
|
||||
**The decision.** Ally, rob, or abandon. Drawing on the gang takes **+1 Bane** — the Host does not
|
||||
care for guests who behave badly in the house.
|
||||
|
||||
**Failure case.** Gang dead or gone → A5's trap fires unwarned. Visibly the party's doing.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Weeping Gallery
|
||||
*Encounter: fight · no reward · Banes: —*
|
||||
|
||||
> *(read aloud or paraphrase)* A long gallery of portraits, floor to ceiling, all of the same
|
||||
> woman. In every one the eyes have been scratched out — not slashed, *worked at*, patiently, by
|
||||
> fingernails.
|
||||
|
||||
**Truth.** The court did this, two centuries in, unable to bear being looked at. Then they ate
|
||||
each other.
|
||||
|
||||
**Consequence.** **One Ghoul** — atk 4, **def 2** (gorged, slow), Grit 4, Nerve 5. On a hit, a free
|
||||
Called Shot to freeze the victim for a round. It is **slow**: a fighter can dictate range. Heat:
|
||||
**fight**. It attacks anyone in court dress or wearing the Gilded Mask first.
|
||||
|
||||
**Temptation — none.** This area gives nothing. That is its job.
|
||||
|
||||
**Failure case.** Cuttable. If cut, move a scratched portrait into A5 so the motif completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Wardrobe of the Court
|
||||
*No encounter · the hoard · the choice that sets the finale · Banes: +1 per relic use*
|
||||
|
||||
> *(read aloud or paraphrase)* Every dress the court ever wore, on three hundred hangers, in a
|
||||
> room with no dust in it at all. At the far end, facing away from the door, one covered frame the
|
||||
> height of a person.
|
||||
|
||||
**Truth.** Uncovering the portrait wakes the Queen — it is the only place she can be seen.
|
||||
|
||||
**Consequence.** Hoard: **chest** tier. **The door is the trap**, not the frame: it closes on
|
||||
whatever reaches past the threshold `[VANITY: Brawn or Flair — 2 successes; failure = Maimed]`.
|
||||
Forewarned parties simply wedge it.
|
||||
|
||||
**In the hoard, always: *the Cantor's silvered blade*.** Plain, old, faintly warm. **It harms the
|
||||
incorporeal.** Put it in front of the party's mundane fighter — without it, A6 has no room for them.
|
||||
|
||||
**Temptation — Dorian's ever-young portrait.**
|
||||
- *Cue:* a young woman in gold, painted so well she seems about to speak.
|
||||
- *Benefit:* you do not age; once per session **refuse a condition** — the portrait takes it,
|
||||
visibly. *Accept:* **free.** *Use:* **+1 Bane.**
|
||||
- *Standing:* the Queen always knows where it is.
|
||||
|
||||
**The decision.**
|
||||
|
||||
| Choice | Consequence |
|
||||
|---|---|
|
||||
| **Uncover and take it** | The Queen wakes **here**, on her ground. A6 comes to you. |
|
||||
| **Leave it covered** | You reach A6 on your terms, with nothing she wants. |
|
||||
|
||||
**Failure case.** Taking it and running = the finale in the Mourners' Steps. The blank faces make
|
||||
a better room for it anyway.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Gilded Audience
|
||||
### ⟨ AUTHORED SLOT — this is the seam ⟩
|
||||
|
||||
**Areas 1–5 are what DELVE generates. This is what it hands over.** A worked example; swap freely.
|
||||
|
||||
> *(read aloud or paraphrase)* The throne room is full of a court that has stopped pretending. On
|
||||
> the throne, in a gown that still holds its shape, sits a woman-shaped absence — a gap in the air
|
||||
> wearing gold. She turns towards you and there is nothing in the collar to turn.
|
||||
|
||||
**The Gilded Queen** — Wraith, **Grit 6**, atk 5 (chill touch, ignores armour, drains 1 max Grit),
|
||||
def 3, Nerve 5, Resolve (Nemesis). **Incorporeal.**
|
||||
|
||||
> **Say this before initiative, always:** she is not solid; steel passes through her. **Blessed,
|
||||
> silvered or magical weapons harm her** — Wren's mace, the Cantor's blade from A5, Yrsa's spells.
|
||||
> In v1 the table discovered this by failing, and it was fatal. Never let that happen.
|
||||
|
||||
**She opens by asking:** *"How do I look?"*
|
||||
|
||||
| Party's route | The audience |
|
||||
|---|---|
|
||||
| **Anyone bowed, even once** | She listens. She trades the portrait for a *true* description — someone must look at her and say what is actually there. `[VANITY: Nerve — 2 successes]` or take **Rattled**. |
|
||||
| **Nobody bowed, tab under 4** | She **offers a Vice** — *"Be beautiful for me and I will be generous"*: +1 Vanity, +1 Bane, and the conversation opens. Refusing is allowed, and starts the fight. |
|
||||
| **Carrying the portrait** | She wants it more than you. Offering it ends everything — she looks at herself and comes apart. You leave with all but the prize. |
|
||||
| **Fighting** | Grit 6, no reinforcements. Winnable by two engaged heroes in ~4 rounds. |
|
||||
|
||||
**The Reckoning.** If the tab reads six mid-audience, stop and roll it. A delve about vanity
|
||||
billing you in front of the Queen of Vanity is the best ending this has; do not soften it.
|
||||
|
||||
**Failure case.** Fleeing with the portrait: they win and are haunted. She follows, and any later
|
||||
session may open with the question again.
|
||||
|
||||
---
|
||||
|
||||
## 7. NPC roster
|
||||
|
||||
| Who | Essence | Numbers |
|
||||
|---|---|---|
|
||||
| **The Gilded Queen** | Cannot see herself; will pay anything to be described | Wraith · **Grit 6** · atk 5 · def 3 · incorporeal |
|
||||
| **Pell Quill** | Wants out alive, will sell anyone to manage it | Bandit · Grit 2 · Nerve 2 |
|
||||
| **Quill Gang** ×3 | Three days past brave — break after one round | Bandit · Grit 2 · Nerve 2 |
|
||||
| **The Silent Host** | A guest is not an intruder | Skeleton · atk 3 · **def 2** · Grit 1 |
|
||||
| **The One That Ate** | Could not bear being looked at | Ghoul · atk 4 · **def 2** · Grit 4 · slow |
|
||||
|
||||
## 8. Temptation index
|
||||
|
||||
| Item | Where | Accept | Use | Standing |
|
||||
|---|---|---|---|---|
|
||||
| **Gilded Mask** | A2 | free | +1 Bane per save | cannot be recognised by name |
|
||||
| **Cantor's silvered blade** | A5 | free | — | harms the incorporeal |
|
||||
| **Chest hoard** | A5 | free | — | — |
|
||||
| **Dorian's portrait** | A5 | free | +1 Bane per refusal | the Queen always knows where it is |
|
||||
|
||||
## 9. Bane sources (the tab's engine)
|
||||
|
||||
Flattery **+1** each · mirror-noticing in A3 **+1** · drawing on the gang **+1** · Gilded Mask use
|
||||
**+1** · portrait use **+1** · Push **+1** · Stumbles when they happen. Target: **4–6 by Area 6**.
|
||||
|
||||
## 10. What DELVE would have generated
|
||||
|
||||
| Layer | Tool | Hand |
|
||||
|---|---|---|
|
||||
| Prize, bottom problem, factions, motif, foreshadow chain | ✓ | |
|
||||
| Area count, order, heat, hoard tier | ✓ | |
|
||||
| Maps (5 × `forgeStage` barrow) | ✓ | |
|
||||
| Encounter composition, mood | ✓ (recorded, not reproducible) | |
|
||||
| **Read-aloud cue text** | | ✓ |
|
||||
| **The flattery mechanic** | | ✓ |
|
||||
| **Area 6** | | ✓ |
|
||||
|
||||
DELVE cannot write the cue paragraphs, and they are most of what makes this playable. **v1 must
|
||||
ship theme-pack cue fragments, not generated prose.**
|
||||
|
||||
---
|
||||
|
||||
## 11. What the runs are testing
|
||||
|
||||
| # | Question | Pass 1 result |
|
||||
|---|---|---|
|
||||
| 1 | Do 6 areas fit ~3 hours? | **No** — 2h55 with the finale unresolved |
|
||||
| 2 | Are 2 planned fights enough? | Untested — a Hostile reaction made it 3 |
|
||||
| 3 | Is `acceptanceCost: null` tempting? | **Unanswered** — mask taken without discussion |
|
||||
| 4 | Does flattery/Bane produce decisions? | Partly — one hero bowed, gate then killed them |
|
||||
| 5 | Does the Reckoning land in the finale? | **No** — 1 Bane in ~40 rolls |
|
||||
| 6 | Is the authored seam visible to players? | Not reached |
|
||||
| 7 | Is four-faces-per-field too heavy? | **Yes** — Cue + Truth only; answered, closed |
|
||||
|
||||
Pass 2 is **contrarian**: Court Door, nobody bows, ally the gang, take the portrait.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"generatorVersion": "0.1.0",
|
||||
"catalogVersion": "barrow@1",
|
||||
"seed": "gilded-court-404",
|
||||
"params": {
|
||||
"areas": 6,
|
||||
"depth": 2,
|
||||
"party": 4,
|
||||
"deadliness": "standard",
|
||||
"density": "standard",
|
||||
"greed": "standard",
|
||||
"clock": "standard",
|
||||
"ending": "authored",
|
||||
"theme": "barrow"
|
||||
},
|
||||
"skeleton": {
|
||||
"placeName": "The Cairn of the Patient Lord",
|
||||
"knot": {
|
||||
"appetiteId": "first",
|
||||
"accommodationId": "unmade",
|
||||
"claimant": "Patient Lord",
|
||||
"conscript": "a procession",
|
||||
"transgression": "The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"cruelty": 3
|
||||
},
|
||||
"motif": {
|
||||
"id": "precedence",
|
||||
"danger": "procession",
|
||||
"dangerLine": "the barrow keeps its order, and you have joined the queue",
|
||||
"trigger": "announced"
|
||||
},
|
||||
"prize": {
|
||||
"kind": "regalia",
|
||||
"label": "the crown worn first, and only once",
|
||||
"function": "the proof of precedence"
|
||||
},
|
||||
"bottomProblem": {
|
||||
"label": "The Patient Lord",
|
||||
"wantNow": "to be first in all things",
|
||||
"failureState": "is second now, and cannot bear it",
|
||||
"handledBy": "giving them what they want, at a price"
|
||||
},
|
||||
"factions": [
|
||||
{
|
||||
"id": "host",
|
||||
"name": "The Silent Host",
|
||||
"kind": "undead",
|
||||
"essence": "still performing the ritual they were buried for",
|
||||
"handledBy": "courtesy",
|
||||
"conscriptedAs": "a procession"
|
||||
},
|
||||
{
|
||||
"id": "wardens",
|
||||
"name": "the Barrow-Wardens",
|
||||
"kind": "undead",
|
||||
"essence": "sealed this place and stayed to make sure",
|
||||
"handledBy": "proof of right"
|
||||
}
|
||||
],
|
||||
"foreshadow": [
|
||||
{
|
||||
"index": 1,
|
||||
"role": "approach",
|
||||
"facet": "institution",
|
||||
"fragment": "a stair too grand for the door it serves"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"role": "complication",
|
||||
"facet": "ritual",
|
||||
"fragment": "banners in sequence, each house a place further back"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"role": "complication",
|
||||
"facet": "ritual",
|
||||
"fragment": "a herald's script with the order still marked"
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"role": "turn",
|
||||
"facet": "demand",
|
||||
"fragment": "a door only one person may pass at a time, and only first"
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"role": "descent",
|
||||
"facet": "wound",
|
||||
"fragment": "a procession that doubled back on itself"
|
||||
},
|
||||
{
|
||||
"index": 6,
|
||||
"role": "threshold",
|
||||
"facet": "anchor",
|
||||
"fragment": "a seat at the head of everything, occupied"
|
||||
},
|
||||
{
|
||||
"index": 7,
|
||||
"role": "ending",
|
||||
"facet": "demand",
|
||||
"fragment": "a corridor that will not let two walk abreast"
|
||||
}
|
||||
]
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"index": 1,
|
||||
"role": "approach",
|
||||
"name": "The Threshold Stones",
|
||||
"facet": "institution",
|
||||
"cueFragments": [
|
||||
"a stair too grand for the door it serves",
|
||||
"a floor mosaic worn only along one path"
|
||||
],
|
||||
"truth": "this is how the place was ordered, and it still is. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "a member of the faction, alone and not yet hostile",
|
||||
"cost": "talking costs time; not talking costs later"
|
||||
},
|
||||
"encounter": null,
|
||||
"hoard": null,
|
||||
"temptation": null,
|
||||
"trigger": "announced",
|
||||
"baneBeat": "announcing",
|
||||
"clue": {
|
||||
"automatic": "a stair too grand for the door it serves — 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 Hall of Bows",
|
||||
"facet": "ritual",
|
||||
"cueFragments": [
|
||||
"banners in sequence, each house a place further back",
|
||||
"grave-goods stacked with obsessive neatness"
|
||||
],
|
||||
"truth": "the office is still being performed, for someone who is no longer served by it. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "two ways on: one narrow and dark, one wide and announced",
|
||||
"cost": "the wide way is faster and the barrow hears it"
|
||||
},
|
||||
"encounter": null,
|
||||
"hoard": null,
|
||||
"temptation": null,
|
||||
"trigger": "loud",
|
||||
"baneBeat": "noticing",
|
||||
"clue": {
|
||||
"automatic": "banners in sequence, each house a place further back — 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": "complication",
|
||||
"name": "The Lesser Vault",
|
||||
"facet": "ritual",
|
||||
"cueFragments": [
|
||||
"a herald's script with the order still marked",
|
||||
"an alcove emptied in a hurry"
|
||||
],
|
||||
"truth": "the office is still being performed, for someone who is no longer served by it. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "something valuable, awkward to carry",
|
||||
"cost": "carrying it slows you and it is not yours"
|
||||
},
|
||||
"encounter": null,
|
||||
"hoard": null,
|
||||
"temptation": null,
|
||||
"trigger": "announced",
|
||||
"baneBeat": "relicUse",
|
||||
"clue": {
|
||||
"automatic": "a herald's script with the order still marked — 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": "turn",
|
||||
"name": "The Records Room",
|
||||
"facet": "demand",
|
||||
"cueFragments": [
|
||||
"a door only one person may pass at a time, and only first",
|
||||
"a collapse that has opened a way the builders never meant"
|
||||
],
|
||||
"truth": "here the want is explicit — the place asks something of whoever enters. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "a sealed door, and a crack you could squeeze through",
|
||||
"cost": "the crack costs skin and a Bane on a failure"
|
||||
},
|
||||
"encounter": {
|
||||
"heat": "fight",
|
||||
"kind": "undead"
|
||||
},
|
||||
"hoard": "cache",
|
||||
"temptation": {
|
||||
"id": "coronet",
|
||||
"cue": "a small crown, sized for someone who never grew",
|
||||
"benefit": "+1 die when others must defer to you",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "you must be addressed correctly or lose the benefit"
|
||||
},
|
||||
"trigger": "announced",
|
||||
"baneBeat": "relicUse",
|
||||
"clue": {
|
||||
"automatic": "a door only one person may pass at a time, and only first — 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": "descent",
|
||||
"name": "The Sunken Nave",
|
||||
"facet": "wound",
|
||||
"cueFragments": [
|
||||
"a procession that doubled back on itself",
|
||||
"a lamp still burning on oil that should be long gone"
|
||||
],
|
||||
"truth": "this is the damage the want did to those who had to satisfy it. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "a shortcut down, through water",
|
||||
"cost": "everything gets wet, including what burns"
|
||||
},
|
||||
"encounter": null,
|
||||
"hoard": "pocket",
|
||||
"temptation": {
|
||||
"id": "glass",
|
||||
"cue": "a hand-mirror that shows the room a moment behind",
|
||||
"benefit": "once per scene, see what was just here",
|
||||
"useCost": {
|
||||
"bane": 1
|
||||
},
|
||||
"standingDrawback": "your reflection lags, and others notice"
|
||||
},
|
||||
"trigger": "seenTwice",
|
||||
"baneBeat": "refusal",
|
||||
"clue": {
|
||||
"automatic": "a procession that doubled back on itself — 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": 6,
|
||||
"role": "threshold",
|
||||
"name": "The Antechamber Royal",
|
||||
"facet": "anchor",
|
||||
"cueFragments": [
|
||||
"a seat at the head of everything, occupied",
|
||||
"a door that has been wedged open from the inside"
|
||||
],
|
||||
"truth": "the thing the whole barrow is arranged around is close. The Patient Lord had the faces struck from a procession, so that someone would always go behind them.",
|
||||
"decision": {
|
||||
"cue": "a way back, still open",
|
||||
"cost": "leaving now means it will be worse when you return"
|
||||
},
|
||||
"encounter": {
|
||||
"heat": "battle",
|
||||
"kind": "undead"
|
||||
},
|
||||
"hoard": "chest",
|
||||
"temptation": {
|
||||
"id": "ledger",
|
||||
"cue": "a book of names, one line left blank",
|
||||
"benefit": "write a name and the Host obeys it once",
|
||||
"acceptanceCost": {
|
||||
"vice": "Pride"
|
||||
},
|
||||
"useCost": {
|
||||
"bane": 2
|
||||
},
|
||||
"standingDrawback": "your own name is now in it"
|
||||
},
|
||||
"trigger": "disturbed",
|
||||
"baneBeat": "relicUse",
|
||||
"clue": {
|
||||
"automatic": "a seat at the head of everything, occupied — 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": 7,
|
||||
"role": "ending",
|
||||
"mode": "authored",
|
||||
"heat": null,
|
||||
"hoard": null,
|
||||
"name": "The Gilded Audience",
|
||||
"cueFragments": [
|
||||
"a corridor that will not let two walk abreast"
|
||||
],
|
||||
"question": "\"Who goes first?\"",
|
||||
"authored": true
|
||||
},
|
||||
"budget": {
|
||||
"plannedFights": 2,
|
||||
"heatBase": "fight",
|
||||
"wantFights": 2
|
||||
},
|
||||
"pressure": {
|
||||
"turn": 0,
|
||||
"clockRolls": 0,
|
||||
"clockFired": 0,
|
||||
"triggerNumber": 1,
|
||||
"escalated": false,
|
||||
"tab": 6,
|
||||
"target": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
# THE CAIRN OF THE PATIENT LORD
|
||||
|
||||
> *the barrow keeps its order, and you have joined the queue.*
|
||||
|
||||
**VANITY · Barrow · Depth 2 · 4 heroes · 6 areas + an authored ending**
|
||||
**Seed `gilded-court-404` · generator 0.1.0 · catalog barrow@1**
|
||||
|
||||
> **Skeleton seed, not a replay seed.** It reproduces the fiction, the area order, the
|
||||
> heat and the hoard tiers. It does **not** reproduce encounter composition, mood or
|
||||
> hoard contents — those come from the Forge, which is not seeded.
|
||||
|
||||
---
|
||||
|
||||
## The truth (GM only)
|
||||
|
||||
**The Cairn of the Patient Lord.** The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them. The Patient Lord is second now, and cannot bear it. The prize is the crown worn first, and only once — the proof of precedence. The Silent Host are still performing the ritual they were buried for; they are handled by courtesy. The motif is **precedence**, and the danger is procession: the barrow keeps its order, and you have joined the queue.
|
||||
|
||||
**They will ask you:** "Who goes first?"
|
||||
|
||||
## Pressure
|
||||
|
||||
- **The clock.** Roll 1d6 on a trigger, or every 3 Turns. **1 or under — something comes.** Telegraph one Turn ahead.
|
||||
- **Triggers this delve:** announced · loud · seenTwice · disturbed
|
||||
- **The tab.** Target **5 Banes** by the ending. Banes are *offered by named beats*, never hoped for.
|
||||
- **The danger is procession:** the barrow keeps its order, and you have joined the queue.
|
||||
|
||||
## The way through — the appeasement
|
||||
|
||||
Anyone may **stand aside and let it go ahead of you**. `[VANITY: Poise — 2 successes]`
|
||||
On success it goes first, and The Silent Host will not open hostilities first.
|
||||
**Cost: +1 Vanity now, +1 Bane on the tab.** Shine now, bill later.
|
||||
**Repeatable in every area.** Say the Bane aloud when it banks — the table must be able to see the route is open.
|
||||
|
||||
## Factions
|
||||
|
||||
- **The Silent Host** (undead) — still performing the ritual they were buried for. Handled by *courtesy*.
|
||||
- **the Barrow-Wardens** (undead) — sealed this place and stayed to make sure. Handled by *proof of right*.
|
||||
|
||||
**Planned fights:** 2 of 6 areas · base heat *fight*
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Threshold Stones
|
||||
*approach · facet: institution · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a stair too grand for the door it serves
|
||||
- a floor mosaic worn only along one path
|
||||
|
||||
**Truth.** this is how the place was ordered, and it still is. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** a stair too grand for the door it serves — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a member of the faction, alone and not yet hostile — *talking costs time; not talking costs later*
|
||||
|
||||
|
||||
**Attention trigger:** announced.
|
||||
**Bane beat — announcing.** Who: *the whole party*. When: *once, on entering loudly*. once only. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The Lesser Vault
|
||||
*complication · facet: ritual · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- banners in sequence, each house a place further back
|
||||
- grave-goods stacked with obsessive neatness
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** banners in sequence, each house a place further back — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** two ways on: one narrow and dark, one wide and announced — *the wide way is faster and the barrow hears it*
|
||||
|
||||
|
||||
**Attention trigger:** loud.
|
||||
**Bane beat — noticing.** Who: *whoever lingers on their own reflection*. When: *once per hero here*. repeatable across areas. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Waiting Room
|
||||
*complication · facet: ritual · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a herald's script with the order still marked
|
||||
- an alcove emptied in a hurry
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** a herald's script with the order still marked — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** something valuable, awkward to carry — *carrying it slows you and it is not yours*
|
||||
|
||||
|
||||
**Attention trigger:** announced.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Records Room
|
||||
*turn · facet: demand · encounter: fight · hoard: cache*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a door only one person may pass at a time, and only first
|
||||
- a collapse that has opened a way the builders never meant
|
||||
|
||||
**Truth.** here the want is explicit — the place asks something of whoever enters. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** a door only one person may pass at a time, and only first — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a sealed door, and a crack you could squeeze through — *the crack costs skin and a Bane on a failure*
|
||||
|
||||
**Encounter — fight.** 1 Ghoul and 2 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Ghoul | 4 | 3 | 4 | 5 | on a hit, a free Called Shot to freeze for a round |
|
||||
| 2× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** anything
|
||||
- **Avoidable:** the Ghoul goes for court dress first — send someone plain
|
||||
- *Forge:* `forgeEncounter({heat:"fight", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **cache**.
|
||||
**Temptation — coronet.** a small crown, sized for someone who never grew
|
||||
- *Benefit:* +1 die when others must defer to you
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* you must be addressed correctly or lose the benefit
|
||||
|
||||
**Attention trigger:** announced.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Sunken Nave
|
||||
*descent · facet: wound · no encounter · hoard: pocket*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a procession that doubled back on itself
|
||||
- a lamp still burning on oil that should be long gone
|
||||
|
||||
**Truth.** this is the damage the want did to those who had to satisfy it. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** a procession that doubled back on itself — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a shortcut down, through water — *everything gets wet, including what burns*
|
||||
|
||||
**Hoard.** Tier **pocket**.
|
||||
**Temptation — glass.** a hand-mirror that shows the room a moment behind
|
||||
- *Benefit:* once per scene, see what was just here
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* your reflection lags, and others notice
|
||||
|
||||
**Attention trigger:** seenTwice.
|
||||
**Bane beat — refusal.** Who: *whoever refuses the offer*. When: *once*. once only. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Last Door
|
||||
*threshold · facet: anchor · encounter: battle · hoard: chest*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a seat at the head of everything, occupied
|
||||
- a door that has been wedged open from the inside
|
||||
|
||||
**Truth.** the thing the whole barrow is arranged around is close. The Patient Lord bound a procession by oath they could not break, so that someone would always go behind them.
|
||||
|
||||
**The clue is automatic:** a seat at the head of everything, occupied — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a way back, still open — *leaving now means it will be worse when you return*
|
||||
|
||||
**Encounter — battle.** 1 Wraith and 3 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Wraith | 5 | 3 | 10 | 5 | INCORPOREAL — chill touch ignores armour and drains 1 max Grit |
|
||||
| 3× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** blessed, silvered or magical weapons ONLY — **say this before initiative**
|
||||
- **Avoidable:** it will take an appeasement over a fight every time
|
||||
- *Forge:* `forgeEncounter({heat:"battle", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **chest**.
|
||||
**Temptation — ledger.** a book of names, one line left blank
|
||||
- *Benefit:* write a name and the Host obeys it once
|
||||
- *Accept:* take the Vice **Pride** · *Use:* **+2 Banes** on the tab
|
||||
- *Standing:* your own name is now in it
|
||||
|
||||
**Attention trigger:** disturbed.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, the ending opens with them holding nothing the bottom problem wants.
|
||||
|
||||
---
|
||||
|
||||
### ENDING — The Audience
|
||||
### ⟨ AUTHORED SLOT — this is the seam ⟩
|
||||
|
||||
**Areas 1–6 are what DELVE generates. This is what it hands over.**
|
||||
|
||||
**Cue** — *fragments*
|
||||
- a corridor that will not let two walk abreast
|
||||
|
||||
**The Patient Lord** — is second now, and cannot bear it. Wants: *to be first in all things*.
|
||||
Handled by giving them what they want, at a price.
|
||||
|
||||
**They open by asking:** "Who goes first?"
|
||||
|
||||
| Party's route | The audience |
|
||||
|---|---|
|
||||
| Appeased it even once (**stand aside and let it go ahead of you**) | It listens. It will trade the crown worn first, and only once for the real thing, done to its face. `[VANITY: Poise — 2]` or take **Rattled**. |
|
||||
| Refused throughout, tab under 4 | It offers a Vice — take the shine, bank the bill. Refusing starts the fight. |
|
||||
| Carrying the crown worn first, and only once | It wants that more than it wants you. |
|
||||
| Fighting | Winnable by two engaged heroes. **Name what harms it before initiative.** |
|
||||
|
||||
> **Before initiative, always say what can hurt it.** Playtest 1 was unwinnable because
|
||||
> the table discovered the immunity by failing.
|
||||
|
||||
## Trigger glossary
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,264 @@
|
||||
# THE VAULT OF THE SILENT MARGRAVE
|
||||
|
||||
> *nothing here is allowed to change, including you.*
|
||||
|
||||
**VANITY · Barrow · Depth 2 · 4 heroes · 6 areas + an authored ending**
|
||||
**Seed `pale-tally-737` · generator 0.1.0 · catalog barrow@1**
|
||||
|
||||
> **Skeleton seed, not a replay seed.** It reproduces the fiction, the area order, the
|
||||
> heat and the hoard tiers. It does **not** reproduce encounter composition, mood or
|
||||
> hoard contents — those come from the Forge, which is not seeded.
|
||||
|
||||
---
|
||||
|
||||
## The truth (GM only)
|
||||
|
||||
**The Vault of the Silent Margrave.** The Silent Margrave bought the young from those with nothing left, so that nothing would ever age. The Silent Margrave is old now, and cannot bear it. The prize is a sealed jar holding one good year — the only thing here that has not aged. The Silent Host are still performing the ritual they were buried for; they are handled by courtesy. The motif is **keeping**, and the danger is preservation: nothing here is allowed to change, including you.
|
||||
|
||||
**They will ask you:** "Tell me I have not changed."
|
||||
|
||||
## Pressure
|
||||
|
||||
- **The clock.** Roll 1d6 on a trigger, or every 3 Turns. **1 or under — something comes.** Telegraph one Turn ahead.
|
||||
- **Triggers this delve:** disturbed · lingering · seenTwice
|
||||
- **The tab.** Target **5 Banes** by the ending. Banes are *offered by named beats*, never hoped for.
|
||||
- **The danger is preservation:** nothing here is allowed to change, including you.
|
||||
|
||||
## The way through — the appeasement
|
||||
|
||||
Anyone may **tell it that it has not changed**. `[VANITY: Flair — 2 successes]`
|
||||
On success it hears that nothing has aged, and The Silent Host will not open hostilities first.
|
||||
**Cost: +1 Vanity now, +1 Bane on the tab.** Shine now, bill later.
|
||||
**Repeatable in every area.** Say the Bane aloud when it banks — the table must be able to see the route is open.
|
||||
|
||||
## Factions
|
||||
|
||||
- **The Silent Host** (undead) — still performing the ritual they were buried for. Handled by *courtesy*.
|
||||
- **the Barrow-Wardens** (undead) — sealed this place and stayed to make sure. Handled by *proof of right*.
|
||||
|
||||
**Planned fights:** 3 of 6 areas · base heat *fight*
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Outer Cut
|
||||
*approach · facet: institution · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- lamps trimmed and filled, all of them
|
||||
- water that has got in and taken three centuries to do it
|
||||
|
||||
**Truth.** this is how the place was ordered, and it still is. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** lamps trimmed and filled, all of them — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** two ways on: one narrow and dark, one wide and announced — *the wide way is faster and the barrow hears it*
|
||||
|
||||
|
||||
**Attention trigger:** disturbed.
|
||||
**Bane beat — noticing.** Who: *whoever lingers on their own reflection*. When: *once per hero here*. repeatable across areas. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The Lesser Vault
|
||||
*complication · facet: ritual · encounter: fight · hoard: cache*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a meal laid out, still warm, for people long past eating
|
||||
- a collapse that has opened a way the builders never meant
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** a meal laid out, still warm, for people long past eating — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** something valuable, awkward to carry — *carrying it slows you and it is not yours*
|
||||
|
||||
**Encounter — fight.** 1 Ghoul and 2 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Ghoul | 4 | 3 | 4 | 5 | on a hit, a free Called Shot to freeze for a round |
|
||||
| 2× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** anything
|
||||
- **Avoidable:** the Ghoul goes for court dress first — send someone plain
|
||||
- *Forge:* `forgeEncounter({heat:"fight", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **cache**.
|
||||
**Temptation — ring.** a signet, still warm
|
||||
- *Benefit:* doors that answer to rank open for you
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* the barrow's servants bring you their problems
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Attendants' Row
|
||||
*complication · facet: ritual · encounter: fight · hoard: cache*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a clock wound by somebody, recently
|
||||
- grave-goods stacked with obsessive neatness
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** a clock wound by somebody, recently — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a member of the faction, alone and not yet hostile — *talking costs time; not talking costs later*
|
||||
|
||||
**Encounter — fight.** 1 Ghoul and 2 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Ghoul | 4 | 3 | 4 | 5 | on a hit, a free Called Shot to freeze for a round |
|
||||
| 2× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** anything
|
||||
- **Avoidable:** the Ghoul goes for court dress first — send someone plain
|
||||
- *Forge:* `forgeEncounter({heat:"fight", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **cache**.
|
||||
**Temptation — ledger.** a book of names, one line left blank
|
||||
- *Benefit:* write a name and the Host obeys it once
|
||||
- *Accept:* take the Vice **Pride** · *Use:* **+2 Banes** on the tab
|
||||
- *Standing:* your own name is now in it
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Turning Hall
|
||||
*turn · facet: demand · no encounter · hoard: pocket*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a room that resets itself if you look away
|
||||
- a floor mosaic worn only along one path
|
||||
|
||||
**Truth.** here the want is explicit — the place asks something of whoever enters. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** a room that resets itself if you look away — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a shortcut down, through water — *everything gets wet, including what burns*
|
||||
|
||||
**Hoard.** Tier **pocket**.
|
||||
**Temptation — glass.** a hand-mirror that shows the room a moment behind
|
||||
- *Benefit:* once per scene, see what was just here
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* your reflection lags, and others notice
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — flattery.** Who: *whoever performs the appeasement*. When: *each time it is used*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Sunken Nave
|
||||
*descent · facet: wound · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- something kept so well it has forgotten how to end
|
||||
- a door that has been wedged open from the inside
|
||||
|
||||
**Truth.** this is the damage the want did to those who had to satisfy it. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** something kept so well it has forgotten how to end — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a sealed door, and a crack you could squeeze through — *the crack costs skin and a Bane on a failure*
|
||||
|
||||
|
||||
**Attention trigger:** seenTwice.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Antechamber Royal
|
||||
*threshold · facet: anchor · encounter: battle · hoard: chest*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a casket sized for something smaller than a body
|
||||
- a lamp still burning on oil that should be long gone
|
||||
|
||||
**Truth.** the thing the whole barrow is arranged around is close. The Silent Margrave bought the young from those with nothing left, so that nothing would ever age.
|
||||
|
||||
**The clue is automatic:** a casket sized for something smaller than a body — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a way back, still open — *leaving now means it will be worse when you return*
|
||||
|
||||
**Encounter — battle.** 1 Wraith and 3 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Wraith | 5 | 3 | 10 | 5 | INCORPOREAL — chill touch ignores armour and drains 1 max Grit |
|
||||
| 3× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** blessed, silvered or magical weapons ONLY — **say this before initiative**
|
||||
- **Avoidable:** it will take an appeasement over a fight every time
|
||||
- *Forge:* `forgeEncounter({heat:"battle", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **chest**.
|
||||
**Temptation — ledger.** a book of names, one line left blank
|
||||
- *Benefit:* write a name and the Host obeys it once
|
||||
- *Accept:* take the Vice **Pride** · *Use:* **+2 Banes** on the tab
|
||||
- *Standing:* your own name is now in it
|
||||
|
||||
**Attention trigger:** seenTwice.
|
||||
**Bane beat — refusal.** Who: *whoever refuses the offer*. When: *once*. once only. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, the ending opens with them holding nothing the bottom problem wants.
|
||||
|
||||
---
|
||||
|
||||
### ENDING — The Audience
|
||||
### ⟨ AUTHORED SLOT — this is the seam ⟩
|
||||
|
||||
**Areas 1–6 are what DELVE generates. This is what it hands over.**
|
||||
|
||||
**Cue** — *fragments*
|
||||
- jars along the wall, each holding a single year
|
||||
|
||||
**The Silent Margrave** — is old now, and cannot bear it. Wants: *never to age*.
|
||||
Handled by giving them what they want, at a price.
|
||||
|
||||
**They open by asking:** "Tell me I have not changed."
|
||||
|
||||
| Party's route | The audience |
|
||||
|---|---|
|
||||
| Appeased it even once (**tell it that it has not changed**) | It listens. It will trade a sealed jar holding one good year for the real thing, done to its face. `[VANITY: Flair — 2]` or take **Rattled**. |
|
||||
| Refused throughout, tab under 4 | It offers a Vice — take the shine, bank the bill. Refusing starts the fight. |
|
||||
| Carrying a sealed jar holding one good year | It wants that more than it wants you. |
|
||||
| Fighting | Winnable by two engaged heroes. **Name what harms it before initiative.** |
|
||||
|
||||
> **Before initiative, always say what can hurt it.** Playtest 1 was unwinnable because
|
||||
> the table discovered the immunity by failing.
|
||||
|
||||
## Trigger glossary
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,264 @@
|
||||
# THE CAIRN OF THE PEACOCK PRINCE
|
||||
|
||||
> *the barrow does not like it when people leave.*
|
||||
|
||||
**VANITY · Barrow · Depth 2 · 4 heroes · 6 areas + an authored ending**
|
||||
**Seed `salt-vigil-201` · generator 0.1.0 · catalog barrow@1**
|
||||
|
||||
> **Skeleton seed, not a replay seed.** It reproduces the fiction, the area order, the
|
||||
> heat and the hoard tiers. It does **not** reproduce encounter composition, mood or
|
||||
> hoard contents — those come from the Forge, which is not seeded.
|
||||
|
||||
---
|
||||
|
||||
## The truth (GM only)
|
||||
|
||||
**The Cairn of the Peacock Prince.** The Peacock Prince bound servants by oath they could not break, so that someone would always be there. The Peacock Prince is alone now, and cannot bear it. The prize is a keepsake they would not be parted from — the promise that someone would stay. The Silent Host are still performing the ritual they were buried for; they are handled by courtesy. The motif is **company**, and the danger is attachment: the barrow does not like it when people leave.
|
||||
|
||||
**They will ask you:** "You are not leaving."
|
||||
|
||||
## Pressure
|
||||
|
||||
- **The clock.** Roll 1d6 on a trigger, or every 3 Turns. **1 or under — something comes.** Telegraph one Turn ahead.
|
||||
- **Triggers this delve:** lingering · seenTwice · loud · disturbed
|
||||
- **The tab.** Target **5 Banes** by the ending. Banes are *offered by named beats*, never hoped for.
|
||||
- **The danger is attachment:** the barrow does not like it when people leave.
|
||||
|
||||
## The way through — the appeasement
|
||||
|
||||
Anyone may **sit down and stay a while**. `[VANITY: Poise — 2 successes]`
|
||||
On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
**Cost: +1 Vanity now, +1 Bane on the tab.** Shine now, bill later.
|
||||
**Repeatable in every area.** Say the Bane aloud when it banks — the table must be able to see the route is open.
|
||||
|
||||
## Factions
|
||||
|
||||
- **The Silent Host** (undead) — still performing the ritual they were buried for. Handled by *courtesy*.
|
||||
- **grave-robbers** (living) — got in first and cannot get out. Handled by *trade*.
|
||||
|
||||
**Planned fights:** 3 of 6 areas · base heat *fight*
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Weeping Stair
|
||||
*approach · facet: institution · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a coat-rack, full
|
||||
- a door that has been wedged open from the inside
|
||||
|
||||
**Truth.** this is how the place was ordered, and it still is. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** a coat-rack, full — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a way back, still open — *leaving now means it will be worse when you return*
|
||||
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — noticing.** Who: *whoever lingers on their own reflection*. When: *once per hero here*. repeatable across areas. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The First Hall
|
||||
*complication · facet: ritual · no encounter · hoard: pocket*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- places set every evening for three hundred years
|
||||
- a collapse that has opened a way the builders never meant
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** places set every evening for three hundred years — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a shortcut down, through water — *everything gets wet, including what burns*
|
||||
|
||||
**Hoard.** Tier **pocket**.
|
||||
**Temptation — coronet.** a small crown, sized for someone who never grew
|
||||
- *Benefit:* +1 die when others must defer to you
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* you must be addressed correctly or lose the benefit
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Waiting Room
|
||||
*complication · facet: ritual · encounter: fight · hoard: cache*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- chairs drawn up in a circle, all facing one seat
|
||||
- a lamp still burning on oil that should be long gone
|
||||
|
||||
**Truth.** the office is still being performed, for someone who is no longer served by it. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** chairs drawn up in a circle, all facing one seat — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a member of the faction, alone and not yet hostile — *talking costs time; not talking costs later*
|
||||
|
||||
**Encounter — fight.** 1 Ghoul and 2 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Ghoul | 4 | 3 | 4 | 5 | on a hit, a free Called Shot to freeze for a round |
|
||||
| 2× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** anything
|
||||
- **Avoidable:** the Ghoul goes for court dress first — send someone plain
|
||||
- *Forge:* `forgeEncounter({heat:"fight", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **cache**.
|
||||
**Temptation — mask.** a courtier's mask, gold leaf over lime-wood, the mouth fixed in a small smile
|
||||
- *Benefit:* the Host receives you as court
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* while worn you cannot be recognised by name
|
||||
|
||||
**Attention trigger:** seenTwice.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Turning Hall
|
||||
*turn · facet: demand · encounter: fight · hoard: cache*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a doorway with the hinges taken off
|
||||
- a floor mosaic worn only along one path
|
||||
|
||||
**Truth.** here the want is explicit — the place asks something of whoever enters. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** a doorway with the hinges taken off — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** something valuable, awkward to carry — *carrying it slows you and it is not yours*
|
||||
|
||||
**Encounter — fight.** 1 Ghoul and 2 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Ghoul | 4 | 3 | 4 | 5 | on a hit, a free Called Shot to freeze for a round |
|
||||
| 2× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** anything
|
||||
- **Avoidable:** the Ghoul goes for court dress first — send someone plain
|
||||
- *Forge:* `forgeEncounter({heat:"fight", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **cache**.
|
||||
**Temptation — mask.** a courtier's mask, gold leaf over lime-wood, the mouth fixed in a small smile
|
||||
- *Benefit:* the Host receives you as court
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* while worn you cannot be recognised by name
|
||||
|
||||
**Attention trigger:** lingering.
|
||||
**Bane beat — relicUse.** Who: *whoever spends the relic*. When: *each use*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Servants' Deep
|
||||
*descent · facet: wound · no encounter*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a name called so often it wore into the wall
|
||||
- a hole broken through a wall, recently, and not by you
|
||||
|
||||
**Truth.** this is the damage the want did to those who had to satisfy it. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** a name called so often it wore into the wall — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** a sealed door, and a crack you could squeeze through — *the crack costs skin and a Bane on a failure*
|
||||
|
||||
|
||||
**Attention trigger:** loud.
|
||||
**Bane beat — refusal.** Who: *whoever refuses the offer*. When: *once*. once only. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, move its cue fragment into the next area so the motif still completes.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Quiet Room
|
||||
*threshold · facet: anchor · encounter: battle · hoard: chest*
|
||||
|
||||
**Cue** — *assemble aloud; these are fragments, not read-aloud text*
|
||||
- a chair kept empty and dusted
|
||||
- an alcove emptied in a hurry
|
||||
|
||||
**Truth.** the thing the whole barrow is arranged around is close. The Peacock Prince bound servants by oath they could not break, so that someone would always be there.
|
||||
|
||||
**The clue is automatic:** a chair kept empty and dusted — and it was done deliberately.
|
||||
[VANITY: Observation — 1 success] buys who did it, and roughly when
|
||||
|
||||
**The decision.** two ways on: one narrow and dark, one wide and announced — *the wide way is faster and the barrow hears it*
|
||||
|
||||
**Encounter — battle.** 1 Wraith and 3 Skeletons
|
||||
|
||||
| Foe | atk | def | Grit | Nerve | |
|
||||
|---|---|---|---|---|---|
|
||||
| 1× Wraith | 5 | 3 | 10 | 5 | INCORPOREAL — chill touch ignores armour and drains 1 max Grit |
|
||||
| 3× Skeleton | 3 | 2 | 1 | 5 | fearless |
|
||||
|
||||
- **Harmed by:** blessed, silvered or magical weapons ONLY — **say this before initiative**
|
||||
- **Avoidable:** it will take an appeasement over a fight every time
|
||||
- *Forge:* `forgeEncounter({heat:"battle", hoard:false, post:false, folderId})`
|
||||
**Hoard.** Tier **chest**.
|
||||
**Temptation — coronet.** a small crown, sized for someone who never grew
|
||||
- *Benefit:* +1 die when others must defer to you
|
||||
- *Accept:* free · *Use:* **+1 Bane** on the tab
|
||||
- *Standing:* you must be addressed correctly or lose the benefit
|
||||
|
||||
**Attention trigger:** disturbed.
|
||||
**Bane beat — flattery.** Who: *whoever performs the appeasement*. When: *each time it is used*. repeatable. Say it aloud when it banks.
|
||||
**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.
|
||||
|
||||
*Failure case.* If they skip this, the ending opens with them holding nothing the bottom problem wants.
|
||||
|
||||
---
|
||||
|
||||
### ENDING — The Audience
|
||||
### ⟨ AUTHORED SLOT — this is the seam ⟩
|
||||
|
||||
**Areas 1–6 are what DELVE generates. This is what it hands over.**
|
||||
|
||||
**Cue** — *fragments*
|
||||
- a room arranged for a conversation that never ended
|
||||
|
||||
**The Peacock Prince** — is alone now, and cannot bear it. Wants: *never to be alone*.
|
||||
Handled by giving them what they want, at a price.
|
||||
|
||||
**They open by asking:** "You are not leaving."
|
||||
|
||||
| Party's route | The audience |
|
||||
|---|---|
|
||||
| Appeased it even once (**sit down and stay a while**) | It listens. It will trade a keepsake they would not be parted from for the real thing, done to its face. `[VANITY: Poise — 2]` or take **Rattled**. |
|
||||
| Refused throughout, tab under 4 | It offers a Vice — take the shine, bank the bill. Refusing starts the fight. |
|
||||
| Carrying a keepsake they would not be parted from | It wants that more than it wants you. |
|
||||
| Fighting | Winnable by two engaged heroes. **Name what harms it before initiative.** |
|
||||
|
||||
> **Before initiative, always say what can hurt it.** Playtest 1 was unwinnable because
|
||||
> the table discovered the immunity by failing.
|
||||
|
||||
## Trigger glossary
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,107 @@
|
||||
# SEAM SPIKE — measured results
|
||||
|
||||
**Build-order step 2. 2026-08-04.**
|
||||
**Method:** VANITY's Forge run *outside Foundry* against an instrumented stub harness
|
||||
(`harness.mjs`), driven by `spike.mjs`. Every Foundry global is faked; the side-effecting calls
|
||||
(`ChatMessage.create`, `Folder.create`, `Actor.create`, `Scene.create`, `FilePicker.upload`) are
|
||||
recorders, and `Math.random` is counted. **Nothing real was created** — no world was touched.
|
||||
|
||||
Booted successfully: `game.vanity.forge = { hero, monster, face, hoard, encounter, stage, adventure }`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Measurements
|
||||
|
||||
| Call | returns | chat | folders | actors | scenes | uploads | `Math.random` |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `forgeHoard({size:'cache'})` | **undefined** ⚠ | 1 | 0 | 0 | 0 | 0 | 9 |
|
||||
| `forgeEncounter({heat:'fight'})` | object ✅ | **2** | 1 | 5 | 0 | 0 | 58 |
|
||||
| `forgeStage({populate:false})` | Scene ✅ | 1 | 0 | 0 | 1 | 1 | 360 |
|
||||
| `forgeStage({populate:true,heat:'fight'})` | Scene ✅ | **3** | 1 | 3 | 1 | 1 | 552 |
|
||||
| **3 linked areas** | — | **9** | **3** | **16** | 3 | 3 | **1472** |
|
||||
|
||||
**Per area: 3 chat cards · 1 folder · 5.3 actors · 1 scene · 1 upload · ~490 `Math.random` calls.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Seam list, corrected against measurement
|
||||
|
||||
| # | Seam | Verdict |
|
||||
|---|---|---|
|
||||
| 1 | `hoard: false` | **Confirmed needed.** `forgeEncounter` posts 2 chat cards — its own and a forced hoard. |
|
||||
| 2 | `populate: false` | **ALREADY EXISTS — remove from the list.** `forgeStage({populate:false})` runs clean: 0 actors, 0 folders, scene created. It is already the escape hatch. |
|
||||
| 3 | `post: false` | **Confirmed, and it is the big one.** 3 cards per area, 9 for a three-area delve, none suppressible. |
|
||||
| 4 | Structured returns | **Confirmed, but narrower than drafted.** `forgeHoard` returns `undefined`. `forgeEncounter` *already* returns `{actors, situation, terrain, complication, mood, folder}` — only the hoard needs work. |
|
||||
| 5 | Caller-supplied folder | **Confirmed.** 1 folder + 5.3 actors per area → 3 folders and 16 loose Actors for three areas. |
|
||||
| 6 | Live Reaction as a `Roll` | **Confirmed.** No `Roll` was ever constructed during `forgeEncounter`; mood comes from `rollDice()` over `Math.random`. |
|
||||
| 7 | Injectable RNG | **Confirmed large.** ~490 calls per area, 1472 for three. Draft 4 called this "small"; it is not. |
|
||||
|
||||
**Net: draft 5's seven seams become six.** Seam 2 was already solved and I did not check before writing it down.
|
||||
|
||||
---
|
||||
|
||||
## 3. Two findings the spike produced that no review did
|
||||
|
||||
**A. `forgeStage` cannot run outside a browser.** It needs `Image`, `document.createElement('canvas')`,
|
||||
`canvas.toBlob`, `XMLSerializer` and `URL.createObjectURL` to rasterise its SVG. Draft 5 says "the
|
||||
CLI is map-free" as a design choice — it is not a choice, it is a **hard constraint**. Any
|
||||
non-Foundry surface for DELVE can never produce maps without reimplementing rasterisation.
|
||||
|
||||
**B. An unguarded `rnd()` on a possibly-empty array — `vanity.mjs:1969`.**
|
||||
```js
|
||||
const consumables = gear.filter(g => g.system.consumable);
|
||||
if (Math.random() < tier.consum) found.push(rnd(consumables)); // rnd([]) → undefined
|
||||
...found.map(d => `@UUID[${d.uuid}]...`) // throws
|
||||
```
|
||||
`rnd()` returns `undefined` on an empty array and `.uuid` is unread-guarded. My harness triggered
|
||||
this with unfiltered fake gear. **In the shipped system the gear pack has consumables, so it
|
||||
almost certainly never fires** — but it is a one-line guard worth taking while the seams are open.
|
||||
|
||||
---
|
||||
|
||||
## 4. What the spike cannot tell you
|
||||
|
||||
**Latency.** The harness stubs rasterisation and upload, so everything ran in 1–2 ms. The real
|
||||
`forgeStage` encodes a PNG and uploads it before the scene is usable. **The prewarm requirement in
|
||||
draft 5 §10 remains unmeasured** and must be timed inside real Foundry before it is designed
|
||||
around.
|
||||
|
||||
**Table feel.** Nothing here says whether the output is *good* — only how much of it there is.
|
||||
|
||||
---
|
||||
|
||||
## 5. The economic question
|
||||
|
||||
Codex's draft-5 verdict set the right gate: *the next test is economic, not conceptual — does
|
||||
DELVE as a module beat DELVE as a well-made document?* The spike gives the first hard number
|
||||
towards it.
|
||||
|
||||
**Unmodified, the Forge produces per three-area delve: 9 chat cards, 3 Actor folders, 16 loose
|
||||
Actors — none of which the caller can suppress, redirect or own.**
|
||||
|
||||
- A **document** cannot fix this. It has no way to intervene between the Forge and the table.
|
||||
- A **module without seams 1, 3, 4, 5** cannot fix it either. It would be a wrapper watching the
|
||||
Forge talk over it.
|
||||
- A **module with them** can: one folder per delve, output DELVE decides, hoards on its own terms.
|
||||
|
||||
So the module's economic case rests entirely on landing four small seams. That is a genuinely
|
||||
cheap, genuinely testable bet — and if the seams are refused, the honest answer is to ship the
|
||||
document.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommendation
|
||||
|
||||
1. **Update draft 5 §5**: seven seams → six; seam 2 already exists; seam 4 narrows to `forgeHoard`.
|
||||
2. **Add the browser-only constraint** to §13 as a stated non-goal, not a design preference.
|
||||
3. **Land seams 1, 3, 4, 5** — all small, all in `vanity.mjs`, and together they *are* the module's
|
||||
reason to exist. Defer 6 and 7.
|
||||
4. **Time `forgeStage` in real Foundry** before designing prewarm.
|
||||
5. Take the `rnd()` guard while in there.
|
||||
|
||||
Step 2 is complete. The thesis survived: DELVE is a sequencing layer, and the Forge can be
|
||||
sequenced — but only if it stops talking to the table on its own.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
@@ -0,0 +1,161 @@
|
||||
// DELVE seam spike — runs VANITY's Forge outside Foundry against instrumented stubs,
|
||||
// to measure the side effects draft 5 §5 claims. Read-only: creates nothing real.
|
||||
export const log = { chat: [], folders: [], actors: [], scenes: [], uploads: [], random: 0, notifications: [] };
|
||||
|
||||
// A permissive auto-stub: callable, constructible, and any property yields another stub.
|
||||
const stub = (name = 'stub') => new Proxy(function () {}, {
|
||||
get(t, p) {
|
||||
if (p === Symbol.toPrimitive) return () => name;
|
||||
if (p === 'then') return undefined; // never look thenable
|
||||
if (p === Symbol.iterator) return function* () {};
|
||||
if (p === 'name') return name;
|
||||
if (p === 'prototype') return t.prototype;
|
||||
if (!(p in t)) t[p] = stub(`${name}.${String(p)}`);
|
||||
return t[p];
|
||||
},
|
||||
apply: () => stub(`${name}()`),
|
||||
construct: () => stub(`new ${name}`),
|
||||
});
|
||||
|
||||
const hooks = { init: [], ready: [], other: [] };
|
||||
globalThis.Hooks = {
|
||||
once: (h, fn) => (hooks[h] ?? hooks.other).push(fn),
|
||||
on: (h, fn) => (hooks[h] ?? hooks.other).push(fn),
|
||||
callAll: () => true, call: () => true,
|
||||
};
|
||||
|
||||
globalThis.CONFIG = stub('CONFIG');
|
||||
globalThis.CONST = new Proxy({}, { get: (_t, p) => {
|
||||
if (p === 'CHAT_MESSAGE_STYLES' || p === 'CHAT_MESSAGE_TYPES') return { OTHER: 0, OOC: 1, IC: 2, EMOTE: 3, WHISPER: 4, ROLL: 5 };
|
||||
if (p === 'DOCUMENT_OWNERSHIP_LEVELS') return { NONE: 0, LIMITED: 1, OBSERVER: 2, OWNER: 3 };
|
||||
if (p === 'TOKEN_DISPOSITIONS') return { HOSTILE: -1, NEUTRAL: 0, FRIENDLY: 1, SECRET: -2 };
|
||||
if (p === 'GRID_TYPES') return { GRIDLESS: 0, SQUARE: 1 };
|
||||
return 0;
|
||||
} });
|
||||
globalThis.Handlebars = { registerHelper: () => {} };
|
||||
globalThis.canvas = stub('canvas');
|
||||
globalThis.fromUuid = async () => null;
|
||||
globalThis.mergeObject = (a, b) => ({ ...a, ...b });
|
||||
globalThis.duplicate = o => JSON.parse(JSON.stringify(o));
|
||||
globalThis.Dialog = stub('Dialog');
|
||||
globalThis.ui = { notifications: {
|
||||
info: m => log.notifications.push(['info', m]),
|
||||
warn: m => log.notifications.push(['warn', m]),
|
||||
error: m => log.notifications.push(['error', m]),
|
||||
} };
|
||||
|
||||
let uid = 0;
|
||||
const doc = (kind, data) => {
|
||||
const id = `${kind}${++uid}`;
|
||||
return { id, _id: id, uuid: `${kind}.${id}`, name: data?.name ?? id, img: data?.img ?? '',
|
||||
...data, update: async () => {}, createEmbeddedDocuments: async () => [],
|
||||
setFlag: async () => {}, getFlag: () => undefined, sheet: { render: () => {} } };
|
||||
};
|
||||
|
||||
// These must be CLASSES — the system subclasses them (`class VanityActor extends Actor`).
|
||||
class BaseDoc {
|
||||
constructor(data = {}) { Object.assign(this, doc('doc', data)); }
|
||||
async update() {} async createEmbeddedDocuments() { return []; }
|
||||
async setFlag() {} getFlag() { return undefined; }
|
||||
prepareData() {} prepareDerivedData() {}
|
||||
static async create(d) { return doc('doc', d); }
|
||||
}
|
||||
globalThis.ChatMessage = class ChatMessage extends BaseDoc {
|
||||
static async create(d) { log.chat.push(d?.content ?? ''); return doc('msg', d); }
|
||||
static getWhisperRecipients() { return ['gm']; }
|
||||
static applyRollMode(d) { return d; }
|
||||
};
|
||||
globalThis.Folder = class Folder extends BaseDoc {
|
||||
static async create(d) { log.folders.push(d?.name); return doc('fld', d); }
|
||||
};
|
||||
globalThis.Actor = class Actor extends BaseDoc {
|
||||
static async create(d) { log.actors.push(d?.name); return doc('act', d); }
|
||||
};
|
||||
globalThis.Item = class Item extends BaseDoc {
|
||||
static async create(d) { return doc('itm', d); }
|
||||
};
|
||||
globalThis.Scene = class Scene extends BaseDoc {
|
||||
static async create(d) { log.scenes.push(d?.name); return doc('scn', d); }
|
||||
};
|
||||
|
||||
class FakeRoll {
|
||||
constructor(f) { this.formula = f; this.total = 0; this.terms = []; this.dice = []; }
|
||||
async evaluate() { const n = parseInt(this.formula) || 1; this.total = n; return this; }
|
||||
async toMessage(d) { log.chat.push(`[Roll ${this.formula}]`); return doc('msg', d); }
|
||||
}
|
||||
globalThis.Roll = FakeRoll;
|
||||
|
||||
globalThis.foundry = new Proxy({}, { get(t, p) {
|
||||
if (p === 'appv1') return { sheets: { ActorSheet: class {}, ItemSheet: class {} } };
|
||||
if (p === 'utils') return { mergeObject: globalThis.mergeObject, duplicate: globalThis.duplicate, randomID: () => `id${++uid}` };
|
||||
if (p === 'applications') return { apps: { FilePicker: { implementation: {
|
||||
createDirectory: async () => {},
|
||||
upload: async (_s, dir, file) => { log.uploads.push(`${dir}/${file?.name ?? 'file'}`); return { path: `${dir}/x.png` }; },
|
||||
} } }, api: { ApplicationV2: class {}, HandlebarsApplicationMixin: c => c }, sheets: { ActorSheetV2: class {}, ItemSheetV2: class {} } };
|
||||
if (p === 'documents') return new Proxy({}, { get(_t, q) {
|
||||
if (q === 'collections') return {
|
||||
Actors: { registerSheet: () => {}, unregisterSheet: () => {} },
|
||||
Items: { registerSheet: () => {}, unregisterSheet: () => {} } };
|
||||
return class extends BaseDoc {}; // Combatant, Combat, TokenDocument, ...
|
||||
} });
|
||||
return stub(`foundry.${String(p)}`);
|
||||
} });
|
||||
globalThis.FilePicker = globalThis.foundry.applications.apps.FilePicker.implementation;
|
||||
|
||||
// Browser APIs forgeStage() needs to rasterise its SVG. Their absence in Node is itself a
|
||||
// finding: the Forge's map path cannot run outside a browser at all.
|
||||
globalThis.Image = class { set src(v) { this._s = v; queueMicrotask(() => this.onload?.()); } get src() { return this._s; } };
|
||||
globalThis.Blob = globalThis.Blob ?? class { constructor(p, o) { this.parts = p; this.type = o?.type; } };
|
||||
globalThis.File = globalThis.File ?? class { constructor(p, n, o) { this.name = n; this.type = o?.type; } };
|
||||
globalThis.URL.createObjectURL = () => 'blob:fake';
|
||||
globalThis.URL.revokeObjectURL = () => {};
|
||||
globalThis.XMLSerializer = class { serializeToString() { return '<svg/>'; } };
|
||||
globalThis.document = {
|
||||
createElement: tag => tag === 'canvas'
|
||||
? { width: 0, height: 0, getContext: () => ({ drawImage() {}, fillRect() {} }),
|
||||
toBlob: cb => cb(new globalThis.Blob([''], { type: 'image/png' })) }
|
||||
: { style: {}, setAttribute() {}, appendChild() {}, remove() {} },
|
||||
querySelector: () => null, body: { appendChild() {}, removeChild() {} },
|
||||
};
|
||||
|
||||
// Foundry extends String.prototype; the Forge relies on it.
|
||||
String.prototype.slugify = function (o) { return this.toLowerCase().replace(/[^a-z0-9]+/g, o?.replacement ?? '-').replace(/^-|-$/g, ''); };
|
||||
String.prototype.titleCase = function () { return this.replace(/\b\w/g, c => c.toUpperCase()); };
|
||||
|
||||
// A pack stub so compendium-backed generators (hoards, bestiary picks) can complete.
|
||||
const fakePack = (name) => ({
|
||||
metadata: { id: `vanity.${name}`, label: name },
|
||||
index: { contents: [], find: () => null, filter: () => [] },
|
||||
// Some gear must be consumable: forgeHoard does rnd(gear.filter(g => g.system.consumable))
|
||||
// and rnd() on an empty array returns undefined, which then throws on .uuid (vanity.mjs:1969).
|
||||
getDocuments: async () => Array.from({ length: 40 }, (_, i) =>
|
||||
doc('itm', { name: `${name} item ${i + 1}`, type: 'gear',
|
||||
system: { consumable: i % 3 === 0, relic: i % 5 === 0 } })),
|
||||
getDocument: async () => doc('itm', { name: `${name} item`, type: 'gear', system: {} }),
|
||||
getIndex: async () => [],
|
||||
});
|
||||
|
||||
globalThis.game = {
|
||||
user: { isGM: true, id: 'gm', name: 'GM' },
|
||||
users: { filter: () => [], find: () => null },
|
||||
i18n: { localize: s => s, format: s => s },
|
||||
settings: { register: () => {}, get: () => true, set: async () => {} },
|
||||
packs: { get: id => fakePack(String(id).split('.').pop()), find: () => fakePack('x'), filter: () => [] },
|
||||
folders: { find: () => null, filter: () => [] },
|
||||
actors: { find: () => null, filter: () => [], get: () => null },
|
||||
items: { find: () => null, filter: () => [] },
|
||||
scenes: { find: () => null, filter: () => [] },
|
||||
tables: { find: () => null, getName: () => null },
|
||||
messages: { get: () => null },
|
||||
combat: null, combats: [], system: { version: '0.10.3' },
|
||||
modules: { get: () => ({ active: false }) },
|
||||
};
|
||||
|
||||
export async function boot(systemPath) {
|
||||
const realRandom = Math.random;
|
||||
Math.random = () => { log.random++; return realRandom(); };
|
||||
await import(systemPath);
|
||||
for (const fn of hooks.init) await fn();
|
||||
return globalThis.game.vanity;
|
||||
}
|
||||
export function reset() { for (const k of ['chat','folders','actors','scenes','uploads','notifications']) log[k] = []; log.random = 0; }
|
||||
@@ -0,0 +1,54 @@
|
||||
// DELVE seam spike — three linked areas through the Forge, measuring the seams in draft 5 §5.
|
||||
import { boot, log, reset } from './harness.mjs';
|
||||
const SYS = 'file:///Users/timevans/projects/Vanity/vanity.mjs';
|
||||
|
||||
const forge = await boot(SYS);
|
||||
if (!forge) { console.log('BOOT FAILED — game.vanity not populated'); process.exit(1); }
|
||||
console.log('booted. forge API:', Object.keys(forge.forge).join(', '), '\n');
|
||||
|
||||
const snap = () => ({ chat: log.chat.length, folders: log.folders.length, actors: log.actors.length,
|
||||
scenes: log.scenes.length, uploads: log.uploads.length, random: log.random });
|
||||
async function measure(label, fn) {
|
||||
reset();
|
||||
const t0 = Date.now();
|
||||
let ret, err = null;
|
||||
try { ret = await fn(); } catch (e) { err = e; }
|
||||
const ms = Date.now() - t0;
|
||||
const s = snap();
|
||||
console.log(`── ${label}`);
|
||||
if (err) { console.log(` THREW: ${err.message.slice(0,90)} @ ${(String(err.stack).match(/vanity\.mjs:(\d+)/)||[])[0] ?? "?"}`); }
|
||||
console.log(` returns : ${ret === undefined ? 'undefined ⚠ NOTHING RETURNED' : Array.isArray(ret) ? `array(${ret.length})` : typeof ret === 'object' && ret ? `object {${Object.keys(ret).join(',')}}` : String(ret)}`);
|
||||
console.log(` chat=${s.chat} folders=${s.folders} actors=${s.actors} scenes=${s.scenes} uploads=${s.uploads} Math.random=${s.random} ${ms}ms`);
|
||||
return { ret, s, err };
|
||||
}
|
||||
|
||||
console.log('=== SEAM 1+4: forgeHoard — forced coupling target, and does it return? ===');
|
||||
await measure("forgeHoard({size:'cache'})", () => forge.forge.hoard({ size: 'cache' }));
|
||||
|
||||
console.log('\n=== SEAM 1+3+5: forgeEncounter — does it force a hoard? own folder? own chat? ===');
|
||||
await measure("forgeEncounter({heat:'fight',kind:'undead'})", () => forge.forge.encounter({ heat: 'fight', kind: 'undead' }));
|
||||
|
||||
console.log('\n=== SEAM 2: forgeStage populate:false vs true — is the nesting escapable? ===');
|
||||
await measure("forgeStage({type:'barrow',populate:false})", () => forge.forge.stage({ type: 'barrow', size: 'small', populate: false, activate: false }));
|
||||
await measure("forgeStage({type:'barrow',populate:true,heat:'fight'})", () => forge.forge.stage({ type: 'barrow', size: 'small', populate: true, heat: 'fight', activate: false }));
|
||||
|
||||
console.log('\n=== THE SPIKE: three linked areas, as DELVE would sequence them ===');
|
||||
reset();
|
||||
const t0 = Date.now();
|
||||
for (const [i, heat] of [['1','skirmish'],['2','fight'],['3','battle']]) {
|
||||
try { await forge.forge.stage({ type: 'barrow', size: 'small', name: `Area ${i}`, populate: true, heat, activate: false }); }
|
||||
catch (e) { console.log(` area ${i} threw: ${e.message.slice(0,80)}`); }
|
||||
}
|
||||
const s = snap();
|
||||
console.log(` 3 areas → chat=${s.chat} folders=${s.folders} actors=${s.actors} scenes=${s.scenes} uploads=${s.uploads} Math.random=${s.random} ${Date.now()-t0}ms`);
|
||||
console.log(` per area: ${(s.chat/3).toFixed(1)} chat cards, ${(s.folders/3).toFixed(1)} folders, ${(s.actors/3).toFixed(1)} actors`);
|
||||
|
||||
console.log('\n=== SEAMS: verifying the patched clone ===');
|
||||
await measure("forgeHoard({post:false})", () => forge.forge.hoard({ size: 'cache', post: false }));
|
||||
await measure("forgeEncounter({hoard:false,post:false})", () => forge.forge.encounter({ heat: 'fight', hoard: false, post: false }));
|
||||
await measure("forgeEncounter({folderId:'delve-1',post:false,hoard:false})", () => forge.forge.encounter({ heat: 'fight', folderId: 'delve-1', post: false, hoard: false }));
|
||||
await measure("forgeStage({populate:true,post:false})", () => forge.forge.stage({ type:'barrow', size:'small', populate: true, heat:'fight', activate:false, post:false, folderId:'delve-1' }));
|
||||
reset();
|
||||
for (const h of ['skirmish','fight','battle']) await forge.forge.stage({ type:'barrow', size:'small', populate:true, heat:h, activate:false, post:false, folderId:'delve-1' });
|
||||
const t = snap();
|
||||
console.log(` 3 SILENT areas → chat=${t.chat} folders=${t.folders} actors=${t.actors} scenes=${t.scenes}`);
|
||||
@@ -0,0 +1,44 @@
|
||||
// DELVE seam spike — three linked areas through the Forge, measuring the seams in draft 5 §5.
|
||||
import { boot, log, reset } from './harness.mjs';
|
||||
const SYS = 'file:///Users/timevans/Library/Application%20Support/FoundryVTT/Data/systems/vanity/vanity.mjs';
|
||||
|
||||
const forge = await boot(SYS);
|
||||
if (!forge) { console.log('BOOT FAILED — game.vanity not populated'); process.exit(1); }
|
||||
console.log('booted. forge API:', Object.keys(forge.forge).join(', '), '\n');
|
||||
|
||||
const snap = () => ({ chat: log.chat.length, folders: log.folders.length, actors: log.actors.length,
|
||||
scenes: log.scenes.length, uploads: log.uploads.length, random: log.random });
|
||||
async function measure(label, fn) {
|
||||
reset();
|
||||
const t0 = Date.now();
|
||||
let ret, err = null;
|
||||
try { ret = await fn(); } catch (e) { err = e; }
|
||||
const ms = Date.now() - t0;
|
||||
const s = snap();
|
||||
console.log(`── ${label}`);
|
||||
if (err) { console.log(` THREW: ${err.message.slice(0,90)} @ ${(String(err.stack).match(/vanity\.mjs:(\d+)/)||[])[0] ?? "?"}`); }
|
||||
console.log(` returns : ${ret === undefined ? 'undefined ⚠ NOTHING RETURNED' : Array.isArray(ret) ? `array(${ret.length})` : typeof ret === 'object' && ret ? `object {${Object.keys(ret).join(',')}}` : String(ret)}`);
|
||||
console.log(` chat=${s.chat} folders=${s.folders} actors=${s.actors} scenes=${s.scenes} uploads=${s.uploads} Math.random=${s.random} ${ms}ms`);
|
||||
return { ret, s, err };
|
||||
}
|
||||
|
||||
console.log('=== SEAM 1+4: forgeHoard — forced coupling target, and does it return? ===');
|
||||
await measure("forgeHoard({size:'cache'})", () => forge.forge.hoard({ size: 'cache' }));
|
||||
|
||||
console.log('\n=== SEAM 1+3+5: forgeEncounter — does it force a hoard? own folder? own chat? ===');
|
||||
await measure("forgeEncounter({heat:'fight',kind:'undead'})", () => forge.forge.encounter({ heat: 'fight', kind: 'undead' }));
|
||||
|
||||
console.log('\n=== SEAM 2: forgeStage populate:false vs true — is the nesting escapable? ===');
|
||||
await measure("forgeStage({type:'barrow',populate:false})", () => forge.forge.stage({ type: 'barrow', size: 'small', populate: false, activate: false }));
|
||||
await measure("forgeStage({type:'barrow',populate:true,heat:'fight'})", () => forge.forge.stage({ type: 'barrow', size: 'small', populate: true, heat: 'fight', activate: false }));
|
||||
|
||||
console.log('\n=== THE SPIKE: three linked areas, as DELVE would sequence them ===');
|
||||
reset();
|
||||
const t0 = Date.now();
|
||||
for (const [i, heat] of [['1','skirmish'],['2','fight'],['3','battle']]) {
|
||||
try { await forge.forge.stage({ type: 'barrow', size: 'small', name: `Area ${i}`, populate: true, heat, activate: false }); }
|
||||
catch (e) { console.log(` area ${i} threw: ${e.message.slice(0,80)}`); }
|
||||
}
|
||||
const s = snap();
|
||||
console.log(` 3 areas → chat=${s.chat} folders=${s.folders} actors=${s.actors} scenes=${s.scenes} uploads=${s.uploads} Math.random=${s.random} ${Date.now()-t0}ms`);
|
||||
console.log(` per area: ${(s.chat/3).toFixed(1)} chat cards, ${(s.folders/3).toFixed(1)} folders, ${(s.actors/3).toFixed(1)} actors`);
|
||||
Reference in New Issue
Block a user