Last verified: v0.5.22
This page is the canonical operational reference. Check it when you need specifics not covered in your system prompt.
| Goal | Tool |
|---|---|
| Read any document | read_document ONLY |
| Find a document ID | search_documents or list_documents |
| Change document fields | read_document first, then update_document |
| Call actor/combat methods | run_javascript |
| Complex multi-doc queries | run_javascript |
❌ Common mistakes:
update_document to read content — it changes data, don't use it for readingread_document before update_document (system will reject the update)system.health.value system.health.max
system.shields.value system.shields.max
system.tempHp.value system.tempHp.max
system.sanity.value system.sanity.max
system.effectiveHp.value system.effectiveHp.max ← derived, read-only
system.resources.<key>.value
system.resources.<key>.max
system.resources.<key>.label
Default keys: surges, mana. Actors may have custom resources — read the actor to see them.
The world also keeps a slug-keyed registry of every custom resource ever defined,
in the world setting body-mind-and-soul.customResources:
{ [slug]: { label, defaults: { max, min, recoverOnShort, recoverOnExtended, recoveryType, surgeValue } } }
Action cost pickers read from this registry merged with the local actor's resources, so
item-owned actions (no actor attached) can still pick a resource cost. Helpers live in
module/helpers/custom-resources.mjs and are re-exported on helpers/_module.mjs. See
common-scripts.html for usage examples.
Optional: This feature requires the GlitchSmith-lib module to be active in the world.
A character's Wallet is a display list of currencies they carry. Wallet currencies are BMS-side state (flags.body-mind-and-soul.walletCurrencies array of currency ids), completely separate from GlitchSmith-lib's balance storage. Balances live entirely in GlitchSmith's wallet, fetched live on demand, never cached in BMS schema. This lets characters add/remove currencies from their personal Wallet display without affecting underlying GlitchSmith balances.
Available functions in game.bms.glitchsmith:
getWorldCurrencyDefinitions() — returns array of { id, name, symbol, icon, precision, integer } for all virtual currencies in the worldgetDefaultCurrencyId() — returns the world setting body-mind-and-soul.bmsCurrencyId (default: "gold"); used as default/preselect in pickersgetActorWalletCurrencyIds(actor) — returns the actor's display-list array (ids currently in their wallet)addCurrencyToWallet(actor, currencyId) — adds the currency to the wallet display list (no-op if already present)removeCurrencyFromWallet(actor, currencyId) — removes from display list (GlitchSmith balance untouched)getActorWalletData(actor) — returns array of { id, label, symbol, balance } ready for UI iteration — all currencies in the wallet with their live balancesgetActorBalance(actor, currencyId) — returns current balance for one currency (live from GlitchSmith)modifyActorBalance(actor, currencyId, delta, opts) — adds/subtracts from balance (delta can be negative); returns result valuesetActorBalance(actor, currencyId, value, opts) — sets balance directlytransferCurrency(fromActor, toActor, currencyId, amount, opts) — async; transfers between characters; returns { success, error? }Example: Add a currency and check its balance:
// In run_javascript
const actor = game.actors.get(actorId);
const defs = game.bms.glitchsmith.getWorldCurrencyDefinitions();
const goldDef = defs.find(d => d.id === "gold");
if (goldDef) {
await game.bms.glitchsmith.addCurrencyToWallet(actor, "gold");
const balance = await game.bms.glitchsmith.getActorBalance(actor, "gold");
console.log(`${actor.name} has ${balance} ${goldDef.symbol}`);
}
World setting: Owners can set the default currency via System Settings → Body Mind & Soul → Currency ID (GlitchSmith).
Hooks: When any currency balance changes, BMS fires bms.currencyChanged with hookArgs[1] = { actorId, currencyId, previousBalance, newBalance } (see Valid Hooks below).
system.attributes.<arctype>.<attribute>.value
system.attributes.<arctype>.<attribute>.bonus
system.attributes.<arctype>.<attribute>.effectsBonus ← derived: combined bonus from feature modifications (additive) + active effect modifications (take-highest)
system.attributes.<arctype>.<attribute>.total ← derived: value + bonus + effectsBonus
Arctypes: body, mind, soul. Read the actor to see which attributes each arctype contains.
system.resistances.<damageType>
Values are stage multipliers: 0 = immune, 0.25 = very resistant, 0.5 = resist, 1 = normal, 1.5 = vulnerable, 2 = weak, 3 = crit-weak
Damage types (v0.4.0+): slash, pierce, bludgeon, explosive, psionic (kinetic), storm, solar, glacial, celestial, void (elemental), energetic, magnetic, chemical, atomic, entropic (technical), unsourced, cosmic, primordial, unaspected (uncategorized)
Note: Resistance values are derived at runtime when modifications are present. The stored value is the base; apply stage math to get effective value. Feature modifications (local) stack additively; active effect modifications (applied) use take-highest — both combine into the final stage shift.
system.vectors.<vector>.value
system.vectors.<vector>.bonus
system.vectors.<vector>.effectsBonus ← derived: combined bonus from feature modifications (additive) + active effect modifications (take-highest)
system.vectors.<vector>.total ← derived: value + bonus + effectsBonus
system.tags[] ← string array of tags assigned to this actor via the Tags tab; populated from Character Tags system setting
Owners assign and remove tags via a combobox+chips interface on the Tags tab. Non-owners see a read-only chip display.
system.incomingDamageMultiplier ← derived: product of all damage.incoming multiply modifications (default 1)
system.outgoingDamageMultiplier ← derived: product of all damage.outgoing multiply modifications (default 1)
system.outgoingResistancePiercing ← { all: number, [type]: number } from damage.outgoing.piercing[.<type>|.group.<arc>] modifications (addBonus only)
system.incomingStartingLayer ← derived: "tempHp"|"shields"|"health"|null — forces incoming damage to bypass earlier layers (deepest value from all damage.incoming.startingLayer set mods; layer name stored in mod.label)
system.incomingRequireConfirmNegate ← boolean — attackers must Confirm-to-Negate when targeting this actor (damage.incoming.confirmNegate set=1)
system.outgoingRequireConfirmNegate ← boolean — this actor's own Negates must be Confirmed (damage.outgoing.confirmNegate set=1)
system.incomingRequireConfirmCrit ← boolean — crits against this actor must be Confirmed (damage.incoming.confirmCrit set=1)
system.outgoingRequireConfirmCrit ← boolean — this actor's own Crits must be Confirmed (damage.outgoing.confirmCrit set=1)
Applied in applyDamage() and trigger outcome applyDamage.
Confirm-to-Negate: when incomingRequireConfirmNegate (on target) OR outgoingRequireConfirmNegate (on attacker) is set, a drawn Negate triggers a second confirm card draw. If confirm card value > 0 and not a Negate/Crit: Negate is defeated and damage result = 0. Otherwise the Negate stands.
Confirm-to-Crit: when incomingRequireConfirmCrit (on target) OR outgoingRequireConfirmCrit (on attacker) is set, a drawn Crit requires a confirm card draw. If confirm value ≤ 0 or Negate: Crit is denied and a replacement card is drawn; damage = original_amount − crit_card_face_value + replacement_card_value. If confirm passes: Crit stands.
Damage starting layer: incomingStartingLayer overrides the layer param to applyDamage() — if the override is deeper in the cascade (tempHp < shields < health), it replaces the requested layer. Applied in both applyDamage() (actual) and calculateDamage() (preview/rollDamage).
Resistance piercing: when attacker.system.outgoingResistancePiercing.all + outgoingResistancePiercing[type] + extraPiercing is positive, the target's resistance ladder index is shifted up (toward neutral) by that many stages. Capped at multipliers.indexOf(1) — piercing can chip away resistance but cannot push the target into vulnerability. The pierceResistance trigger outcome adds to extraPiercing for the current trigger context (place it before the applyDamage outcome it should affect).
type: "stackable")ActiveEffects with type === "stackable" extend the base AE with a numeric stack
count and a sparse map of "stages" — each stage is a payload of name/img/description/
changes/modifications/triggers/drawAppend that gets projected onto the live AE when
stackCount ≥ stage threshold.
system.stackCount ← integer ≥ 0
system.maxStacks ← integer | null (null = uncapped)
system.refreshOnApply ← boolean (true: applyEffect resets the duration entry)
system.stages.<n> ← TypedObjectField keyed by stringified threshold int
.name ← overrides AE name when active
.img ← overrides AE img when active
.description ← overrides AE description when active
.changes ← native AE changes appended to parent.changes
.modifications ← ModificationModel[] appended to system.modifications
.triggers ← TriggerModel[] appended to system.triggers
.drawAppend ← appended to system.drawAppend
isSuppressed returns true when no stage is active (stackCount is 0 or no qualifying threshold), so Foundry skips the AE entirely.applyEffects outcome on a stackable AE: increments stackCount by 1 (clamped to maxStacks); duration queue refreshes if refreshOnApply is true, otherwise leaves the existing tracker entry intact.addStack / removeStack outcomes target the AE that owns the trigger; both take config.count (default 1). removeStack past 0 deletes the AE only if origin is set AND the origin is not on the same parent document (i.e. the AE is a trigger-applied copy from an external source such as a bmsEffect page or an AE on a different actor/item). AEs with no origin, or whose origin resolves to the same parent document (including the AE itself), stay put at count 0 so authors can re-apply them.update({"system.stages.5": {...}}) to add/replace; update({"system.stages.5": new foundry.data.operators.ForcedDeletion()}) to delete.ActiveEffects may have system.modifications[] — an array of ModificationModel objects computed in prepareDerivedData():
system.modifications[]
.id ← unique ID
.field ← field key: "resistance.slash", "attribute.str", "vital.health.max", "damage.incoming", etc.
.operation ← "addStages" | "setStage" | "addBonus" | "multiply"
.value ← numeric value
.label ← optional display label
Stacking: best buff wins + worst debuff wins (they combine). setStage overrides all addStages; last setStage wins.
Key field patterns for tracker visibility (set=1 only, string payload via mod.label where noted):
tracker.revealActions — forces target's queued actions visible; label "caster" = visible to AE origin's owner only; label "all" = visible to all players. Applied at queue time, not in prepareDerivedData.tracker.revealKeywords — when set on target, their action's keywords appear in the tracker tooltip for any player who can see the action. Sets actor.system.trackerRevealKeywords = true.Check advantage field patterns (operations: addStages / setStage):
check.advantage — global advantage stages on all compel checks. Stage +N → roll (N+1)d20 keep highest; stage -N → keep lowest. Derived into actor.system.checkAdvantage.global.check.keyword.<kw>.advantage — advantage stages that apply only when the check's effective keywords include <kw>. Derived into actor.system.checkAdvantage[kw].actor.system.checkAdvantage schema (derived, read-only):
{ global: number, [keyword: string]: number }
Success-dice vectors (e.g. Luck) are unaffected by advantage stages — only the attribute d20 roll is affected.
Standalone effect definitions stored as bmsEffect or bmsStackableEffect JournalEntryPages. These serve as master templates for ActiveEffects with full sidebar organization (journal folders, compendiums).
Two page types:
bmsEffect — produces a type: "base" AE.bmsStackableEffect — produces a type: "stackable" AE. The page carries no live stackCount; each spawned AE instance tracks its own. Pages of this type add maxStacks, refreshOnApply, and stages to the base schema.Origin-based propagation: When an AE has an origin UUID pointing to a bmsEffect or bmsStackableEffect page (or another AE), the AE auto-syncs all its fields from the source during prepareBaseData(). Only _id, disabled, origin, and (for stackable AEs) stackCount are preserved per-instance — everything else is forwarded from the master, including stages, maxStacks, and refreshOnApply.
Applying from pages: Drop a page onto an actor, or use the applyEffect trigger outcome with effectPageUuid. The AE is created with origin set to the page UUID, enabling auto-sync.
Schema — bmsEffect pages: img, description, transfer, tint, showIcon, changes[], drawAppend, triggers[], modifications[], plus all aura fields (isAura, auraType, auraRange, etc.).
Aura lighting — aura effects with system.auraLightEnabled = true emit light constrained to the aura region polygon via the bmsRegionLight custom region behavior. There is no separate AmbientLight document; the light is embedded in the backing Region and auto-cleans up with it. Key fields:
auraLightEnabled (bool) — whether the aura emits lightauraLightConfig (LightData) — stores color, alpha, luminosity, animation.type/speed/intensity, and crucially:
negative (bool) — Polarity: controls whether the light is emitted (false) or darkness is cast (true)darkness.min / darkness.max (numbers) — Activation range: the light is only active when the scene's canvas.darknessLevel is between these two values (inclusive). This gates whether the light functions based on scene darkness level.animation.type — Polarity-scoped: this is a key into CONFIG.Canvas.lightAnimations when negative: false, or CONFIG.Canvas.darknessAnimations when negative: true — the registries have different, non-overlapping keys. If you flip the polarity (e.g., from normal light to darkness source), any animation not supported in the new registry is silently nulled to prevent animation failures.auraRadiate (bool) — when false, light is blocked by walls; when true, light radiates freelyThe distinction between negative (polarity: light vs darkness) and darkness.min/max (activation: when the light should be active) was the root cause of a previous bug where darkness-source auras weren't properly gated by scene darkness level. GMs can also add a bmsRegionLight behavior manually to any scene Region via the Region config.
Advanced Lighting Options dialog live-sync: The dialog that edits auraLightConfig uses a two-part preview-sync pattern (module/helpers/aura-light-config.mjs). When the user toggles "Is Darkness Source" (the negative field), the animation-type dropdown must update immediately to show animations from the correct registry (light vs darkness). This requires both an _onChangeForm() override that runs preview sync unconditionally (since the dialog sets preview: false to skip canvas rendering) and a _previewChanges() call that keeps the temporary preview document in sync with form edits. Together, these ensure the dropdown reflects live form state rather than a stale snapshot from when the dialog opened.
bmsRegionLight behavior — Renders ambient colored light bounded by the region polygon, or suppresses light when darkness mode is enabled. When darkness=false, uses BmsRegionLightSource extends PointLightSource; when darkness=true, uses BmsRegionDarknessSource extends PointDarknessSource which registers into canvas.effects.darknessSources instead of lightSources. Both override _createShapes() to substitute region polygon via ClockwiseSweepPolygon with boundaryShapes instead of radial wall-sweep. Registered via source.add() — all animations work through Foundry's native animation system. Fields: color (hex), alpha, luminosity, animationType (key from CONFIG.Canvas.lightAnimations when darkness=false, or from CONFIG.Canvas.darknessAnimations when darkness=true; or "none"), animationSpeed, animationIntensity, walls (bool), darkness (bool, default false). Source type is rebuilt on darkness field change — old source destroyed, new one created.
Additional fields on bmsStackableEffect pages:
system.maxStacks ← integer | null (null = uncapped)
system.refreshOnApply ← boolean
system.stages.<n> ← same StageModel map as BodyMindSoulStackableEffect
The tracker is on the active Combat document. Access via game.combat in run_javascript.
system.bms.downs[] ← array of 13 CombatDownModel (downNumber 0–12)
.downNumber ← 0 = resolving now, 12 = furthest future
.actions[]
.id ← unique action ID
.combatantId ← links to combat.combatants collection
.action ← display name
.type ← "foreswing" | "hold" | "resolve" | "backswing"
.actionItemId ← Item document ID on the actor (standalone action OR parent item for embedded)
.embeddedActionId ← non-null when action is an embedded Item (type "action") in a gear item's actions collection
.swingIndex ← which swing within the action is queued
.resolutionTime ← current down number
.originalResolutionTime ← original duration
.repetitions ← repeats remaining (0 = one-shot)
.playerVisible ← visible to all non-GM players when true
.revealedToUserIds ← Set of specific user IDs who can see this action (used by tracker.revealActions "caster" modification)
.effects[]
.id
.combatantId
.activeEffectId ← links to an ActiveEffect document
.name
.description
.icon
.resolutionTime
.originalResolutionTime
.repetitions
system.bms.tickNumber ← current tick
system.bms.nextTickIn ← downs until next tick
system.bms.downsTilRound ← downs until round boundary
⚠️ Never update system.bms.downs directly via update_document. Always use run_javascript with BMS API calls (they apply redistribution logic and fire correct hooks).
type: "action")system.swings[] ← id-based merge (id field)
.id
.duration ← number of downs
.type ← "foreswing" | "hold" | "resolve" | "backswing"
.triggers[] ← SwingTriggerModel array (id-based merge)
Each entry has `continueOnNegate: boolean` — when true, a negated outcome
(e.g. applyDamage drawing a Negate) records `wasNegated: true` in the
accumulator but does NOT abort the swing or route to negateTriggers; the
next trigger in the list runs normally.
Multi-shot pattern: N triggers (1 applyDamage outcome each), all with
continueOnNegate: true except the last. Gate a follow-up trigger with
`allPriorTriggersHit` to react to whether every shot landed.
Example — Vicious Staccato: triggers 1 & 2 have continueOnNegate: true,
trigger 3 has continueOnNegate: false (last shot; its negate routes to
negateTriggers if any are defined).
.negateTriggers[] ← SwingTriggerModel array (id-based merge)
system.actionStyle ← "rahmara" | "slots" | "resource"
system.groupKey ← which action group this belongs to
system.resourceData ← present when actionStyle="resource", nullable otherwise
.costs ← TypedObjectField keyed by cost ID (object, not array)
[costId] ← ResourceCostModel instance
.id ← unique cost ID
.type ← "rune" | "slot" | "resource"
.runeName
.groupKey
.slotLevel
.resourceKey
.quantity
.order
._sortedCosts ← derived: sorted array of ResourceCostModel instances for template iteration
Cloning an action: When duplicating an action for persistence (e.g. Ctrl+drag on actor sheet, or dropping onto a gear/glyph item), use action.system.duplicate() instead of toObject(). This returns a plain object with all cost IDs regenerated, preventing save conflicts if the two copies are edited independently.
SwingTriggerModel fields (on entries in system.swings[].triggers[] and system.swings[].negateTriggers[]):
| Field | Default | Description |
|---|---|---|
iterator |
"owner" |
Targeting mode |
continueOnNegate |
false |
Continue to next trigger on negate instead of routing to negate triggers |
reuseTargeting |
true |
When false, bypasses the iterator cache and re-prompts for targets independently for this trigger (Magic Missile pattern) |
loopCount |
1 |
Repeat this trigger's full execution N times in a row (min 1). Each iteration independently collects targets, resolves formula, and fires outcomes. |
loopReuseTargeting |
true |
When loopCount > 1: true = same targets reused across all iterations; false = targets re-collected each iteration. Independent from reuseTargeting. |
templateConfig |
{…} |
Config for template iterators |
burstConfig |
{…} |
Config for close burst iterators |
combatConfig |
{…} |
Config for eachCombat/allCombat iterators |
type: "feature")system.triggers[] ← TriggerModel array (id-based merge)
system.featureConfig[] ← id-based merge (key field)
.key
.value
system.modifications[] ← ModificationModel array (id-based merge). Feature mods use additive stacking (each one adds);
active effect mods use take-highest stacking (best buff + worst debuff wins among AEs).
Both pools combine: localSum + bestBuff + worstDebuff.
Toggleable features only contribute mods while enabled.
type: "item")system.quantity
system.usable ← boolean
system.equipable ← boolean
system.equipped ← boolean
system.triggers[] ← TriggerModel array (id-based merge)
system.actions[] ← ArrayField(ObjectField) — raw action source objects (plain objects with _id)
// Each entry: { _id, name, type:"action", img, system:{swings,actionStyle,...}, effects:[], flags:{} }
item.actions ← getter returning SyntheticActionItem[] synthesized from system.actions
// SyntheticActionItem extends Item; .update()/.delete() redirect to parent gear item's system.actions
// Use gearItem.actions.find(a => a.id === id) to retrieve by id
// To add: push to deepClone(gearItem.system.actions), call gearItem.update({"system.actions": [...]})
// SyntheticActionItem.gearItem → parent gear item; .actor → gear item's owning actor
system.sockets[] ← id-based merge (uuid field)
.uuid
.name
.img
.type
.expressions
.currentExpressions
system.actionResources[] ← id-based merge (id field)
.id
.groupMatch ← action group key filter (blank = all groups)
.levels[] ← id-based merge (key field)
.key
.label
.max
.value
.restoreOnShort
.restoreOnExtended
Triggers live in system.triggers[] on Features, Usable Items, ActiveEffects, Cards, and Decks. Each trigger:
{
"id": "auto-generated",
"hook": "bms.itemUsed",
"enabled": true,
"targetMode": "default",
"conditions": [],
"outcomes": []
}
targetMode controls whether a target-selection prompt appears before the trigger runs:
"self" — no prompt; the trigger owner (drawing actor, item user, etc.) is the implicit target. Use this for card triggers that apply to the drawing character (e.g. "gain 10 resource when you draw this card")."default" — shows a target-picker dialog before resolving. Use when the trigger should affect a different token chosen at runtime.⚠️ Forgetting targetMode: "self" on card triggers is the most common authoring mistake — the card appears to "do nothing" because the player dismissed the unexpected target-picker prompt.
Both conditions and outcomes have a target field. Conditions accept "target" (default) or "self"; outcomes additionally accept "attacker".
"target" — applies to whoever the iterator/hook provides as the current target"self" — overrides the target to be the trigger owner, but still fires once per iterator iteration"attacker" (outcomes only) — applies to the actor that caused the triggering event (e.g. the striking actor on a damage/weapon-strike hook), falling back to the trigger owner when no such actor is recorded for that hookThis allows a single trigger to affect both the target and the caster. For example, a mana drain with eachTarget iterator:
changeResource mana -200, target: "target" → each enemy loses 200changeResource mana +200, target: "self" → owner gains 200 per target hitWhich scope options are actually offered for a given condition/outcome type is declared per-type by the system (not every type supports every scope) — the picker only shows the scopes that type's evaluate/execute actually honors. "Attacker" never appears as an option on conditions; only outcomes can use it.
// Example outcome with target scope
{ "id": "outcomeId", "type": "changeResource", "target": "self", "config": { "resource": "mana", "operation": "add", "amount": "200" } }
Formula fields (outcome amount, rollCheck/compelCheck DC formula, chatMessage/floatingText text, etc.) support namespaced tokens that reach a specific actor rather than whichever single actor the formula is evaluated against by default:
{self.foo} — the trigger owner{target.foo} — the resolved target for that condition/outcome's own scope{attacker.foo} — the recorded attacker/triggerer for the firing hook (falls back to the owner if none){owner.foo} — alias for {self.foo}foo can be an attribute shorthand ({target.str}) or a full dotted property path ({target.system.vitals.health.value}). These combine with the existing single-actor {AttributeName}/@attributeKey tokens and dice notation/{{draw}} substitution — e.g. a Roll Check outcome scoped "Self" can still write a DC formula like {target.con} + 2 to reference the enemy's stats.
bms.itemUsed, bms.cardDrawn, bms.deckShuffled, bms.scryPerformed, bms.characterRested, bms.damageApplied, bms.restoreApplied, bms.resourceChanged, bms.checkRolled, bms.beforeCheck, bms.actionResolved, bms.actionQueued, bms.effectQueued, bms.effectExpired, bms.effectToggled, bms.combatDown, bms.combatTick, bms.combatRound, bms.currencyChanged
bms.checkRolled — fires after any attribute, vector, general, or saving-throw check resolves. hookArgs[1] carries { attribute, archetype, isAttribute, isVector, isGeneralCheck, isSavingThrow, keywords[], roll, total, success, dc, compelled }.
bms.beforeCheck — fires before any check rolls (pre-roll query hook). Outcomes on this hook may modify advantageMod and dcMod in hookArgs[2] to alter the roll before it happens. Evaluated by the GM with full world permissions.
bms.currencyChanged — fires when an actor's GlitchSmith-lib wallet balance changes. Requires GlitchSmith-lib module to be active. hookArgs[1] carries { actorId, currencyId, previousBalance, newBalance }. Use the currencyBalance condition to filter on balance thresholds; use modifyCurrency outcome to adjust balances from triggers.
| Condition | Notes |
|---|---|
always |
Always passes (use when no filter needed) |
ownerInCombat |
Actor has a combatant in active combat |
ownerIsResolving |
Actor resolving at down 0 |
itemIsEquipable |
Used item is equipable (bms.itemUsed) |
itemIsEquipped |
Used item is equipped (bms.itemUsed) |
itemHasSockets |
Used item has sockets (bms.itemUsed) |
isThisCharacter |
Hook target actor IS the trigger owner |
resourceName |
Changed resource label matches value (bms.resourceChanged) |
restType |
Rest type matches value ("short"/"extended") (bms.characterRested) |
damageAmount |
Damage meets comparison |
damageType |
Damage type matches value |
targetVital |
Damage layer matches value |
wasCrit |
Drawn card name matches crit pattern (bms.cardDrawn) |
wasNegate |
Drawn card is a negate card |
attackerInFrontArc |
Swing-trigger only. True when the attacker (action owner) is in the front arc (±45° of facing) of targetActor. Returns false if the target token has no facing flag set. |
attackerInFlank |
Swing-trigger only. True when the attacker is in either flank arc (45°–135° from target's facing). |
attackerInRearArc |
Swing-trigger only. True when the attacker is in the rear arc (>135° from target's facing). |
allPriorTriggersHit |
Swing/keyword trigger only. True when every prior trigger in this swing landed (no wasNegated entry) AND at least one prior trigger ran. Pair with continueOnNegate: true on the prior triggers so a miss doesn't abort the swing. |
priorTriggerHitCount |
Swing/keyword trigger only. Numeric comparison over the count of prior triggers that landed. value: plain number (default >=) or {op,val} JSON. |
rollCheck |
Target rolls an attribute or vector against a formula-based DC. Config: attribute (key), formula (DC expression), op (default >=). Fires on bms.damageApplied and bms.checkRolled. |
isAttributeCheck |
True when the triggering check is an attribute roll. Optional value: attribute key (blank = any attribute). Fires on bms.checkRolled and bms.beforeCheck. |
isVectorCheck |
True when the triggering check is a vector roll. Optional value: vector key (blank = any vector). Fires on bms.checkRolled and bms.beforeCheck. |
isGeneralCheck |
True when the triggering check is a plain d20 general check (no attribute). No config needed. Fires on bms.checkRolled and bms.beforeCheck. |
isSavingThrow |
True when the triggering check is a saving throw. No config needed. Fires on bms.checkRolled and bms.beforeCheck. |
checkHasKeyword |
True when the check's keywords include the specified key. Required value: keyword key. Fires on bms.checkRolled and bms.beforeCheck. |
compelCheck |
Presents target with a menu of attribute options; they choose one and roll against its DC. Config: options[] ({ attribute, label?, formula, keywords?, advantage? }), allowTake10, hideDCs, globalKeywords (string[] applied to all options), advantage (global check advantage stage). NPC resolution mode is read from actor.system.compelCheckMode. Advantage stacks: passive check.advantage mods + per-option options[].advantage + global cond.config.advantage. Fires on bms.damageApplied. |
resolvedAgainstThisCharacter |
True when any resolving action (bms.actionResolved) has this actor's combatant in its targets. No config needed. |
hitByAction |
True when this actor's combatant is targeted by a resolving action matching a specific item by UUID. Config: actionUuid — paste or drag the action item's UUID. Fires on bms.actionResolved. |
hasTag |
True when the subject actor has the specified character tag in system.tags. Config: value (string — exact tag name, selected from Character Tags setting via combobox). Allows target scope: self / target. Fires on all hooks. |
script |
Custom JS expression in value |
⛔ isThisItem is NOT a valid condition — it does not exist and will silently block the trigger every time. The existing "Restoration Potion" in this world has a broken trigger because of this. Use always or leave conditions: [] for item-owned triggers (since the trigger is already on that specific item, no filtering is needed).
| Outcome | Required config fields |
|---|---|
changeResource |
resource (key e.g. "surges"), operation ("add"/"set"), amount (string); when max === 0, resource is uncapped (no upper clamp) |
changeVital |
vital ("health"/"shields"/"tempHp"/"sanity"), operation, amount, overflowMax (optional bool); when max === 0, vital is uncapped (no upper clamp); overflowMax: true allows exceeding vital.max while still clamping at 0 |
chatMessage |
content (string with substitution tokens) |
applyEffects |
effectIds (array of UUIDs of EXISTING ActiveEffect/bmsEffect/bmsStackableEffect documents to enable), optionally duration (number), durationUnit ("downs"/"ticks"/"rounds"). PREFERRED, structured way to apply a status/buff/debuff — never fake one with a script outcome. Cannot create a brand-new effect type; the GM must create it first, then bind its UUID. On a stackable AE already applied, re-firing applyEffects increments stackCount instead of no-oping — use addStack/removeStack if you specifically want to adjust an already-applied stackable's count without risking a duration-refresh side effect. (Legacy alias: applyEffect, singular, migrated automatically to applyEffects on load — always author new triggers with the plural form.) |
addStack / removeStack |
count (number, stacks to add/remove), effectName (optional — targets a named stackable AE on the target; blank = the owning effect, effect-context only). Adjusts an ALREADY-APPLIED stackable effect's count; does not apply/enable the effect if absent — use applyEffects first. removeStack deletes the AE at count 0. |
toggleEffect |
mode ("toggle"/"on"/"off"). Acts SELF-REFERENTIALLY on the effect that owns this trigger — only valid on an ActiveEffect's own trigger. Use toggleEffectByName instead for any other effect. |
toggleEffectByName / removeEffectByName / suspendEffectByName |
effectName (string, plus mode for toggle: "toggle"/"on"/"off"). Find a named ActiveEffect on the target (or owner) by name rather than acting on the trigger's own owning effect. removeEffectByName deletes permanently; suspendEffectByName disables reversibly (re-enable via toggleEffectByName/"on"). |
applyRestore |
targetKey ("health"/"shields"/"sanity"/"tempHp"/"resources.<key>"), amount, useSurge/consumeSurge (default true). PREFERRED healing/restore outcome — respects the surge mechanic. Use changeVital/changeResource instead only for a raw, surge-less add/set (e.g. DoT ticks, non-heal bookkeeping). |
applyDamage |
type (damage type), amount, optionally layer |
queueAction |
actionName, actionType, resolutionTime, repetitions. Builds a FRESH queued action from scratch with a chosen swing type/duration — not tied to an existing action item unless actionItemId is set. Use useAction instead to fire an action item that already exists. |
useAction |
actionUuid (preferred, drag-drop or paste full UUID) or actionName (fallback, searches actor's items by name). promptConfirm (boolean, default true) — set to false to suppress the confirmResolve dialog and auto-resolve immediately (use only with self/closeBurst/inCombat iterators). Fires an EXISTING action item instantly (resolutionTime 0), including self-referencing for a ricochet/chain pattern. Use queueAction instead when there's no existing action item to point at. |
advanceSelf |
downs (number). Speeds up the OWNER's already-queued action(s) generally. Use accelerateQueuedAction instead to target specifically the action just queued (on bms.actionQueued); use advanceEffect instead for an effect's tracker duration, not an action. |
accelerateQueuedAction |
amount (formula string). Modifies the resolutionTime of the action just queued: positive amounts accelerate (move earlier in tracker), negative amounts delay (move later). Use on bms.actionQueued with an actionCombatantIsThisCharacter condition. Scoped to that one action, not the owner's queue in general (use advanceSelf for that). |
advanceEffect |
amount (formula string). Decrements the tracker resolutionTime of the effect that owns this trigger — self-referential like toggleEffect, effects only (use advanceSelf/accelerateQueuedAction for actions). |
pushTempStack |
sourceDeckId, cardIds (or cardIdsText, newline-delimited), label?, returnOnExpire? ("source"/"discard"/"remove"), expiryType? ("never"/"encounter"/"nDowns"). Sets aside SPECIFIC, already-known cards (you supply the IDs) as a TempStack. Use scryFromTargets instead when the cards need to come from a live scry/arrange step. |
pushDrawOverride |
sourceType ("deck"/"tempStack"/"lastTempStack"), mode? ("optional"/"forced"), charges? (null = unlimited). Does not draw/set aside cards itself — redirects the target's FUTURE draws to the given source. Pair with pushTempStack/scryFromTargets (sourceType: "lastTempStack") to draw from a stack just built. |
scryFromTargets |
count? (default 5), stackLabel?, plus the same returnOnExpire?/expiryType? shape as pushTempStack. Scries cards from each target's default deck, prompts the granter to arrange/discard, sets kept cards aside as a TempStack. Use pushTempStack instead when the exact card IDs are already known. |
modifyAdvantage |
stages (number, positive = advantage, negative = disadvantage). General swing-wide advantage for the rest of the swing. Use modifyCheckAdvantage instead when reacting specifically to a bms.beforeCheck roll, not the swing at large. |
changeAttribute |
attribute (key), field ("value"/"bonus"), operation, amount |
script |
script (JS string) |
saveVariable |
variableName (string key), source (formula string), mode ("set"/"accumulate", default "accumulate"). Stores a value in the action's channelVariables bag. Only available in reactive (While Channeling/Holding) and cancel triggers. |
executeTarget |
thresholdPercent (0–100, default 20), lethal (boolean, default true). When target's HP % is at or below thresholdPercent, prompts the GM with Execute / Spare. On Execute, sets target.system.health.value directly: lethal → -max (-100%), non-lethal → -surgeValue (per-vital override on system.vitalSurgeValues.health, else max/4). Bypasses applyDamage and resistances; isDead/isDowned derive from the resulting HP. |
pierceResistance |
stages (number, default 1). Adds N stages of resistance piercing to subsequent applyDamage / rollDamage outcomes within the same trigger context. Stacks additively with the attacker's passive damage.outgoing.piercing modifications. Place this outcome before the damage outcome it should affect. |
disableNextDrawnCard |
Effect-only. On bms.cardDrawn: disables the drawn card in the target's deck (excluded from all future draws/shuffles) and immediately draws a replacement. Config: restoreOn — "effect" (default, re-enable when this AE is removed/disabled), "rest" (re-enable on actor rest), "both". Typically paired with a removeEffect outcome for one-shot use (disable one card then self-remove). |
modifyCheckAdvantage |
bms.beforeCheck only. Self-only. Adds N stages of advantage (negative = disadvantage) to the pending check. Config: value (integer, e.g. 2 for two stages advantage). Stacks additively with other modifiers. |
modifyCheckDc |
bms.beforeCheck only. Self-only. Adds N to the effective DC of the pending check (negative = lower DC = easier). Config: value (integer). |
userChoice |
Meta outcome — no attacker scope, only target/self. Prompts the resolved subject's controller with a modal listing author-defined options and runs the chosen option's own nested outcome list. Config: promptTitle (string, optional), options (array of { id, name, description, outcomes } — outcomes is a plain-object list, same shape as TriggerOutcomeModel fields, NOT an EmbeddedDataField). Only { id, name, description } per option crosses the socket boundary to the prompt; nested outcomes never leave the executor client. On response, resolves the chosen option and runs its outcomes via _runOutcomeList (trigger-outcomes.mjs) against the same execution context, so NegateSignal/actor-update-batching/outcomeResults all compose normally. Stamps outcomeResults.userChoice.<subjectId> = { chosenOptionId, chosenOptionName } for downstream {result.chosenOptionName}-style chaining. Not nestable — a userChoice outcome inside another userChoice option's outcomes list is rejected by the config UI and defensively filtered at execution time. |
Player/GM-facing feature (not an AI tool). Every trigger panel (Feature/Effect/Item/Card, Action Swing, Channel/Hold, Stackable Effect Stage, Keyword) has an Import Trigger button that generates a plain-text prompt describing the valid trigger JSON schema for that specific panel, for the user to paste into an external LLM alongside their own request. The LLM's JSON reply is pasted back in, validated, and appended (never replaces existing triggers).
// module/helpers/trigger-schema-prompt.mjs
buildTriggerImportPrompt(consumerType) // → prompt text string, filtered to consumerType's valid hooks/conditions/outcomes
validateImportedTrigger(raw, consumerType) // → { valid, triggers, errors }; pure, does not touch any document
raw is an already-JSON.parsed payload (single trigger object or array). On success, triggers is normalized to the system's live keyed-map shape (conditions/outcomes as {id: {...}}, id/order assigned). On any validation failure, valid is false and triggers is empty — all-or-nothing across the whole batch.
// module/helpers/trigger-import-adapters.mjs
TRIGGER_IMPORT_ADAPTERS[surface].insert(document, triggers, extra)
surface is one of standard (Feature/Effect/Item/Card), keyword, swing/negate/cancel/channel/hold (Action swing family — extra: { swingIndex, triggersKey }), or stage (Stackable Effect — extra: { threshold }). Each adapter writes to the same document path the matching "Add Trigger" sheet handler uses, so imported triggers are indistinguishable from manually-added ones.
{actor.name} — trigger-owning actor's name (NOT @actor){target.name} — target actor (swing triggers){feature.name} — item/feature that owns the trigger{effect.name} — the active effect (effect triggers){card.name} — drawn card name (card triggers){changeResource.surges.amount} — result from a prior changeResource outcome in the same trigger{channel.variableName} — channel variable saved by saveVariable outcome (reactive + cancel triggers){targetCount} — number of targets resolved by the iterator (post-filter, pre-condition){targetIndex} — 0-based position of the current target in the iteration{fireCount} — lifetime fire count of this trigger (effect & feature triggers only). Starts at 1 on the first eval that passes conditions; persists across reloads on the host doc as flags.body-mind-and-soul.triggerFires.<triggerId>. Reset on toggle (effect.disabled change / feature.system.enabled change) and stripped on copy. Powers ramping mechanics like applyDamage with amount: "5 * {fireCount}". Also exposed to script outcomes as the fireCount argument.Many BMS arrays support id-based partial updates. When any item in the update array contains the array's merge key, it is merged into the matching existing element rather than replacing the whole array. Include only the fields you want to change — omitted fields are preserved.
NEVER use dot-path array indexing (e.g. "system.triggers.0.hook"). Always update via the array field.
| Array | Document | Merge key |
|---|---|---|
system.triggers |
Feature, Item, Effect, Card | id |
system.triggers[].conditions |
same | id |
system.triggers[].outcomes |
same | id |
system.actionGroups |
Actor | key |
system.swings |
Action item | id |
system.swings[].triggers |
Action item | id |
system.swings[].negateTriggers |
Action item | id |
system.resourceData.costs |
Action item (resource style) | id |
system.actionResources |
Usable item | id |
system.actionResources[].levels |
Usable item | key |
system.sockets |
Usable item | uuid |
system.actions |
Item (gear/consumable) | id |
system.actions[].swings |
Item embedded action | id |
system.featureConfig |
Feature item | key |
system.modifications |
Feature, Effect, GearUpgrade | id |
Minimal trigger patch — change just the hook, conditions/outcomes untouched:
{ "system.triggers": [{ "id": "triggerId", "hook": "bms.combatDown" }] }
Patch one outcome's config, other outcomes untouched:
{
"system.triggers": [{
"id": "triggerId",
"outcomes": [{ "id": "outcomeId", "config": { "content": "Updated." } }]
}]
}
Change a swing's duration without touching its triggers:
{ "system.swings": [{ "id": "swingId", "duration": 3 }] }
Update one slot level's max value:
{
"system.actionResources": [{
"id": "arId",
"levels": [{ "key": "level1", "max": 4 }]
}]
}
To delete an item from a mergeable array, include "_delete": true alongside the merge key. Items without the key, or items where _delete is absent/false, are added or merged as usual.
Delete a trigger:
{ "system.triggers": [{ "id": "triggerId", "_delete": true }] }
Delete a condition from a trigger:
{ "system.triggers": [{ "id": "triggerId", "conditions": [{ "id": "conditionId", "_delete": true }] }] }
Delete an action group (key-based):
{ "system.actionGroups": [{ "key": "groupKey", "_delete": true }] }
Delete a swing:
{ "system.swings": [{ "id": "swingId", "_delete": true }] }
Full replacement still works — omit the merge key from all items:
{
"system.triggers": [{
"hook": "bms.itemUsed",
"enabled": true,
"conditions": [],
"outcomes": [
{ "id": "o1", "type": "changeResource", "config": { "resource": "surges", "operation": "add", "amount": "1" } },
{ "id": "o2", "type": "chatMessage", "config": { "content": "{actor.name} restores 1 surge." } }
]
}]
}
// Damage & healing
await actor.applyDamage(amount, type, layer, { source, attacker })
// layer: "tempHp" | "shields" | "health" | "sanity"
// returns { original, actual }
actor.system.calculateDamage(amount, type, layer)
// preview only, no state change
// returns { original, actual, vitalDamage }
await actor.applyRestore(targetKey, { useSurge, consumeSurge, allowOverflow, bonusValue })
// targetKey: "health" | "shields" | "tempHp" | "sanity" | "resources.<key>"
// Combat tracker
await combat.advanceTracker() // one down
await combat.advanceToNextResolution() // skip to next occupied down
await combat.queueActionFromSheet(actorOrId, actionItemId, swingIndexOverride)
await combat.queueItemActionFromSheet(actor, parentItemId, embeddedActionId, swingIndex)
// Queues an embedded action Item from a gear/consumable. Decrements quantity for usable non-equipable items.
// embeddedActionId is the ID of the embedded Item in gearItem.actions (EmbeddedCollection).
await combat.addActionToDown(downIndex, actionData)
await combat.addEffectToDown(downIndex, effectData)
await combat.removeEffectFromDown(downNumber, effectId)
// Lookup helpers on the combat DataModel
combat.system.findDown(downNumber) // → live CombatDownModel
combat.system.findAction(downNumber, id) // → live TrackerActionModel
combat.system.findEffect(downNumber, id) // → live TrackerEffectModel
combat.system.findNextOccupiedDownIndex() // → number (-1 if none)
// Helpers on CombatDownModel
down.hasActions // boolean
down.hasEffects // boolean
down.isEmpty // boolean
Convenience helpers available globally in run_javascript and trigger scripts.
| Helper | Description |
|---|---|
await game.bms.triggerFromUuid(uuid) |
Resolves a trigger UUID (parentDoc.uuid + "#bms:" + triggerId) to a TriggerProxy. Async — works for compendium items. Returns null if not found. |
game.bms.triggerFromUuidSync(uuid) |
Same as above but synchronous. Only works for documents already loaded in world collections. |
A lightweight handle returned by the triggerFromUuid / triggerFromUuidSync helpers.
Trigger UUID format: ${parentDoc.uuid}#bms:${triggerId}
context.triggerProxy is available in all script condition/outcome contexts — use it to read or write back to the trigger that is currently executing without needing to know its array path.
// Read a field
context.triggerProxy.loopCount
// Write back to the trigger (partial update — other fields preserved)
await context.triggerProxy.update({ loopCount: 3, loopReuseTargeting: false })
// Find actor by name
game.actors.getName("Character Name")
// Find actor's item by name
actor.items.getName("Action Name")
// Get all combatants in active combat
game.combat?.combatants.contents
// Get a combatant's actor
combat.combatants.get(combatantId)?.actor
// Find active combat
game.combat
type: "glyph")Glyphs are enchanted items socketed into gear. They provide passive effects and actions powered by geo synergy (environmental tagging).
system.rank ← 1-5 integer
system.glyphType ← "enhancer" | "manipulator" | "emission" | "conjuror"
system.geoSynergyTags ← SetField(StringField) — geo type keys that activate this glyph's synergies
system.elementalAffinity ← string (conjuror creature type hint)
system.durability.value ← current charge
system.durability.max ← maximum charge
system.actions[] ← ArrayField(ObjectField) — raw embedded action objects (same as gear items)
._id
.name
.system.swings[]
.system.isGeoSynergy ← boolean — true = only usable when geo synergy active
.system.durabilityCost ← number — durability deducted on use
system.isSocketable ← boolean (default true)
Glyphs also have an embedded effects collection (standard ActiveEffects). Effects with flags["body-mind-and-soul"].isGeoSynergy = true only propagate to the actor when geo synergy is active.
A glyph is active when it is socketed (via the socket UUID) in an equipped gear item (type: "item", system.equipped: true).
Active glyph effects propagate via actor.allApplicableEffects():
isGeoSynergy effects: only propagated when actor._getActiveGeoTypes() ∩ glyph.system.geoSynergyTags ≠ ∅system.sockets[] ← id-based merge (uuid field)
.uuid ← the socketed item's UUID
.name
.img
.type ← "rune" | "glyph" | other
.expressions ← rune tracking (null for glyphs)
.currentExpressions ← rune tracking (null for glyphs)
.durability ← glyph current durability (null for runes)
.maxDurability ← glyph max durability (null for runes)
Environmental tags are stored in scene/tile flags:
scene.flags["body-mind-and-soul"].isSurveyed ← boolean
scene.flags["body-mind-and-soul"].geoTag.{type} ← boolean per geo type
tile.flags["body-mind-and-soul"].isSurveyed ← boolean
tile.flags["body-mind-and-soul"].geoTag.{type} ← boolean per geo type
Geo types: fire, water, earth, air, lightning, ice, void, radiant
A location's tags are inert until isSurveyed = true. Scene is always surveyed on first survey; tiles are only surveyed if the token is standing on them at survey time.
Survey Environment is a synthetic action (no item ID) injected in the actor sheet's "Glyph Actions" tab when any glyphs are active. When queued to the combat tracker (type: "surveyEnvironment") and resolved at RT=0, it opens a GM dialog to set scene/tile tags.
system.equipmentMaintenanceActions ← integer (default 8) — shared extended-rest downtime pool
During extended rest:
rahmara.extendedRestExpressions)repairAttempts × (2d4 + soul.sav) total durability to distributebms.kengaicGlyphRepaired(actor, glyphItem, amount) — fires per glyph after repair dialog submitbms.rahmaricExpressionRestored(actor, runeItem|null, amount) — fires per rune restore during restbms.geoSynergyChanged — reserved for future geo synergy state transitionssystem.appendKeywords[] — each entry { id, append, requiresKeyword }.append keywords get unioned into the action's effective keyword list.requiresKeyword empty → unconditional. Set → only appends when the action's NATIVE keywords include that string. Filter checks native only — no recursion across rules.effectiveKeywords(actor, nativeKeywords) in module/helpers/keywords.mjs. Used by _actionKeywords() in trigger-conditions.mjs (powering actionHasKeyword / actionLacksKeyword) and by applyDamage/rollDamage outcomes (per-keyword damage scaling + absolute resistance bypass).bms.actionResolved.flags.body-mind-and-soul.facing (number, 0–359; 0 = up/north, clockwise).attackerInFrontArc / attackerInFlank / attackerInRearArc swing-trigger conditions to build flanking/backstab mechanics.Scene-controls toolbar (token group) exposes GM-only Short Rest All and Extended Rest All buttons. Scope is selected tokens, falling back to every token on the active scene.
Dispatch rules per actor:
system.compelCheckMode:
"direct" → GM-local dialog."smart" / "dumb" → fullRestoreNpc(actor, restType) runs silently: every vital, resource, slot level, rune expression, and socketed rune is set to max; sideboards persist-flagged for the mode are pruned.Prompt spec: { type: "rest", actorUuid, lockedMode }; result { applied: boolean }.
2026-08-15: Action acceleration notification — When a trigger outcome (accelerateQueuedAction or advanceSelf) pushes an action's resolutionTime to 0 mid-round (via _decrementResolutionTime in module/helpers/trigger-outcomes.mjs), the combat document update now passes { bmsAccelerated: true }, which Combat#_onUpdate detects and fires a distinct notification: "
2026-08-15: playAnimation teleport-preset client relay — The playAnimation trigger outcome gained a needsClientRelay config checkbox ("Teleport-Type Preset", default off). When enabled, instead of calling window.AutomatedAnimations.playAnimation(...) directly on the GM's client (which would place the crosshair prompt on the GM's screen), the outcome builds a { type: "playAnimation", ... } spec and relays it via promptFor(actor, spec) to the acting player's own client. This ensures Automated Animations' teleport destination-picker crosshair opens on the correct player's screen. Renderer: _renderPlayAnimation in module/helpers/prompt-renderers.mjs. If the relay fails (player not viewing scene, AA inactive), BmsPromptAbort routes to the prompt reprompt flow, allowing manual fallback. Unchecked (default) preserves the exact old direct-call behavior for non-teleport presets.
2026-08-15: userChoice meta outcome — New trigger outcome (OUTCOME_REGISTRY.userChoice, module/helpers/trigger-outcomes.mjs) that prompts the resolved subject's controller with a modal listing author-defined options (name + description) and, once they pick, runs that option's own nested outcome list. The nested-list execution is factored into a new shared primitive, _runOutcomeList(outcomes, context), extracted from the top-level executeOutcomes's loop body — both the trigger executor and userChoice now call the same code (sorting by order, per-outcome scope resolution via withSubject, combatMutex serialization for COMBAT_SERIAL_OUTCOMES, _withFormulaRetry). Prompt spec: { type: "userChoice", promptTitle, actorName, options: [{ id, name, description }] } — routed through _renderUserChoice (module/helpers/prompt-renderers.mjs, a Dialog.wait with one button per option), the third consumer of the promptFor/prompts.mjs relay pattern alongside summonNpc/scryArrange/playAnimation. Deliberately not nestable — a userChoice option's outcomes list cannot itself contain a userChoice outcome; enforced client-side in the config dialog's outcome-type picker and defensively re-filtered in execute(). Config UI: new configDialogFor("userChoice") entry with its own in-memory option/outcome-list editor (templates/config-dialogs/user-choice.hbs) — see .claude/contexts/config-dialog-registry.md's "userChoice Entry — Special Patterns" section for the nested-editor mechanics.
Look these up with read_document when you need more detail: