The foe block broke twice in two releases, both times because each surface decided for itself, in its own hand-written ternary, which of the cases it was in. classifyFoes() in the new module/foes.mjs makes that choice once — forged, planned, unavailable, none — and both surfaces switch on it. foes.mjs is Foundry-free on purpose, so foundry-module/ test.mjs can run it under plain node with no shim. Fourteen tests, each one a case that shipped broken or nearly did. enter() now survives a Forge that throws. It had no catch, so a failure left the scene raised, no cards posted and the turn not advanced, and a retry would stage the area twice — while the comment above it claimed the branch handled exactly that. A failed stage stops before the turn advances, since there is nothing to run; a failed population keeps going, because the scene is up and the planned roster stands in. An area with combat heat but neither actors nor a roster used to render nothing at all. It now says so and points at the decision and fallback. The "say so before initiative" addendum is gone from all three renderers. It was gated on the literal word ONLY, and the palace, port and village battle rosters already end with that exact phrase, so it printed twice. The packs that need the instruction carry it themselves. Also: draft() spreads params before the pack, like raiseDungeon; and packById validates the cached pack rather than trusting it, so the fail-closed rule holds at boot too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
2.3 KiB
JavaScript
49 lines
2.3 KiB
JavaScript
/**
|
|
* Who is actually in the room.
|
|
*
|
|
* A pack's roster is the encounter DELVE *planned* — "1 Ghoul and 2 Skeletons", with stats and,
|
|
* in some themes, tactical notes naming specific monsters. The Forge takes no cast: it rolls its
|
|
* own from the heat. So the plan and the world are two different lists, and the rule both surfaces
|
|
* follow is **show what exists**:
|
|
*
|
|
* forged the Forge made actors — run those, and say the plan's tactics are not about them
|
|
* planned nothing was forged (population off, or the Forge failed) — the plan IS the
|
|
* encounter, so it renders in full and its guidance applies
|
|
* unavailable an area carries combat heat but has neither — say so rather than render nothing
|
|
* none no encounter here at all
|
|
*
|
|
* This file is deliberately Foundry-free so the decision can be tested without a VTT. The two
|
|
* surfaces disagreed about it twice — 0.6.2 lost the planned roster entirely when nothing was
|
|
* forged, and the caption claimed the plan's guidance applied either way — because each surface
|
|
* made the choice for itself in a ternary. They now both switch on `kind`.
|
|
*/
|
|
|
|
/** What a forged actor actually is, read off the document rather than off the plan. */
|
|
export const foeStats = a => ({
|
|
name: a.name,
|
|
uuid: a.uuid,
|
|
atk: a.system?.attack1?.pool ?? null,
|
|
def: a.system?.defence?.pool ?? null,
|
|
grit: a.system?.grit?.value ?? null,
|
|
nerve: a.system?.nerve ?? null,
|
|
trick: a.system?.trick ?? '',
|
|
});
|
|
|
|
/** One forged foe as a line of stats, in the roster's vocabulary so the two read alike. */
|
|
export const foeLine = f =>
|
|
`<b>@UUID[${f.uuid}]{${f.name}}</b> — ${f.atk ?? '?'}/${f.def ?? '?'}/${f.grit ?? '?'}, Nerve ${f.nerve ?? '?'}${f.trick ? `. <i>${f.trick}</i>` : ''}`;
|
|
|
|
/**
|
|
* Decide which of the four cases an area is in.
|
|
* @param {object} area a delve area
|
|
* @param {Array} forged actors the Forge created, already through foeStats — [] if it failed
|
|
*/
|
|
export function classifyFoes(area, forged = []) {
|
|
if (!area?.encounter) return { kind: 'none' };
|
|
const heat = area.encounter.heat;
|
|
const planned = area.encounter.roster ?? null;
|
|
if (forged.length) return { kind: 'forged', heat, foes: forged, planned };
|
|
if (planned) return { kind: 'planned', heat, roster: planned };
|
|
return { kind: 'unavailable', heat };
|
|
}
|