Body, Mind & Soul — API Reference
    Preparing search index...

    _CONTEXT — Body Mind & Soul AI Reference

    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:

    • Using update_document to read content — it changes data, don't use it for reading
    • Forgetting to read_document before update_document (system will reject the update)
    • Guessing document IDs — always search first

    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.maxderived, 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>.effectsBonusderived: combined bonus from feature modifications (additive) + active effect modifications (take-highest)
    system.attributes.<arctype>.<attribute>.totalderived: 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>.effectsBonusderived: combined bonus from feature modifications (additive) + active effect modifications (take-highest)
    system.vectors.<vector>.totalderived: 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.incomingDamageMultiplierderived: product of all damage.incoming multiply modifications (default 1)
    system.outgoingDamageMultiplierderived: 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.incomingStartingLayerderived: "tempHp"|"shields"|"health"|nullforces incoming damage to bypass earlier layers (deepest value from all damage.incoming.startingLayer set mods; layer name stored in mod.label)
    system.incomingRequireConfirmNegatebooleanattackers must Confirm-to-Negate when targeting this actor (damage.incoming.confirmNegate set=1)
    system.outgoingRequireConfirmNegatebooleanthis actor's own Negates must be Confirmed (damage.outgoing.confirmNegate set=1)
    system.incomingRequireConfirmCritbooleancrits against this actor must be Confirmed (damage.incoming.confirmCrit set=1)
    system.outgoingRequireConfirmCritbooleanthis 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).

    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.stackCountinteger0
    system.maxStacksinteger | null (null = uncapped)
    system.refreshOnApplyboolean (true: applyEffect resets the duration entry)
    system.stages.<n> ← TypedObjectField keyed by stringified threshold int
    .nameoverrides AE name when active
    .imgoverrides AE img when active
    .descriptionoverrides AE description when active
    .changesnative AE changes appended to parent.changes
    .modificationsModificationModel[] appended to system.modifications
    .triggersTriggerModel[] appended to system.triggers
    .drawAppendappended to system.drawAppend
    • Active stage = stage with highest threshold ≤ stackCount; sparse keys floor down (e.g. {1, 2, 4} at stackCount 3 uses the level-2 stage).
    • 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 stages with dot-path object semantics: 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[]
    .idunique ID
    .fieldfield key: "resistance.slash", "attribute.str", "vital.health.max", "damage.incoming", etc.
    .operation"addStages" | "setStage" | "addBonus" | "multiply"
    .valuenumeric value
    .labeloptional 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 light
    • auraLightConfig (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.typePolarity-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 freely

    The 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.maxStacksinteger | null (null = uncapped)
    system.refreshOnApplyboolean
    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 012)
    .downNumber0 = resolving now, 12 = furthest future
    .actions[]
    .idunique action ID
    .combatantIdlinks to combat.combatants collection
    .actiondisplay name
    .type"foreswing" | "hold" | "resolve" | "backswing"
    .actionItemIdItem document ID on the actor (standalone action OR parent item for embedded)
    .embeddedActionIdnon-null when action is an embedded Item (type "action") in a gear item's actions collection
    .swingIndexwhich swing within the action is queued
    .resolutionTimecurrent down number
    .originalResolutionTimeoriginal duration
    .repetitionsrepeats remaining (0 = one-shot)
    .playerVisiblevisible to all non-GM players when true
    .revealedToUserIdsSet of specific user IDs who can see this action (used by tracker.revealActions "caster" modification)
    .effects[]
    .id
    .combatantId
    .activeEffectIdlinks to an ActiveEffect document
    .name
    .description
    .icon
    .resolutionTime
    .originalResolutionTime
    .repetitions

    system.bms.tickNumbercurrent tick
    system.bms.nextTickIndowns until next tick
    system.bms.downsTilRounddowns 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).


    system.swings[]              ← id-based merge (id field)
    .id
    .durationnumber 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.
    ExampleVicious 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.groupKeywhich action group this belongs to
    system.resourceDatapresent when actionStyle="resource", nullable otherwise
    .costsTypedObjectField keyed by cost ID (object, not array)
    [costId] ← ResourceCostModel instance
    .idunique cost ID
    .type"rune" | "slot" | "resource"
    .runeName
    .groupKey
    .slotLevel
    .resourceKey
    .quantity
    .order
    ._sortedCostsderived: 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
    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.
    system.quantity
    system.usableboolean
    system.equipableboolean
    system.equippedboolean
    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.actionsgetter 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
    .groupMatchaction 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 iteration

    This allows a single trigger to affect both the target and the caster. For example, a mana drain with eachTarget iterator:

    • Outcome 1: changeResource mana -200, target: "target" → each enemy loses 200
    • Outcome 2: changeResource mana +200, target: "self" → owner gains 200 per target hit

    Only 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

    Glyphs are enchanted items socketed into gear. They provide passive effects and actions powered by geo synergy (environmental tagging).

    system.rank1-5 integer
    system.glyphType"enhancer" | "manipulator" | "emission" | "conjuror"
    system.geoSynergyTagsSetField(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.durabilityCostnumberdurability deducted on use
    system.isSocketableboolean (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():

    • Always-active effects: propagated whenever the glyph is active
    • isGeoSynergy effects: only propagated when actor._getActiveGeoTypes()glyph.system.geoSynergyTags ≠ ∅
    system.sockets[]         ← id-based merge (uuid field)
    .uuidthe socketed item's UUID
    .name
    .img
    .type"rune" | "glyph" | other
    .expressionsrune tracking (null for glyphs)
    .currentExpressionsrune tracking (null for glyphs)
    .durabilityglyph current durability (null for runes)
    .maxDurabilityglyph max durability (null for runes)

    Environmental tags are stored in scene/tile flags:

    scene.flags["body-mind-and-soul"].isSurveyedboolean
    scene.flags["body-mind-and-soul"].geoTag.{type} ← boolean per geo type
    tile.flags["body-mind-and-soul"].isSurveyedboolean
    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.equipmentMaintenanceActionsinteger (default 8) — shared extended-rest downtime pool
    

    During extended rest:

    • Each rune expression restore costs 1 maintenance action (also bounded by rahmara.extendedRestExpressions)
    • Each glyph repair attempt costs 1 maintenance action
    • Repair attempts roll repairAttempts × (2d4 + soul.sav) total durability to distribute
    • bms.kengaicGlyphRepaired(actor, glyphItem, amount) — fires per glyph after repair dialog submit
    • bms.rahmaricExpressionRestored(actor, runeItem|null, amount) — fires per rune restore during rest
    • bms.geoSynergyChanged — reserved for future geo synergy state transitions
    • New AE field system.appendKeywords[] — each entry { id, append, requiresKeyword }.
    • Passive: when an action fires, the acting actor's enabled non-suppressed effects are scanned and matching 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.
    • Read via 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).
    • "Next action gets keyword X" pattern: AE with the append rule + a self-disable trigger on bms.actionResolved.
    • UI lives at the bottom of the effect-sheet Modifications tab.
    • Per-token compass heading stored at flags.body-mind-and-soul.facing (number, 0–359; 0 = up/north, clockwise).
    • Ctrl+wheel over a hovered owned token rotates facing (15° snap, 45° with Shift); when no token is hovered, native Foundry rotation runs unmodified.
    • Visualised as a small triangle inside the token portrait pointing in the facing direction; absent flag = no overlay, no facing-based trigger conditions match.
    • Consumed by 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:

    • PC — dialog goes to the delegate user (representation / OWNER); offline delegate falls back to GM-local dialog. Dialog is mode-locked to the chosen rest type; the opposite tab is disabled.
    • NPC, non-GM OWNER exists — dialog goes to that delegate (player-controlled NPCs always prompt).
    • NPC, GM-only ownership — branch on 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:

    • common-scripts — Ready-to-paste run_javascript scripts for common tasks
    • workflows — Step-by-step multi-tool workflows for GM tasks
    • bms-schema — Exhaustive field path listing