Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/duplicate-content-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@opencharacter/spec': patch
'@opencharacter/engine': patch
---

loadRuleset reports `duplicate-content-id` (data `id`, `pack`) when two content items of one
pack share an ID, instead of keeping the last silently.
9 changes: 9 additions & 0 deletions .changeset/spec-docs-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@opencharacter/spec': patch
---

dice-and-actions.md and engine-actions.md catch up with the engine: `to: applied` routing, the
reaction route through situational choices, every action and roll part property, the full dice
notation, every safety limit and every error startAction can return. The Fate group example is
valid, and Healing Grace bounds `spend`. dice-modifiers.json gives every notation form and
`rerollOnce`'s `count`.
10 changes: 10 additions & 0 deletions .changeset/spec-docs-effects-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@opencharacter/spec': patch
---

effects.md, content.md, characters.md and display-hints.md catch up with the format: the full
ops and property tables (`params`, `scope`, the `changes` and `option` value shapes), examples
that match the packs (Null Field, `effectsWhen` gates, `appliedBy: self`), every content item and
choice property in the tables, and the once-per-attack route for copy-state costs.
effect-operations.json, stacking-policies.json and the character schema's `applied.aura`
descriptions match.
10 changes: 10 additions & 0 deletions .changeset/spec-docs-engine-algorithms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@opencharacter/spec': patch
---

engine-core.md, engine-interface.md and upgrades.md catch up with the engine: aura entries and
the `auras` output, `unknown-situational` on an adjust, the pack limit on `fields` queries,
`setField`, `appliers` on every call that takes them, every error kind, the full `choices`,
`offers` and applyChanges output shapes, pick copies in upgrade steps, and derived parameters and
added bounds in release checks. change-types.json, rules-issue-codes.json, upgrade-steps.json and
error-codes.json descriptions match.
10 changes: 10 additions & 0 deletions .changeset/spec-docs-formulas-packs-i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@opencharacter/spec': patch
---

formulas.md gives every root where it is bound and typed (`appliedBy`, `applier`, `grant`, `i`,
`changes` and `action` effects, derived parameters, a situational choice's copy changes) and
every place's expected type. i18n.md and the en.yaml header list the `invalid-added-param`
reasons and how an added parameter is named. packs.md describes `LicenseRef-` licenses. The
README, rules-and-fields.md and versioning.md are brought up to date, and formula-context.json
and safety-limits.json descriptions match.
9 changes: 6 additions & 3 deletions packages/engine/src/core/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,12 @@ export function loadPacks(packs: PackRelease[], options: LoadOptions = {}): Load
return set;
};
for (const pack of byId.values()) {
for (const item of pack.content)
if (item.id !== undefined)
ruleset.content[`${pack.manifest.id}:${item.id}`] = { ...item } as LoadedItem;
for (const item of pack.content) {
if (item.id === undefined) continue;
const qid = `${pack.manifest.id}:${item.id}`;
if (qid in ruleset.content) err('duplicate-content-id', { id: qid, pack: pack.manifest.id });
else ruleset.content[qid] = { ...item } as LoadedItem;
}
}

