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 })),
downs: c.system.bms.downs
.filter(d => !d.isEmpty)
.map(d => ({
down: d.downNumber,
actions: 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: 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";
await game.combat.advanceToNextResolution();
return "Advanced to next resolution";
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 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),
};