Ruleset Sheets

Use this page to design a ruleset's character sheet: abilities, skills, fields and lists, live states, wound tracks, penalties, rests and the Game Master text, plus the party sheet and progress clocks. It is for ruleset authors. Before you start, read Writing Game Mode Rulesets.

The sheet

Anything that reads a number names it with a value reference, which is an object with exactly one key: const, field, derived, abilityScore, abilityMod, abilityModFromField, skillMod, saveMod, listSum, livePool, liveTrack, or itemStat. For example, a pool whose maximum is a derived value: "max": { "derived": "grit_max" }.

What a check does untrained. A skill or save the character has no training in (its tier is the first one) rolls as usual unless the ruleset says otherwise, with untrained on the skill or save or on the section it sits in (the skill's or save's own rule wins):

"sections": [{ "id": "labour", "label": "Labour", "untrained": { "by": -1 } }],
"skills": [
  { "id": "dig", "label": "Dig", "ability": "sinew", "section": "labour", "untrained": "refuse" },
  { "id": "listen", "label": "Listen", "ability": "nerve", "untrained": "harder" }
]

Sections on abilities, skills and saves, and untrained anywhere, are Capability API 1.41 for a packaged ruleset.

A skill or save may carry a cap, a value reference that is the most its check may ever come to: a rating it cannot outgrow, or a track that holds it down. Gravewatch caps Soothe at the Resolve a warden has left, "cap": { "livePool": "resolve" }. The capped number is the one the sheet shows and every check rolls, and a with= swap is worked out on the number before the cap and then capped again. A cap cannot read a skill or save modifier, and neither can the derived value it reads or any derived value declared above that one, so the sheet is still worked out once, top to bottom. Capability API 1.39.

hideWhen hides a field, a derived value, a list, a pool, or a plain track by another field's value, in exactly one of three ways: equals one value, notEquals one value (hidden whenever the field holds anything else, which is how "only for this kind of character" is said), or in a list of values. Every value named must be one the field can hold, or the rule could never match, and it is checked one value at a time; for notEquals and in that includes a number field's range and a text field's length. The rule reads the value the sheet editor shows: an unset field is the value a blank sheet starts with, and an enum value the ruleset no longer offers is the field's default. The 5e file uses equals to hide spell slots from a character who does not cast spells; Gravewatch keeps lantern oil off the sheet of every warden who is not on the night watch with "hideWhen": { "field": "watch", "notEquals": "night" }. A layer cannot remove a value any of these compares with. A wound track cannot be hidden, because rolls and fights read it whatever the sheet shows. notEquals and in are Capability API 1.39.

A plain track's max may be a value reference instead of a number, so a rating the character has sets how far it goes: "max": { "field": "willpower_rating" }. Its min and default stay numbers. A wound track with named levels has a max equal to its number of levels; a track of numbered boxes is as long as its own max (see Numbered boxes below). alwaysShow: true prints a track in the Game Master's sheet block even at its default, for a rating that matters on every turn; without it a track at its default is left out, because three death saves at zero say nothing. These three are Capability API 1.37 for a packaged ruleset.

Live states: a form, a stance, a light

An enum field is chosen when a character is made and stays chosen, and a condition is only on or off. A live state is one value out of a closed set that changes in play: a shapeshifter's form, a fighting stance, how lit a lantern is.

"live": {
  "states": [
    {
      "id": "stance",
      "label": "Stance",
      "values": ["guarded", "steady", "reckless"],
      "valueLabels": { "guarded": "Guarded", "steady": "Steady", "reckless": "Reckless" },
      "default": "steady"
    }
  ]
}

Numbers that follow. A derived value with "op": "enumTable" gives a number for each value of an enum field or of a live state:

{
  "id": "stance_brawn",
  "label": "Stance on Brawn",
  "op": "enumTable",
  "from": { "liveState": "stance" },
  "table": { "guarded": -1, "reckless": 2 },
  "default": 0
}

Live states, enumTable and a rest step that names a state are Capability API 1.42 for a packaged ruleset.

Wound tracks: health that is a track, not a number

Plenty of systems do not count hit points at all. They have a column of boxes, each worse than the last, and you tick one when you get hurt. Give a live.tracks entry kinds and either named levels or numbered boxes (below) and it stops being a number on a scale and becomes one of those:

