Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"themes": [
|
||||
{
|
||||
"id": "barrow",
|
||||
"label": "Barrow",
|
||||
"geometry": "barrow",
|
||||
"motifs": 8
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"label": "Castle",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "forest",
|
||||
"label": "Forest",
|
||||
"geometry": "forest",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "town",
|
||||
"label": "Town",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
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.3.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
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"themes": [
|
||||
{
|
||||
"id": "barrow",
|
||||
"label": "Barrow",
|
||||
"geometry": "barrow",
|
||||
"motifs": 8
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"label": "Castle",
|
||||
"geometry": "barrow",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "forest",
|
||||
"label": "Forest",
|
||||
"geometry": "forest",
|
||||
"motifs": 4
|
||||
},
|
||||
{
|
||||
"id": "town",
|
||||
"label": "Town",
|
||||
"geometry": "village",
|
||||
"motifs": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
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