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

    _CONTEXT — Body Mind & Soul AI Reference

    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:

    • 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.

    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 world
    • getDefaultCurrencyId() — returns the world setting body-mind-and-soul.bmsCurrencyId (default: "gold"); used as default/preselect in pickers
    • getActorWalletCurrencyIds(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 balances
    • getActorBalance(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 value
    • setActorBalance(actor, currencyId, value, opts) — sets balance directly
    • transferCurrency(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>.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. 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 hook

    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

    Which 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

    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 }.


    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: "'s action is imminent!" plus a unique audio cue. This provides player feedback when an ability expedites a queued swing forward, distinct from the existing "is resolving" 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:

    • 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