diff --git a/apps/website/glossary.md b/apps/website/glossary.md index c2eb63f70..6aee97e38 100644 --- a/apps/website/glossary.md +++ b/apps/website/glossary.md @@ -5,7 +5,9 @@ One-line definitions of OCF terms, consistent with the [specification](../../pac - **absorbedBy:** fields that drain first when a numeric input field takes an incoming decrease (temporary hit points before hit points). - **Action:** something a character can do that needs dice or decisions, defined as a cascade of steps (roll, choice, allocate, branch, group, changes, action, outcome). - **Action log:** the record of every die face and choice in an action, and who supplied it; replaying it gives the same result. +- **Activation:** one piece of active content as computeSheet lists it in `activations`: what it is, the copy or pick it belongs to, and how it was reached. - **Active content:** content whose effects, grants and choices currently apply: owned features, inventory, picked options, granted content, applied effects and templates on active copies. +- **Added parameter:** a parameter a `params` effect adds to the actions it targets (an ability's damage type chosen on each use), named by `addedBy` in describeAction; an entry with only `min` and `max` on a parameter the action already has adds bounds instead (`addedBounds`; an `out-of-range` for one names its content in `boundBy`). - **Aggregate:** a formula macro that counts, sums or takes the highest or lowest of something over the character's copies (`countCopies`), its saved picks or active grants (`countPicks`, `countGrants`), the ruleset's fields (`countFields`) or a list (`countList`), with a root (`copy`, `pick`, `grant`, `f`, `x`) bound to each element, and over a list `i`, its position. - **Allocate step:** an action step that spends a budget across fields (FitD's creation dots, 5E point buy) or hands out a list of values, one per field (the standard array); a checkpoint answered with amounts. - **App:** the program using the engine; it owns the experience (dice presentation, time, choices, storage, display). @@ -42,7 +44,7 @@ One-line definitions of OCF terms, consistent with the [specification](../../pac - **Display hint:** optional presentation data (groups, priority, format, unit, `showWhen`, hidden) that never affects calculations. A display group holds fields, actions or content. - **Effect:** a change active content makes to a field, a roll or an incoming change, with an operation, value and condition. - **effectsWhen:** a condition on a content type or item that every effect of its items needs (5E attunement: `'!item.requiresAttunement || self.attuned'`), ANDed into each holder effect's `when` and read as it is (not applied effects); an item's replaces its type's. -- **Effect operation:** one of the fixed set: add, set, multiply, atLeast, atMost, grant, rollModifier, tag (tags added to a roll's parts), and, for tagged action steps, option (an extra choice option) and changes (extra proposed changes, or flags set), and, for tagged actions, params (extra parameters). +- **Effect operation:** one of the fixed set: add, set, multiply, atLeast, atMost, grant, rollModifier, tag (tags added to a roll's parts), and, for tagged action steps, option (an extra choice option), changes (extra proposed changes, or flags set) and action (a nested run, such as the target's saving throw on a hit), and, for tagged actions, params (extra parameters). - **Effect scope:** whether an effect applies to a character who has the content (`holder`) or one it is applied to (`applied`). - **Engine:** an implementation of the spec's calls; the TypeScript one is the reference engine, others are ports. - **Entry (copy):** the character's copy of a content item: base item, templates, equipped flag, quantity, state, picks, and the state of content it grants. @@ -61,14 +63,14 @@ One-line definitions of OCF terms, consistent with the [specification](../../pac - **Grant `available`:** a condition on the computed sheet that a grant's content's actions, follow-ups and auras wait on (a treasure's ability, usable only with a matching kit); the content stays active, marked `unavailable` in computeSheet's `activations`. - **Grant-only content:** content held only through what grants or picks it (a class feature, a subclass), marked `grantOnly` on its content type or item; `addItem` of it is `not-addable`, and apps leave it out of add-item catalogs. - **Group step:** an action step that runs its steps again and again, one pass at a time, while its `repeat.while` holds and up to `repeat.max` passes (a reroll, see the result, reroll again). Each pass's steps are at `[]/`. -- **Handoff:** an action step (`as: participant`) run on a participant's own character by the app, which answers the paused action with that run's outcome. +- **Handoff:** an action step, or an `action` effect content adds to a step, run with `as: participant` on a participant's own character by the app, which answers the paused action with that run's outcome. - **Homebrew:** a character's own content and rules additions, saved in its file and loaded on top of its pinned packs as the pack `homebrew` (`homebrew:`); it may add, never override, and upgrades rewrite it with the saved state. - **I18n key:** the permanent key under which player-facing text is looked up in translation files. - **Immunity:** applied content a character can't be affected by, declared by content or the rules (`immuneTo`); a matching `applied` entry is blocked and reported as the rules issue `immune`. A query can read the candidate entry as `entry`, and any activation condition the raw `character.applied` list. - **Incoming change:** a change about to be applied to a character (damage), which effects can modify first (resistance). - **Inline option:** an option defined inside a choice rather than as separate content, with a name and an optional description in the pack's text. - **Input field:** a field whose value the player supplies and the character file saves. -- **Interface change:** a difference between two pack releases that can break a character's homebrew (a removed derived field, content type, item field, enum value, tag or rules action); it needs a breaking bump but no upgrade script. +- **Interface change:** a difference between two pack releases that can break a character's homebrew (a removed derived field, content type, item field, enum value, tag, action or action parameter, a parameter retyped or made required or derived, a removed or retyped situational choice, a choice gaining or losing `addAs`); it needs a breaking bump but no upgrade script. - **Item field:** a typed property declared by a content type and set on each item (damage, weight). - **List item field:** an item field of type `list`: members in the order written (a spell's cast options, a class's ability priority), duplicates allowed unless `unique`; a `set` is sorted instead. - **Limits:** bounds on a field's final value, after effects; a value outside them is the rules issue `out-of-limits`. @@ -92,7 +94,7 @@ One-line definitions of OCF terms, consistent with the [specification](../../pac - **Prerequisite:** a condition for having content; a false one is a rules issue. - **Preset choice:** a choice of granted content that the grant answers (`choices`); never pending, and `choose` and `unchoose` refuse it (`preset-choice`). - **Priority:** a display hint ordering groups and fields; whole numbers, 0 highest. -- **Proposed change:** a change an action returns for the app to apply, to the acting character, a target or a linked character. +- **Proposed change:** a change an action returns for the app to apply, to the acting character, a role's participant, a linked character, or whoever holds what the actor applied (`to: applied`). - **Proposed sheet:** the character with the changes an action has proposed so far `to: self` applied, which a step with `sheet: proposed` reads; built afresh in each call, never stored. - **Query:** a link to a set of things written as a rule: a content type plus a `where` condition. - **Result band:** a named range of roll results (6− / 7–9 / 10+), declared in a game's rules data. A band set may work out a `value` once, which its bands read (`roll.value`), and an effective `total` the roll reports. @@ -105,10 +107,11 @@ One-line definitions of OCF terms, consistent with the [specification](../../pac - **Roll effect `then`:** changes a roll effect proposes after a roll step it changed (or, with `when: applied`, was in play on), once per action run or, with `once: roll`, per roll: ending a condition, spending a use, or, `to` another character, ending the target's entry. - **Safety limit:** a spec-defined cap (exploding dice, nesting depth, action steps) protecting engines from untrusted packs. - **Sender (`from`):** the character whose action or follow-up proposed an adjust for someone else, carried on the adjust; an `on` follow-up reads it as `change.from` (the creature that damaged you). -- **Shape change:** a difference between two pack releases that changes what saved state means (removed content, field, state key, choice or option; a new required choice); it needs a breaking bump and an upgrade script that covers it. +- **Shape change:** a difference between two pack releases that changes what saved state means (removed content, field, state key, choice or option; content that changes type, stops being granted or becomes standard; a new required choice); it needs a breaking bump and an upgrade script that covers it. - **Shape error:** input that doesn't match the spec's JSON Schema, rejected before running. +- **Shared action:** a rules action that items list with `use` (optionally fixing `params`), so one `cast-spell` runs every spell; content joins in through its tagged steps (`option`, `changes` and `action` effects). - **Shared sheet:** a character file holding a pool several characters spend from (hero tokens, the Director's Malice, a crew), linked from each through one slot; the app applies each one's spend in turn. -- **Situational choice:** a roll-time choice declared in game data (advantage, cover, push yourself), or a choice the receiving player makes on one incoming change (Uncanny Dodge) or one batch of them (Deflect Attacks), which may carry its own cost (`changes`). +- **Situational choice:** a roll-time choice declared in game data (advantage, cover, push yourself), or a choice the receiving player makes on one incoming change (Uncanny Dodge) or one batch of them (Deflect Attacks), which may carry its own cost (`changes`: a field, or a held copy's state such as a shield's damage). - **Source:** where a value, roll item or issue came from (field, item, entry, template, effect, situational, rules, step, app). - **Spec upgrade:** data shipped with the spec that moves a character file across a spec version that changed the file's shape; `upgrade` runs the ones between the character's `specVersion` and the engine's, then writes the engine's version. - **Spec version:** the version of the OCF formats and behavior, which is the `@opencharacter/spec` package version; packs declare the minimum they need, and an engine lists the major.minor versions it implements (`IMPLEMENTS_SPEC`). diff --git a/apps/website/guides/README.md b/apps/website/guides/README.md index bd6f9803b..e1b818014 100644 --- a/apps/website/guides/README.md +++ b/apps/website/guides/README.md @@ -125,7 +125,8 @@ Read them in order the first time. - **[Rolling dice](rolling-dice.md):** actions, checkpoint policies, your own dice, paused actions and logs. - **[Other characters](other-characters.md):** attacks on a target, monsters and NPCs, effects - cast by one character on another, linked sheets such as a FitD crew. + cast on others, auras, turns, shared pools, linked sheets such as a FitD crew, and reacting to + other characters' events. ## Two kinds of app diff --git a/apps/website/guides/building-characters.md b/apps/website/guides/building-characters.md index bcad8670b..75e0e4deb 100644 --- a/apps/website/guides/building-characters.md +++ b/apps/website/guides/building-characters.md @@ -143,6 +143,9 @@ After every change, `computeSheet` lists the choices that apply now and still ne special ability of a table's own playbook (the Bruiser of `fitd-ash.yaml`), under the playbook choice. - **`min`**, **`max`** and **`picked`** say how many picks it needs and has. +- A rules-level choice's path stays bare (`{ choice: ['vice'] }`), but its issues name it with + its pack, `fitd-scoundrel:rules/vice` + ([Text and translation](text-and-translation.md#from-an-id-to-a-key)). - A choice ID with a slash (`5e-srd-5.1:features.wizard.spellcasting/cantrips`) belongs to content granted to the entry rather than to the entry's own item. @@ -188,7 +191,10 @@ that applies, in the same order as `pending`, answered or not: - A pick of a choice that adds its pick as a copy ("a trinket of your choice") brings that copy into `inventory` or `features`: `choose` adds it and saves its instance ID as the pick's `entry`, and `unchoose` removes it. `removeItem` of that copy fails with `picked-copy`, so - offer to change the pick instead. + offer to change the pick instead. A saved link that doesn't hold (no `entry`, an entry that is + gone, of another item or named by an earlier pick) is the rules issue `picked-copy-invalid` + (`data.problem`: `missing`, `other-item`, `shared`, `not-held-as-copy`); the copy is then an + ordinary entry. Offer to pick again. Show each entry as a step. To change an answer, `unchoose` and `choose` in one batch; the next sheet lists the new answer's own choices where the old answer's were (after the choices of the diff --git a/apps/website/guides/changing-characters.md b/apps/website/guides/changing-characters.md index d81f9f49e..e0c39ae37 100644 --- a/apps/website/guides/changing-characters.md +++ b/apps/website/guides/changing-characters.md @@ -26,27 +26,36 @@ There's a small, fixed set of generic change types ([change.schema.json](../../../packages/spec/schemas/change.schema.json)). What's inside a change (does this field exist, is 25 in range) is checked against the ruleset. -| Type | Does | Example | -| ---------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| `set` | Sets an input field | `{ type: 'set', field: '5e-srd-5.1:abilities.str.base', value: 16 }` | -| `adjust` | Adds to a numeric input field, through resistances | `{ type: 'adjust', field: '5e-srd-5.1:hp.current', by: -10, tags: ['damage.fire'] }` | -| `addItem` | Adds a copy of content to `inventory` or `features` | `{ type: 'addItem', entry: { id: 'inv-6', item: '5e-srd-5.1:weapons.dagger' } }` | -| `removeItem` | Removes a copy, with its picks (not a standard copy) | `{ type: 'removeItem', entry: 'inv-6' }` | -| `equip` | Equips or unequips a copy | `{ type: 'equip', entry: 'inv-4', equipped: true }` | -| `setState` | Sets a per-copy state value | `{ type: 'setState', entry: 'inv-2', key: 'ignited', value: true }` | -| `adjustState` | Adds to a numeric per-copy state value | `{ type: 'adjustState', entry: 'fighter', key: 'level', by: 1 }` | -| `setField` | Sets a copy's own value of a `perCopy` item field (`null` removes it) | `{ type: 'setField', entry: 'stunt-1', key: 'limit', value: 'scene' }` | -| `setQuantity` | Sets a copy's quantity (not below 0; a copy at 0 stays) | `{ type: 'setQuantity', entry: 'inv-3', value: 20 }` | -| `adjustQuantity` | Adds to a copy's quantity (spending an arrow) | `{ type: 'adjustQuantity', entry: 'inv-3', by: -1 }` | -| `applyTemplate` | Applies a template to a copy | `{ type: 'applyTemplate', entry: 'inv-6', template: '5e-srd-5.1:magic-items.weapon-plus-one' }` | -| `removeTemplate` | Removes a template from a copy | `{ type: 'removeTemplate', entry: 'inv-6', template: '…' }` | -| `applyEffect` | Starts a temporary effect from content not owned | `{ type: 'applyEffect', id: 'bless-1', source: '5e-srd-5.1:spells.bless' }` | -| `toggleEffect` | Turns a temporary effect on or off | `{ type: 'toggleEffect', id: 'bless-1', active: false }` | -| `removeEffect` | Removes a temporary effect | `{ type: 'removeEffect', id: 'bless-1' }` | -| `choose` | Picks an option for a choice | `{ type: 'choose', path: { entry: 'fighter', choice: ['fighting-style'] }, option: '…' }` | -| `unchoose` | Un-picks an option, and everything nested under it | `{ type: 'unchoose', path: { entry: 'fighter', choice: ['fighting-style'] }, option: '…' }` | -| `setExtension` | Replaces your app's own data | `{ type: 'setExtension', namespace: 'com.example.vtt', value: { … } }` | -| `setHomebrew` | Replaces (or, with `null`, removes) the character's own homebrew | `{ type: 'setHomebrew', homebrew: { content: [ … ] } }` | +| Type | Does | Example | +| ----------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| `set` | Sets an input field | `{ type: 'set', field: '5e-srd-5.1:abilities.str.base', value: 16 }` | +| `adjust` | Adds to a numeric input field, through resistances | `{ type: 'adjust', field: '5e-srd-5.1:hp.current', by: -10, tags: ['damage.fire'] }` | +| `addItem` | Adds a copy of content to `inventory` or `features` | `{ type: 'addItem', entry: { id: 'inv-6', item: '5e-srd-5.1:weapons.dagger' } }` | +| `removeItem` | Removes a copy, with its picks (not a standard copy) | `{ type: 'removeItem', entry: 'inv-6' }` | +| `equip` | Equips or unequips a copy | `{ type: 'equip', entry: 'inv-4', equipped: true }` | +| `setState` | Sets a per-copy state value | `{ type: 'setState', entry: 'inv-2', key: 'ignited', value: true }` | +| `adjustState` | Adds to a numeric per-copy state value | `{ type: 'adjustState', entry: 'fighter', key: 'level', by: 1 }` | +| `setField` | Sets a copy's own value of a `perCopy` item field (`null` removes it) | `{ type: 'setField', entry: 'stunt-1', key: 'limit', value: 'scene' }` | +| `setQuantity` | Sets a copy's quantity (not below 0; a copy at 0 stays) | `{ type: 'setQuantity', entry: 'inv-3', value: 20 }` | +| `adjustQuantity` | Adds to a copy's quantity (spending an arrow) | `{ type: 'adjustQuantity', entry: 'inv-3', by: -1 }` | +| `applyTemplate` | Applies a template to a copy | `{ type: 'applyTemplate', entry: 'inv-6', template: '5e-srd-5.1:magic-items.weapon-plus-one' }` | +| `removeTemplate` | Removes a template from a copy | `{ type: 'removeTemplate', entry: 'inv-6', template: '…' }` | +| `applyEffect` | Starts a temporary effect from content not owned (`appliedBy`: who applied it; `aura`: an aura's entry) | `{ type: 'applyEffect', id: 'bless-1', source: '5e-srd-5.1:spells.bless', appliedBy: 'cleric' }` | +| `toggleEffect` | Turns a temporary effect on or off | `{ type: 'toggleEffect', id: 'bless-1', active: false }` | +| `removeEffect` | Removes a temporary effect | `{ type: 'removeEffect', id: 'bless-1' }` | +| `choose` | Picks an option for a choice | `{ type: 'choose', path: { entry: 'fighter', choice: ['fighting-style'] }, option: '…' }` | +| `unchoose` | Un-picks an option, and everything nested under it | `{ type: 'unchoose', path: { entry: 'fighter', choice: ['fighting-style'] }, option: '…' }` | +| `setExtension` | Replaces your app's own data | `{ type: 'setExtension', namespace: 'com.example.vtt', value: { … } }` | +| `setHomebrew` | Replaces (or, with `null`, removes) the character's own homebrew | `{ type: 'setHomebrew', homebrew: { content: [ … ] } }` | +| `finishCreation` | Ends creation (clears `creating`) | `{ type: 'finishCreation' }` | +| `offerFollowUp` | Stores an offer of a follow-up the character has (actions propose it) | `{ type: 'offerFollowUp', followUp: 'draw-steel-heroes:features.conduit.piety-in-combat#domain-effect' }` | +| `clearFollowUp` | Clears an offer acted on | `{ type: 'clearFollowUp', id: 'concentration-1' }` | +| `dismissFollowUp` | Deletes an offer unrun | `{ type: 'dismissFollowUp', id: 'concentration-1' }` | +| `observe` | Another character's batch, for this one's follow-ups to react to | the `observe` another `applyChanges` returned | + +`finishCreation` is covered in [Building characters](building-characters.md#three-ways-to-create-a-character), +the follow-up changes in [What the rules call for next](#what-the-rules-call-for-next), and +`observe` in [Reacting to other characters](other-characters.md#reacting-to-other-characters). Every field and content ID is **qualified** with its pack. Entries (copies) are named by the instance `id` you gave them. @@ -179,7 +188,8 @@ Some choices are made by the character being hit, for that hit only: 5E's Uncann one attack's damage. The pack declares them as situational choices with `incoming`, and computeSheet lists the ones offered in `situational`. Before applying a change, show the receiving player the offered choices whose `incoming` matches it (its field and tags), and put -the ones they pick on the change: +the ones they pick on the change. Name each with `text.situational(id)`, and an enum choice's +value with `text.situationalValue(id, value)` ([Showing text](showing-text.md#situational-choices)): ```ts applyChanges(ruleset, rogue, [ @@ -207,7 +217,9 @@ Two kinds of choice ask a little more of your app: and a follow-up it offers is in `followUps`. A cost may change a held copy's state too (Shield Block's shield takes the damage its hardness didn't stop): its `{ copies }` entry names the choice as `source`. Don't apply the cost yourself. The choice's - `available` says whether it can be paid; a cost past the field's minimum clamps. + `available` says whether it can be paid; a cost past the field's minimum clamps. A held copy's + state doesn't clamp: past its bound the whole batch fails (`out-of-range`), so packs clamp it + themselves. - **A batch-scoped choice** (`scope: batch`: 5E 5.2's Deflect Attacks, which reduces the attack's total damage once) goes on the attack's first change only, and reaches the batch's later changes its `incoming` matches. Apply one attack's changes as one batch, and log the batch as diff --git a/apps/website/guides/implementing.md b/apps/website/guides/implementing.md index 45b0e35d8..7331c1cdc 100644 --- a/apps/website/guides/implementing.md +++ b/apps/website/guides/implementing.md @@ -9,15 +9,15 @@ OCF covers everything from a static stat block to a virtual tabletop's attack re app needs all of it on day one. Each level below builds on the one before, and each is a complete, useful app on its own. Most apps get real value at level 2 or 3. -| Level | Your app | Engine calls | -| -------------------------- | ---------------------------------------------------------------------------- | ------------------------------- | -| **1. Read-only sheet** | Loads packs, computes a character, shows every value and where it came from | `loadRuleset`, `computeSheet` | -| **2. Editable form** | Turns the pack's input fields into a form and sends edits back as changes | `applyChanges` | -| **3. Content and choices** | Adds items and features, levels up, asks pending choices, creates characters | `listOptions` | -| **4. Live rules** | Turns conditions and spells on and off, shows competing options and issues | the same calls, more output | -| **5. Rolls and actions** | Runs attacks and checks, pausing for your own dice and animation | `startAction`, `continueAction` | -| **6. Many characters** | Targets, helpers, monsters, linked sheets such as a FitD crew | actions with participants | -| **7. Lifecycle** | Pins pack versions, adds homebrew, upgrades saved characters | `upgrade` | +| Level | Your app | Engine calls | +| -------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| **1. Read-only sheet** | Loads packs, computes a character, shows every value and where it came from | `loadRuleset`, `computeSheet` | +| **2. Editable form** | Turns the pack's input fields into a form and sends edits back as changes | `applyChanges` | +| **3. Content and choices** | Adds items and features, levels up, asks pending choices, creates characters | `listOptions` | +| **4. Live rules** | Turns conditions and spells on and off, shows competing options and issues | the same calls, more output | +| **5. Rolls and actions** | Runs attacks and checks, pausing for your own dice and animation | `describeAction`, `startAction`, `continueAction` | +| **6. Many characters** | Targets, helpers, monsters, linked and shared sheets, auras, turn changes, events between characters | actions with participants | +| **7. Lifecycle** | Pins pack versions, adds homebrew, upgrades saved characters | `upgrade` | ### 1. Read-only sheet @@ -71,7 +71,9 @@ Read: [Rolling dice](rolling-dice.md), [Showing text](showing-text.md). An attack has a target, a group action has helpers, and a FitD crew is a sheet linked to its scoundrels. Other characters take part as participants, and the changes an action proposes say -which character they're for. +which character they're for. Your app routes what the engine can't see: who stands in an aura +(`appliers`), whose turn it is (`turn-boundary`), and one character's batch to the characters +that react to it (`observe`). Read: [Other characters](other-characters.md). @@ -86,7 +88,7 @@ Read: [Packs and rulesets](packs-and-rulesets.md), [Upgrading characters](upgrad ## Where the engine runs -The engine is a library with no runtime dependencies, so it runs wherever your code does. Where you +The engine is a library that doesn't depend on Node, so it runs wherever your code does. Where you put it decides who is trusted and how characters sync. | Shape | The engine runs | Good for | Watch for | diff --git a/apps/website/guides/other-characters.md b/apps/website/guides/other-characters.md index d9951fd5f..96fd26800 100644 --- a/apps/website/guides/other-characters.md +++ b/apps/website/guides/other-characters.md @@ -91,7 +91,9 @@ while ('status' in cast && cast.status === 'paused' && cast.needs.kind === 'hand - **The log records each answer** (`kind: 'handoff'`), so a paused action replays exactly. - **Content can add handoffs to an action**: a feature's `action` effect can hand off its own save on a hit. Its `needs.step` is the hit step's path, then the content and the effect's ID - (`deal/#stun[0]`); answer it like any other. + (`deal/#stun[0]`); answer it like any other. Without a participant the added run + happens on the actor: its rolls and choices pause like any nested action's, at that path, so a + policy rule on `step` can target them. - **A handoff may name other characters for the run**: `needs.run.participants` lists IDs by role, the running character's own `id` included. Pass their views as the run's `participants`, as you would to any action. A Fate attack names the attacker this way, so the @@ -219,37 +221,65 @@ A null's field, a paladin's aura, a banner: an aura puts effects on whoever is i creatures stand is your app's business, so the pack lists the auras and you place their entries. The holder's `computeSheet` lists what it projects now in `auras`; apply an entry of each to every creature inside that `affects` names, and remove it when they leave or the aura drops off the -list: +list. The creatures inside need the aura's pack in their own ruleset, so an ally statted with +the same packs: ```ts -const nullSheet = computeSheet(rulesetFor(nullHero), nullHero); -// nullSheet.auras: [{ source: 'draw-steel-heroes:abilities.null.null-field', aura: 'field', -// entry: 'null-field', size: 1, affects: 'enemies' }] -for (const a of nullSheet.auras) - for (const enemy of enemiesWithin(nullHero, a.size)) - applyChanges(rulesetFor(enemy), enemy, [ - { type: 'applyEffect', source: a.source, aura: a.aura, appliedBy: nullHero.id }, +const paladinSheet = computeSheet(rulesetFor(paladin), paladin); +// paladinSheet.auras: [{ source: '5e-srd-5.1:features.paladin.aura-of-protection', +// aura: 'protection', entry: 'paladin', size: 10, affects: 'allies' }] +// `entry` is the copy that holds it (here, the paladin class). +for (const a of paladinSheet.auras) + for (const ally of alliesWithin(paladin, a.size)) { + const placed = applyChanges(rulesetFor(ally), ally, [ + { + type: 'applyEffect', + id: `${a.aura}-${paladin.id}`, + source: a.source, + aura: a.aura, + appliedBy: paladin.id, + }, ]); + if (placed.ok) save(ally.id, placed.character); // your app's storage + } +// When the ally leaves: { type: 'removeEffect', id: `protection-${paladin.id}` }. ``` -The aura's effects read the holder's live state (the extra effect the null bought this round), -so pass the holder's view as `appliers` whenever you compute or act for a creature inside: +Give each entry an ID of your own, as above, so you can remove it later. Name the entry on the +creature's sheet with the key `.auras..name` in the source's pack +([Showing text](showing-text.md#effects-and-auras)). + +The aura's effects read the holder's live values (the paladin's Charisma modifier now), so pass +the holder's view as `appliers` whenever you compute or act for a creature inside: ```ts -const appliers = [view(nullHero, nullSheet)]; -const goblinSheet = computeSheet(rulesetFor(goblin), goblin, [], {}, appliers); -startAction(rulesetFor(goblin), goblin, { action: { id: 'monster-attack' }, appliers }); +const appliers = [view(paladin, paladinSheet)]; +const allySheet = computeSheet(rulesetFor(tamsin), tamsin, [], {}, appliers); +// allySheet.values['5e-srd-5.1:saves.wis'].value: 4 (2 without the view) +startAction(rulesetFor(tamsin), tamsin, { + action: { id: '5e-srd-5.1:saving-throw' }, + params: { ability: 'wis', dc: 15 }, + appliers, +}); ``` -Without the view, the effects read the defaults the aura declares. Nothing in the goblin's file -changes when the null's does: recompute it with the new view. +Pass the same `appliers` to every call for that creature, since an effect that reads `applier` +can run in any of them: + +- `applyChanges(ruleset, tamsin, changes, false, {}, undefined, appliers)`: the 7th argument, + after `strict`, `links` and `dice`. +- `listOptions(ruleset, tamsin, path, {}, appliers)` and + `describeAction(ruleset, tamsin, action, {}, appliers)`. + +Without the view, the effects read the defaults the aura declares (a Charisma modifier of 1 +here). Nothing in the ally's file changes when the paladin's does: recompute it with the new view. An aura can also make whoever is inside immune to something (Aura of Courage: no one inside is frightened). The entry you placed blocks the matching condition on that creature, reported as the rules issue `immune`; you do nothing more. -Some auras come from a cast rather than from what the character holds: Bless or Spirit Guardians -around the caster. The cast puts the spell on the caster as an applied entry, and the caster's +Some auras come from a cast rather than from what the character holds: orc-2e's bless or 5E's +Spirit Guardians around the caster. The cast puts the spell on the caster as an applied entry, and the caster's `auras` lists the aura once per cast, with `entry` that applied entry's ID. Place and remove its entries as for any aura. Two casts are two listings, told apart by `entry`, and a cast's aura leaves the list when its entry ends (removed or turned off), wherever the spell was cast from. @@ -285,7 +315,8 @@ function turnChange(everyone, ended, started) { } ``` -It needs nothing else from you and never pauses: every parameter has a default, no role is +Pass each creature's `links` and `appliers` as for any call; an aura's effects can read the turn +too. It needs nothing else from you and never pauses: every parameter has a default, no role is required, no step asks a choice, and its rolls roll with your dice function. The pack uses it to reset what lasts a turn and to end what the creature whose turn starts applied until then (its entries record it in `appliedBy`). @@ -437,12 +468,20 @@ attacker's ID in `character`: ```ts const hit = applyChanges(rulesetFor(talent), talent, forTalent); -for (const offer of hit.followUps) { - if (offer.to !== 'character') continue; - // offer.character: 'goblin-1'; offer.changes: an applyEffect, appliedBy the talent. - const other = byId[offer.character]; - applyChanges(rulesetFor(other), other, offer.changes); - applyChanges(rulesetFor(talent), talent, [{ type: 'clearFollowUp', id: offer.id }]); +if (hit.ok) { + talent = hit.character; + for (const offer of hit.followUps) { + if (offer.to !== 'character') continue; + // offer.character: 'goblin-1'; offer.changes: an applyEffect, appliedBy the talent. + const other = byId[offer.character]; + const slowed = applyChanges(rulesetFor(other), other, offer.changes); + if (slowed.ok) save(other.id, slowed.character); + const cleared = applyChanges(rulesetFor(talent), talent, [ + { type: 'clearFollowUp', id: offer.id }, + ]); + if (cleared.ok) talent = cleared.character; + } + save(talent.id, talent); } ``` @@ -470,7 +509,9 @@ const observes = new Map(); // character ID -> the fields its follow-ups watch o function apply(c, changes) { const result = applyChanges(rulesetFor(c), c, changes); - if (result.ok) observes.set(c.id, computeSheet(rulesetFor(c), result.character).observes ?? []); + if (!result.ok) return result; + save(c.id, result.character); + observes.set(c.id, computeSheet(rulesetFor(c), result.character).observes ?? []); return result; } @@ -481,13 +522,16 @@ function applyAndShare(target, changes) { for (const other of everyone) { if (other.id === target.id) continue; if (!(observes.get(other.id) ?? []).some((f) => fields.has(f))) continue; - const seen = applyChanges(rulesetFor(other), other, [result.observe]); + const seen = apply(other, [result.observe]); // its offers live in its file now // seen.followUps: the tactician's `mark-damaged` offer, say. Show it; don't run it here. } return result; } ``` +- **Save the observer's file.** The offers an `observe` fires are stored in the observer's + file, like any ephemeral offer, so keep the `character` its `applyChanges` returns, as `apply` + does above. - **Log it like any change.** The `observe` change is in the observer's change list, so replaying the list stores the same follow-ups. Routed events grow every observer's change log, so send them only where `observes` says they matter. diff --git a/apps/website/guides/packs-and-rulesets.md b/apps/website/guides/packs-and-rulesets.md index 7d21652e6..e09de777a 100644 --- a/apps/website/guides/packs-and-rulesets.md +++ b/apps/website/guides/packs-and-rulesets.md @@ -53,6 +53,12 @@ no rules text by design, but may still have a credit and notices: the FitD packs SRD (CC-BY), and the MÖRK BORG pack carries what its third-party license requires. Show them the same way. +A pack used with the rights holder's permission has a `LicenseRef-` license: Draw Steel Heroes +under the DRAW STEEL Creator License (`LicenseRef-draw-steel-creator-license`), and the Sine +Nomine packs (Stars Without Number, Godbound, Ashes Without Number) under the author's permission +for their free editions (`LicenseRef-sine-nomine-free-edition`). Each carries a credit line and a +`permission` notice; show both the same way. + ## Loading: `loadRuleset` ```ts diff --git a/apps/website/guides/quick-start.md b/apps/website/guides/quick-start.md index 01fc2a221..1265fcb0f 100644 --- a/apps/website/guides/quick-start.md +++ b/apps/website/guides/quick-start.md @@ -9,8 +9,9 @@ rolls an attack. The other guides explain each step in depth. npm install @opencharacter/engine @opencharacter/5e-srd-5.1 ``` -- `@opencharacter/engine` is the TypeScript reference engine. It has no runtime dependencies - on Node, so it runs in browsers, servers and workers. It ships as ES modules with type +- `@opencharacter/engine` is the TypeScript reference engine. It doesn't depend on Node (its + only dependencies are `@opencharacter/spec` and `semver`), so it runs in browsers, servers and + workers. It ships as ES modules with type declarations, for Node 22 and later and current browsers. - `@opencharacter/5e-srd-5.1` is one content pack: its rules, content and English text. diff --git a/apps/website/guides/rolling-dice.md b/apps/website/guides/rolling-dice.md index 6d7f81245..fe6774ba6 100644 --- a/apps/website/guides/rolling-dice.md +++ b/apps/website/guides/rolling-dice.md @@ -66,7 +66,8 @@ const described = describeAction(ruleset, character, { - **`params`** in declaration order: `required` (neither optional, defaulted nor fixed), `default`, `min` and `max` worked out on the sheet, an enum's `values` (an add-on's included), and for a field, content or tag parameter the `options` startAction takes, its - `where` applied. Offer those as the picker. + `where` applied. Offer those as the picker. A bound that reads other parameters + (`params.spend - 1`) isn't listed; startAction checks it once every parameter has its value. - **A parameter the item's `use` fixes** has `fixed: true` and its `value`: don't ask for it. - **A derived parameter** has `derived: true`: startAction works it out from the others (a Cunning Strike's cost in Sneak Attack dice). Don't ask for it; giving one is `invalid-param`. @@ -111,7 +112,9 @@ Some actions take parameters: a target's AC, which skill to check, which rating declared on the action, each with a type and whether it's `optional`; [`describeAction`](#describing-an-action) gives them for any listed action, with their options. A missing required one is the error `missing-param`; a number outside its `min`/`max` is `out-of-range` (the bounds may read the character: at most the Effort -it has free). +it has free). A derived parameter is never passed: startAction works it out, and a value for one +is `invalid-param`. Participant errors (`invalid-participants`) come before parameter errors, +since a derived parameter may read the participants. A **field parameter** (`type: id, of: field`) takes a field ID: a FitD action roll takes the rating to roll (`{ rating: 'ratings.skirmish' }`), and its `where` says which fields are allowed @@ -285,7 +288,7 @@ It's never the recommendation, so an automatic run keeps the rules' choice. To a about every push, pause on the step's tag: `{ rules: [{ kind: 'choice', tags: ['push-cost'], then: 'pause' }] }`. -A paid improvement after the roll (Draw Steel's Brawny, PF2e's Hero Points) is such a step with +A paid improvement after the roll (Draw Steel's Brawny, orc-2e's Hero Points) is such a step with only added options and `#done`, and `#done` recommended: an automatic run declines, and an app that pauses on the step's tag offers the player the option and its cost. The step is skipped when the character has nothing to offer there. @@ -362,7 +365,9 @@ when the action started, the inputs and every answer so far. So you can: - continue it on a different server. `continueAction` replays the action from the start with the recorded answers, so a paused action -needs nothing from memory. Pass the paused result back exactly as you got it. +needs nothing from memory. Pass the paused result back exactly as you got it, with the ruleset it +started with. Against a newer pack release it may fail with `unknown-situational` or +`situational-unavailable`: start the action again. ### Keeping only the answers @@ -485,7 +490,8 @@ Each roll is a list of structured items, not a display string: Build your own labels from it: **27 = 17 (2d20, kept higher) + 4 Strength modifier + 2 Proficiency bonus + 1 Longsword +1 + 3 Bless**. Each item has one source, so every number has -exactly one name. The dice modifiers are a fixed set +exactly one name. Each item also has `tags`, including those content added with a `tag` effect +(Hurl Element's damage type), which `split` changes follow. The dice modifiers are a fixed set ([dice-modifiers.json](../../../packages/spec/data/dice-modifiers.json)), so you can describe them in words ("kept the higher die"). diff --git a/apps/website/guides/showing-a-sheet.md b/apps/website/guides/showing-a-sheet.md index c19f4d0e6..d8ea87f63 100644 --- a/apps/website/guides/showing-a-sheet.md +++ b/apps/website/guides/showing-a-sheet.md @@ -11,26 +11,28 @@ const sheet = computeSheet(ruleset, character); `computeSheet` **never fails**. Problems in the character file go in `sheet.errors`, and the sheet is computed without the broken part. -| Output | What it is | Typical UI | -| ------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------- | -| `values` | Every field's value, with its base, a breakdown and whether it's shown | The sheet itself | -| `groups` | Each display group: whether it's shown, and its members, sorted | Sections of the sheet | -| `effects` | Every active effect, with its source; stacked-out ones `suppressed: true` | "Active effects" panel, tooltips | -| `actions` | Actions available now, with the entry each belongs to | Roll buttons ([Rolling dice](rolling-dice.md)) | -| `situational` | Situational choices offered now (advantage, cover) | Toggles next to roll buttons | -| `pending` | Choices that apply and still need picks | "Choose your fighting style" prompts | -| `offers` | What the rules offer now: derived (level-up) and stored ephemeral offers | "Level up", "Concentration check (12 damage)" | -| `choices` | Every choice that applies, answered or not, in order | A creation wizard's steps | -| `competing` | Fields where several effects compete to set the value | A picker on the field | -| `issues` | Rules issues: valid, but the rules object | Warnings | -| `copies` | Each copy's computed values (an item's quality), by instance ID | Next to the item in the inventory | -| `activationValues` | Picked or granted content's computed values (an ability's damage), by activation | On the ability's card | -| `copyNames` | Copies the player named (`nameFrom`), by instance ID | In place of the item's name | -| `templated` | Every item with templates, as merged (a Longsword +1, a longer cantrip) | Item cards | -| `standardCopies` | The copies every character holds, and whether the file saves each | The inventory, unsaved items greyed out | -| `activations` | Every piece of active content and how it was reached | Content groups | -| `copyLimits` | Per content type, how many copies the character may hold and holds | Replace instead of Add | -| `errors` | Problems with the file itself (unknown items, wrong types) | An import warning | +| Output | What it is | Typical UI | +| ------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `values` | Every field's value, with its base, a breakdown and whether it's shown | The sheet itself | +| `groups` | Each display group: whether it's shown, and its members, sorted | Sections of the sheet | +| `effects` | Every active effect, with its source; stacked-out ones `suppressed: true` | "Active effects" panel, tooltips | +| `actions` | Actions available now, with the entry each belongs to | Roll buttons ([Rolling dice](rolling-dice.md)) | +| `situational` | Situational choices offered now (advantage, cover) | Toggles next to roll buttons | +| `pending` | Choices that apply and still need picks | "Choose your fighting style" prompts | +| `offers` | What the rules offer now: derived (level-up) and stored ephemeral offers | "Level up", "Concentration check (12 damage)" | +| `choices` | Every choice that applies, answered or not, in order | A creation wizard's steps | +| `competing` | Fields where several effects compete to set the value | A picker on the field | +| `issues` | Rules issues: valid, but the rules object | Warnings | +| `copies` | Each copy's computed values (an item's quality), by instance ID | Next to the item in the inventory | +| `activationValues` | Picked or granted content's computed values (an ability's damage), by activation | On the ability's card | +| `copyNames` | Copies the player named (`nameFrom`), by instance ID | In place of the item's name | +| `templated` | Every item with templates, as merged (a Longsword +1, a longer cantrip) | Item cards | +| `standardCopies` | The copies every character holds, and whether the file saves each | The inventory, unsaved items greyed out | +| `activations` | Every piece of active content and how it was reached | Content groups | +| `copyLimits` | Per content type, how many copies the character may hold and holds | Replace instead of Add | +| `auras` | Auras the character projects now, with whom they affect and how far | Placing entries on creatures inside ([Other characters](other-characters.md#auras)) | +| `observes` | Fields of other characters this one's follow-ups react to (absent when none) | Routing `observe` ([Other characters](other-characters.md#reacting-to-other-characters)) | +| `errors` | Problems with the file itself (unknown items, wrong types) | An import warning | The full shape is in [compute-sheet.schema.json](../../../packages/spec/schemas/engine/compute-sheet.schema.json). @@ -235,6 +237,13 @@ engine never tracks time: when Bless ends, remove or toggle it A character can be **immune** to applied content: a monk with Purity of Body can't be poisoned. The entry stays in the list but has no effect, and `issues` has an `immune` issue whose `data` names the entry (`{ entry, item }`), so you can show it struck through or offer to remove it. +An aura the creature stands in can do the same (Aura of Courage: no one inside is frightened); +the issue's `id` then names the aura (`5e-srd-5.1:features.paladin.aura-of-courage#courage/frightened`). + +A content type or an item can make all its effects wait on its state (`effectsWhen`): a magic +item's effects wait for attunement, an orc-2e item's for investment. Until the condition holds, +the item is listed but its effects aren't in `effects` or `values`. Its `scope: applied` effects +(what it does when cast on someone else) don't wait on it. ## Competing options @@ -278,6 +287,10 @@ These picks aren't part of the character file. Store them yourself (in `extensio should travel with the character), and pass them on every `computeSheet` call. A pick that no longer names an option falls back to the recommendation. +Option IDs are stable within a spec version. Spec 0.4 qualified those of a rules-level choice's +effects with their pack (`orc-2e:rules/qi-tradition/divine#0`, once `rules/qi-tradition/divine#0`), +so re-key picks you saved under `rules/…`: they would otherwise fall back to the recommendation. + ## Pending choices `sheet.pending` lists choices that apply now and still need picks: a new fighter's fighting diff --git a/apps/website/guides/showing-text.md b/apps/website/guides/showing-text.md index 74f319a9c..45cb3b624 100644 --- a/apps/website/guides/showing-text.md +++ b/apps/website/guides/showing-text.md @@ -141,9 +141,16 @@ A form labels each input with the pack's text, never a humanized ID: text.typeField('mork-borg-core:weapon', 'attackDrModifier'); // 'Attack DR modifier' text.typeFieldDescription('5e-srd-5.1:armor', 'maxDex'); // 'The most Dexterity modifier AC adds.' text.templateField('5e-srd-5.1:acBonus'); // 'AC bonus', for a custom template's form +text.templateFieldDescription('5e-srd-5.1:acBonus'); // its hint, or undefined text.param('mork-borg-core:test', 'dr'); // 'Difficulty rating (DR)' text.paramDescription('mork-borg-core:test', 'dr'); // 'The target to beat. 12 for most rolls; the GM raises or lowers it.' + +// A parameter content adds (describeAction's `addedBy`): Hurl Element's damage type. +const hurl = 'draw-steel-heroes:abilities.elementalist.hurl-element'; +text.addedParam(hurl, 'element'); // 'Element' +text.addedParamValue(hurl, 'element', 'fire'); // 'Fire' +text.addedParamDescription(hurl, 'element'); // its hint, or undefined ``` - **Every item field has a name** (`types..fields..name`); a hint @@ -152,6 +159,37 @@ text.paramDescription('mork-borg-core:test', 'dr'); - **A parameter's hint** is `.params..description`, optional. `action` is `needs.action`'s form, so an inline action is `#`. A parameter with an explicit `label` key is named by `text.lookup` with that key. +- **A parameter content adds** is named from the content's text + (`.params..name`, `.description`, `.values..name`), so pass describeAction's + `addedBy`, not the action. + +## Situational choices + +The toggles computeSheet lists in `situational` (advantage, a Bardic Inspiration die) and the +reactions a change offers ([Changing characters](changing-characters.md#reactions-to-one-change)) +are named by their ID: + +```ts +text.situational('5e-srd-5.1:advantage'); // 'Advantage' +text.situational('5e-srd-5.1:bardicInspiration'); // 'Bardic Inspiration die' +text.situationalValue('5e-srd-5.1:bardicInspiration', 'd8'); // 'd8' +``` + +The keys are `situational..name` and `situational..values..name` in the choice's +pack. A choice with its own `label` key is named by `text.lookup`. + +## Effects and auras + +An applied entry is named by its content: `text.content(entry.source)` ("Bless"). An aura's +entry may have a name of its own, `.auras..name` in the source's pack, which no +official pack writes yet. The helper has no method for it: look it up with `text.message`, and +fall back to the source's name: + +```ts +const [pack, local] = entry.source.split(/:(.*)/); +text.message(pack, `${local}.auras.${entry.aura}.name`) ?? text.content(entry.source); +// 'Aura of Protection' +``` ## Headlines @@ -246,6 +284,9 @@ if (result.status === 'paused') { | `dice` | the roll step's name, else the action's | `dice`, `roll`, and `targets` for a banded roll | | `handoff` | the name of the action the participant runs | | +For a pause you word yourself, `text.stepPrompt(needs)` gives the step's prompt and +`text.stepName(needs)` its name, each falling back to the action's name. + A FitD action roll with a push, for a scoundrel with Battleborn: ```ts @@ -320,7 +361,10 @@ text.error(error); // the spec's text, data filled with its choice's prompt (`issues.too-many-picks.choice`), and a `safety-limit-exceeded` has a sentence per limit (`errors.safety-limit-exceeded.maxOverflowChain`). - A parameter's error names the parameter by its label, and its values by theirs, from the - action's text (the error's `action`). + action's text (the error's `action`), or from the content's for an added one (`addedBy`): + `invalid-param` reads `"Cost" is worked out by the rules, so it cannot be given.` An + `out-of-range` for a bound content added has `boundBy`, which the text doesn't name; add + `text.content(error.data.boundBy)` if you want to say why. - Text whose data is missing an argument is never shown raw. A code with no text, or whose text misses an argument, reads as the error's `data.message` when it has one, else `errors.other` ("Something went wrong."). Neither shows the code: log the error itself for developers. diff --git a/apps/website/guides/storing-characters.md b/apps/website/guides/storing-characters.md index 33ff4f1ec..e7ad9e2cc 100644 --- a/apps/website/guides/storing-characters.md +++ b/apps/website/guides/storing-characters.md @@ -26,7 +26,7 @@ inventory: # carried content templates: [5e-srd-5.1:magic-items.weapon-plus-one] equipped: true applied: # temporary effects from content the character doesn't own - - { id: bless-1, source: 5e-srd-5.1:spells.bless, active: true } + - { id: bless-1, source: 5e-srd-5.1:spells.bless, active: true, appliedBy: example-cleric } choices: {} # picks for rules-level choices homebrew: {} # the player's own content, if any (below) extensions: # your app's own data @@ -43,6 +43,10 @@ attack bonuses and maximum HP are **never** in the file. They come from `compute time, so a saved character can't disagree with the rules, and a pack fix reaches every character without migrating anything. +An applied entry may name who applied it (`appliedBy`), the aura it belongs to (`aura`), and +state of its own (`state`). A pick that adds a copy (`addAs: copy`) saves that copy's instance ID +as `entry`. Round-trip them all. + Content an item grants can keep state of its own: an artifact's gift whose Effort stays committed. It is saved on the copy (or the pick) that grants it, under `grants`, by the granted content's qualified ID, and left out while at its defaults: @@ -266,7 +270,9 @@ The engine has no partial or incremental compute, so cache yourself: - Compute the sheet when a character changes (after `applyChanges`) and keep the values you need next to the file, keyed by character `id`. -- Throw the cache away when the character, its pinned packs, or the engine version changes. +- Throw the cache away when the character, its pinned packs, the engine version, or a linked or + aura-holding character's values change: a sheet also depends on the `links` and `appliers` + views you pass in. - Never write cached values into the character file. ## Monsters and NPCs diff --git a/apps/website/guides/text-and-translation.md b/apps/website/guides/text-and-translation.md index 5ecf4a3ee..c31740d9c 100644 --- a/apps/website/guides/text-and-translation.md +++ b/apps/website/guides/text-and-translation.md @@ -85,34 +85,39 @@ function actionName(qid: string) { The key conventions ([Translation](../../../packages/spec/docs/i18n.md#key-conventions)): -| What | The engine gives you | Key, in the ID's pack | -| --------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- | -| The pack | `pack.id` | `pack.name` | -| A content item | `5e-srd-5.1:armor.shield` | `armor.shield.name`, `armor.shield.description` | -| A field | `5e-srd-5.1:combat.ac` | `fields.combat.ac.name`, `fields.combat.ac.short` | -| A content type | `5e-srd-5.1:weapon` | `types.weapon.name` | -| A display group | group `combat` (with its `pack`) | `groups.combat.label` | -| A rules action | `5e-srd-5.1:weapon-attack` | `actions.weapon-attack.name` | -| An action on an item | `5e-srd-5.1:features.fighter.second-wind#second-wind` | `features.fighter.second-wind.actions.second-wind.name` | -| An action on a content type | `fate-core:stunts.psychologist#use` | `stunts.psychologist.actions.use.name`, else `types.stunt.use.name` | -| An action's choice option | option `hit` of `weapon-attack` | `actions.weapon-attack.options.hit.name` | -| An action's choice question | `needs` `{ action, step: 'hit/confirm' }` | `actions.weapon-attack.steps.confirm.prompt` | -| An action's roll (optional) | `needs` or log `{ action, step: 'damage' }` | `actions.weapon-attack.steps.damage.name` | -| An action's parameter | parameter `targetAc` of `weapon-attack` | `actions.weapon-attack.params.targetAc.name` | -| An action's result | `outcome.result: hit` of `weapon-attack` | `actions.weapon-attack.results.hit.name` | -| Longer text (optional) | an action, a field | `actions.weapon-attack.description`, `fields.hp.current.description` | -| A situational choice | `5e-srd-5.1:advantage` | `situational.advantage.name` | -| A result band | log `band: success`, `bandSet: …:action` | `bands.action.success.name` | -| A named text (optional) | a spell's type declares `texts: [material, trigger]` | `spells.shield.texts.trigger` | -| A copy's value (optional) | a cohort's computed `quality`, its state `harm` | `types.cohort.computed.quality.name`, `types.cohort.state.harm.name` | -| A tag (optional) | tag `finesse` on a weapon | `tags.finesse.name`, `tags.finesse.description` | -| A card header (optional) | a spell, `5e-srd-5.1:spells.fire-bolt` | `spells.fire-bolt.block`, else `types.spell.block` | -| An enum value (optional) | a spell's `school: evocation` | `types.spell.fields.school.values.evocation.name` | -| The headline (optional) | the character, "Level 3 Hill dwarf Wizard" | `headline`; a member's piece `types.class.headline` | -| A choice on content | path `{ entry, choice: ['fighting-style'] }` on a fighter | `classes.fighter.choices.fighting-style.prompt` | -| A rules-level choice | path `{ choice: ['playbook'] }` | `rules.choices.playbook.prompt` | -| A prerequisite (issue `id`) | `5e-srd-5.1:classes.fighter/multiclass` | `classes.fighter.prerequisites.multiclass` | -| A constraint (issue `id`) | `5e-srd-5.1:attunement-limit` | `constraints.attunement-limit` | +| What | The engine gives you | Key, in the ID's pack | +| ----------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| The pack | `pack.id` | `pack.name` | +| A content item | `5e-srd-5.1:armor.shield` | `armor.shield.name`, `armor.shield.description` | +| A field | `5e-srd-5.1:combat.ac` | `fields.combat.ac.name`, `fields.combat.ac.short` | +| A content type | `5e-srd-5.1:weapon` | `types.weapon.name` | +| A display group | group `combat` (with its `pack`) | `groups.combat.label` | +| A rules action | `5e-srd-5.1:weapon-attack` | `actions.weapon-attack.name` | +| An action on an item | `5e-srd-5.1:features.fighter.second-wind#second-wind` | `features.fighter.second-wind.actions.second-wind.name` | +| An action on a content type | `fate-core:stunts.psychologist#use` | `stunts.psychologist.actions.use.name`, else `types.stunt.use.name` | +| An action's choice option | option `hit` of `weapon-attack` | `actions.weapon-attack.options.hit.name` | +| An action's choice question | `needs` `{ action, step: 'hit/confirm' }` | `actions.weapon-attack.steps.confirm.prompt` | +| An action's roll (optional) | `needs` or log `{ action, step: 'damage' }` | `actions.weapon-attack.steps.damage.name` | +| An action's parameter | parameter `targetAc` of `weapon-attack` | `actions.weapon-attack.params.targetAc.name` | +| A parameter content adds | `element`, `addedBy` Hurl Element | `abilities.elementalist.hurl-element.params.element.name` (`types..…` a type's, `rules.…` the rules') | +| An action's result | `outcome.result: hit` of `weapon-attack` | `actions.weapon-attack.results.hit.name` | +| Longer text (optional) | an action, a field | `actions.weapon-attack.description`, `fields.hp.current.description` | +| A situational choice | `5e-srd-5.1:advantage` | `situational.advantage.name` | +| A result band | log `band: success`, `bandSet: …:action` | `bands.action.success.name` | +| A named text (optional) | a spell's type declares `texts: [material, trigger]` | `spells.shield.texts.trigger` | +| A copy's value (optional) | a cohort's computed `quality`, its state `harm` | `types.cohort.computed.quality.name`, `types.cohort.state.harm.name` | +| Content's computed value (optional) | a technique's `healBonus` in `activationValues` | `techniques.psychic-succor.computed.healBonus.name` | +| An aura (optional) | an aura entry's `source` and `aura` | `features.paladin.aura-of-protection.auras.protection.name` | +| A tag (optional) | tag `finesse` on a weapon | `tags.finesse.name`, `tags.finesse.description` | +| A card header (optional) | a spell, `5e-srd-5.1:spells.fire-bolt` | `spells.fire-bolt.block`, else `types.spell.block` | +| An enum value (optional) | a spell's `school: evocation` | `types.spell.fields.school.values.evocation.name` | +| The headline (optional) | the character, "Level 3 Hill dwarf Wizard" | `headline`; a member's piece `types.class.headline` | +| A choice on content | path `{ entry, choice: ['fighting-style'] }` on a fighter | `classes.fighter.choices.fighting-style.prompt` | +| A rules-level choice | path `{ choice: ['playbook'] }` | `rules.choices.playbook.prompt` | +| A rules-level choice's issue (`id`) | `fitd-scoundrel:rules/vice` | `rules.choices.vice.prompt`, in the pack the ID names | +| An inline option | option `disavowed` of a career's `incident` choice | `careers.agent.choices.incident.options.disavowed.name`, `.description` (optional) | +| A prerequisite (issue `id`) | `5e-srd-5.1:classes.fighter/multiclass` | `classes.fighter.prerequisites.multiclass` | +| A constraint (issue `id`) | `5e-srd-5.1:attunement-limit` | `constraints.attunement-limit` | Some details: @@ -125,7 +130,9 @@ Some details: `playbooks.bruiser.choices.special-ability.prompt` in its homebrew text. A choice ID with a slash names its owner itself: `5e-srd-5.1:features.wizard.spellcasting/cantrips` (a choice of granted content) is `features.wizard.spellcasting.choices.cantrips.prompt`. A rules-level choice's ID in a path is bare; - find its pack from `ruleset.rules.choices`, whose keys are qualified. + find its pack from `ruleset.rules.choices`, whose keys are qualified. Issues name it qualified + (`:rules/`, and an inline option of one `:rules//