# Characters

A **character file** ([`character.schema.json`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/schemas/character.schema.json)) is the one strict, standard file for exchanging a character between apps. Apps MUST be able to import and export it, and it is what the engine reads. How an app stores characters internally is up to the app.

Monsters and NPCs use the same format. A stat block is a ready-made character with mostly fixed values, and damage to a goblin is `applyChanges` on the goblin.

## Only base state

A character file holds only what everything else is calculated from:

- input field values,
- owned content and per-copy state,
- picks,
- which temporary effects are on,
- which moments the rules called for are still owed (ephemeral follow-ups): an
  event, unlike a value, can't be derived again from the rest.

Derived values are **never** saved, so a character can never disagree with the rules.

## Structure

```yaml
specVersion: 0.1.0
id: 7f1c... # unique within the app or campaign
packs: # exact versions, in load order
  - { id: 5e-srd-5.1, version: 0.1.0 }
creating: false # true while the character is being created (the default is false)
values: # input fields, qualified IDs
  5e-srd-5.1:abilities.str.base: 16
features: # owned content that isn't carried
  - id: f-fighter
    item: 5e-srd-5.1:classes.fighter
    state: { level: 3 }
    choices:
      fighting-style:
        - option: 5e-srd-5.1:fighting-styles.dueling
inventory: # carried content
  - id: inv-1
    item: 5e-srd-5.1:weapons.longsword
    templates: [5e-srd-5.1:magic-items.weapon-plus-one]
    equipped: true
applied: # temporary effects from content not owned
  - { id: a-bless, source: 5e-srd-5.1:spells.bless, active: true }
followUps: # ephemeral follow-ups the engine stored, until acted on or dismissed
  - id: concentration-1
    followUp: 5e-srd-5.1:concentration
    params: { damage: 12 }
    cause:
      {
        field: 5e-srd-5.1:hp.current,
        by: -12,
        requested: -12,
        taken: -12,
        reason: requested,
        clamped: false,
        tags: [damage],
      }
choices: {} # character-level (rules) choices
links: # who fills each link slot (another character's id)
  fitd-scoundrel:crew: example-fitd-crew-ravens
homebrew: {} # the character's own content and rules additions (see Homebrew)
extensions:
  com.example.vtt: { portrait: '...' }
```