Which shape does your system want? A pool records how MUCH harm landed; a track records how much AND what kind each piece of it was. If your system says a wound is bashing, lethal or aggravated, and which one it was still matters after the blow, because aggravated heals slower or cannot be soaked or is what finally kills, then that kind has to live somewhere after the roll, and only a mark carries one. A pool of points cannot: once damage is subtracted it is only a smaller number, and nothing on the sheet remembers which points were which. That is why combat.damageKinds is refused on a ruleset whose health is a pool rather than being quietly ignored. A pool can still have damageTypes, and an opponent can still resist or be immune to them, because that is a question about how much of the blow lands rather than about what the wound is afterwards.

{
  "id": "harm",
  "label": "Harm",
  "min": 0,
  "max": 4,
  "levels": [
    { "label": "Scuffed", "penalty": 0 },
    { "label": "Winded", "penalty": -1 },
    { "label": "Bleeding", "penalty": -3 },
    { "label": "Down", "penalty": -99 }
  ],
  "kinds": [
    { "id": "knock", "label": "K", "severity": 0 },
    { "id": "tear", "label": "T", "severity": 1 }
  ]
}

The rules, exactly, because a vague reading produces the wrong track:

Marking it in play. The Game Master writes [sheet: op="damage" track="harm" kind="knock" amount="1"], and heals with a negative amount. On a track that fills by box it adds box="3" for the box the hit lands on. The pool form of damage, which names pool= instead, is unchanged. The plain track command is refused on a wound track: a bare number cannot say what the new marks are. The player can also mark and clear boxes by hand on the sheet, which is what these systems expect.

A fight can mark one too. Point combat.health at the track instead of a pool and the fight marks it: a blow that lands marks the boxes combat.damageKinds.marks says it does, of the kind that block maps its damage type onto, and a character whose track is full is down, which is what your dying rule reads. On a track that fills by box, per-point damage names the box it lands on and per-blow aims at the first, and a blow that no box can take puts the character down, which is what being taken out means in the systems that keep such a track. Healing clears one mark, the lightest, of any kind. Temporary points are refused, because a track has no buffer for them to sit in. The Engine reads a track as the levels it has LEFT, so everything else about a fight, going down, being revived, the log and the recap, is unchanged.

A rest can heal a wound track. A restore step naming one with "to" clears it down to that many marks, overflow and all; one naming it with a NEGATIVE "by" clears that many, overflow first, so "by": { "const": -2 } clears two. A step that also names a kind clears only marks of that kind: Gravewatch's "Catch your breath" is { "track": "harm", "kind": "knock", "to": "min" }, which mends the knocks and leaves the tears. A step that would ADD marks does nothing, because a rest marks no harm, and that includes a positive "by": it is skipped without a word, so write the minus sign.

Numbered boxes instead of named levels

Where a track is as long as a character is tough, give it boxes instead of levels. It has as many boxes as its own max, which may be a value the sheet works out (never one that reads the live state), up to 64:

{
  "id": "strain",
  "label": "Strain",
  "min": 0,
  "max": { "derived": "strain_boxes" },
  "boxes": {
    "penalty": {
      "by": "remaining",
      "table": [
        [0, -1],
        [1, 0]
      ]
    }
  },
  "kinds": [{ "id": "strain", "label": "S", "severity": 0 }],
  "fill": "indexed",
  "onFull": "refuse"
}

Filling by box, and tracks that refuse

Levels a list adds

"extra": { "list": "scars", "countColumn": "levels", "penaltyColumn": "penalty" } on a track with named levels lets a list on the sheet lengthen it per character. Each row, in order, adds its count of levels (at most 16) at its penalty, after the last level whose penalty is at least as good, named after that level when the penalties match and by the penalty otherwise. Gravewatch's scars work this way. A list the sheet hides adds nothing, and if a row is deleted while its levels are marked, the marks that no longer fit are kept as overflow: harm is not undone by editing the sheet.

boxes, fill, onFull, extra and a rest step's kind are Capability API 1.40 for a packaged ruleset.

The penalty on your rolls

resolution.penaltyFrom names the wound track whose penalty applies to every check this ruleset rolls. It is declared rather than assumed, so a ruleset that leaves it out rolls exactly as it did before wound tracks existed.

What the penalty DOES is your resolution kind's business, exactly like the sheet's own number:

The track it names has to be a wound track. A plain track carries no penalty to apply, and naming one is refused at import. The result says which penalty was applied, so a player can see why they rolled fewer dice, and the Game Master's own sheet block shows the rung and what it costs.

Modifiers off the sheet

Some numbers on a sheet ride along on every roll they touch: a heavy pack on every climb, an armour penalty on every sneak, a blessing on everything. resolution.adjust says so, on either kind:

"adjust": [{ "value": { "derived": "burdened" }, "abilities": ["brawn"] }]

The result says what the sheet added (adjust="-2" on the record, and a line on the dice card), so a player can see why a roll came out as it did. It is Capability API 1.38 for a packaged ruleset.

Rests

A rest is a list of restore steps and things to clear. Each step names one target (pool, poolGroup, listPools, track, or state) and either sets it ("to": "max", "to": "min", or a number) or changes it ("by": { "const": 1 }, or "by": { "fractionOfMax": 0.5 }). A step naming a wound track can only heal it, so its by is negative; see above. A step naming a live state only sets it, to "default" or one of its values. A poolGroup step with a fractionOfMax changes each pool of the group by its own fraction; add "shared": true (Capability API 1.77) to work the fraction out on the whole group's maximum instead and hand it to the group's pools in the order the sheet lists them, each up to what it is missing. 5e's long rest, "half of the character's total number of" hit dice kept in one pool per die, is { "poolGroup": "hit_dice", "by": { "fractionOfMax": 0.5, "round": "down", "min": 1 }, "shared": true }.

Game Master text

Full GM rules reference

The optional top-level gmReference holds the ruleset's full rules reference for the Game Master, up to 16,000 characters. Put extended adjudication rules here and keep the short check and resource instructions in gm.checkGuidance and gm.sheetGuidance. Those fields, and gm.worldGuidance, still have their 1,500-character limits; layer guidance keeps its existing limits too.

"gmReference": "When a journey crosses hostile country, judge supplies, shelter and exposure before the next leg. A failed navigation check costs time rather than silently moving the party to its destination."

The reference appears under Game Master rules reference in the stable system prompt, once per request. It is never copied into the per-turn reminder. Like other prompt-safe text, it must be one line, without control characters, square brackets or macro braces ({{ and }}). When the request still exceeds the context limit after the usual history and reply budget adjustments, the reference is shortened with a marked cut before core instructions are cut. Leave it unset to keep the existing prompts. A package using it must declare manifest schemaVersion: 2 and capabilityApi: { "major": 1, "minor": 145 } or newer.

Party and scene progress clocks (clocks)

Capability API 1.143 adds an optional top-level clocks object. Omit it to keep clock commands disabled. The object is strict: only these three keys are accepted.

"clocks": {
  "sizes": [4, 6, 8],
  "max": 8,
  "text": "Create clocks for looming threats and long projects. Tick them when the fiction advances."
}

sizes allows one to six unique integer segment counts, each from 2 to 12; it defaults to [4, 6, 8]. max is the maximum number of clocks kept at once, from 1 to 12, and defaults to 8. Optional text is one prompt-safe line of up to 400 characters explaining when the GM should make and tick clocks. An empty object enables the defaults.

Clocks belong to the party/scene, not to a character. The GM reminder lists their names and fill every turn and teaches these commands:

an existing name (case-insensitive), or reaching max refuses creation.

fill is bounded to 0..size. A full clock is done, remains visible, and counts toward max until cleared. A negative tick can reopen it.

Names are single lines of 1–100 characters with no double quotes, square brackets, control characters or macro braces. Each tag requests exactly one action. Commands apply in reply order, so a clock can be created and ticked in the same reply. The Engine rewrites each tag with result="ok" now="4/6" (or now="cleared"), or with result="refused" reason="...". Model-supplied outcome attributes are discarded. Unknown clocks and malformed requests are refused; at most 40 commands apply per reply.

Game Mode shows a segmented circle, name and fill beside the party status. Tags stay in the saved reply as receipts and are hidden from narration. Clocks are stored in each game-state snapshot as rulesetClocks; missing data means no clocks. Regenerating starts from the preceding snapshot, continuing starts from the current one, and selecting or removing swipes/messages restores clocks with the rest of that snapshot.

Party sheet: a crew, ship or shared Story Points

