v0.6.3 — the plan stays runnable when nothing is forged
0.6.2 fixed the foe block by showing the actors the Forge created, and in
doing so broke the populate-off path: with nothing forged there were no
combat numbers at all, and the caption still pointed the GM at "the table
above" when no table had rendered. Turning population off used to leave
the planned roster runnable. It does again.
One rule now governs both surfaces: show what exists. The roster's
tactical guidance describes the planned foes, so it travels with the plan
and only when the plan IS the encounter. With foes forged, the plan is a
one-line note saying its tactics do not describe them; with nothing
forged, the full planned roster renders and its guidance applies, because
there it is the encounter. The previous wording claimed the guidance
applied either way, which asserted exactly what the fix existed to deny,
and gating the caveat on the literal word ONLY missed the Troll that
regenerates unless burned, the Ogre's 12 Grit and the Skeletons that
return until their Necromancer stops.
packFor no longer falls back to barrow. Generated files always record a
theme, but load() takes hand-edited JSON too, and guessing the geometry
is the bug it was written to prevent — it now fails closed, and also
rejects a pack with no forgeStageType. draft() keeps a default because
drafting chooses a theme rather than being told one.
raiseDungeon spreads caller params before the pack, so a programmatic
raise({pack}) can no longer generate from one pack while the maps are
staged from another.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f5804e2f1
commit
3a180479cb
@@ -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.6.2",
|
"version": "0.6.3",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "14.365"
|
"verified": "14.365"
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export async function raiseDungeon(params = {}) {
|
|||||||
const PACK = await game.delve.loadPack(params.theme ?? 'barrow');
|
const PACK = await game.delve.loadPack(params.theme ?? 'barrow');
|
||||||
if (!PACK) return ui.notifications.error(`DELVE: could not load the ${params.theme} theme.`);
|
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 });
|
// pack last: a programmatic caller passing params.pack would otherwise generate from one pack
|
||||||
|
// while the maps below are staged from the one actually loaded.
|
||||||
|
const d = generateDelve({ ...params, seed, pack: PACK });
|
||||||
const sk = d.skeleton;
|
const sk = d.skeleton;
|
||||||
const title = sk.placeName;
|
const title = sk.placeName;
|
||||||
|
|
||||||
@@ -242,9 +244,13 @@ function buildPages(d, scenes) {
|
|||||||
${a._foes?.length ? `<p><b>${esc(cap(a.encounter.heat))} — in the world.</b> These are the actors the Forge created; run the fight off these.</p>
|
${a._foes?.length ? `<p><b>${esc(cap(a.encounter.heat))} — in the world.</b> These are the actors the Forge created; run the fight off these.</p>
|
||||||
<table><thead><tr><th>Foe</th><th>atk</th><th>def</th><th>Grit</th><th>Nerve</th><th></th></tr></thead><tbody>
|
<table><thead><tr><th>Foe</th><th>atk</th><th>def</th><th>Grit</th><th>Nerve</th><th></th></tr></thead><tbody>
|
||||||
${a._foes.map(f => `<tr><td>@UUID[${f.uuid}]{${esc(f.name)}}</td><td>${f.atk ?? '?'}</td><td>${f.def ?? '?'}</td><td>${f.grit ?? '?'}</td><td>${f.nerve ?? '?'}</td><td><i>${esc(f.trick)}</i></td></tr>`).join('')}
|
${a._foes.map(f => `<tr><td>@UUID[${f.uuid}]{${esc(f.name)}}</td><td>${f.atk ?? '?'}</td><td>${f.def ?? '?'}</td><td>${f.grit ?? '?'}</td><td>${f.nerve ?? '?'}</td><td><i>${esc(f.trick)}</i></td></tr>`).join('')}
|
||||||
</tbody></table>` : ''}
|
</tbody></table>
|
||||||
${R ? `<p><b>DELVE planned ${esc(R.line)}</b> — the Forge rolls its own foes, so the plan and the table above are different lists.
|
${R ? `<p><i>DELVE planned ${esc(R.line)}. The Forge rolls its own cast, so the plan's foes are not these — its tactical notes describe monsters that were not created.</i></p>` : ''}`
|
||||||
Its guidance still applies to the scene: harmed by ${esc(R.harmedBy)}${R.harmedBy.includes('ONLY') ? ' — <b>only true if you cast the fight yourself</b>' : ''}. <i>${esc(R.avoid)}.</i></p>` : ''}
|
: R ? `<p><b>${esc(cap(a.encounter.heat))} — not cast.</b> Nothing was forged for this area, so the plan is the encounter. Cast it by hand:</p>
|
||||||
|
<table><thead><tr><th>Foe</th><th>atk</th><th>def</th><th>Grit</th><th>Nerve</th><th></th></tr></thead><tbody>
|
||||||
|
${R.foes.map(f => `<tr><td>${f.n}× ${esc(f.name)}</td><td>${f.atk}</td><td>${f.def}</td><td>${f.grit}</td><td>${f.nerve}</td><td><i>${esc(f.note)}</i></td></tr>`).join('')}
|
||||||
|
</tbody></table>
|
||||||
|
<p>Harmed by ${esc(R.harmedBy)}${R.harmedBy.includes('ONLY') ? ' — <b>say so before initiative</b>' : ''}. <i>${esc(R.avoid)}.</i></p>` : ''}
|
||||||
${a.temptation ? `<p><b>${esc(cap(a.temptation.id))}</b> — ${esc(a.temptation.cue)}: ${esc(a.temptation.benefit)}.<br>
|
${a.temptation ? `<p><b>${esc(cap(a.temptation.id))}</b> — ${esc(a.temptation.cue)}: ${esc(a.temptation.benefit)}.<br>
|
||||||
<i>Using it costs ${a.temptation.useCost?.bane ? `+${a.temptation.useCost.bane} Bane` : '—'}. While carried, ${esc(a.temptation.standingDrawback)}.</i></p>` : ''}
|
<i>Using it costs ${a.temptation.useCost?.bane ? `+${a.temptation.useCost.bane} Bane` : '—'}. While carried, ${esc(a.temptation.standingDrawback)}.</i></p>` : ''}
|
||||||
${a._hoard?.length ? `<p><b>Hoard.</b></p><ul>${a._hoard.map(l => `<li>${l}</li>`).join('')}</ul>` : ''}
|
${a._hoard?.length ? `<p><b>Hoard.</b></p><ul>${a._hoard.map(l => `<li>${l}</li>`).join('')}</ul>` : ''}
|
||||||
|
|||||||
@@ -36,18 +36,28 @@ const setState = async s => game.settings.set(MOD, FLAG, s);
|
|||||||
* says quayside and the geometry says burial chamber. Resolved per call rather than once at load,
|
* says quayside and the geometry says burial chamber. Resolved per call rather than once at load,
|
||||||
* because the global resets on a page reload while the staged delve in world state does not.
|
* because the global resets on a page reload while the staged delve in world state does not.
|
||||||
*
|
*
|
||||||
* `params.theme` is written from `pack.id` at generation time, so it is always present and always
|
* `params.theme` is written from `pack.id` at generation time, so every delve the generator made
|
||||||
* right, whatever the caller passed.
|
* carries it. `load()` also accepts hand-edited JSON, and there the field may be absent — which is
|
||||||
|
* why this fails closed rather than falling back to barrow. Guessing the geometry is the bug.
|
||||||
*/
|
*/
|
||||||
async function packFor(d) {
|
async function packById(id) {
|
||||||
const id = d?.params?.theme ?? 'barrow';
|
if (!id) return null;
|
||||||
if (PACK?.id === id) return PACK;
|
if (PACK?.id === id) return PACK;
|
||||||
const p = await loadPack(id);
|
const p = await loadPack(id);
|
||||||
if (!p) {
|
if (!p?.forgeStageType) return null;
|
||||||
ui.notifications.error(`DELVE: could not load the ${id} theme — refusing to stage, it would use the wrong geometry.`);
|
PACK = p;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The pack a staged delve was authored against. Never guesses. */
|
||||||
|
async function packFor(d) {
|
||||||
|
const id = d?.params?.theme;
|
||||||
|
if (!id) {
|
||||||
|
ui.notifications.error('DELVE: that delve does not record a theme — refusing to stage, the geometry would be a guess.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
PACK = p;
|
const p = await packById(id);
|
||||||
|
if (!p) ui.notifications.error(`DELVE: could not load the ${id} theme — refusing to stage, it would use the wrong geometry.`);
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||||||
@@ -99,8 +109,9 @@ async function loadFile(name) {
|
|||||||
/** Generate an unfinished draft in-world. Convenience only — the desk is the right place. */
|
/** Generate an unfinished draft in-world. Convenience only — the desk is the right place. */
|
||||||
async function draft(params = {}) {
|
async function draft(params = {}) {
|
||||||
const seed = params.seed || coinSeed(new Rng(String(game.world.id)));
|
const seed = params.seed || coinSeed(new Rng(String(game.world.id)));
|
||||||
const pack = await packFor({ params });
|
const theme = params.theme ?? 'barrow'; // drafting picks a theme; staging must be told one
|
||||||
if (!pack) return;
|
const pack = await packById(theme);
|
||||||
|
if (!pack) return ui.notifications.error(`DELVE: could not load the ${theme} theme.`);
|
||||||
const d = newWorkingFile({ pack, ...params, seed });
|
const d = newWorkingFile({ pack, ...params, seed });
|
||||||
ui.notifications.warn('DELVE: unfinished draft. Write the read-aloud in the worksheet first.');
|
ui.notifications.warn('DELVE: unfinished draft. Write the read-aloud in the worksheet first.');
|
||||||
return load(d);
|
return load(d);
|
||||||
@@ -136,12 +147,26 @@ async function enter() {
|
|||||||
const R = area.encounter?.roster;
|
const R = area.encounter?.roster;
|
||||||
const foes = (enc?.actors ?? []).map(foeStats);
|
const foes = (enc?.actors ?? []).map(foeStats);
|
||||||
const rv = area.decision?.resolve ?? {};
|
const rv = area.decision?.resolve ?? {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One rule, both surfaces: show what exists. The roster's tactical guidance — what harms it, how
|
||||||
|
* to avoid it — describes the planned foes, so it travels with the plan and only when the plan
|
||||||
|
* IS the encounter. With nothing forged (populate off, or the Forge failed) the plan is all
|
||||||
|
* there is, and it must stay runnable.
|
||||||
|
*/
|
||||||
|
const foeBlock = foes.length
|
||||||
|
? `<p><b>${cap(area.encounter.heat)} — in the world:</b></p>${list(foes.map(foeLine))}${
|
||||||
|
R ? `<p><i>DELVE planned ${R.line}; the Forge rolled its own, so the plan's tactics do not describe these.</i></p>` : ''}`
|
||||||
|
: R
|
||||||
|
? `<p><b>${cap(area.encounter.heat)} — not cast.</b> Nothing was forged; run the plan by hand:</p>${
|
||||||
|
list(R.foes.map(f => `${f.n}× <b>${f.name}</b> — ${f.atk}/${f.def}/${f.grit}, Nerve ${f.nerve}. <i>${f.note}</i>`))
|
||||||
|
}<p><b>Harmed by ${R.harmedBy}.</b> ${R.avoid}.</p>`
|
||||||
|
: '';
|
||||||
await gmCard(`⛏ ${area.index} · ${name}`, `${area.role} · ${area.facet}`,
|
await gmCard(`⛏ ${area.index} · ${name}`, `${area.role} · ${area.facet}`,
|
||||||
`${area.situation ? `<p><b>Here:</b> ${cap(area.situation.occupant)}, ${area.situation.doing} — ${area.situation.onArrival}.<br>
|
`${area.situation ? `<p><b>Here:</b> ${cap(area.situation.occupant)}, ${area.situation.doing} — ${area.situation.onArrival}.<br>
|
||||||
<b>They can:</b> ${area.situation.offer}. <i>${cap(area.situation.because)}.</i></p>` : ''}
|
<b>They can:</b> ${area.situation.offer}. <i>${cap(area.situation.because)}.</i></p>` : ''}
|
||||||
<p><b>${cap(area.decision.cue)}</b>${rv.roll ? ` — [${rv.roll}] ${rv.success}` : ''}${rv.failure ? `<br><b>Miss:</b> ${rv.failure}` : ''}${rv.orElse ? `<br><b>Or:</b> ${rv.orElse}` : ''}</p>
|
<p><b>${cap(area.decision.cue)}</b>${rv.roll ? ` — [${rv.roll}] ${rv.success}` : ''}${rv.failure ? `<br><b>Miss:</b> ${rv.failure}` : ''}${rv.orElse ? `<br><b>Or:</b> ${rv.orElse}` : ''}</p>
|
||||||
${foes.length ? `<p><b>${cap(area.encounter.heat)} — in the world:</b></p>${list(foes.map(foeLine))}` : ''}
|
${foeBlock}
|
||||||
${R ? `<p><b>DELVE planned ${R.line}</b> — the Forge rolled its own, so run the block above. ${R.avoid}.</p>` : ''}
|
|
||||||
${area.temptation ? `<p><b>${cap(area.temptation.id)}:</b> ${area.temptation.benefit}. <i>Use: ${area.temptation.useCost?.bane ? `+${area.temptation.useCost.bane} Bane` : '—'}. ${area.temptation.standingDrawback}.</i></p>` : ''}
|
${area.temptation ? `<p><b>${cap(area.temptation.id)}:</b> ${area.temptation.benefit}. <i>Use: ${area.temptation.useCost?.bane ? `+${area.temptation.useCost.bane} Bane` : '—'}. ${area.temptation.standingDrawback}.</i></p>` : ''}
|
||||||
${w.notes ? `<p><b>Your note:</b> ${w.notes}</p>` : ''}
|
${w.notes ? `<p><b>Your note:</b> ${w.notes}</p>` : ''}
|
||||||
<p><code>${area.trigger}${area.baneBeat ? ` · ${area.baneBeat}` : ''} · fallback: ${area.fallback.route}</code></p>`);
|
<p><code>${area.trigger}${area.baneBeat ? ` · ${area.baneBeat}` : ''} · fallback: ${area.fallback.route}</code></p>`);
|
||||||
|
|||||||
Reference in New Issue
Block a user