| Property      | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `specVersion` | The spec version the file was written against                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `id`          | Character ID (a UUID is a good choice)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `packs`       | Pinned exact pack versions, in load order. The app decides when, or whether, to upgrade ([upgrades.md](https://opencharacterformat.org/spec/upgrades/)).                                                                                                                                                                                                                                                                                                                                                                                                  |
| `creating`    | Whether the character is being created. Default false: see [Creating a character](#creating-a-character)                                                                                                                                                                                                                                                                                                                                                                                                               |
| `values`      | Input field values by **qualified** field ID. Missing fields take their default. A value for a derived field is a runtime error (`not-input-field`).                                                                                                                                                                                                                                                                                                                                                                   |
| `features`    | Entries for content whose type is not `carried`: classes (with `level` state), feats, traits                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `inventory`   | Entries for `carried` content                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `applied`     | Temporary effects from content the character doesn't own (Bless cast by an ally, a condition). The app turns them on and off; the engine never tracks time. An entry the character is immune to is blocked: it stays, has no effect and is reported as `immune` ([content.md](https://opencharacterformat.org/spec/content/#when-content-is-active)).                                                                                                                                                                                                     |
| `followUps`   | Ephemeral follow-ups: what the rules called for when a batch fired an `on` or `becomes` follow-up or an action offered one (`offerFollowUp`), stored by that applyChanges as `{ id, followUp, source?, to?, character?, params?, cause? }` until it is acted on (`clearFollowUp`) or dismissed (`dismissFollowUp`); a `becomes` one also lapses, deleted by the next applyChanges, once its condition is false. The engine writes them; derived offers are never stored ([engine-core.md](https://opencharacterformat.org/spec/engine-core/#follow-ups)). |
| `choices`     | Picks for rules-level choices                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `links`       | Which character fills each link slot, by qualified slot ID: that character's `id` ([rules-and-fields.md](https://opencharacterformat.org/spec/rules-and-fields/#link-slots)). A slot no pinned pack declares is a runtime error (`unknown-link`).                                                                                                                                                                                                                                                                                                         |
| `homebrew`    | The character's own content and rules additions, loaded as the pack `homebrew`: see [Homebrew](#homebrew)                                                                                                                                                                                                                                                                                                                                                                                                              |
| `extensions`  | App extras                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

Every content and field reference in a character file is **fully qualified** (`pack:id`).
The character's own homebrew is qualified with the reserved pack ID `homebrew`.

## Entries

An entry ([`character.schema.json#/$defs/entry`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/schemas/character.schema.json)) is the character's copy of an item: a base plus templates.

- `id`: unique within the file. Changes refer to copies by this ID.
- `item`: the base content item.
- `templates`: applied in order. Each MUST match the template's `appliesTo`.
- `equipped`: only for `equippable` types.
- `quantity`: an integer ≥ 0, default 1. `setQuantity` and `adjustQuantity` change it; a copy
  at 0 stays until removed.
- `state`: values for keys declared by the content type, the item, or its templates.
- `choices`: picks for choices declared by the item and its templates.

**Standard copies**. Copies every character holds
([content.md](https://opencharacterformat.org/spec/content/#standard-copies)) aren't listed while they're at their defaults: not
equipped, quantity 1, no templates or picks, and no state other than the defaults.

- An entry whose `id` is a standard copy's derived ID and whose `item` is that item **is** the
  standard copy, saved with its changes, and keeps its place in the file. With another `item` it
  is the runtime error `duplicate-id`, and the entry is skipped.
- Any other entry for a standard item is an ordinary extra copy (a second lantern).
- An entry whose item stops being standard (the ability that holds it is unpicked, or a pack
  update drops the tag) stays as an ordinary entry with its state.
- `applyChanges` writes an unsaved standard copy before changing it, and removes a saved one
  that is back at its defaults, so files stay minimal.

```yaml
# The table tags its own lantern and armor `standard`, so every scoundrel holds them. This one
# carries both on this score; the ritual its Ritual ability starts with is a standard copy at its
# defaults, so the file leaves it out.
inventory:
  - id: homebrew-gear-lantern
    item: homebrew:gear.lantern
    equipped: true
  - { id: homebrew-gear-armor, item: homebrew:gear.armor, equipped: true }
```

An applied entry has an `id` unique within the file too, its `source` (a content item, not a
template), `active` (default true) and `state`: values for keys declared by the source's content
type or item. An action may apply one without choosing its `id`: applyChanges gives it
`<source local id, dots as hyphens>-<n>` (`conditions-blinded-1`). Changes reach
an applied entry by its ID, or by query (`applied: { where }`), and `setState` and
`adjustState` change its state as a copy's.

## Choices tree

Picks are stored as a tree keyed by choice ID. Each pick has an `option`, which is a qualified content ID, an inline option ID, or a value. A pick may have nested `choices` for the choices that option contains:

```yaml
choices:
  martial-archetype:
    - option: 5e-srd-5.1:subclasses.champion
      choices:
        additional-fighting-style: []
```

A pick may also keep `state`, for state keys its picked content declares: a
once-a-day feat's `used`, as a copy's state would. A key at its default is left out.

Choices declared on an item are stored on that item's entry. Choices declared in the rules are stored in the top-level `choices`. Picked content is never also listed in `features`.

Keys in a `choices` tree are bare choice IDs for the tree's own item (the entry's item, or the
picked option). Choices declared by templates or granted content in the same tree use
`<qualified owner id>/<choice id>`, such as `5e-srd-5.1:magic-items.some-template/damage-type`.

## Creating a character

A pack offers ways to start a character in its rules' `creation` list
([rules-and-fields.md](https://opencharacterformat.org/spec/rules-and-fields/#creation)). Whatever the way, an app:

1. makes a character file with the packs and `creating: true`;
2. either runs a start's action and applies its proposed changes (random or guided), or lets
   the player fill in the blank sheet (manual);
3. finishes with the change `finishCreation`, which clears `creating`. A creation action proposes
   it as its last change, so applying the action's changes is the player's confirmation.

`finishCreation` is rejected (`creation-incomplete`) while the character has rules issues or
pending choices; the failed result lists them as `issues`, pending choices as `too-few-picks`.
An app can then apply the other changes on their own and let the player resolve what's listed.
Constraints that hold only at creation guard on `character.creating`
(`'!character.creating || ratings.highest <= 2'`), so they stop applying once the character is
finished.

A character with every field at its default loads and computes without engine errors; what it
lacks shows as rules issues and pending choices.

**A linked character**: an offer with `create` (a MÖRK BORG companion's sheet)
asks for a new character for one of this character's empty link slots. The app makes the file
with `creating: true` and the offer's `pack`, runs the named start with the offer's `params` as
above, and saves the new character's `id` in this character's `links` under the slot, which ends
the offer ([engine-core.md](https://opencharacterformat.org/spec/engine-core/#the-offer-list)).

## Homebrew

A character may carry its own content and rules additions in `homebrew`
([`homebrew.schema.json`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/schemas/homebrew.schema.json)): a custom
weapon, a custom skill or feat, a table's own special ability. It is a small pack that belongs
to the character, so the character file stays self-contained when it is shared.

```yaml
specVersion: 0.1.0
id: example-mothership-reyes
packs:
  - { id: mothership-1e, version: 0.1.0 }
homebrew:
  defaultLanguage: en
  rules: # optional: a rules document, without settings
    tags: [energy]
  content:
    - id: gear.blaster
      type: weapon # resolves to mothership-1e:weapon
      tags: [ranged, energy]
      fields: { damage: 2d10, magazine: 12, range: long, woundType: fire }
  i18n:
    en:
      gear.blaster.name: Blaster
inventory:
  - { id: inv-1, item: homebrew:gear.blaster, equipped: true }
```

- **A pack named `homebrew`.** Engines load the section as a last pack with the reserved ID
  `homebrew` ([packs.md](https://opencharacterformat.org/spec/packs/#ids)), so its definitions are `homebrew:<id>` everywhere else
  in the file. Every character's homebrew uses the same prefix; it is only ever loaded with its
  own character.
- **Scope.** The homebrew depends on every pinned pack, in pin order. A reference resolves in
  the homebrew first, then in each pinned pack and its dependencies, in pin order. So `weapon`
  above is `mothership-1e:weapon`, and the blaster has that type's state, effects and `shoot`
  action. Content-only queries see homebrew items, so a homebrew skill joins a class's skill
  picks, and prerequisites can name it (`@skills.xenoarchaeology in skills.known`, an [ID literal](https://opencharacterformat.org/spec/formulas/#id-literals) resolving to `homebrew:skills.xenoarchaeology`).
- **What it may define:** content items (each with `id` and `type`) and any rules-level
  definition a rules document has: fields, content types, tags, situational choices, result
  bands, actions, effects, follow-ups, rules-level choices, constraints, display hints.
- **What it may not do:** override or hide pinned packs' content, or change settings
  (`homebrew-not-allowed`). A table rule that changes the official rules ("longswords do 1d10")
  belongs in a standalone pack that every player at the table loads.
- **Text** lives in `homebrew.i18n`, keyed as in a pack's i18n files. `defaultLanguage` is
  required with `i18n`. Engines never read it.
- **No license.** A character's homebrew is the player's own data, like the rest of the file.
- **Size.** The safety limits `maxHomebrewItems` (content items and rules-level definitions) and
  `maxHomebrewFormulas` (formulas written as text) bound it, since a shared character file is
  untrusted ([safety limits](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/data/safety-limits.json)).
- **Loading** is the engine's: every call that takes a character loads its homebrew on top of
  the ruleset, and a homebrew that doesn't load is `homebrew-error`
  ([engine-core.md](https://opencharacterformat.org/spec/engine-core/#a-characters-homebrew)).
- **Editing** is the change `setHomebrew`, which replaces the section (`null` removes it). It is
  rejected while the rest of the file still uses homebrew content it removes, so deleting a
  custom item in use takes a batch that removes its uses first
  ([engine-core.md](https://opencharacterformat.org/spec/engine-core/#applychanges)).
- **Upgrades** rewrite the homebrew along with saved state: a renamed field, content ID, content
  type, item field, tag or action is renamed in it too, and what can't be migrated is reported
  ([upgrades.md](https://opencharacterformat.org/spec/upgrades/#a-characters-homebrew)).
- **Sharing a custom item between characters** means moving it to a standalone pack. That is
  app work: add the item to the pack, then rename `homebrew:<id>` to the pack's qualified ID in
  each character (as `renameContent` does with qualified IDs), and drop it from the homebrew.

## Extensions

`extensions` holds app-specific extras (portrait, notes, token settings), each under the app's own namespace: reverse DNS (`com.example.vtt`) or an npm package name (`@example/vtt`). Apps MUST carry other apps' extensions along untouched on import and export. The engine ignores them except for the `setExtension` change.

## Legality

Legality is checked against the current character only, never its history. A character with rules issues is still a valid file. Runtime errors are the things that would make a file invalid (unknown IDs, wrong types, out-of-range inputs, templates that don't apply), and the engine never produces such a file.