Capability API 1.144 adds an optional top-level party, beside sheet. It holds one shared sheet per game, such as a Blades crew, a Lasers & Feelings ship, or Genesys Story Points:

"party": {
  "label": "Crew",
  "fields": [
    { "id": "heat", "label": "Heat", "type": "number", "min": 0, "max": 9, "default": 0 },
    { "id": "vault", "label": "Vault capacity", "type": "number", "min": 0, "max": 16, "default": 8 },
    { "id": "upgrades", "label": "Upgrades", "type": "text", "maxLength": 400 }
  ],
  "pools": [{ "id": "coin", "label": "Coin", "max": { "field": "vault" }, "start": "empty" }],
  "advancement": { "fields": ["heat", "vault", "upgrades"], "text": "Record heat after a score." },
  "text": "Coin belongs to the crew."
}

fields uses the same field schema as sheet.fields; pools uses sheet.live.pools, including start, group, hideWhen and allowTemp. At least one field or pool is required, and their ids must be unique within the party sheet. Character and party ids may coincide. Party sheets have no sections, lists, abilities, derived values or character-sheet migrations in this minor. A pool's max reads a constant or a numeric party field. Character formulas cannot read party values, and party formulas cannot read character values. hideWhen also names party fields only.

advancement.fields names number, text or longtext fields the GM may change permanently. advancement.text and text each take one prompt-safe line of at most 400 characters. Optional summary reuses gm.sheetSummary: its fields select party fields; derived and lists must be empty. Without a summary, all visible party fields are shown. Pools always appear.

[sheet: sheet="party" op="restore" pool="coin" amount="3"]
[sheet: sheet="party" op="spend" pool="coin" amount="2"]
[sheet: sheet="party" op="advance" field="heat" by="2"]
[sheet: sheet="party" op="advance" field="upgrades" to="Hidden vault"]

sheet="party" selects the shared sheet; leave who out. Existing who="party" still applies to every character and never changes the shared sheet. This minor permits spend, restore and field advance on the party sheet. Unknown sheets, unsupported operations, insufficient pools, undeclared advancement and invalid values are refused and rewritten with the Engine's result. Party pools follow the same current/maximum arithmetic and defaults as character pools.

The GM receives the party label, fields and pools beside the character sheet blocks each turn. Game Mode offers a collapsible read-only party sheet beside the character portraits, using the same sheet presentation. Permanent values live once in chat metadata (gamePartySheet), beside gameCharacterCards, and share advancement receipts. Live pools use the reserved $party entry of rulesetLive; normalized character names cannot collide with it. Regenerate starts before the reply, and swipe selection or deletion restores both permanent and live values through the existing history. Rulesets without party keep their existing behavior.

Shared party creation (Capability API 1.163). A party sheet may carry lists (up to 12, written as the character sheet's lists), a guided builder of its own, and fromMembers: up to 8 party number fields read off the party's characters, each { "field", "count": true } (how many characters there are) or { "field", "sum": "<character number field>" } (what theirs add up to, such as Totem dots). Member fields are worked out from the characters as they stand whenever the shared sheet is read, so a character joining or leaving changes them at once. With lists, the party summary.lists may name them.

The builder is written exactly as a character's (pools, steps, priced rows, spend offers, profiles) and is held to the same rules against the party's own fields and lists. Its catalog and row-copy steps may read the ruleset's catalogs that feed party lists. In Game Mode any player opens the shared sheet's Edit, which offers the guided setup, and saves it for the whole party. Each save names the revision it was opened on (gamePartySheetRevision in chat metadata, raised by every save and by the story's own changes): a save opened before someone else's landed is refused, the newer sheet is shown, and the player's draft stays open, so saving again is their choice to replace it.

"party": {
  "label": "The Crew",
  "fields": [
    { "id": "tier", "label": "Tier", "type": "number", "min": 0, "max": 4, "default": 0 },
    { "id": "size", "label": "Crew size", "type": "number", "min": 0, "max": 12, "default": 0 }
  ],
  "lists": [{ "id": "upgrades", "label": "Upgrades", "columns": [{ "id": "name", "label": "Name", "type": "text" }] }],
  "builder": { "steps": [{ "id": "upgrades", "label": "Upgrades", "kind": "rows", "list": "upgrades" }] },
  "fromMembers": [{ "field": "size", "count": true }]
}