Skip to content

Spec: derived parameters, computed values in action-time effects, bounds on an action's own parameter (#251, #252) - #262

Merged
mpigsley merged 4 commits into
mainfrom
spec/computed-in-effects
Nov 2, 2026
Merged

mpigsley merged 4 commits into
mainfrom
spec/computed-in-effects

Conversation

@mpigsley

@mpigsley mpigsley commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Two spec gaps from the round-2 pack adoptions (#249, #250). One commit per issue.

Closes #252: a params effect bounds a parameter the action already declares

Took the recommended option.

Design. A params effect's entry that names a parameter the run already has, with only min and/or max (no type), adds bounds to that parameter instead of declaring it.

  • The bounds see the effect's roots and params, as an added parameter's bounds do. They are checked with the parameter's own, after them, and every one must hold.
  • out-of-range names that parameter. Its min/max are the failing bound's, and the new boundBy field is the content that added it.
  • describeAction lists them under the parameter's new addedBounds ({ addedBy, min?, max? }). A bound that reads params is left out, as for min and max.
  • Two effects bounding one parameter don't conflict. An entry with a type stays a declaration (invalid-added-param, reason declared). A bound on a name that no action declares and no effect adds is invalid-added-param with the new reason undeclared, to catch typos. A bound on a parameter a given run doesn't have is ignored.

Why. This is the smallest change that puts the overspend error on spend. It needs no new op or error shape.

Closes #251: derived parameters, and computed values in action-time effects

Took the recommended option (1, derived parameters) and the computed-value read from option 2, which the issue calls independent and small.

Derived parameters. An action's parameter, or one a params effect adds, may declare formula instead of being given (integer, decimal, boolean or string; no default or optional).

  • When: as the action starts, once every given parameter has its value and before any bound is checked, in declaration order (the action's own, then the added ones).
  • What it sees: what a bound sees, plus the run's participants, plus params holding the given parameters and the derived ones declared before it.
  • No loops: among an action's own parameters, or within one params effect, reading a later derived parameter, or itself, is unknown-name at load. Another effect's derived parameter that comes later is simply absent when the formula runs.
  • Reading it: params.<name>, everywhere params is read, bounds included.
  • Never given: a value from the app, a use, an action step or a handoff is the new runtime error invalid-param (reason derived). The build also reports a use's, a creation start's, a field roll's, an action step's and an action effect's.
  • describeAction lists it with derived: true and required: false. Its label is optional.
  • A formula on an item field or a state key is a build error.

Deviation: derived parameters read participants. To allow that, startAction now checks participants before parameters, so participant errors (invalid-participants, duplicate-id, invalid-link-value) are reported ahead of parameter errors. The issue suggested participant in an each step. That doesn't fit a value computed once per run, and since #256 the Trip check in the pack reads participants.target[0] anyway. With participants, the whole Cunning Strike cost, the Trip size exception included, becomes one parameter.

Computed values in effects. An effect that acts as an action runs reads its content's computed values as self.<name>, as the content's actions already do. That covers effects on rolls, choice steps, changes steps (changes and action), listed actions (params, with its bounds and derived parameters) and incoming changes. It covers their when, value and then, and the content's choices' and inline options' effects of those kinds.

  • Order and cycles: computed values are worked out after every field. These effects run on the computed sheet and never feed a field, so they can't loop.
  • Field effects can't read them. That covers a field target, a set of fields, a formula target and their when. Computed values read every field, so a computed value feeding a field could loop. At load, reading one there is unknown-name, with a message that says why. Applied effects and auras' effects can't read them either, because their self is an applied entry. Field formulas, constraints and activation conditions (including effectsWhen) stay as they were.
  • Engine: each effect instance records the activation whose computed values its self reads. For an inline option, that is the content it was picked under. Sheet.effectRoots() merges those values at the sites that run while an action runs: roll candidates, option, changes and action effects, added params, and incoming effects (one line in changes.ts).

Tests

  • Engine unit tests: added-params.test.ts (bounds: load, runtime data, describe), derived-params.test.ts (order, bounds, invalid-param from the app and from an action step, type, later-reference load error, added via an effect, participants, describe), computed-in-effects.test.ts (roll, changes, params bounds, granted content's own computed values; field and applied effects rejected).
  • Conformance: actions/added-params-bound-own, actions/added-params-bound-own-describe, loads/added-params-bound-undeclared, actions/derived-param-run (an added derived parameter reading self.<computed>), actions/derived-param-given, actions/derived-param-describe, loads/derived-param-reads-later, loads/computed-in-field-effect. Two test features were added to test-action-params (grace, overload).
  • Validator: a use and an action step giving a derived parameter, and a formula on a state key. Engine: one params effect's derived parameter reading a later one fails to load.

App-facing changes

  • describeAction: a parameter may have derived: true (don't ask for it) and addedBounds.
  • Errors: the new invalid-param (reason derived). out-of-range may carry boundBy. invalid-added-param has a new reason, undeclared. invalid-participants now comes before parameter errors.
  • The standard i18n text, error codes, schemas (pnpm types), spec docs, guides (rolling-dice.md) and glossary are updated. No saved shape changes. Changesets: spec and engine, minor.

Pack follow-up

Not changed in this PR.

  • 5e Cunning Strike (packs/5e/srd-5.2/rules/class-rogue.yaml, packs/5e/srd-5.2/content/features/rogue/cunning-strike.yaml): declare one derived parameter, cunningStrikeCost, in the feature's first params effect. It reads cunningStrike and cunningStrike2 (behind has()), rogue.strikeEffects, the Trip check on the target's size (guarded as the pack does now: has(participants.target) && size(participants.target) > 0 && has(participants.target[0].creature.size) && ...; like the current rules effect, it assumes a single target, since a derived parameter is one value per run), and whether the pair fits in rogue.sneakAttack. The rules' sneakAttack effect (its value and when) and the two strike action effects (their when) then read params.cunningStrikeCost, replacing the four copies of the cost expression. The DC becomes a plain computed value (strikeDc: 8 + abilities.dex.mod + proficiency.bonus) that the strike effects read as self.strikeDc, so the rogue.strikeDc field, its set effect and its display entry can go. Update the README's Cunning Strike notes (packs/5e/srd-5.2/README.md, around lines 163-169 and 305) to match.
  • Draw Steel Healing Grace (packs/draw-steel/heroes/content/abilities/conduit/healing-grace.yaml): replace the max on graceRecoveries with spend: { min: 'params.graceRecoveries + (params.graceStand ? 1 : 0) + (params.graceEnd ? 1 : 0)' }, so an overspend names spend. Update the README line that says the overspend is reported on graceRecoveries (packs/draw-steel/heroes/README.md, around lines 99-101).
  • I found no other pack that restates a parameter-derived value or mirrors a computed value into a field so its effects can read it. The other computed entries that read a field (cwn-srd drones, draw-steel-heroes potency, the orc spell DCs) read genuinely shared fields, and 5e monk DCs are shared by several features.

@mpigsley
mpigsley requested a review from a team as a code owner November 2, 2026 17:19
An entry with only min and/or max bounds the action's own parameter (or
one another effect adds) instead of declaring it. Every bound must hold;
out-of-range names that parameter, with boundBy. describeAction lists
them as addedBounds. A bound on a name nothing declares is
invalid-added-param (reason undeclared).

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>
…251)

A parameter may declare a formula instead of being given: worked out as
the action starts, after the given ones and before bounds, in
declaration order, reading only earlier derived ones (a later one is
unknown-name at load). Giving one is the new error invalid-param;
describeAction marks it derived.

Effects that act as an action runs (rolls, choice and changes steps,
listed actions, incoming changes) read their content's computed values
as self.<name>, as its actions do. Field effects still can't: computed
values are worked out after every field.

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>
…y with only addedBy (#252)

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>
…one effect's later one (#251)

Also: participants' errors come before any parameter's (documented), and
the checker restores the aura flag it sets.

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>
@mpigsley
mpigsley force-pushed the spec/computed-in-effects branch from c21c684 to e181acc Compare November 2, 2026 17:40
@mpigsley
mpigsley merged commit 66e21e3 into main Nov 2, 2026
8 checks passed
@mpigsley
mpigsley deleted the spec/computed-in-effects branch November 2, 2026 17:45
mpigsley added a commit that referenced this pull request Nov 2, 2026
… (#266)

* Website: name the LicenseRef- licenses in the home packs table

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: other characters: a runnable aura sample, appliers on every call, keep the observer's file

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: showing text: added parameters, situational choices, aura names, step text, parameter errors

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: changing characters: every change type, naming reactions, held-copy costs don't clamp

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: storing characters: appliedBy, aura and pick entry; caches depend on links and appliers

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: showing a sheet: auras, observes, effectsWhen, aura immunity, re-keying rules-level option IDs

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: text and translation: keys for added parameters, computed values, auras, inline options, rules-level choice issues

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: upgrading: changeContentType, grantsToCopies, grant-not-copied, removed parameters and situational choices

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: rolling dice: derived parameters never passed, unlisted param bounds, replay needs the same release, item tags

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: building characters: picked-copy-invalid, rules-level choice issue IDs

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: implementing, quick start, index: level 6 scope, describeAction, the engine's dependencies

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Guides: packs and rulesets: LicenseRef- packs carry a permission notice

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

* Glossary: activation, added parameter, shared action; update effect operation, handoff, interface and shape change, proposed change

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>

---------

Signed-off-by: Mitchel Pigsley <mpigsley@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant