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.
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: "target" (default) or "self".
"target" — applies to whoever the iterator provides (the current target actor)"self" — overrides the target to be the trigger owner, but still fires once per iterator iterationThis 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 hitOnly certain conditions support target scoping (where the condition evaluates a single actor's property): isUnderEffect, rollCheck, compelCheck, targetDisposition. Other conditions are fixed-subject or hookArgs-based and ignore the field.
// Example outcome with target scope
{ "id": "outcomeId", "type": "changeResource", "target": "self", "config": { "resource": "mana", "operation": "add", "amount": "200" } }
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.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.
| 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) |
applyEffect |
effectId OR effectPageUuid, optionally duration (number), durationUnit ("downs"/"ticks"/"rounds"). Use effectPageUuid for bmsEffect journal pages (Effect Library). |
applyRestore |
targetKey ("health"/"shields"/"sanity"/"tempHp"), amount |
applyDamage |
type (damage type), amount, optionally layer |
queueAction |
actionName, actionType, resolutionTime, repetitions |
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). |
advanceSelf |
downs (number) |
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). |
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 }.
Look these up with read_document when you need more detail: