All snippets are async function bodies. Replace ACTOR_ID, etc. with real values.
game.bms.ai provides query shortcuts that don't require knowing field paths.
// Full actor summary (vitals, resources, attributes, resistances, status)
return game.bms.ai.actor("ACTOR_ID or Name");
// All actors, brief
return game.bms.ai.actors();
// Actor's items grouped by type
return game.bms.ai.items("ACTOR_ID or Name");
// Combat tracker state (non-empty downs only)
return game.bms.ai.combat();
// Actor's active effects
return game.bms.ai.effects("ACTOR_ID or Name");
const actor = game.actors.getName("CHARACTER NAME");
return actor ? { id: actor.id, name: actor.name, type: actor.type } : "Not found";
return game.actors.contents.map(a => ({
id: a.id,
name: a.name,
type: a.type,
hp: `${a.system.health.value}/${a.system.health.max}`,
shields: `${a.system.shields.value}/${a.system.shields.max}`,
}));
const actor = game.actors.get("ACTOR_ID");
const result = {};
for (const [arctype, attrs] of Object.entries(actor.system.attributes)) {
result[arctype] = {};
for (const [attr, data] of Object.entries(attrs)) {
result[arctype][attr] = { value: data.value, bonus: data.bonus, total: data.total };
}
}
return result;
const actor = game.actors.get("ACTOR_ID");
return Object.fromEntries(
Object.entries(actor.system.resistances).filter(([k, v]) => v !== 1)
);
const actor = game.actors.get("ACTOR_ID");
// layer: "tempHp" | "shields" | "health" | "sanity"
// type: "slash" | "fire" | "cold" | "blunt" | "pierce" | etc.
const result = await actor.applyDamage(AMOUNT, "TYPE", "tempHp");
return result; // { original, actual }
const actor = game.actors.get("ACTOR_ID");
return actor.system.calculateDamage(AMOUNT, "TYPE", "tempHp");
// returns { original, actual, vitalDamage: { tempHp, shields, health, sanity } }
const actor = game.actors.get("ACTOR_ID");
return await actor.applyDamage(AMOUNT, "TYPE", "health"); // skips tempHp and shields
const actor = game.actors.get("ACTOR_ID");
return await actor.applyDamage(AMOUNT, "psychic", "sanity");
const actor = game.actors.get("ACTOR_ID");
// targetKey: "health" | "shields" | "tempHp" | "sanity" | "resources.mana"
await actor.applyRestore("health", {
useSurge: true, // add floor(max/4) to restoration
consumeSurge: true, // spend 1 surge
allowOverflow: false,
bonusValue: 0 // add flat bonus on top of surge value
});
const actor = game.actors.get("ACTOR_ID");
await actor.update({ "system.health.value": NEW_VALUE });
const actor = game.actors.get("ACTOR_ID");
await actor.update({ "system.resources.surges.value": NEW_VALUE });
The world keeps a slug-keyed registry of every custom resource ever defined on a character so action cost pickers (including those on item-owned actions) can offer the full set. Use the helpers below instead of poking the setting directly.
const helpers = await import("/systems/body-mind-and-soul/module/helpers/custom-resources.mjs");
// Read the whole registry: { [slug]: { label, defaults: {...} } }
helpers.getResourceRegistry();
// Slug from a label (camelCase, whitespace-only split). "Mana Pool" → "manaPool".
helpers.resourceSlug("Mana Pool");
// Idempotent upsert (first-write wins on label/defaults). Safe to call repeatedly.
await helpers.registerResource({
slug: "mana", label: "Mana", defaults: { max: 10, recoverOnExtended: 10, recoveryType: "set" },
});
// Picker-shaped list (registry ∪ actor-local; actor labels win for shared slugs).
helpers.mergeWithActorResources(actor); // actor may be null for item-owned actions
// GM-only: import every actor's custom resources into the registry. Idempotent.
await helpers.backfillFromActors();
// Admin: rename a label, update defaults, or remove an entry from the registry.
// Removal does NOT cascade to actor-local copies.
await helpers.renameResource("mana", "Manapool");
await helpers.updateResourceDefaults("mana", { max: 12 });
await helpers.unregisterResource("mana");
// Advisory usage count for "what would break if I prune this slug?"
helpers.countResourceUsage("mana");
The world setting body-mind-and-soul.customResources is auto-populated by:
ResourceConfig._onAdd when a player adds a resource on a character sheetcreateActor / updateActor hooks on the lead GMbackfillFromActors pass on every readyManage the registry through Game Settings → Configure Settings → Manage Custom Resources (GM-only).
// applyRest honors dialog-shaped selections — good when you want to simulate a
// specific player's short/extended choices.
await game.bms.applyRest(actor, "extended", {
vitalChoices: { health: true, shields: true, sanity: true },
surgeSpend: { health: 0, shields: 0, sanity: 0 }, // short rest only
runeDistribution: {}, // itemId -> count
resetDecks: true,
});
// fullRestoreNpc is the "All Rest" auto-path for GM-controlled NPCs with
// compelCheckMode smart/dumb: everything to max, regardless of restType.
await game.bms.fullRestoreNpc(actor, "extended");
// Dispatches a rest dialog to every in-scope actor's delegate user, with
// mode locked to "short" or "extended". GM-owned NPCs with compelCheckMode
// smart/dumb auto-full-restore; direct NPCs get a GM-local dialog.
// Scope: selected tokens on canvas, or all tokens on the active scene.
await game.bms.restAll("short"); // or "extended"
const c = game.combat;
if (!c) return "No active combat";
return {
round: c.round,
tick: c.system.bms.tickNumber,
nextTickIn: c.system.bms.nextTickIn,
downsTilRound: c.system.bms.downsTilRound,
combatants: c.combatants.contents.map(cb => ({ id: cb.id, name: cb.name, defeated: cb.defeated })),
// down.actions/down.effects are id-keyed objects (TypedObjectField), not
// arrays — use Object.values() to iterate.
downs: c.system.bms.downs
.filter(d => !d.isEmpty)
.map(d => ({
down: d.downNumber,
actions: Object.values(d.actions).map(a => ({
id: a.id,
action: a.action,
combatant: c.combatants.get(a.combatantId)?.name ?? a.combatantId,
type: a.type,
rt: a.resolutionTime,
itemId: a.actionItemId,
})),
effects: Object.values(d.effects).map(e => ({
id: e.id,
name: e.name,
combatant: e.combatantId ? c.combatants.get(e.combatantId)?.name : null,
rt: e.resolutionTime,
})),
}))
};
const combat = game.combat;
// Find the actor's combatant
const combatant = combat.combatants.find(c => c.actor?.name === "CHARACTER NAME");
const actor = combatant?.actor;
const actionItem = actor?.items.getName("ACTION ITEM NAME");
if (!actionItem) return "Action item not found";
await combat.queueActionFromSheet(actor, actionItem.id);
return `Queued ${actionItem.name} for ${actor.name}`;
await game.combat.advanceTracker();
return "Advanced one down";
Note: this no-ops (with a console warning) if down 0 still has unresolved due actions, or a resolution is genuinely still in progress (e.g. blocked on a player's target-selection prompt) even if the due-action count momentarily reads 0 — advancing does NOT always work regardless of tracker state. Check game.combat.system.getDueUnresolvedActions().length first, or just try resolveCurrentDown() below when in doubt. If nothing is due but the tracker UI still shows "Resolve pending actions" instead of a plain advance arrow, the GM just needs to wait for the pending prompt to be answered — clicking advance again is safe now (it's refused cleanly, never cancels anything) but won't skip the wait.
await game.combat.advanceToNextResolution();
return "Advanced to next resolution";
Same guard applies — no-ops if down 0 has unresolved due actions or a resolution is genuinely in flight.
// When advanceTracker()/advanceToNextResolution() won't advance because down 0
// has actions that are due (resolutionTime <= 0) but not yet resolved — e.g. the
// GM's browser was interrupted mid-resolve — run this first. It only resolves
// what's already due; it never decrements/sweeps/advances the tracker.
await game.combat.resolveCurrentDown();
return "Resolved pending down-0 actions";
// Skips the action's actual resolve effects entirely — no damage, no triggers,
// no prompts. Only use this for an action that's genuinely broken/stuck and
// won't clear via resolveCurrentDown(). Confirm with the GM before running.
// Also works when the action is currently in progress (e.g. hung on a target
// picker or other prompt) — it force-cancels that specific action's in-flight
// prompt first, then marks it resolved. If there's nothing cancellable found
// (an unexpected state), it notifies an error instead of doing nothing.
await game.combat.markActionResolved("ACTION_ID");
return "Marked action resolved (effects skipped)";
const combat = game.combat;
const downIndex = 4; // duration in downs
await combat.addEffectToDown(downIndex, {
name: "Burning",
description: "On fire",
icon: "icons/svg/fire.svg",
combatantId: null, // or a combatant ID
activeEffectId: null, // or a linked ActiveEffect ID
resolutionTime: downIndex,
originalResolutionTime: downIndex,
});
const combat = game.combat;
let removed = false;
for (const down of combat.system.bms.downs) {
for (const effect of Object.values(down.effects)) {
if (effect.name === "EFFECT NAME") {
await combat.removeEffectFromDown(down.downNumber, effect.id);
removed = true;
break;
}
}
if (removed) break;
}
return removed ? "Removed" : "Not found";
const actor = game.actors.get("ACTOR_ID");
const ae = actor.effects.getName("EFFECT NAME");
if (!ae) return "Effect not found";
await ae.update({ disabled: !ae.disabled });
return `Effect ${ae.name} is now ${ae.disabled ? "disabled" : "enabled"}`;
const page = await fromUuid("JournalEntry.xxx.JournalEntryPage.yyy");
if (!page || page.type !== "bmsEffect") return "Not a bmsEffect page";
const actor = game.actors.get("ACTOR_ID");
const effectData = page.system.toActiveEffectData(page.name);
effectData.origin = page.uuid; // enables auto-sync from master
await actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
return `Applied ${page.name} to ${actor.name}`;
const actor = game.actors.get("ACTOR_ID");
return actor.effects.contents.map(e => ({
id: e.id,
name: e.name,
disabled: e.disabled,
icon: e.img,
}));
const actor = game.actors.get("ACTOR_ID");
// types: "action", "feature", "item", "rune"
return actor.items.filter(i => i.type === "action").map(i => ({ id: i.id, name: i.name }));
const actor = game.actors.get("ACTOR_ID");
const item = actor.items.getName("ITEM NAME");
if (!item?.system.usable) return "Not a usable item";
await item.use();
const roll = await new Roll("2d6 + 4").evaluate();
return { formula: roll.formula, total: roll.total, terms: roll.terms.map(t => t.total ?? t.number) };
await ChatMessage.create({
content: "<p>Your message here</p>",
speaker: { alias: "GM" }
});
return {
world: game.world.title,
system: game.system.id,
scene: canvas.scene?.name ?? "none",
combat: game.combat ? `Round ${game.combat.round}` : "none",
players: game.users.filter(u => u.active).map(u => u.name),
};