Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d22421efbd | ||
|
|
b4990533a8 | ||
|
|
075be5c1de | ||
|
|
e2d1de2fb8 | ||
|
|
592d540910 | ||
|
|
1be441f775 | ||
|
|
decb3bf930 |
@@ -308,4 +308,4 @@ failure outcomes, concrete clue answers, and a full ending packet with negotiati
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*VANITY development notes — part of [DELVE](https://github.com/slaguru666/vanity-delve).*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 slaguru666
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,54 @@
|
||||
# DELVE
|
||||
|
||||
An authoring system and Foundry VTT module for the **VANITY** tabletop RPG. Press one button and
|
||||
it generates a whole dungeon — fiction, areas, maps, encounters, hoards and a journal you can read
|
||||
cold — then drops it into your world.
|
||||
|
||||
## Install (Foundry)
|
||||
|
||||
**Add-on Modules → Install Module → Manifest URL:**
|
||||
|
||||
```
|
||||
https://github.com/slaguru666/vanity-delve/releases/latest/download/module.json
|
||||
```
|
||||
|
||||
Requires the [VANITY](https://github.com/slaguru666/Vanity) system, **0.10.5 or later**.
|
||||
|
||||
* **⛏** raise a dungeon
|
||||
* **🗑** remove one you don't want — it deletes exactly what it made and nothing else
|
||||
* **⏩** stage a delve you finished at the desk
|
||||
|
||||
## Also a CLI
|
||||
|
||||
`core/` is pure JavaScript with no Foundry dependencies, so a delve can be generated, rerolled and
|
||||
finished outside the VTT:
|
||||
|
||||
```bash
|
||||
node core/cli.mjs --new --seed=my-delve --file=my.json
|
||||
node core/cli.mjs --file=my.json --worksheet # everything, with prompts for what only you can write
|
||||
node core/cli.mjs --file=my.json --reroll=3:situation
|
||||
node core/cli.mjs --file=my.json --play # the tight, table-facing sheet
|
||||
```
|
||||
|
||||
## What it does and does not do
|
||||
|
||||
It generates the fiction, the order and the pressure. It does **not** write your read-aloud text
|
||||
or your climax, and it says so — those are prompts, not output. It gets you to a strong first
|
||||
draft in a minute instead of an evening, then gets out of the way.
|
||||
|
||||
[`DESIGN.md`](DESIGN.md) has the whole design history, including four rounds of adversarial review
|
||||
and two paper playtests that failed before anything worked.
|
||||
|
||||
## Layout
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `core/` | the generator — pure JS, 39 tests |
|
||||
| `foundry-module/` | the Foundry module, with `core/` vendored |
|
||||
| `paper/` | a hand-written delve and both playtest logs |
|
||||
| `spike/` | the harness that measured VANITY's Forge outside Foundry |
|
||||
| `samples/` | generated output |
|
||||
|
||||
## Licence
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Regenerate content/index.json — the browser cannot list a directory, so packs need a manifest. */
|
||||
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
const dir = join(dirname(fileURLToPath(import.meta.url)), 'content');
|
||||
const themes = readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'index.json')
|
||||
.map(f => JSON.parse(readFileSync(join(dir, f), 'utf8')))
|
||||
.map(p => ({ id: p.id, label: p.label ?? p.id, geometry: p.forgeStageType, motifs: Object.keys(p.motifs ?? {}).length }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
writeFileSync(join(dir, 'index.json'), JSON.stringify({ schema: 1, themes }, null, 2));
|
||||
console.log(` ${themes.length} themes: ${themes.map(t => t.id).join(', ')}`);
|
||||
+8
-1
@@ -24,7 +24,14 @@ const args = Object.fromEntries(process.argv.slice(2).map(a => {
|
||||
return [k, v === '' ? true : (/^\d+$/.test(v) ? Number(v) : v)];
|
||||
}));
|
||||
|
||||
const pack = JSON.parse(readFileSync(join(here, 'content', `${args.theme ?? 'barrow'}.json`), 'utf8'));
|
||||
const index = JSON.parse(readFileSync(join(here, 'content', 'index.json'), 'utf8'));
|
||||
if (args.themes) { console.log(index.themes.map(t => `${t.id.padEnd(10)} ${t.geometry.padEnd(8)} ${t.motifs} motifs`).join('\n')); process.exit(0); }
|
||||
const themeId = args.theme ?? 'barrow';
|
||||
if (!index.themes.some(t => t.id === themeId)) {
|
||||
console.error(`unknown theme "${themeId}" — available: ${index.themes.map(t => t.id).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const pack = JSON.parse(readFileSync(join(here, 'content', `${themeId}.json`), 'utf8'));
|
||||
const file = args.file;
|
||||
const load = () => JSON.parse(readFileSync(file, 'utf8'));
|
||||
const save = d => writeFileSync(file, JSON.stringify(d, null, 2));
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"How do I look?\""
|
||||
},
|
||||
{
|
||||
"id": "remembered",
|
||||
@@ -47,7 +48,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Say my name.\""
|
||||
},
|
||||
{
|
||||
"id": "obeyed",
|
||||
@@ -69,7 +71,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Do as you are told.\""
|
||||
},
|
||||
{
|
||||
"id": "young",
|
||||
@@ -91,7 +94,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Tell me I have not changed.\""
|
||||
},
|
||||
{
|
||||
"id": "envied",
|
||||
@@ -113,7 +117,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"What did you bring me?\""
|
||||
},
|
||||
{
|
||||
"id": "attended",
|
||||
@@ -135,7 +140,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"You are not leaving.\""
|
||||
},
|
||||
{
|
||||
"id": "forgiven",
|
||||
@@ -157,7 +163,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Tell me it was not a sin.\""
|
||||
},
|
||||
{
|
||||
"id": "first",
|
||||
@@ -179,7 +186,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Who goes first?\""
|
||||
}
|
||||
],
|
||||
"accommodations": [
|
||||
@@ -2146,5 +2154,6 @@
|
||||
"lingering": "stayed in one place longer than it takes to search it",
|
||||
"disturbed": "struck or moved something that had not moved in centuries"
|
||||
},
|
||||
"_globalPoolsNote": "decisions/temptations/features below are FALLBACKS ONLY. Real content is motif-scoped under motifs.<id>."
|
||||
"_globalPoolsNote": "decisions/temptations/features below are FALLBACKS ONLY. Real content is motif-scoped under motifs.<id>.",
|
||||
"placeNameStyle": "claimant"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"themes": [
|
||||
{
|
||||
"id": "barrow",
|
||||
"label": "Barrow",
|
||||
"geometry": "barrow",
|
||||
"motifs": 8
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"label": "Castle",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "church",
|
||||
"label": "Church",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "city",
|
||||
"label": "City",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "forest",
|
||||
"label": "Forest",
|
||||
"geometry": "forest",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "market",
|
||||
"label": "Market",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "palace",
|
||||
"label": "Palace",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "port",
|
||||
"label": "Port",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "temple",
|
||||
"label": "Temple",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "town",
|
||||
"label": "Town",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "village",
|
||||
"label": "Village",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+9
-14
@@ -103,7 +103,7 @@ export function generateDelve(params = {}) {
|
||||
...plan.ending,
|
||||
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
||||
cueFragments: [endFs?.fragment].filter(Boolean),
|
||||
question: endingQuestion(skeleton),
|
||||
question: endingQuestion(skeleton, pack),
|
||||
authored: plan.ending.mode === 'authored',
|
||||
},
|
||||
triggerGlossary: pack.triggerGlossary ?? {},
|
||||
@@ -112,17 +112,12 @@ export function generateDelve(params = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 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?"';
|
||||
/**
|
||||
* The bottom problem asks one thing, and it is the same question the whole delve has been about.
|
||||
* It lives on the appetite in the theme pack — hardcoding it here meant any theme whose appetite
|
||||
* ids differed from barrow's silently fell through to a generic line.
|
||||
*/
|
||||
function endingQuestion(sk, pack) {
|
||||
const appetite = pack?.appetites?.find(a => a.id === sk.knot.appetiteId);
|
||||
return appetite?.asks ?? '"Well?"';
|
||||
}
|
||||
|
||||
+1
-1
@@ -165,6 +165,6 @@ export function renderMarkdown(d) {
|
||||
}
|
||||
L.push('---');
|
||||
L.push('');
|
||||
L.push('*Private VANITY development material — not for sale or distribution.*');
|
||||
L.push('*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*');
|
||||
return L.join('\n');
|
||||
}
|
||||
|
||||
+6
-2
@@ -64,6 +64,7 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
|
||||
const transgression =
|
||||
`The ${claimant} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` +
|
||||
`so that ${appetite.purpose}.`;
|
||||
// The bottom problem is the claimant itself — a person in a tomb, an office in a town.
|
||||
|
||||
const bottomProblem = {
|
||||
label: `The ${claimant}`,
|
||||
@@ -79,8 +80,11 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
|
||||
};
|
||||
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}`;
|
||||
// A barrow or keep is named for its occupant — drawing that separately produced tombs named
|
||||
// after strangers. A settlement is not: it has its own name and the claimant merely runs it.
|
||||
const placeName = pack.placeNameStyle === 'own'
|
||||
? `${r.derive('place1').pick(pack.placeNames.first)} ${r.derive('place2').pick(pack.placeNames.second)}`
|
||||
: `${r.derive('place').pick(pack.placeNames.first)} ${claimant}`;
|
||||
|
||||
// ---- the foreshadow chain ----------------------------------------------
|
||||
const progression = pack.facetProgression ?? {};
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Theme pack validator.
|
||||
*
|
||||
* A pack is ~136 authored entries and every structural rule in it was learned the hard way:
|
||||
* situations that render as broken English, decisions with no way through, motifs too thin to
|
||||
* fill six areas without repeating. Rather than rediscover those per theme, this checks them.
|
||||
*
|
||||
* Run: node validate-pack.mjs [name ...] (default: every pack in content/)
|
||||
*/
|
||||
import { readFileSync, readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const FACETS = ['institution', 'ritual', 'demand', 'wound', 'anchor'];
|
||||
const ROLES = ['approach', 'complication', 'turn', 'descent', 'threshold', 'ending'];
|
||||
const GEOMETRIES = ['barrow', 'cave', 'fen', 'village', 'forest'];
|
||||
const HEATS = ['skirmish', 'fight', 'battle', 'nightmare'];
|
||||
|
||||
/** Minimums sized so a 6-area delve never repeats — the default and most common case. */
|
||||
const MIN = { fragmentsPerFacet: 3, features: 6, decisions: 6, temptations: 5, situations: 6, areaNamesPerRole: 4 };
|
||||
|
||||
export function validatePack(pack, name = pack?.id ?? '?') {
|
||||
const errs = [], warns = [];
|
||||
const E = m => errs.push(m), W = m => warns.push(m);
|
||||
|
||||
if (!pack.id) E('no id');
|
||||
if (!GEOMETRIES.includes(pack.forgeStageType))
|
||||
E(`forgeStageType "${pack.forgeStageType}" is not one VANITY can build (${GEOMETRIES.join(', ')})`);
|
||||
|
||||
for (const k of ['appetites', 'accommodations', 'claimants', 'motifs', 'prizes', 'areaNames', 'factions', 'rosters'])
|
||||
if (!pack[k]) E(`missing ${k}`);
|
||||
if (errs.length) return { name, errs, warns };
|
||||
|
||||
const motifIds = Object.keys(pack.motifs);
|
||||
|
||||
for (const a of pack.appetites) {
|
||||
for (const f of ['id', 'want', 'lack', 'purpose', 'motif', 'prize', 'conscript', 'appeasement', 'asks'])
|
||||
if (!a[f]) E(`appetite ${a.id ?? '?'}: missing ${f}`);
|
||||
if (a.motif && !motifIds.includes(a.motif)) E(`appetite ${a.id}: motif "${a.motif}" does not exist`);
|
||||
if (a.prize && !pack.prizes?.[a.prize]) E(`appetite ${a.id}: prize "${a.prize}" has no entry`);
|
||||
const ap = a.appeasement ?? {};
|
||||
for (const f of ['move', 'attribute', 'successes', 'why', 'gain', 'cost'])
|
||||
if (ap[f] === undefined) E(`appetite ${a.id}: appeasement missing ${f}`);
|
||||
}
|
||||
|
||||
// Every appetite must have at least one accommodation that fits it, or the kernel cannot form.
|
||||
for (const a of pack.appetites) {
|
||||
const fits = pack.accommodations.filter(x => !x.fits || x.fits.includes(a.id));
|
||||
if (!fits.length) E(`appetite ${a.id}: no accommodation fits it`);
|
||||
}
|
||||
for (const acc of pack.accommodations) {
|
||||
if (!acc.text?.includes('{conscript}')) W(`accommodation ${acc.id}: no {conscript} slot — will read oddly`);
|
||||
}
|
||||
|
||||
for (const [id, m] of Object.entries(pack.motifs)) {
|
||||
for (const f of ['danger', 'dangerLine', 'trigger', 'facets'])
|
||||
if (!m[f]) E(`motif ${id}: missing ${f}`);
|
||||
for (const facet of FACETS) {
|
||||
const n = (m.facets?.[facet] ?? []).length;
|
||||
if (n < MIN.fragmentsPerFacet) E(`motif ${id}: facet ${facet} has ${n} fragments, needs ${MIN.fragmentsPerFacet}`);
|
||||
}
|
||||
for (const [key, min] of [['features', MIN.features], ['decisions', MIN.decisions],
|
||||
['temptations', MIN.temptations], ['situations', MIN.situations]]) {
|
||||
const n = (m[key] ?? []).length;
|
||||
if (n < min) E(`motif ${id}: ${n} ${key}, needs ${min} so a 6-area delve never repeats`);
|
||||
}
|
||||
|
||||
for (const d of m.decisions ?? []) {
|
||||
const r = d.resolve;
|
||||
if (!r) { E(`motif ${id}: decision "${String(d.cue).slice(0, 40)}" has no resolve`); continue; }
|
||||
if (!r.roll) E(`motif ${id}: decision "${String(d.cue).slice(0, 40)}" has no attempt roll — a pick, not a situation`);
|
||||
if (r.roll && (!r.success || !r.failure)) E(`motif ${id}: decision "${String(d.cue).slice(0, 40)}" states only half its outcomes`);
|
||||
if (!r.orElse) E(`motif ${id}: decision "${String(d.cue).slice(0, 40)}" gates progress with no alternative`);
|
||||
}
|
||||
|
||||
// The opener reads "<occupant> — <doing>. When you walk in, <onArrival>."
|
||||
for (const s of m.situations ?? []) {
|
||||
for (const f of ['occupant', 'doing', 'onArrival', 'because', 'offer'])
|
||||
if (!s[f]) E(`motif ${id}: situation missing ${f}`);
|
||||
if (/\b(is|are|but)\b|—/.test(s.occupant ?? '')) E(`motif ${id}: occupant "${s.occupant}" carries a verb; must be a noun phrase`);
|
||||
if (!/^\w+ing\b/.test(s.doing ?? '') || /^(some|no|any|every)thing\b/.test(s.doing ?? ''))
|
||||
E(`motif ${id}: doing "${String(s.doing).slice(0, 40)}" must be a participle phrase`);
|
||||
}
|
||||
|
||||
for (const t of m.temptations ?? []) {
|
||||
for (const f of ['id', 'cue', 'benefit', 'useCost', 'standingDrawback'])
|
||||
if (!t[f]) E(`motif ${id}: temptation ${t.id ?? '?'} missing ${f}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of ROLES) {
|
||||
const n = (pack.areaNames?.[r] ?? []).length;
|
||||
if (n < MIN.areaNamesPerRole) E(`areaNames.${r}: ${n} names, needs ${MIN.areaNamesPerRole}`);
|
||||
}
|
||||
const allNames = Object.values(pack.areaNames ?? {}).flat();
|
||||
if (new Set(allNames).size !== allNames.length) E('areaNames contains duplicates across roles');
|
||||
|
||||
for (const h of HEATS) {
|
||||
const r = pack.rosters?.[h];
|
||||
if (!r) { E(`rosters.${h} missing`); continue; }
|
||||
if (!r.line || !r.foes?.length) E(`rosters.${h}: needs a line and foes`);
|
||||
if (!r.harmedBy) E(`rosters.${h}: must say what harms them — a party discovering an immunity by failing is how playtest 1 died`);
|
||||
if (!r.avoid) W(`rosters.${h}: no avoidance note`);
|
||||
for (const f of r.foes ?? [])
|
||||
for (const k of ['n', 'name', 'atk', 'def', 'grit', 'nerve'])
|
||||
if (f[k] === undefined) E(`rosters.${h}: foe ${f.name ?? '?'} missing ${k}`);
|
||||
}
|
||||
|
||||
for (const r of ROLES) if (!pack.facetProgression?.[r]) E(`facetProgression.${r} missing`);
|
||||
if (!pack.triggerGlossary || Object.keys(pack.triggerGlossary).length < 3) W('triggerGlossary is thin');
|
||||
|
||||
return { name, errs, warns };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const dir = join(here, 'content');
|
||||
const names = process.argv.slice(2).length
|
||||
? process.argv.slice(2)
|
||||
: readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'index.json').map(f => f.replace('.json', ''));
|
||||
let bad = 0;
|
||||
for (const n of names) {
|
||||
const pack = JSON.parse(readFileSync(join(dir, `${n}.json`), 'utf8'));
|
||||
const { errs, warns } = validatePack(pack, n);
|
||||
const counts = `${Object.keys(pack.motifs ?? {}).length} motifs`;
|
||||
if (!errs.length) console.log(` ✅ ${n.padEnd(10)} ${counts}${warns.length ? ` (${warns.length} warnings)` : ''}`);
|
||||
else { bad++; console.log(` ❌ ${n.padEnd(10)} ${errs.length} errors`); for (const e of errs.slice(0, 12)) console.log(` ${e}`); }
|
||||
for (const w of warns.slice(0, 4)) console.log(` ⚠ ${w}`);
|
||||
}
|
||||
process.exit(bad ? 1 : 0);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 slaguru666
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "vanity-delve",
|
||||
"title": "DELVE — a dungeon layer for VANITY",
|
||||
"description": "Generates a coherent delve and sequences VANITY's Forge to stage it, one area at a time.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.5.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "14.365"
|
||||
@@ -11,7 +11,8 @@
|
||||
"systems": [
|
||||
{
|
||||
"id": "vanity",
|
||||
"type": "requires",
|
||||
"type": "system",
|
||||
"manifest": "https://raw.githubusercontent.com/slaguru666/Vanity/main/system.json",
|
||||
"compatibility": {
|
||||
"minimum": "0.10.0"
|
||||
}
|
||||
@@ -32,5 +33,6 @@
|
||||
"manifest": "https://github.com/slaguru666/vanity-delve/releases/latest/download/module.json",
|
||||
"download": "https://github.com/slaguru666/vanity-delve/releases/latest/download/vanity-delve.zip",
|
||||
"url": "https://github.com/slaguru666/vanity-delve",
|
||||
"readme": "https://github.com/slaguru666/vanity-delve/blob/main/foundry-module/README.md"
|
||||
"readme": "https://github.com/slaguru666/vanity-delve/blob/main/foundry-module/README.md",
|
||||
"license": "https://github.com/slaguru666/vanity-delve/blob/main/LICENSE"
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"How do I look?\""
|
||||
},
|
||||
{
|
||||
"id": "remembered",
|
||||
@@ -47,7 +48,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Say my name.\""
|
||||
},
|
||||
{
|
||||
"id": "obeyed",
|
||||
@@ -69,7 +71,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Do as you are told.\""
|
||||
},
|
||||
{
|
||||
"id": "young",
|
||||
@@ -91,7 +94,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Tell me I have not changed.\""
|
||||
},
|
||||
{
|
||||
"id": "envied",
|
||||
@@ -113,7 +117,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"What did you bring me?\""
|
||||
},
|
||||
{
|
||||
"id": "attended",
|
||||
@@ -135,7 +140,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"You are not leaving.\""
|
||||
},
|
||||
{
|
||||
"id": "forgiven",
|
||||
@@ -157,7 +163,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Tell me it was not a sin.\""
|
||||
},
|
||||
{
|
||||
"id": "first",
|
||||
@@ -179,7 +186,8 @@
|
||||
"bane": 1
|
||||
},
|
||||
"repeatable": true
|
||||
}
|
||||
},
|
||||
"asks": "\"Who goes first?\""
|
||||
}
|
||||
],
|
||||
"accommodations": [
|
||||
@@ -2146,5 +2154,6 @@
|
||||
"lingering": "stayed in one place longer than it takes to search it",
|
||||
"disturbed": "struck or moved something that had not moved in centuries"
|
||||
},
|
||||
"_globalPoolsNote": "decisions/temptations/features below are FALLBACKS ONLY. Real content is motif-scoped under motifs.<id>."
|
||||
"_globalPoolsNote": "decisions/temptations/features below are FALLBACKS ONLY. Real content is motif-scoped under motifs.<id>.",
|
||||
"placeNameStyle": "claimant"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"themes": [
|
||||
{
|
||||
"id": "barrow",
|
||||
"label": "Barrow",
|
||||
"geometry": "barrow",
|
||||
"motifs": 8
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"label": "Castle",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "church",
|
||||
"label": "Church",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "city",
|
||||
"label": "City",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "forest",
|
||||
"label": "Forest",
|
||||
"geometry": "forest",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "market",
|
||||
"label": "Market",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "palace",
|
||||
"label": "Palace",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "port",
|
||||
"label": "Port",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "temple",
|
||||
"label": "Temple",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "town",
|
||||
"label": "Town",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "village",
|
||||
"label": "Village",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -103,7 +103,7 @@ export function generateDelve(params = {}) {
|
||||
...plan.ending,
|
||||
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
||||
cueFragments: [endFs?.fragment].filter(Boolean),
|
||||
question: endingQuestion(skeleton),
|
||||
question: endingQuestion(skeleton, pack),
|
||||
authored: plan.ending.mode === 'authored',
|
||||
},
|
||||
triggerGlossary: pack.triggerGlossary ?? {},
|
||||
@@ -112,17 +112,12 @@ export function generateDelve(params = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 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?"';
|
||||
/**
|
||||
* The bottom problem asks one thing, and it is the same question the whole delve has been about.
|
||||
* It lives on the appetite in the theme pack — hardcoding it here meant any theme whose appetite
|
||||
* ids differed from barrow's silently fell through to a generic line.
|
||||
*/
|
||||
function endingQuestion(sk, pack) {
|
||||
const appetite = pack?.appetites?.find(a => a.id === sk.knot.appetiteId);
|
||||
return appetite?.asks ?? '"Well?"';
|
||||
}
|
||||
|
||||
@@ -165,6 +165,6 @@ export function renderMarkdown(d) {
|
||||
}
|
||||
L.push('---');
|
||||
L.push('');
|
||||
L.push('*Private VANITY development material — not for sale or distribution.*');
|
||||
L.push('*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*');
|
||||
return L.join('\n');
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
|
||||
const transgression =
|
||||
`The ${claimant} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` +
|
||||
`so that ${appetite.purpose}.`;
|
||||
// The bottom problem is the claimant itself — a person in a tomb, an office in a town.
|
||||
|
||||
const bottomProblem = {
|
||||
label: `The ${claimant}`,
|
||||
@@ -79,8 +80,11 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
|
||||
};
|
||||
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}`;
|
||||
// A barrow or keep is named for its occupant — drawing that separately produced tombs named
|
||||
// after strangers. A settlement is not: it has its own name and the claimant merely runs it.
|
||||
const placeName = pack.placeNameStyle === 'own'
|
||||
? `${r.derive('place1').pick(pack.placeNames.first)} ${r.derive('place2').pick(pack.placeNames.second)}`
|
||||
: `${r.derive('place').pick(pack.placeNames.first)} ${claimant}`;
|
||||
|
||||
// ---- the foreshadow chain ----------------------------------------------
|
||||
const progression = pack.facetProgression ?? {};
|
||||
|
||||
@@ -18,7 +18,9 @@ const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||||
const esc = s => foundry.utils.escapeHTML?.(String(s ?? '')) ?? String(s ?? '');
|
||||
|
||||
const MOD = 'vanity-delve';
|
||||
const THEMES = ['barrow'];
|
||||
let THEMES = [{ id: 'barrow', label: 'Barrow' }];
|
||||
|
||||
export function setThemes(list) { THEMES = list; }
|
||||
const PICKS = {
|
||||
depth: [1, 2, 3, 4, 5],
|
||||
party: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
@@ -76,7 +78,9 @@ export class DelveForgeApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
*/
|
||||
export async function raiseDungeon(params = {}) {
|
||||
if (!game.user.isGM) return ui.notifications.warn('DELVE is a GM tool.');
|
||||
const PACK = game.delve.pack;
|
||||
// Load the chosen theme's pack on demand — only the default is preloaded.
|
||||
const PACK = await game.delve.loadPack(params.theme ?? 'barrow');
|
||||
if (!PACK) return ui.notifications.error(`DELVE: could not load the ${params.theme} theme.`);
|
||||
const seed = params.seed || coinSeed(new Rng(String(Date.now())));
|
||||
const d = generateDelve({ pack: PACK, ...params, seed });
|
||||
const sk = d.skeleton;
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
*/
|
||||
import { coinSeed, Rng } from './core/rng.mjs';
|
||||
import { newWorkingFile, outstanding, readyToPlay } from './core/authoring.mjs';
|
||||
import { DelveForgeApp, raiseDungeon, listDungeons, removeDungeon, removeDungeonDialog } from './forge-app.mjs';
|
||||
import { DelveForgeApp, raiseDungeon, listDungeons, removeDungeon, removeDungeonDialog, setThemes } from './forge-app.mjs';
|
||||
|
||||
const MOD = 'vanity-delve';
|
||||
const FLAG = 'state';
|
||||
|
||||
let PACK = null;
|
||||
let seamsPresent = false;
|
||||
let loadPack = async () => null;
|
||||
|
||||
const getState = () => game.settings.get(MOD, FLAG) ?? null;
|
||||
const setState = async s => game.settings.set(MOD, FLAG, s);
|
||||
@@ -157,8 +158,13 @@ Hooks.once('init', () => {
|
||||
|
||||
Hooks.once('ready', async () => {
|
||||
if (!game.user.isGM) return;
|
||||
PACK = await foundry.utils.fetchJsonWithTimeout(`modules/${MOD}/module/core/content/barrow.json`)
|
||||
.catch(() => fetch(`modules/${MOD}/module/core/content/barrow.json`).then(r => r.json()));
|
||||
const fetchJson = p => foundry.utils.fetchJsonWithTimeout(p).catch(() => fetch(p).then(r => r.json()).catch(() => null));
|
||||
const base = `modules/${MOD}/module/core/content`;
|
||||
const index = await fetchJson(`${base}/index.json`);
|
||||
if (index?.themes?.length) setThemes(index.themes);
|
||||
const packs = {};
|
||||
loadPack = async id => (packs[id] ??= await fetchJson(`${base}/${id}.json`));
|
||||
PACK = await loadPack('barrow');
|
||||
seamsPresent = /post\s*=\s*true/.test(String(game.vanity?.forge?.hoard ?? ''));
|
||||
|
||||
game.delve = { forge: () => new DelveForgeApp().render(true), raise: raiseDungeon,
|
||||
@@ -166,6 +172,7 @@ Hooks.once('ready', async () => {
|
||||
load, loadFile, draft, enter, ending, bane, clock, state: getState,
|
||||
outstanding: () => outstanding(getState()?.delve ?? { areas: [] }),
|
||||
ready: () => readyToPlay(getState()?.delve ?? { areas: [] }),
|
||||
loadPack: id => loadPack(id),
|
||||
get pack() { return PACK; } };
|
||||
console.log(`DELVE | ready. Forge seams ${seamsPresent ? 'present' : 'ABSENT'}.`);
|
||||
if (!seamsPresent) ui.notifications.warn('DELVE: Forge seams not installed — see the delve-seams branch.');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="delve-grid">
|
||||
<label>Theme
|
||||
<select name="theme">
|
||||
{{#each themes}}<option value="{{this}}" {{#if (eq this ../v.theme)}}selected{{/if}}>{{this}}</option>{{/each}}
|
||||
{{#each themes}}<option value="{{this.id}}" {{#if (eq this.id ../v.theme)}}selected{{/if}}>{{this.label}}</option>{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label>Areas
|
||||
|
||||
@@ -209,4 +209,4 @@ the gang, take the portrait in A5, and have nobody bow — the path v1 punishes
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*VANITY development notes — part of [DELVE](https://github.com/slaguru666/vanity-delve).*
|
||||
|
||||
@@ -170,4 +170,4 @@ Next: **step 2, the seam spike.**
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*VANITY development notes — part of [DELVE](https://github.com/slaguru666/vanity-delve).*
|
||||
|
||||
@@ -344,4 +344,4 @@ Pass 2 is **contrarian**: Court Door, nobody bows, ally the gang, take the portr
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*VANITY development notes — part of [DELVE](https://github.com/slaguru666/vanity-delve).*
|
||||
|
||||
@@ -40,7 +40,7 @@ On success it goes first, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Threshold Stones
|
||||
### AREA 1 — The Mourners' Steps
|
||||
*approach · institution*
|
||||
|
||||
> A stair too grand for the door it serves. The queue disturbed — three of the dead pushed aside. One of the Host — reading out an order of precedence to an empty hall. When you walk in, it pauses and asks where you fall in it. None of it is accident — it was done this way on purpose.
|
||||
@@ -76,7 +76,7 @@ On success it goes first, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Waiting Room
|
||||
### AREA 3 — The Antechamber
|
||||
*complication · ritual*
|
||||
|
||||
> A herald's script with the order still marked. Someone has walked the corridor the wrong way, scuffing one side. One of the Host with a herald's staff — announcing arrivals to an empty stair. When you walk in, it announces you, inventing a rank. None of it is accident — it was done this way on purpose.
|
||||
@@ -122,7 +122,7 @@ On success it goes first, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Sunken Nave
|
||||
### AREA 5 — The Lower Cut
|
||||
*descent · wound · pocket*
|
||||
|
||||
> A procession that doubled back on itself. Banners pulled down and stacked without regard for order. A procession — passing through, unhurried, with you in its way. When you walk in, it does not stop or go round. None of it is accident — it was done this way on purpose.
|
||||
@@ -143,7 +143,7 @@ On success it goes first, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Last Door
|
||||
### AREA 6 — The Robing Room
|
||||
*threshold · anchor · battle · chest*
|
||||
|
||||
> A seat at the head of everything, occupied. A rank-plate prised off and dropped, still shiny where it sat. A queue of the Host — waiting to pass a door, in strict order, not moving. When you walk in, the queue opens a gap at exactly your rank. None of it is accident — it was done this way on purpose.
|
||||
@@ -204,4 +204,4 @@ Handled by giving them what they want, at a price.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*
|
||||
@@ -40,7 +40,7 @@ On success it was obeyed, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Weeping Stair
|
||||
### AREA 1 — The Outer Cut
|
||||
*approach · institution*
|
||||
|
||||
> Servants' bones arranged as though mid-errand. Someone has been talking here, and stopped when you arrived. Three of the Host in servants' dress — carrying out an errand that has no destination left. When you walk in, they stop, and wait to be told what to do. None of it is accident — it was done this way on purpose.
|
||||
@@ -58,7 +58,7 @@ On success it was obeyed, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The Lesser Vault
|
||||
### AREA 2 — The Waiting Room
|
||||
*complication · ritual · fight · cache*
|
||||
|
||||
> Orders scratched into the floor, the last one unfinished. A door braced shut from within by furniture. A living grave-robber — repeating a phrase over and over, trying to get the intonation right. When you walk in, they go quiet and put their back to the wall. None of it is accident — it was done this way on purpose.
|
||||
@@ -86,7 +86,7 @@ On success it was obeyed, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Crossing
|
||||
### AREA 3 — The Middle Vault
|
||||
*turn · demand*
|
||||
|
||||
> A hall where your own voice arrives before you speak. An order half-scratched out and rewritten by a different hand. The bells — ringing a summons room by room, coming closer. When you walk in, the bell in this room begins. None of it is accident — it was done this way on purpose.
|
||||
@@ -104,7 +104,7 @@ On success it was obeyed, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Servants' Deep
|
||||
### AREA 4 — The Cold Gallery
|
||||
*descent · wound · pocket*
|
||||
|
||||
> An instruction repeated until the stone gave out. Boot-prints that stop mid-corridor and do not resume. A single figure at a speaking-tube — reporting, at length, to a listener who died first. When you walk in, it pauses mid-sentence and asks whether you are the reply. None of it is accident — it was done this way on purpose.
|
||||
@@ -186,4 +186,4 @@ Handled by giving them what they want, at a price.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*
|
||||
@@ -40,7 +40,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 1 — The Outer Cut
|
||||
### AREA 1 — The Mourners' Steps
|
||||
*approach · institution*
|
||||
|
||||
> Lamps trimmed and filled, all of them. A seal broken and the contents already spoiled. Two of the Host — repairing something that is not damaged. When you walk in, they set the work down and wait for inspection. None of it is accident — it was done this way on purpose.
|
||||
@@ -58,7 +58,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The Lesser Vault
|
||||
### AREA 2 — The Attendants' Row
|
||||
*complication · ritual · fight · cache*
|
||||
|
||||
> A meal laid out, still warm, for people long past eating. One preserved thing removed, leaving a clean shape in the dust. One of the Host — trimming a lamp that has never gone out. When you walk in, it will not stop, and speaks without looking up. None of it is accident — it was done this way on purpose.
|
||||
@@ -86,7 +86,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Attendants' Row
|
||||
### AREA 3 — The Lesser Vault
|
||||
*complication · ritual · fight · cache*
|
||||
|
||||
> A clock wound by somebody, recently. A fire built recently, of furniture that was three centuries old. Something young — waiting, exactly as instructed, and has been. When you walk in, it asks whether it is allowed to stop now. None of it is accident — it was done this way on purpose.
|
||||
@@ -114,7 +114,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Turning Hall
|
||||
### AREA 4 — The Crossing
|
||||
*turn · demand · pocket*
|
||||
|
||||
> A room that resets itself if you look away. A clock stopped by hand — the pendulum caught and tied. A living robber, badly hurt — refusing to bleed, and frightened by it. When you walk in, they grab you and ask what is wrong with them. None of it is accident — it was done this way on purpose.
|
||||
@@ -135,7 +135,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Sunken Nave
|
||||
### AREA 5 — The Servants' Deep
|
||||
*descent · wound*
|
||||
|
||||
> Something kept so well it has forgotten how to end. Rot, in exactly one place, spreading. The room — undoing your presence, footprints filling behind you, dust returning. When you walk in, it works faster. None of it is accident — it was done this way on purpose.
|
||||
@@ -153,7 +153,7 @@ On success it hears that nothing has aged, and The Silent Host will not open hos
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Antechamber Royal
|
||||
### AREA 6 — The Quiet Room
|
||||
*threshold · anchor · battle · chest*
|
||||
|
||||
> A casket sized for something smaller than a body. Something here has been eaten, which should not be possible. Two of the Host — setting a table for a meal that is always about to happen. When you walk in, they lay two more places. None of it is accident — it was done this way on purpose.
|
||||
@@ -214,4 +214,4 @@ Handled by giving them what they want, at a price.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*
|
||||
@@ -58,7 +58,7 @@ On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 2 — The First Hall
|
||||
### AREA 2 — The Waiting Room
|
||||
*complication · ritual · pocket*
|
||||
|
||||
> Places set every evening for three hundred years. Someone has been counting the dead — tally marks by the entrance. Two of the Host, hand in hand — refusing to be parted to let anyone past. When you walk in, they look at you and do not move. None of it is accident — it was done this way on purpose.
|
||||
@@ -79,7 +79,7 @@ On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 3 — The Waiting Room
|
||||
### AREA 3 — The Attendants' Row
|
||||
*complication · ritual · fight · cache*
|
||||
|
||||
> Chairs drawn up in a circle, all facing one seat. A bedroll, used, and not by anyone buried here. A dozen of the Host — sitting in a circle, facing one empty chair. When you walk in, they shuffle round to make room for you. None of it is accident — it was done this way on purpose.
|
||||
@@ -107,7 +107,7 @@ On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 4 — The Turning Hall
|
||||
### AREA 4 — The Gallery
|
||||
*turn · demand · fight · cache*
|
||||
|
||||
> A doorway with the hinges taken off. A name carved beside another, joining them, badly. A second set of footsteps — keeping pace with yours, close. When you walk in, they stop when you stop, close. None of it is accident — it was done this way on purpose.
|
||||
@@ -135,7 +135,7 @@ On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 5 — The Servants' Deep
|
||||
### AREA 5 — The Lower Cut
|
||||
*descent · wound*
|
||||
|
||||
> A name called so often it wore into the wall. A door tied open with a belt, so it could not close. Three of the Host — laying out a game for four. When you walk in, they look up and count you. None of it is accident — it was done this way on purpose.
|
||||
@@ -153,7 +153,7 @@ On success it is not alone, and The Silent Host will not open hostilities first.
|
||||
|
||||
---
|
||||
|
||||
### AREA 6 — The Quiet Room
|
||||
### AREA 6 — The Antechamber Royal
|
||||
*threshold · anchor · battle · chest*
|
||||
|
||||
> A chair kept empty and dusted. A chair dragged out of position and left where it was needed. A living robber, alone — talking steadily to a companion who is not there. When you walk in, they introduce you to the empty air. None of it is accident — it was done this way on purpose.
|
||||
@@ -214,4 +214,4 @@ Handled by giving them what they want, at a price.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*Generated by [DELVE](https://github.com/slaguru666/vanity-delve) for the VANITY RPG.*
|
||||
@@ -104,4 +104,4 @@ sequenced — but only if it stops talking to the table on its own.
|
||||
|
||||
---
|
||||
|
||||
*Private VANITY development material — not for sale or distribution.*
|
||||
*VANITY development notes — part of [DELVE](https://github.com/slaguru666/vanity-delve).*
|
||||
|
||||
Reference in New Issue
Block a user