const unknown = (kind: string, ref: string, pack: string) =>
Expand Down
6 changes: 3 additions & 3 deletions packages/spec/data/change-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
{
"id": "adjust",
"description": "Adjust a numeric input field by an amount; tags let incoming-change effects (resistance) modify it first."
"description": "Adjust a numeric input field by an amount; tags let incoming-change effects (resistance) modify it first. May carry the receiving player's `situational` choices, the `faces` its incoming-change dice used, and `from`, the character who sent it."
},
{
"id": "addItem",
Expand Down Expand Up @@ -53,7 +53,7 @@
},
{
"id": "applyEffect",
"description": "Start a temporary effect from content the character does not own (Bless). Without `id`, applyChanges picks one from the source."
"description": "Start a temporary effect from content the character does not own (Bless). Without `id`, applyChanges picks one from the source. May give `appliedBy`, who applied it, and `aura`, one aura of the source: the entry then runs only that aura."
},
{
"id": "removeEffect",
Expand All @@ -69,7 +69,7 @@
},
{
"id": "unchoose",
"description": "Un-pick an option, removing its nested picks in the same change. Without `option`, every pick of the choice is removed; a choice with none is unchanged. Not on a choice a grant presets (`preset-choice`)."
"description": "Un-pick an option, removing its nested picks in the same change. Without `option`, every pick of the choice is removed; a choice with none is unchanged. Not on a choice a grant presets (`preset-choice`). An action may propose it by owner (`choice: { owner, id }`); apps receive the path form."
},
{
"id": "setExtension",
Expand Down
6 changes: 3 additions & 3 deletions packages/spec/data/dice-modifiers.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@
"id": "rerollOnce",
"notation": "roN or ro<=N",
"example": "2d6ro<=2",
"description": "Reroll dice matching the comparison once."
"description": "Reroll dice matching the comparison once. From a `reroll` roll modifier with `count`, it also carries `count`: at most that many dice are rerolled."
},
{
"id": "countSuccesses",
"notation": "csN, >=N",
"notation": "csN or cs>=N",
"example": "5d6cs>=5",
"description": "The result is the number of dice matching the comparison instead of their sum."
},
{
"id": "countFailures",
"notation": "cfN",
"notation": "cfN or cf<=N",
"example": "5d10cs>=8cf1",
"description": "Subtract one for each die matching the comparison (used with countSuccesses)."
},
Expand Down
6 changes: 3 additions & 3 deletions packages/spec/data/effect-operations.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"set": "effect-operations",
"specVersion": "0.1.0",
"description": "The fixed set of effect operations. Per field, effects apply in `order` (lower first); atLeast and atMost share the last stage.",
"description": "The fixed set of effect operations. Per field, effects apply in `order` (lower first; atLeast and atMost share a stage). `order` 0 is no field stage: grant comes with the field's own value, and the tag, choice, step and action ops have their own timing (effects.md).",
"items": [
{
"id": "set",
Expand Down Expand Up @@ -55,7 +55,7 @@
"id": "option",
"order": 0,
"targets": ["choice"],
"description": "Add an option to an action's choice steps with a matching tag: `{ id, changes, to? }`, offered after the step's own options while the effect's `when` holds, with ID `<content id>#<id>` (`<pack>:rules#<id>` for the rules' effects)."
"description": "Add an option to an action's choice steps with a matching tag: `{ id, changes?, to?, flags? }`, offered after the step's own options while the effect's `when` holds, with ID `<content id>#<id>` (`<pack>:rules#<id>` for the rules' effects)."
},
{
"id": "changes",
Expand All @@ -73,7 +73,7 @@
"id": "params",
"order": 0,
"targets": ["action"],
"description": "Add parameters to listed actions with a matching tag: `{ <name>: <declaration> }`, each optional or defaulted, after the action's own on the run the app starts while the effect's `when` holds. A name an action the literal tags reach declares, or another effect's other declaration of it, is a load error; through computed tags, the action's own and then the first effect's win."
"description": "Add parameters to listed actions with a matching tag: `{ <name>: <declaration> }`, each optional, defaulted or derived (`formula`), after the action's own on the run the app starts while the effect's `when` holds. Their `min` and `max` may read the action's `params`. An entry with only `min` and/or `max`, naming a parameter the run already has, adds bounds to it instead. A name an action the literal tags reach declares, or another effect's other declaration of it (not between `from: self` effects of different content items that no item holding both has an action both reach), is a load error; through computed tags, the action's own and then the first effect's win."
}
]
}
6 changes: 3 additions & 3 deletions packages/spec/data/error-codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
{
"id": "duplicate-id",
"kind": "runtime",
"description": "An instance ID is already used in this character; or, in startAction, two participants share a view `id`; or an entry has a standard copy's ID but another item, and is skipped. loadRuleset reports it too when two standard items derive the same instance ID. Data: `id`."
"description": "An instance ID is already used in this character; or, in startAction, two participants share a view `id`; or an entry has a standard copy's ID but another item, and is skipped. In computeSheet and the calls that take `appliers`, two applier views share an `id` (the first is read). loadRuleset reports it too when two standard items derive the same instance ID, when one choice list repeats a choice key (a slot's `<id>-<slot>` included), and when a pack adds an inline option whose ID the choice has (data `owner` and `pack` too). Data: `id`."
},
{
"id": "standard-copy",
Expand Down Expand Up @@ -171,12 +171,12 @@
{
"id": "duplicate-content-id",
"kind": "load",
"description": "Two items in one pack share an ID."
"description": "Two content items in one pack share an ID: loadRuleset keeps the first. Data: `id` (the qualified ID), `pack`."
},
{
"id": "unknown-reference",
"kind": "load",
"description": "Content refers to an ID that does not exist. Also raised when an action runs, for an addItem's item or an applyEffect's source that names no content or a template. A kept template (`keepsTemplates`) that names content other than a template is `unknown-reference` too, with kind `template`. A grant's preset `choices` naming a choice the granted content doesn't declare (or on a `scope: applied` grant) is kind `choice`; a literal preset option it doesn't offer, kind `option` or `content`. A situational choice's `offerFollowUp` naming no rules follow-up, or a derived (`while`) one, is kind `follow-up`."
"description": "Content refers to an ID that does not exist. Also raised when an action runs, for an addItem's item or an applyEffect's source that names no content or a template. A kept template (`keepsTemplates`) that names content other than a template is `unknown-reference` too, with kind `template`. A grant's preset `choices` naming a choice the granted content doesn't declare (or on a `scope: applied` grant) is kind `choice`; a literal preset option it doesn't offer, kind `option` or `content`. A situational choice's `offerFollowUp` naming no rules follow-up, or a derived (`while`) one, is kind `follow-up`. An action's `unchoose` by owner (`choice: { owner, id }`) is kind `choice` when it runs: `owner: self` with no content (a rules action), an owner that names nothing, or a route other than the actor's own."
},
{
"id": "circular-formula",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/data/formula-context.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"id": "self",
"description": "The character's copy of the item carrying the formula: `self.equipped`, `self.<state key>`, `self.templates` (saved, then those content keeps on the copy). For content made active by a pick or a grant, the copy that owns the choice or grant (a subclass sees its class copy's `self.level`); picked or held granted content that declares state keys reads them from its pick, or its grant's saved state (else the grant's `state`), with declared defaults, over that copy. Absent for content with no owning copy (picked in a rules-level choice). In a choice step over copies (`where`, `prefer`), the candidate copy; not bound in one over content. In an applied entry's effects and its actions (`scope: applied`), the entry: `id`, `appliedBy` (the ID of the character who applied it, `''` when the file names none) and its state keys. In a change aimed at copies (`copies.where`, a per-copy `by` or `value`, a situational choice's `setState` and `adjustState` included), the matched copy. In a change aimed at applied entries (`applied.where`, a per-entry `by` or `value`), the entry: `id`, `appliedBy` and its state keys with declared defaults. In the copy's own actions (and actions it reaches through `use`) and its follow-ups, `self` also has the copy's computed values, `self.<name>`; in a computed value, those declared before it; everywhere else a computed name is `unknown-name`. In a content display group's `where`, `sectionBy` and `sortBy` and a content-type state hint's `showWhen`, the member copy, absent for picked content with no owning copy. In a `keepsTemplates` query that reads the keeper, and in its `params`, the keeper's. The copy's choices aren't on `self`: `picksOf(<choice>)` reads them."
"description": "The character's copy of the item carrying the formula: `self.equipped`, `self.<state key>`, `self.templates` (saved, then those content keeps on the copy). For content made active by a pick or a grant, the copy that owns the choice or grant (a subclass sees its class copy's `self.level`); picked or held granted content that declares state keys reads them from its pick, or its grant's saved state (else the grant's `state`), with declared defaults, over that copy. Absent for content with no owning copy (picked in a rules-level choice). In a choice step over copies (`where`, `prefer`), the candidate copy; not bound in one over content. In an applied entry's effects and its actions (`scope: applied`), the entry: `id`, `appliedBy` (the ID of the character who applied it, `''` when the file names none) and its state keys. In a change aimed at copies (`copies.where`, a per-copy `by` or `value`, a situational choice's `setState` and `adjustState` included), the matched copy. In a change aimed at applied entries (`applied.where`, a per-entry `by` or `value`), the entry: `id`, `appliedBy` and its state keys with declared defaults. In the copy's own actions (and actions it reaches through `use`) and its follow-ups, and in its effects that act as an action runs (on rolls, choice and changes steps, listed actions and incoming changes; not an applied or aura effect), `self` also has the copy's computed values, `self.<name>`; in a computed value, those declared before it; everywhere else a computed name is `unknown-name`. In a content display group's `where`, `sectionBy` and `sortBy` and a content-type state hint's `showWhen`, the member copy, absent for picked content with no owning copy. In a `keepsTemplates` query that reads the keeper, and in its `params`, the keeper's. The copy's choices aren't on `self`: `picksOf(<choice>)` reads them."
},
{
"id": "pick",
Expand All @@ -41,7 +41,7 @@
},
{
"id": "params",
"description": "In a template's item field values: the template's `params`, as the content keeping it gave them (`keepsTemplates` `params`), each declared default where none was given. In an action: parameters the app supplied. Also in a parameter's `min` and `max` (an action's own, or one a `params` effect adds), evaluated once every parameter has its value. A field parameter (`type: id, of: field`) is `{ id, type, tags, value }`: the field's qualified ID, type, tags on the sheet (written and computed; `[]` if none) and computed value. Substituted as literals in a formula a proposed change passes on."
"description": "In a template's item field values: the template's `params`, as the content keeping it gave them (`keepsTemplates` `params`), each declared default where none was given. In an action: parameters the app supplied, and derived ones (a declaration's `formula`), worked out as the action starts; a derived parameter's `formula` reads only those declared before it. Also in a parameter's `min` and `max` (an action's own, one a `params` effect adds, or a bound a `params` effect adds to one), evaluated once every parameter has its value. A field parameter (`type: id, of: field`) is `{ id, type, tags, value }`: the field's qualified ID, type, tags on the sheet (written and computed; `[]` if none) and computed value. Substituted as literals in a formula a proposed change passes on."
},
{
"id": "participants",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/data/rules-issue-codes.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"set": "rules-issue-codes",
"specVersion": "0.1.0",
"description": "Spec-level rules issue codes. Pack-defined prerequisites and constraints are reported under these codes with the pack's qualified ID.",
"description": "Spec-level rules issue codes. Pack-defined prerequisites and constraints are reported under these codes with the pack's qualified ID. Choice issues carry `<owner>/<choice>`, with `<pack>:rules` as the owner of a rules-level choice.",
"items": [
{
"id": "prerequisite-not-met",
Expand Down Expand Up @@ -29,7 +29,7 @@
},
{
"id": "option-not-eligible",
"description": "A picked option no longer matches the choice's query."
"description": "A picked option no longer fits its choice: its query, its `where`, its `values` formula or its `eligible`."
},
{
"id": "content-hidden",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/data/safety-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
{
"id": "maxActionSteps",
"value": 1000,
"description": "Steps one action may run, including nested actions."
"description": "Steps one action may run, including nested actions (`action` steps and `action` effects)."
},
{
"id": "maxActionDepth",
"value": 16,
"description": "Nesting depth of actions running actions."
"description": "Nesting depth of actions running actions (`action` steps and `action` effects)."
},
{
"id": "maxGroupPasses",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/data/stacking-policies.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"set": "stacking-policies",
"specVersion": "0.1.0",
"description": "How effects sharing a stack key combine on the same target.",
"description": "How effects sharing a stack key combine on the same target, or across fields for a key that spans them (`spansFields`; every policy but `addAll`).",
"items": [
{
"id": "addAll",
Expand Down
8 changes: 4 additions & 4 deletions packages/spec/data/upgrade-steps.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
{
"id": "removeContent",
"description": "Remove copies of, picks for and the saved grant state (`grants`) of content `id`."
"description": "Remove copies of, picks for and the saved grant state (`grants`) of content `id`. A removed pick's copy (`addAs: copy`) goes too."
},
{
"id": "changeContentType",
Expand Down Expand Up @@ -57,11 +57,11 @@
},
{
"id": "removeChoice",
"description": "Remove the picks of choice `choice` on `owner` (or the rules), with their nested picks."
"description": "Remove the picks of choice `choice` on `owner` (or the rules), with their nested picks and their copies (`addAs: copy`)."
},
{
"id": "renameOption",
"description": "Replace a picked option `from` of choice `choice` (on `owner`, or the rules) with `to`, or remove the pick when `to` is absent."
"description": "Replace a picked option `from` of choice `choice` (on `owner`, or the rules) with `to`, or remove the pick (with its copy, `addAs: copy`) when `to` is absent."
},
{
"id": "requireChoice",
Expand Down Expand Up @@ -93,7 +93,7 @@
},
{
"id": "adoptStandard",
"description": "Rename the first entry holding `item` to the item's standard copy ID, unless an entry has it already."
"description": "Rename the first entry holding `item`, not one a pick holds as its copy, to the item's standard copy ID, unless an entry has it already."
}
]
}
Loading
Loading