Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)];
|
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 file = args.file;
|
||||||
const load = () => JSON.parse(readFileSync(file, 'utf8'));
|
const load = () => JSON.parse(readFileSync(file, 'utf8'));
|
||||||
const save = d => writeFileSync(file, JSON.stringify(d, null, 2));
|
const save = d => writeFileSync(file, JSON.stringify(d, null, 2));
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"How do I look?\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "remembered",
|
"id": "remembered",
|
||||||
@@ -47,7 +48,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"Say my name.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "obeyed",
|
"id": "obeyed",
|
||||||
@@ -69,7 +71,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"Do as you are told.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "young",
|
"id": "young",
|
||||||
@@ -91,7 +94,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"Tell me I have not changed.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "envied",
|
"id": "envied",
|
||||||
@@ -113,7 +117,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"What did you bring me?\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "attended",
|
"id": "attended",
|
||||||
@@ -135,7 +140,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"You are not leaving.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "forgiven",
|
"id": "forgiven",
|
||||||
@@ -157,7 +163,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"Tell me it was not a sin.\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "first",
|
"id": "first",
|
||||||
@@ -179,7 +186,8 @@
|
|||||||
"bane": 1
|
"bane": 1
|
||||||
},
|
},
|
||||||
"repeatable": true
|
"repeatable": true
|
||||||
}
|
},
|
||||||
|
"asks": "\"Who goes first?\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"accommodations": [
|
"accommodations": [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"id": "barrow",
|
||||||
|
"label": "Barrow",
|
||||||
|
"geometry": "barrow",
|
||||||
|
"motifs": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "castle",
|
||||||
|
"label": "Castle",
|
||||||
|
"geometry": "barrow",
|
||||||
|
"motifs": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+9
-14
@@ -103,7 +103,7 @@ export function generateDelve(params = {}) {
|
|||||||
...plan.ending,
|
...plan.ending,
|
||||||
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
||||||
cueFragments: [endFs?.fragment].filter(Boolean),
|
cueFragments: [endFs?.fragment].filter(Boolean),
|
||||||
question: endingQuestion(skeleton),
|
question: endingQuestion(skeleton, pack),
|
||||||
authored: plan.ending.mode === 'authored',
|
authored: plan.ending.mode === 'authored',
|
||||||
},
|
},
|
||||||
triggerGlossary: pack.triggerGlossary ?? {},
|
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) {
|
* The bottom problem asks one thing, and it is the same question the whole delve has been about.
|
||||||
const q = {
|
* It lives on the appetite in the theme pack — hardcoding it here meant any theme whose appetite
|
||||||
seen: '"How do I look?"',
|
* ids differed from barrow's silently fell through to a generic line.
|
||||||
remembered: '"Say my name."',
|
*/
|
||||||
obeyed: '"Do as you are told."',
|
function endingQuestion(sk, pack) {
|
||||||
young: '"Tell me I have not changed."',
|
const appetite = pack?.appetites?.find(a => a.id === sk.knot.appetiteId);
|
||||||
envied: '"What did you bring me?"',
|
return appetite?.asks ?? '"Well?"';
|
||||||
attended: '"You are not leaving."',
|
|
||||||
forgiven: '"Tell me it was not a sin."',
|
|
||||||
first: '"Who goes first?"',
|
|
||||||
};
|
|
||||||
return q[sk.knot.appetiteId] ?? '"Well?"';
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -165,6 +165,6 @@ export function renderMarkdown(d) {
|
|||||||
}
|
}
|
||||||
L.push('---');
|
L.push('---');
|
||||||
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');
|
return L.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
"id": "vanity-delve",
|
||||||
"title": "DELVE — a dungeon layer for VANITY",
|
"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.",
|
"description": "Generates a coherent delve and sequences VANITY's Forge to stage it, one area at a time.",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "14.365"
|
"verified": "14.365"
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"systems": [
|
"systems": [
|
||||||
{
|
{
|
||||||
"id": "vanity",
|
"id": "vanity",
|
||||||
"type": "requires",
|
"type": "system",
|
||||||
|
"manifest": "https://raw.githubusercontent.com/slaguru666/Vanity/main/system.json",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "0.10.0"
|
"minimum": "0.10.0"
|
||||||
}
|
}
|
||||||
@@ -32,5 +33,6 @@
|
|||||||
"manifest": "https://github.com/slaguru666/vanity-delve/releases/latest/download/module.json",
|
"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",
|
"download": "https://github.com/slaguru666/vanity-delve/releases/latest/download/vanity-delve.zip",
|
||||||
"url": "https://github.com/slaguru666/vanity-delve",
|
"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"
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"id": "barrow",
|
||||||
|
"label": "Barrow",
|
||||||
|
"geometry": "barrow",
|
||||||
|
"motifs": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "castle",
|
||||||
|
"label": "Castle",
|
||||||
|
"geometry": "barrow",
|
||||||
|
"motifs": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ export function generateDelve(params = {}) {
|
|||||||
...plan.ending,
|
...plan.ending,
|
||||||
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
name: (pack.areaNames?.ending ?? ['The Ending'])[0],
|
||||||
cueFragments: [endFs?.fragment].filter(Boolean),
|
cueFragments: [endFs?.fragment].filter(Boolean),
|
||||||
question: endingQuestion(skeleton),
|
question: endingQuestion(skeleton, pack),
|
||||||
authored: plan.ending.mode === 'authored',
|
authored: plan.ending.mode === 'authored',
|
||||||
},
|
},
|
||||||
triggerGlossary: pack.triggerGlossary ?? {},
|
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) {
|
* The bottom problem asks one thing, and it is the same question the whole delve has been about.
|
||||||
const q = {
|
* It lives on the appetite in the theme pack — hardcoding it here meant any theme whose appetite
|
||||||
seen: '"How do I look?"',
|
* ids differed from barrow's silently fell through to a generic line.
|
||||||
remembered: '"Say my name."',
|
*/
|
||||||
obeyed: '"Do as you are told."',
|
function endingQuestion(sk, pack) {
|
||||||
young: '"Tell me I have not changed."',
|
const appetite = pack?.appetites?.find(a => a.id === sk.knot.appetiteId);
|
||||||
envied: '"What did you bring me?"',
|
return appetite?.asks ?? '"Well?"';
|
||||||
attended: '"You are not leaving."',
|
|
||||||
forgiven: '"Tell me it was not a sin."',
|
|
||||||
first: '"Who goes first?"',
|
|
||||||
};
|
|
||||||
return q[sk.knot.appetiteId] ?? '"Well?"';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,6 @@ export function renderMarkdown(d) {
|
|||||||
}
|
}
|
||||||
L.push('---');
|
L.push('---');
|
||||||
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');
|
return L.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 esc = s => foundry.utils.escapeHTML?.(String(s ?? '')) ?? String(s ?? '');
|
||||||
|
|
||||||
const MOD = 'vanity-delve';
|
const MOD = 'vanity-delve';
|
||||||
const THEMES = ['barrow'];
|
let THEMES = [{ id: 'barrow', label: 'Barrow' }];
|
||||||
|
|
||||||
|
export function setThemes(list) { THEMES = list; }
|
||||||
const PICKS = {
|
const PICKS = {
|
||||||
depth: [1, 2, 3, 4, 5],
|
depth: [1, 2, 3, 4, 5],
|
||||||
party: [1, 2, 3, 4, 5, 6, 7, 8],
|
party: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
@@ -76,7 +78,9 @@ export class DelveForgeApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||||||
*/
|
*/
|
||||||
export async function raiseDungeon(params = {}) {
|
export async function raiseDungeon(params = {}) {
|
||||||
if (!game.user.isGM) return ui.notifications.warn('DELVE is a GM tool.');
|
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 seed = params.seed || coinSeed(new Rng(String(Date.now())));
|
||||||
const d = generateDelve({ pack: PACK, ...params, seed });
|
const d = generateDelve({ pack: PACK, ...params, seed });
|
||||||
const sk = d.skeleton;
|
const sk = d.skeleton;
|
||||||
|
|||||||
@@ -16,13 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
import { coinSeed, Rng } from './core/rng.mjs';
|
import { coinSeed, Rng } from './core/rng.mjs';
|
||||||
import { newWorkingFile, outstanding, readyToPlay } from './core/authoring.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 MOD = 'vanity-delve';
|
||||||
const FLAG = 'state';
|
const FLAG = 'state';
|
||||||
|
|
||||||
let PACK = null;
|
let PACK = null;
|
||||||
let seamsPresent = false;
|
let seamsPresent = false;
|
||||||
|
let loadPack = async () => null;
|
||||||
|
|
||||||
const getState = () => game.settings.get(MOD, FLAG) ?? null;
|
const getState = () => game.settings.get(MOD, FLAG) ?? null;
|
||||||
const setState = async s => game.settings.set(MOD, FLAG, s);
|
const setState = async s => game.settings.set(MOD, FLAG, s);
|
||||||
@@ -157,8 +158,13 @@ Hooks.once('init', () => {
|
|||||||
|
|
||||||
Hooks.once('ready', async () => {
|
Hooks.once('ready', async () => {
|
||||||
if (!game.user.isGM) return;
|
if (!game.user.isGM) return;
|
||||||
PACK = await foundry.utils.fetchJsonWithTimeout(`modules/${MOD}/module/core/content/barrow.json`)
|
const fetchJson = p => foundry.utils.fetchJsonWithTimeout(p).catch(() => fetch(p).then(r => r.json()).catch(() => null));
|
||||||
.catch(() => fetch(`modules/${MOD}/module/core/content/barrow.json`).then(r => r.json()));
|
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 ?? ''));
|
seamsPresent = /post\s*=\s*true/.test(String(game.vanity?.forge?.hoard ?? ''));
|
||||||
|
|
||||||
game.delve = { forge: () => new DelveForgeApp().render(true), raise: raiseDungeon,
|
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,
|
load, loadFile, draft, enter, ending, bane, clock, state: getState,
|
||||||
outstanding: () => outstanding(getState()?.delve ?? { areas: [] }),
|
outstanding: () => outstanding(getState()?.delve ?? { areas: [] }),
|
||||||
ready: () => readyToPlay(getState()?.delve ?? { areas: [] }),
|
ready: () => readyToPlay(getState()?.delve ?? { areas: [] }),
|
||||||
|
loadPack: id => loadPack(id),
|
||||||
get pack() { return PACK; } };
|
get pack() { return PACK; } };
|
||||||
console.log(`DELVE | ready. Forge seams ${seamsPresent ? 'present' : 'ABSENT'}.`);
|
console.log(`DELVE | ready. Forge seams ${seamsPresent ? 'present' : 'ABSENT'}.`);
|
||||||
if (!seamsPresent) ui.notifications.warn('DELVE: Forge seams not installed — see the delve-seams branch.');
|
if (!seamsPresent) ui.notifications.warn('DELVE: Forge seams not installed — see the delve-seams branch.');
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="delve-grid">
|
<div class="delve-grid">
|
||||||
<label>Theme
|
<label>Theme
|
||||||
<select name="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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Areas
|
<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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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*
|
*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.
|
> 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