diff --git a/vanity.mjs b/vanity.mjs
index 8518ba9..bd73733 100644
--- a/vanity.mjs
+++ b/vanity.mjs
@@ -1955,7 +1955,13 @@ const HOARDS = {
kingly: { label: "Kingly", coin: () => `${sum(rollDice(6, 6)) * 10} gp`, draws: 5, consum: 1, trinkets: [2, 2], hook: 1, centerpiece: true, relics: 2 }
};
-async function forgeHoard({ size = "chest" } = {}) {
+/**
+ * Forge a hoard.
+ * @param {string} size pocket | cache | chest | vault | kingly
+ * @param {boolean} post post the GM card. false lets a caller own the presentation.
+ * @returns {{size, label, coin, goods, relics, trinkets, centerpiece, hook, lines, card}}
+ */
+async function forgeHoard({ size = "chest", post = true } = {}) {
const tier = HOARDS[size] ?? HOARDS.chest;
const [weapons, armour, gear, treasure] = await Promise.all(["weapons", "armour", "gear", "treasure"].map(packDocs));
const consumables = gear.filter(g => g.system.consumable);
@@ -1965,14 +1971,17 @@ async function forgeHoard({ size = "chest" } = {}) {
lines.push(`Coin: ${tier.coin()}`);
const found = [];
- if (Math.random() < tier.consum) found.push(rnd(consumables));
- for (let i = 0; i < tier.draws; i++) found.push(rnd(mundane));
+ // rnd() returns undefined on an empty array; a pack with no consumables would then throw on
+ // .uuid below. Guard rather than assume every pack is populated.
+ if (Math.random() < tier.consum && consumables.length) found.push(rnd(consumables));
+ for (let i = 0; i < tier.draws; i++) if (mundane.length) found.push(rnd(mundane));
if (found.length) lines.push(`Goods: ${found.map(d => `@UUID[${d.uuid}]{${d.name}}`).join(" · ")}`);
// A magic relic or two in the richer hoards (a fraction is a chance, an integer a count).
const nRelics = (tier.relics ?? 0) >= 1 ? tier.relics : (Math.random() < (tier.relics ?? 0) ? 1 : 0);
+ let relics = [];
if (nRelics && treasure.length) {
- const relics = pickN(treasure, Math.min(nRelics, treasure.length));
+ relics = pickN(treasure, Math.min(nRelics, treasure.length));
lines.push(`Relic${relics.length > 1 ? "s" : ""}: ${relics.map(d => `@UUID[${d.uuid}]{${d.name}}`).join(" · ")}`);
}
@@ -1985,15 +1994,19 @@ async function forgeHoard({ size = "chest" } = {}) {
}
if (Math.random() < tier.hook) lines.push(`The catch: ${rnd(FORGE_HOOKS)}`);
- await ChatMessage.create({
- speaker: { alias: "The Forge" },
- whisper: ChatMessage.getWhisperRecipients("GM"),
- content: `
+ const card = `
⚒ ${tier.label} hoard
gold you cling to buys nothing — gold you squander buys legend (§27)
${lines.map(l => `
${l}
`).join("")}
-
`
+
`;
+
+ if (post) await ChatMessage.create({
+ speaker: { alias: "The Forge" },
+ whisper: ChatMessage.getWhisperRecipients("GM"),
+ content: card
});
+
+ return { size, label: tier.label, goods: found, relics, lines, card };
}
/* ------------ Forge dialogs & directory buttons ------------ */
@@ -2711,7 +2724,7 @@ async function stageRasterUpload(svg, W, H, name) {
} finally { URL.revokeObjectURL(url); }
}
-async function forgeStage({ type = "barrow", size = "medium", name = "", populate = false, heat = "fight", kind = "", cast = null, activate = true } = {}) {
+async function forgeStage({ type = "barrow", size = "medium", name = "", populate = false, heat = "fight", kind = "", cast = null, activate = true, post = true, folderId = null } = {}) {
if (!game.user.isGM) return;
const [cols, rows] = STAGE_SIZES[size] ?? STAGE_SIZES.medium;
name ||= stageName(type);
@@ -2778,7 +2791,7 @@ async function forgeStage({ type = "barrow", size = "medium", name = "", populat
// placement: a supplied cast, else an auto-encounter
let placed = cast;
if (!placed && populate) {
- const enc = await forgeEncounter({ heat, kind, forStage: name });
+ const enc = await forgeEncounter({ heat, kind, forStage: name, post, folderId });
placed = enc.actors.map(a => ({ actorId: a.id, name: a.name, img: a.img, disposition: -1 }));
}
if (placed?.length) {
@@ -2797,7 +2810,7 @@ async function forgeStage({ type = "barrow", size = "medium", name = "", populat
}
if (activate) await scene.activate();
- await ChatMessage.create({
+ if (post) await ChatMessage.create({
speaker: { alias: "The Stage" },
whisper: ChatMessage.getWhisperRecipients("GM"),
content: `
@@ -2872,10 +2885,19 @@ const HEAT_BUILDS = {
};
const HEAT_HOARD = { skirmish: "pocket", fight: "cache", battle: "chest", nightmare: "vault" };
-async function forgeEncounter({ heat = "fight", kind = "", forStage = "" } = {}) {
+/**
+ * Forge an encounter.
+ * @param {boolean} hoard also roll a hoard. false decouples loot from combat.
+ * @param {boolean} post post the GM card.
+ * @param {string} folderId put the actors in an existing folder instead of a new one, so a
+ * caller running many encounters does not litter the world.
+ */
+async function forgeEncounter({ heat = "fight", kind = "", forStage = "", hoard = true, post = true, folderId = null } = {}) {
const t = FORGE_MONSTERS[kind] ? kind : rnd(Object.keys(FORGE_MONSTERS));
const build = (HEAT_BUILDS[heat] ?? HEAT_BUILDS.fight)();
- const folder = await Folder.create({ name: `Encounter — ${t} (${heat})`, type: "Actor" });
+ const folder = folderId
+ ? (game.folders?.get?.(folderId) ?? { id: folderId })
+ : await Folder.create({ name: `Encounter — ${t} (${heat})`, type: "Actor" });
const actors = [];
for (const [menace, count] of Object.entries(build)) {
@@ -2893,7 +2915,7 @@ async function forgeEncounter({ heat = "fight", kind = "", forStage = "" } = {})
mood = ["Hostile", "Wary", "Neutral", "Friendly"][Math.min(rs, 3)];
}
- await ChatMessage.create({
+ if (post) await ChatMessage.create({
speaker: { alias: "The Forge" },
whisper: ChatMessage.getWhisperRecipients("GM"),
content: `
@@ -2905,8 +2927,8 @@ async function forgeEncounter({ heat = "fight", kind = "", forStage = "" } = {})
The catch: ${complication}
`
});
- await forgeHoard({ size: HEAT_HOARD[heat] ?? "cache" });
- return { actors, situation, terrain, complication, mood, folder };
+ const hoardResult = hoard ? await forgeHoard({ size: HEAT_HOARD[heat] ?? "cache", post }) : null;
+ return { actors, situation, terrain, complication, mood, folder, hoard: hoardResult };
}
/* -------------------------------------------- */