Effects
An effect (effect.schema.json) is how active content changes the character. It applies while the content carrying it is active (content.md) and its when condition is true.
# Gauntlets of Ogre Powereffects: - target: abilities.str.score op: atLeast value: 19 when: self.equipped && self.attuned| Property | Meaning |
|---|---|
id |
Optional, unique within the item, so apps and issues can point at it |
target |
A field, a set of fields, rolls, incoming changes, or choice steps (see below) |
op |
One of the effect operations |
value |
A formula, a grant value, a roll modifier, or an option, depending on op |
when |
Condition. self is the copy carrying the effect. |
stackKey |
See Stacking |
rounding |
Overrides the game’s rounding for this effect’s multiply |
tags |
add on rolls only: tags for the roll item the effect adds (see below) |
multiplied |
add on rolls only: false keeps multiplyDice and multiply off the items the effect adds (see below) |
duration |
Informational only: { amount, unit, concentration }. Time tracking is the app’s job; the engine never ends an effect by itself. |
then |
Incoming changes only: state changes to the effect’s own copy after the adjust is routed (see below) |
Targets
Section titled “Targets”-
A character field, given by field ID:
target: combat.ac. -
A set of fields:
target: { fields: { where: "'attr.dex' in item.tags" } }.whereis a condition over each field’s view asitem(id, the qualified ID;type;tags, its declared tags), and it may read nothing else (unknown-nameat load), soloadRulesetresolves it once, over the merged ruleset: a homebrew field with the tag joins. In a choice’seffectsit may also read the pick,pick(andslotin a slotted choice or its inline options), so it is resolved for each pick: a skilled human’swhere: "'skill-rank' in item.tags && pick.value in item.tags"is the picked skill’s rank. A field whose tags are computed (rules-and-fields.md) is matched on each sheet instead, by its tags there: the effect has an instance on it on that sheet whenwhereholds, and none otherwise (effectsdoesn’t list one). It matchesintegeranddecimalfields only, and its ops areadd,multiply,atLeastandatMost; orgrant, which matchessetfields only and grants its value into each, so a slotted choice grants each pick into its slot’s set:# orc-2e, content/features/spellcasting/repertoire.yaml: a spell known at the slot's rank- target:fields: { where: "item.id == 'orc-2e:spells.repertoire.rank' + string(slot)" }op: grantvalue: { formula: pick.id }A query that matches no field is not an error. The effect is one instance per matching field, in ruleset field order, each exactly as if the effect were written with that field as its
target: same source,when,value,stackKey,scopeandrounding. So it stacks and competes per field, each field’s breakdown lists it, and computeSheet’seffectslists each instance with its field astarget. An instance’s competing-option ID is the effect’s with@<field>added (<content id>#<effect>@<field>). Each instance adds the dependencies an effect on its field adds: an effect on every check whosevaluereads a check is a loop on that check (circular-formula).# orc-2e, content/conditions/frightened.yaml: a status penalty to every check and DC- target: { fields: { where: "'check' in item.tags || 'dc' in item.tags" } }op: addvalue: -self.valuestackKey: status-penaltyscope: applied -
Rolls as they happen:
target: { rolls: [attack, damage], from: self }. The effect applies to any roll carrying one of the tags.from: selflimits it to rolls made by the item carrying the effect (a +1 weapon);from: any(the default) covers every matching roll the character makes (Bless). Roll effects add items to the itemized roll or modify its dice, andatLeastandatMostbound its total once it is rolled (see below). Withtally: <name>, the effect adds to one of the roll’s tallies instead (see below). Withparts: [<tag>], it reaches the roll’s own parts carrying a matching tag instead of the whole roll: asetreplaces them, and astepDieorsetFacesroll modifier changes only their dice (see Replacing a roll’s part). Its formulas seeitemandselfas the content carrying the effect, the roll asrolland the action’s own item and copy asaction.itemandaction.self, so Bracers of Archery test their own copy while a bow attacks. -
Incoming changes:
target: { incoming: { fields: [hp.current], tags: [damage.fire] } }. The effect modifies the amount of anadjustchange aimed at this character before it is applied, so apps don’t apply resistances themselves. Both filters are optional; omitting both matches every incoming adjustment. After these effects, the change is routed through absorbing fields and overflow (rules-and-fields.md). How a multiplied incoming amount rounds is a game setting (settings.roundIncoming). The effect’swhen,valueandthensee the change asincoming(see below), and the receiving player’s situational choices on the change assituational. A situational choice’s own incoming-change effects apply only to a change that selects it. -
Choice steps as actions run:
target: { choices: [push-cost], from: self }. The effect adds an option to every choice step of the character’s actions that carries a matching tag;from: selflimits it to the actions of the item carrying the effect. The only op isoption. -
Changes steps as actions run:
target: { steps: [strike.hit], from: self }. The effect adds changes to everychangesstep of the character’s actions that carries a matching tag, after the step’s own, each time it runs (per participant in aneachstep);from: selflimits it to the actions of the item carrying the effect. The only op ischanges, whose value is{ changes, to? }(todefaults to the step’s). Itswhenand changes see what an added option’s do, with the action’sparticipants(andparticipantin aneachstep) and nopicks. Content items, templates and content types (for each active item of the type) add changes, as for options (a build error on the rules’ effects or a situational choice’s). See engine-actions.md.
Options added to choice steps. An option effect’s value is { id, changes?, to?, flags? }. While the effect’s when holds, a matching choice step offers it after its own options, with the ID <content id>#<id> (fitd-scoundrel:special-abilities.battleborn#special-armor), named by <content id>.options.<id>.name. When it is picked, its changes are proposed like a changes step’s, to the option’s to (default self); an option without changes is only recorded as the step’s pick, for later steps to read (Sculpt Spells’ spare). Its flags are set as the action’s flags first, before its changes; it may set only flags the action sets elsewhere, in a branch case or a written option, which is checked at load. In its when, flags and changes, item and self are the content carrying the effect (entry: self is that copy) and the action’s own item, copy and step are action.item, action.self and action.step; the action’s params, situational, steps and flags are bound too, and when sees picks. Of the options sharing a stackKey on one step, only the first offered counts, whatever the stacking policy; content never makes its option the recommended one. Content items add options, and so do content types: a type’s option effect acts for each active item of the type as if the item wrote it (item and self that item and its copy, the ID <item id>#<id>), and is named by <item id>.options.<id>.name if the item has that text, else by the type’s types.<type>.options.<id>.name, so one option serves every staff, wand or scroll. An option effect in the rules’ effects or a situational choice’s is a build error. See engine-actions.md.
# Battleborn: mark special armor instead of taking stress for a push.- target: { choices: [push-cost] } op: option when: '!armor.special' stackKey: special-armor value: id: special-armor changes: - { type: set, field: armor.special, value: true }The incoming amount, and effects that break their copy. An
incoming-change effect’s when, value and then see incoming: incoming.amount is the
change’s by as requested, before any incoming-change effect (negative for harm), so every
effect sees the same number whatever op runs first; incoming.field is the qualified ID of the
field; incoming.tags is the change’s tags. An incoming-change effect may also carry then: a
list of setState or adjustState changes with entry: self. When the effect counted (its
when held and stacking kept it), applyChanges applies them to the effect’s own copy right after
the adjust is routed, and reports each one in adjustments as { entry, key, value, source }
(engine-core.md). Their value and by are formulas seeing
item, self and incoming. They are not adjusts, so nothing routes or triggers on them. Only
an effect with a copy can carry then: on the rules’ effects, a situational choice’s, a
choice’s effects or an applied-scope effect it is a load error, then-without-copy.
Mothership armor points ignore damage under the armor’s AP; damage at or over it breaks the
armor, and only the excess gets through:
# mothership-1e, contentTypes.armor.effects- id: armor-points-hold target: { incoming: { fields: [health.current], tags: [damage] } } op: multiply value: 0 when: >- self.equipped && !self.destroyed && item.ap > 0 && !('damage.anti-armor' in incoming.tags) && -incoming.amount < item.ap- id: armor-points-break target: { incoming: { fields: [health.current], tags: [damage] } } op: add value: item.ap when: >- self.equipped && !self.destroyed && item.ap > 0 && !('damage.anti-armor' in incoming.tags) && -incoming.amount >= item.ap then: - { type: setState, entry: self, key: destroyed, value: true }A damage threshold alone (damage under it is ignored, otherwise all of it gets through) is the first effect without the second.
Dice on incoming changes. An incoming-change add may be dice; set,
multiply, atLeast and atMost stay numbers. applyChanges rolls it when it routes the adjust,
with faces from the change’s faces and then from the dice function, and writes the faces it
used into the batch it returns, so a replay never rolls
(engine-core.md). MÖRK BORG armor takes its die off
every hit, and never heals:
# mork-borg-core, contentTypes.armor.effects- id: armor-die target: { incoming: { fields: [hp.current], tags: [damage] } } op: add value: 'item.tier - self.damage >= 3 ? 1d6 : item.tier - self.damage == 2 ? 1d4 : 1d2' when: self.equipped && item.tier - self.damage > 0 && !('damage.direct' in incoming.tags)- id: armor-floor target: { incoming: { fields: [hp.current], tags: [damage] } } op: atMost value: 0 when: self.equipped && item.tier - self.damage > 0 && !('damage.direct' in incoming.tags)Tags on added roll items. An add effect on rolls may carry tags for the roll item it adds, so the item’s damage type is known: Flame Tongue adds 2d6 tagged damage.fire. Entries may be formulas ({ formula: "'damage.' + roll.item.damageType" }), each giving one tag or a list of tags. tags is only allowed on add effects that target rolls.
Items that aren’t multiplied. A multiplyDice roll modifier (5E’s critical hit) multiplies every dice item in the roll, including dice added by effects: Flame Tongue’s 2d6 is doubled. A multiply roll modifier (orc-2e’s critical hit) multiplies the value of every rolled item, dice and flat, including those effects add. An add effect on rolls with multiplied: false adds items, dice or flat, that both leave alone, so 5E’s Savage Attacks rolls its extra die once on a critical hit, and orc-2e’s deadly die is added after doubling:
- target: { rolls: [damage.weapon] } op: add value: dieOf(roll.item.damage) tags: [{ formula: "'damage.' + roll.item.damageType" }] multiplied: false when: has(flags.critical) && flags.critical && roll.item.range == 'melee'The roll item records multiplied: false. Like tags, multiplied is only allowed on add effects that target rolls, and not on a roll tally (a schema error). The default is true. A roll part may say multiplied: false too (dice-and-actions.md).
Tag matching
Section titled “Tag matching”A target tag matches the same tag and every tag below it: damage matches damage and damage.fire; damage.fire does not match damage. Roll targets match a roll if any listed tag matches any of the roll’s tags; choice targets match a choice step the same way.
Operations
Section titled “Operations”| Op | Targets | Value |
|---|---|---|
set |
field, roll parts | formula: replaces the field’s own formula or input; on a roll’s parts, replaces them (dice or a number) |
add |
field, roll, incoming | formula. On a roll or an incoming change it may be dice: Bless is op: add, value: 1d4. |
multiply |
field, incoming | formula (0.5 halves, 2 doubles); the result is rounded |
atLeast / atMost |
field, roll, incoming | formula; on a roll, an integer bounding its total once rolled (below) |
grant |
set field, fields |
a formula/literal (one member) or { query } (every match) |
rollModifier |
roll | a roll modifier: advantage, disadvantage, noAdvantage, noDisadvantage, reroll, … |
option |
choice | { id, changes?, to? }: an option for a tagged choice step (above) |
changes |
step | { changes, to? }: changes for a tagged changes step (above) |
There is no separate addDice operation: adding dice to a roll is add with a dice value. Roll modifiers are described in dice-and-actions.md.
Fixed order
Section titled “Fixed order”For each field, the engine computes:
- the field’s own value (formula, input or default) and its grants;
- set: if several
seteffects are active, they compete (see below); - add: the sum of all counted adds;
- multiply: each counted multiplier in turn, rounding after each;
- atLeast / atMost: the highest
atLeastraises the value, then the lowestatMostlowers it. PROVISIONAL: this ordering resolves conflicts between the two.
Multiplying after adding matches how games apply resistance (to the total). Incoming changes follow the same order on the change’s amount.
Effects declared by a content type come before the item’s own effects in instance order and in the breakdown; stacking and competition treat them like any other effect (content.md).
Rounding
Section titled “Rounding”multiply results on integer values round right after each multiplication, by the effect’s rounding, else the field’s, else the game’s settings.rounding (default floor). This holds for field values and for incoming changes. Any other decimal an integer field ends up with (from a formula, or an add with a decimal value) rounds once, at the end, by the field’s rounding, else the game’s. Every implementation therefore gets the same answer.
Every decimal step (each add and multiply on a decimal value, a field’s or a change’s) is first kept to six places (formulas.md), and rounding to a whole number applies to that six-place value: a decimal field adding 0.2 to 0.1 holds 0.3, and an integer field of 50 multiplied by 0.14 and rounding up is 7, not 8 (as doubles, 50 × 0.14 is 7.000000000000001).
Bounds on a roll’s total
Section titled “Bounds on a roll’s total”An effect on rolls without tally may use atLeast or atMost, with an integer
value in the same context as other roll effects. Once the roll is rolled, the highest counted
atLeast raises its total, then the lowest counted atMost lowers it, as for fields and tallies;
stacking applies per op. steps.<id>.total and the step’s band read the bounded total; the roll’s
items and faces are unchanged, and the step’s result and log entry list in bounds each bound
that changed the total, { source, op, value }
(engine-actions.md). 5E’s Indomitable Might:
# 5e-srd-5.1, content/features/barbarian/indomitable-might.yamleffects: - target: { rolls: [check.str] } op: atLeast value: abilities.str.scoreReplacing a roll’s part
Section titled “Replacing a roll’s part”A roll’s parts are the action’s (steps.<id>.parts), and a part may carry tags. A set
effect on rolls names the parts it replaces by tag, target: { rolls: [<roll tags>], parts: [<part tags>] }, so content changes how a shared action works out a value
rather than adding an item that offsets it:
# orc-2e, the ranger's Flurry edge: the multiple attack penalty against the prey.- target: { rolls: [attack], parts: [strike.map] } op: set value: -(flags.map - min(2, flags.prior) * 2) when: '…the target is the prey…'# The investigator's attack stratagem: Intelligence instead of Strength or Dexterity.- target: { rolls: [stratagem], parts: [strike.attribute] } op: set value: attacks.int.mod- What it reaches: the items built from each part whose tags match (a tag matches itself
and every tag below it), after the parts are built and before any other roll effect; never
an item an effect adds. A part whose
whenfailed has no items, so nothing replaces it. - Which one: the counted
seteffects on a part stack by key, as other roll effects do; of those left, the roll takes the onesettings.competing.recommendnames (highest, the default,lowest,firstorlast, compared by value). Unlike a field’s, it isn’t reported as competing. - The new items: the value (a formula, as an
add’s: dice or a number) becomes the part’s items, with the part’s tags. Their source is the effect’s, unless the value is a single field reference: then the field is the source and the item reads it as afieldpart does, carrying the field’s keyed effects into the roll’s stacking. So stupefied reaches a Strike with Intelligence, and clumsy no longer does. A part withmultiplied: falsekeeps it. - The record: the first new item lists the part’s items as built before in
replaces(unrolled roll items), so a breakdown shows “Intelligence +4 (instead of Dexterity +3)”.
set on rolls needs parts (a schema error otherwise), and parts takes only set, stepDie
and setFaces, never a tally.
Roll tallies
Section titled “Roll tallies”A tally is a named integer counter on a roll that isn’t part of its total: FitD’s effect
level and result level, a PbtA “+1 forward” on the outcome. The rules declare the tallies a pack
uses (rollTallies: [effect, resultLevel], rules-and-fields.md),
and an effect targets one with target: { rolls: [action-roll], tally: effect }.
- Ops:
add,atLeastandatMost, with an integervaluein the same context as other roll effects.fromand tag matching work as for other roll targets, and stacking applies per tally. - Content, situational choices and rules-level effects can all target tallies.
- A tally name no visible pack declares is the load error
unknown-tally. - The action reads the sums as
steps.<id>.tallies.<name>, and result bands asroll.tallies.<name>. What a tally means is up to the action’s formulas.
An effect’s scope says who it applies to:
holder(the default): a character who has the content active, through features or inventory, a pick, or a grant.applied: a character the content is applied to, through the character’sappliedlist (a spell cast on them, a condition), or a grant withscope: appliedof content applied to them (unconscious including blinded; content.md).
Content in applied runs only its applied effects, and held content runs only its holder effects. So a caster who knows Bless doesn’t bless themselves, while the targets it’s cast on get +1d4. A spell that also changes its caster uses effects of both scopes. There is no content-type-level default for scope.
Competing options
Section titled “Competing options”When more than one set effect is active on the same field (Mage Armor and Unarmored Defense both setting AC), the engine does not decide. It reports the options and the rules’ recommended pick in computeSheet’s competing output: { field, op, stackKey?, options: [{ id, value, source }], recommended, applied }. The recommendation follows the field’s recommend or settings.competing.recommend (highest, lowest, first, last).
Each entry is one competing group: the set effects on a field (op: set), or a playerChooses stacking group (its op and stackKey). A field can have several groups, such as two AC formulas and a choice of morale bonus, and each is picked on its own.
The app applies the recommendation automatically or asks the player, and passes the picks back through computeSheet’s competing input: a list of option IDs, such as ['5e-srd-5.1:features.unarmored-defense#0']. Each group applies the first of its options that is listed, in instance order; a group with none listed uses its recommended option.
Picks reach only computeSheet. The other calls (listOptions, applyChanges, actions) use each group’s recommended option.
Stacking
Section titled “Stacking”An effect with a stackKey is grouped with other effects that have the same key, target and operation. Each group is combined by the game’s policy for that key (settings.stacking.keys, else settings.stacking.default):
| Policy | Result |
|---|---|
addAll |
everything counts (the default) |
highestOnly |
only the highest value counts |
lowestOnly |
only the lowest value counts |
playerChooses |
only one counts; reported as competing, like set |
The key source is reserved: it means “the content item this effect comes from”. It is how 5E says two Blesses don’t stack (keys: { source: highestOnly }). Any other key is a literal: orc-2e uses item, status and circumstance. Effects without a key always count.
Effects that don’t count because of stacking still appear in computeSheet’s effects with suppressed: true.
An effect on a set of fields stacks on each field it reaches, with that field’s other effects, as if written on it. A roll that reads a field carries the field’s keyed add effects into its own stacking, so a roll-time bonus competes with the field’s bonus of the same key instead of adding on top of it (engine-actions.md).
Showing the work
Section titled “Showing the work”For each field, computeSheet returns the base value and a breakdown: each effect that changed it, in application order, with its source (“AC 17 = 15 Mage Armor + 2 shield”). Roll effects show up as roll items instead (dice-and-actions.md).
A field whose value depends on itself through formulas or effects is a loop. Loops are rejected at load time where they can be detected (circular-formula). Some loops appear only when a specific character combines specific items. In that case computeSheet marks the looping fields with an error instead of a value, and everything else still calculates. An engine MUST NOT hang on a loop.