4 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 7af690205c feat: wild family complete — cave, mountain, river, lake, valley theme packs
Five wild-kernel themes, each 4 motifs:
  cave      dark · deep · echo · keeping        (cave geometry)
  mountain  ascent · cold · pass · weight       (cave)
  river     crossing · current · course · drowned  (fen)
  lake      stillness · mirror · sunken · rising   (fen)
  valley    ridges · shelter · bottom · weather (forest)

Two rendering fixes surfaced by the new claimants, both in skeleton.mjs:
  - `{conscript}'s` rendered "the climbers's"; conscripts are plural noun
    phrases, so fill() now collapses s's to s'.
  - claimants carrying their own article rendered "The The Grey"; named()
    only prefixes when one is absent.

All 16 themes validate; 39 tests pass. Module → 0.6.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:22:01 +01:00
slaguru666andClaude Opus 5 d22421efbd feat: settlement family complete — village, market, city, port theme packs
Four settlement-kernel themes on village geometry, each 4 motifs:
  village  kinship · harvest · custom · quiet
  market   bargain · weights · crowd · credit
  city     works · rolls · wards · liberties
  port     cargo · passage · tide · wreck

All validate. 11 themes total; index.json rebuilt. Module → 0.5.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:57:31 +01:00
slaguru666andClaude Opus 5 b4990533a8 feat: interior family complete — church, temple, palace theme packs
Three interior-kernel themes on barrow geometry, each 4 motifs:
  church   faith · congregation · offering · sanctity
  temple   exactness · sacrifice · silence · exclusion
  palace   service · delight · splendour · audience

All validate. 7 themes total; index.json rebuilt. Module → 0.4.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:48:28 +01:00
tevansandClaude Opus 5 075be5c1de Town and Forest: two new kernels, not just two new packs
Barrow and Castle share a kernel — a dead claimant whose appetite outlived them.
A market or a river has no corpse in it, so the other two families needed the
kernel to generalise, and it does: something wanted a thing, and something was
done to keep it that way. Only who "something" is changes.

TOWN, on village geometry. The appetite belongs to the place and the claimants
are offices — a Corporation, a Watch Committee, a Guild of Mercers. It wants to
be prosperous, respectable, safe or necessary, and it priced people out,
chartered them away or struck them from the rolls to stay that way. Motifs:
ledger, appearances, vigilance, thoroughfare.

FOREST, on forest geometry. The appetite belongs to the wood, and the claimants
are its own names for itself. It wants to be entered, fed, spread or left
uncounted, and it took people, grew through them, or let them wander. Motifs:
paths, tithe, reclaiming, trackless.

Settlements also needed their own naming rule. A barrow is named for its
occupant; a town is not — it has a name and the claimant merely runs it. Packs
declare placeNameStyle, so "The Town of Watch Committee" is now Cold Harrow, run
by the Watch Committee.

