diff --git a/core/beat.mjs b/core/beat.mjs index 53c2af8..970d56f 100644 --- a/core/beat.mjs +++ b/core/beat.mjs @@ -20,10 +20,10 @@ const FACET_TRUTH = { demandEnd: 'the question itself, asked directly', }; -export function buildArea({ skeleton, planned, pack, pressure, rng, baneBeat = null, feature, decision, temptation = null, situation = null }) { +export function buildArea({ skeleton, planned, pack, pressure, rng, baneBeat = null, feature, decision, temptation = null, situation = null, name = null }) { const r = rng.derive('area', String(planned.index)); const fs = skeleton.foreshadow.find(f => f.index === planned.index) ?? {}; - const names = pack.areaNames?.[planned.role] ?? pack.areaNames?.approach ?? ['Unnamed']; + // feature/decision are dealt by the caller without replacement across the delve — drawing them // per-area from pools of 8 and 6 made repeats near-certain inside a single delve. @@ -34,7 +34,7 @@ export function buildArea({ skeleton, planned, pack, pressure, rng, baneBeat = n return { index: planned.index, role: planned.role, - name: r.derive('name').pick(names), + name, facet: fs.facet ?? null, // Cue: fragments, not prose. diff --git a/core/delve.mjs b/core/delve.mjs index 07bc705..a64480e 100644 --- a/core/delve.mjs +++ b/core/delve.mjs @@ -48,6 +48,19 @@ export function generateDelve(params = {}) { // prompt plus a resolver: the review's verdict was "choices attached to prompts, not spaces // that naturally produce play". const situations = deal(motifPack.situations ?? [], plan.areas.length, 'situations'); + + // Names are dealt globally, not per role: two `complication` areas both drawing from the same + // four-name list produced two scenes called "The Lesser Vault" in one dungeon. + const nameRng = rng.derive('names'); + const usedNames = new Set(); + const allNames = Object.values(pack.areaNames ?? {}).flat(); + const areaNames = plan.areas.map(a => { + const forRole = (pack.areaNames?.[a.role] ?? []).filter(n => !usedNames.has(n)); + const pool = forRole.length ? forRole : allNames.filter(n => !usedNames.has(n)); + const pick = nameRng.pick(pool.length ? pool : allNames); + if (pick) usedNames.add(pick); + return pick; + }); const decisions = deal(decisionPool, plan.areas.length, 'decisions', pack.decisions ?? []); // Temptations are dealt across the areas that carry a hoard, for the same reason as decisions: @@ -60,7 +73,7 @@ export function generateDelve(params = {}) { const built = plan.areas.map((planned, i) => { const beat = baneBeatFor(planned, pressure, rng.derive('banebeat', String(planned.index))); if (beat) pressure.bankBane('(offered)', beat, planned.index); - const area = buildArea({ skeleton, planned, pack, pressure, rng, baneBeat: beat, feature: features[i], decision: decisions[i], situation: situations[i], temptation: planned.hoard ? temptations[tIdx++] : null }); + const area = buildArea({ skeleton, planned, pack, pressure, rng, baneBeat: beat, feature: features[i], decision: decisions[i], situation: situations[i], name: areaNames[i], temptation: planned.hoard ? temptations[tIdx++] : null }); area.fallback = fallbackRoute(area, skeleton); return area; }); diff --git a/core/test.mjs b/core/test.mjs index 9f55b3a..94f05c6 100644 --- a/core/test.mjs +++ b/core/test.mjs @@ -84,6 +84,16 @@ for (let i = 0; i < 40; i++) { } } t('no repeated decision inside one delve', dupD === 0, `${dupD}/80`); +{ + let dupNames = 0; + for (let i = 0; i < 60; i++) { + for (const areas of [6, 9, 12]) { + const names = generateDelve({ pack, seed: `nm-${i}`, areas }).areas.map(a => a.name); + if (new Set(names).size !== names.length) dupNames++; + } + } + t('no two areas share a name', dupNames === 0, `${dupNames}/180 delves had a clash`); +} t('no repeated temptation inside one delve', dupT === 0, `${dupT}/80`); // every decision must be resolvable, and no decision may gate progress without an escape diff --git a/foundry-module/module/vanity-delve.mjs b/foundry-module/module/vanity-delve.mjs index e2f8003..d6eb742 100644 --- a/foundry-module/module/vanity-delve.mjs +++ b/foundry-module/module/vanity-delve.mjs @@ -173,21 +173,24 @@ Hooks.once('ready', async () => { Hooks.on('getSceneControlButtons', controls => { if (!game.user.isGM) return; - const group = Array.isArray(controls) ? controls.find(c => c.name === 'token') : controls.token; - if (!group) return; - const tools = Array.isArray(group.tools) ? group.tools : Object.values(group.tools ?? {}); - const open = () => new DelveForgeApp().render(true); - tools.push({ - name: 'delve-forge', title: 'DELVE — raise a dungeon', icon: 'fas fa-mountain', button: true, - onClick: open, onChange: open, - }); - tools.push({ - name: 'delve-remove', title: 'DELVE — remove a generated dungeon', icon: 'fas fa-trash', button: true, - onClick: () => removeDungeonDialog(), onChange: () => removeDungeonDialog(), - }); - tools.push({ - name: 'delve-next', title: 'DELVE — stage the next area (pre-authored delve)', icon: 'fas fa-forward', button: true, - onClick: () => (getState() ? enter() : ui.notifications.info('DELVE: no staged delve — use the ⛏ button to raise one.')), - onChange: () => (getState() ? enter() : ui.notifications.info('DELVE: no staged delve — use the ⛏ button to raise one.')), - }); + // Foundry v13+ passes controls as a record keyed by name — the token group is `tokens`, plural — + // and each group's `tools` is a record too, not an array. Written for the old array shape, this + // hook silently added nothing and the button never appeared. + const group = controls.tokens ?? controls.token; + if (!group?.tools) return; + + const tool = (name, title, icon, order, onClick) => { + const entry = { name, title, icon, order, button: true, + onChange: (event, active) => { if (active !== false) onClick(); }, + onClick }; + if (Array.isArray(group.tools)) group.tools.push(entry); + else group.tools[name] = entry; + }; + + tool('delve-forge', 'DELVE — raise a dungeon', 'fas fa-mountain', 90, + () => new DelveForgeApp().render(true)); + tool('delve-remove', 'DELVE — remove a generated dungeon', 'fas fa-trash-can', 91, + () => removeDungeonDialog()); + tool('delve-next', 'DELVE — stage the next area of a pre-authored delve', 'fas fa-forward', 92, + () => (getState() ? enter() : ui.notifications.info('DELVE: no staged delve — use ⛏ to raise one.'))); });