Three of fifteen requested themes are done. The remaining twelve are the same
job repeated rather than a design question, since all three kernels now exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:41:10 +01:00
37 changed files with 33183 additions and 23 deletions
+2 -1
View File
@@ -2154,5 +2154,6 @@
"lingering": "stayed in one place longer than it takes to search it", "lingering": "stayed in one place longer than it takes to search it",
"disturbed": "struck or moved something that had not moved in centuries" "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"
} }
+2 -1
View File
@@ -1169,5 +1169,6 @@
} }
] ]
} }
} },
"placeNameStyle": "claimant"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+84
View File
@@ -12,6 +12,90 @@
"label": "Castle", "label": "Castle",
"geometry": "barrow", "geometry": "barrow",
"motifs": 4 "motifs": 4
},
{
"id": "cave",
"label": "Cave",
"geometry": "cave",
"motifs": 4
},
{
"id": "church",
"label": "Church",
"geometry": "barrow",
"motifs": 4
},
{
"id": "city",
"label": "City",
"geometry": "village",
"motifs": 4
},
{
"id": "forest",
"label": "Forest",
"geometry": "forest",
"motifs": 4
},
{
"id": "lake",
"label": "Lake",
"geometry": "fen",
"motifs": 4
},
{
"id": "market",
"label": "Market",
"geometry": "village",
"motifs": 4
},
{
"id": "mountain",
"label": "Mountain",
"geometry": "cave",
"motifs": 4
},
{
"id": "palace",
"label": "Palace",
"geometry": "barrow",
"motifs": 4
},
{
"id": "port",
"label": "Port",
"geometry": "village",
"motifs": 4
},
{
"id": "river",
"label": "River",
"geometry": "fen",
"motifs": 4
},
{
"id": "temple",
"label": "Temple",
"geometry": "barrow",
"motifs": 4
},
{
"id": "town",
"label": "Town",
"geometry": "village",
"motifs": 4
},
{
"id": "valley",
"label": "Valley",
"geometry": "forest",
"motifs": 4
},
{
"id": "village",
"label": "Village",
"geometry": "village",
"motifs": 4
} }
] ]
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -22,7 +22,13 @@
import { arcRole } from './director.mjs'; import { arcRole } from './director.mjs';
/** Fills {conscript}-style slots. */ /** Fills {conscript}-style slots. */
const fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`); const fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`)
// Conscripts are plural noun phrases ("the climbers"), so a template's `{conscript}'s`
// renders "the climbers's". English takes a bare apostrophe on a plural already ending in s.
.replace(/s's\b/g, "s'");
/** Claimants may already carry their own article — "The Grey" must not become "The The Grey". */
const named = c => (/^(the|a|an) /i.test(c) ? c : `The ${c}`);
/** Facet order used when a facet is exhausted — borrow from a neighbour before repeating. */ /** Facet order used when a facet is exhausted — borrow from a neighbour before repeating. */
const FACET_ORDER = ['institution', 'ritual', 'demand', 'wound', 'anchor']; const FACET_ORDER = ['institution', 'ritual', 'demand', 'wound', 'anchor'];
@@ -62,11 +68,12 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
const prize = { kind: appetite.prize, ...(pack.prizes?.[appetite.prize] ?? {}) }; const prize = { kind: appetite.prize, ...(pack.prizes?.[appetite.prize] ?? {}) };
const transgression = const transgression =
`The ${claimant} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` + `${named(claimant)} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` +
`so that ${appetite.purpose}.`; `so that ${appetite.purpose}.`;
// The bottom problem is the claimant itself — a person in a tomb, an office in a town.
const bottomProblem = { const bottomProblem = {
label: `The ${claimant}`, label: named(claimant),
wantNow: appetite.want, wantNow: appetite.want,
failureState: `is ${appetite.lack} now, and cannot bear it`, failureState: `is ${appetite.lack} now, and cannot bear it`,
handledBy: 'giving them what they want, at a price', handledBy: 'giving them what they want, at a price',
@@ -79,8 +86,11 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
}; };
const secondary = r.derive('faction2').pick(pack.factions.filter(f => f.id !== primary.id)); 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. // A barrow or keep is named for its occupant — drawing that separately produced tombs named
const placeName = `${r.derive('place').pick(pack.placeNames.first)} ${claimant}`; // 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 ---------------------------------------------- // ---- the foreshadow chain ----------------------------------------------
const progression = pack.facetProgression ?? {}; const progression = pack.facetProgression ?? {};
+1 -1
View File
@@ -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.2.0", "version": "0.6.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "14.365" "verified": "14.365"
+18 -9
View File
@@ -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": [
@@ -2146,5 +2154,6 @@
"lingering": "stayed in one place longer than it takes to search it", "lingering": "stayed in one place longer than it takes to search it",
"disturbed": "struck or moved something that had not moved in centuries" "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"
} }
@@ -1169,5 +1169,6 @@
} }
] ]
} }
} },
"placeNameStyle": "claimant"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -12,6 +12,90 @@
"label": "Castle", "label": "Castle",
"geometry": "barrow", "geometry": "barrow",
"motifs": 4 "motifs": 4
},
{
"id": "cave",
"label": "Cave",
"geometry": "cave",
"motifs": 4
},
{
"id": "church",
"label": "Church",
"geometry": "barrow",
"motifs": 4
},
{
"id": "city",
"label": "City",
"geometry": "village",
"motifs": 4
},
{
"id": "forest",
"label": "Forest",
"geometry": "forest",
"motifs": 4
},
{
"id": "lake",
"label": "Lake",
"geometry": "fen",
"motifs": 4
},
{
"id": "market",
"label": "Market",
"geometry": "village",
"motifs": 4
},
{
"id": "mountain",
"label": "Mountain",
"geometry": "cave",
"motifs": 4
},
{
"id": "palace",
"label": "Palace",
"geometry": "barrow",
"motifs": 4
},
{
"id": "port",
"label": "Port",
"geometry": "village",
"motifs": 4
},
{
"id": "river",
"label": "River",
"geometry": "fen",
"motifs": 4
},
{
"id": "temple",
"label": "Temple",
"geometry": "barrow",
"motifs": 4
},
{
"id": "town",
"label": "Town",
"geometry": "village",
"motifs": 4
},
{
"id": "valley",
"label": "Valley",
"geometry": "forest",
"motifs": 4
},
{
"id": "village",
"label": "Village",
"geometry": "village",
"motifs": 4
} }
] ]
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -22,7 +22,13 @@
import { arcRole } from './director.mjs'; import { arcRole } from './director.mjs';
/** Fills {conscript}-style slots. */ /** Fills {conscript}-style slots. */
const fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`); const fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`)
// Conscripts are plural noun phrases ("the climbers"), so a template's `{conscript}'s`
// renders "the climbers's". English takes a bare apostrophe on a plural already ending in s.
.replace(/s's\b/g, "s'");
/** Claimants may already carry their own article — "The Grey" must not become "The The Grey". */
const named = c => (/^(the|a|an) /i.test(c) ? c : `The ${c}`);
/** Facet order used when a facet is exhausted — borrow from a neighbour before repeating. */ /** Facet order used when a facet is exhausted — borrow from a neighbour before repeating. */
const FACET_ORDER = ['institution', 'ritual', 'demand', 'wound', 'anchor']; const FACET_ORDER = ['institution', 'ritual', 'demand', 'wound', 'anchor'];
@@ -62,11 +68,12 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
const prize = { kind: appetite.prize, ...(pack.prizes?.[appetite.prize] ?? {}) }; const prize = { kind: appetite.prize, ...(pack.prizes?.[appetite.prize] ?? {}) };
const transgression = const transgression =
`The ${claimant} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` + `${named(claimant)} ${fill(accommodation.text, { conscript: appetite.conscript })}, ` +
`so that ${appetite.purpose}.`; `so that ${appetite.purpose}.`;
// The bottom problem is the claimant itself — a person in a tomb, an office in a town.
const bottomProblem = { const bottomProblem = {
label: `The ${claimant}`, label: named(claimant),
wantNow: appetite.want, wantNow: appetite.want,
failureState: `is ${appetite.lack} now, and cannot bear it`, failureState: `is ${appetite.lack} now, and cannot bear it`,
handledBy: 'giving them what they want, at a price', handledBy: 'giving them what they want, at a price',
@@ -79,8 +86,11 @@ export function generateSkeleton({ pack, areas = 6, ending = 'authored', rng })
}; };
const secondary = r.derive('faction2').pick(pack.factions.filter(f => f.id !== primary.id)); 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. // A barrow or keep is named for its occupant — drawing that separately produced tombs named
const placeName = `${r.derive('place').pick(pack.placeNames.first)} ${claimant}`; // 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 ---------------------------------------------- // ---- the foreshadow chain ----------------------------------------------
const progression = pack.facetProgression ?? {}; const progression = pack.facetProgression ?? {};