Skip to main content

Code frameworks

Framework schemas

Declare inputs, actions, derived values, Frames, content and child Frameworks for one kind of Element.

A Framework file

Each file in schemas/ declares one Framework. The reference pages grow the Keeper from the quickstart. It now has a Calling choice, a derived Guard value, a ready-made preset, a section and a child Lamp.

manifest/project.json
schemas/keeper.json
schemas/lamp.json
rules/keeper.ts
rules/lamp.ts
rules/hurt.ts
rules/heal.ts
rules/guard.ts
layouts/keeper.json
layouts/lamp.json
scenarios/new-keeper.json
scenarios/heal.json
scenarios/new-lamp.json

Core fields

FieldRequiredMeaning
idYesThe unique id of the Framework. The manifest and scenarios use it.
nameYesThe display name.
elementTypeYesWhat it builds: character for playable characters, item for objects, spell for spells. Custom types are allowed.
constructorYesThe path of the rules file that builds a new Element.
inputsNoWhat the player enters when creating an Element. Each key matches a constructor parameter.
actionsNoThings an Element can do. Each one has an id, a name and a rules file.
ledgerFunctionsNoDerived values. Each one targets a stored field and has a rules file.
framesYesOne to sixteen Frames. Exactly one has default set to true.
sectionsNoGroups inputs into steps of the creation wizard.
contentNoReady-made presets. Each has an id, a name and input values.
childFrameworksNoMaps a child slot name to another Framework of the same game.

In a GAME, the entry Framework in the manifest must be a playable character.

The entry framework must be a character constructor.

Inputs

An input customizes one constructor parameter, and its key must match the parameter name. Most inputs need only a label. The other keys narrow what a player can enter.

schemas/keeper.json
  "inputs": {
    "name": { "label": "Name" },
    "max_hp": {
      "label": "Maximum HP",
      "description": "How much damage a Keeper can take.",
      "defaultValue": 10,
      "min": 1,
      "max": 40
    },
    "calling": {
      "label": "Calling",
      "options": ["Guard", "Scout"],
      "defaultValue": "Guard"
    },
    "lamp": { "label": "Lamp", "defaultValue": "lamp", "hidden": true }
  },
  1. Lines 8-14 min and max bound a number. A scenario or preset outside the bounds fails.
  2. Lines 15-19 options gives a fixed list of choices. The default must be one of them.
  3. Line 20 A hidden input holds a value the player never sees. Here it wires the child Lamp.
schemas/keeper.json
schemas/keeper.json
{
  "id": "keeper",
  "name": "Keeper",
  "elementType": "character",
  "constructor": "rules/keeper.ts",
  "inputs": {
    "name": { "label": "Name" },
    "max_hp": {
      "label": "Maximum HP",
      "description": "How much damage a Keeper can take.",
      "defaultValue": 10,
      "min": 1,
      "max": 40
    },
    "calling": {
      "label": "Calling",
      "options": ["Guard", "Scout"],
      "defaultValue": "Guard"
    },
    "lamp": { "label": "Lamp", "defaultValue": "lamp", "hidden": true }
  },
  "childFrameworks": { "lamp": "lamp" },
  "sections": [
    {
      "id": "basics",
      "title": "Basics",
      "inputs": ["name", "max_hp", "calling", "lamp"]
    }
  ],
  "actions": [
    { "id": "hurt", "name": "Take damage", "source": "rules/hurt.ts" },
    { "id": "heal", "name": "Heal", "source": "rules/heal.ts" }
  ],
  "ledgerFunctions": [
    {
      "id": "guard",
      "name": "Guard",
      "field": "guard",
      "source": "rules/guard.ts"
    }
  ],
  "frames": [
    { "id": "sheet", "source": "layouts/keeper.json", "default": true }
  ],
  "content": [
    {
      "id": "mira-scout",
      "name": "Mira the Scout",
      "parameters": {
        "name": "Mira",
        "max_hp": 8,
        "calling": "Scout",
        "lamp": "lamp"
      }
    }
  ]
}
KeyMeaning
labelThe text shown to the player.
descriptionHelp text, up to 1024 characters.
defaultValueThe value used when the player leaves the input alone.
optionsA fixed list of 2 to 128 text choices, each up to 128 characters. The default must be one of them.
optionLabelsPlayer-facing labels for the options. The stored value stays the option.
min, maxBounds for a number. min must not exceed max.
minLength, maxLengthBounds for the length of a text.
requiredSet to false to let the player leave the input empty.
hiddenKeeps the input out of the wizard. A hidden input needs a defaultValue.
visibleWhenShows the input only while a condition on other inputs holds.

A default that is not one of the options is refused.

{
  "calling": {
    "label": "Calling",
    "options": ["Guard", "Scout"],
    "defaultValue": "Mage"
  }
}
Default for calling must be one of its options ("Guard", "Scout").

Scenarios and presets must fit the bounds too.

Parameter max_hp 99 is not between 1 and 40.

A hidden input without a default fails with:

Input name is hidden, so it must declare a defaultValue.

Actions and ledger functions

Each action has an id, a name and a source file. Each ledger function has the same, plus a field. The field is the stored value the function derives, and it must be a scalar field that the constructor returns. A ledger function reads values and returns the derived one. It never writes.

schemas/keeper.json
  "ledgerFunctions": [
    {
      "id": "guard",
      "name": "Guard",
      "field": "guard",
      "source": "rules/guard.ts"
    }
  ],
  1. Lines 36-39 id and name identify the function, field is the stored value it derives, and source is its rules file.
rules/guard.ts
function base_guard(calling: string): number {
  return calling === "Guard" ? 3 : 1;
}

export function guard(currentValue: number, self: ElementRef): number {
  const calling: string = element.var(self, "calling") as string;
  return base_guard(calling);
}
  1. Lines 1-3 A helper function. It has no export, and the ledger function calls it by name.
  2. Line 5 A ledger function receives its current value and the Element.
  3. Line 6 Read stored values with element.var.
  4. Line 7 Return the derived value.
Ledger function must target an existing scalar field.

Ids must be unique inside a Framework. Actions and ledger functions share one set of ids.

Duplicate semantic identity or source path.

Two ledger functions may share a source file. An action and a ledger function may not:

action source does not match its owner contract

Action inputs

The inputs of an action are the parameters of its function. The signature declares them, and the schema may add an entry for each one. Each input is a number, text or true/false value.

The player is asked for each input in a dialog when they use the button. The dialog names an input by its parameter name, unless the entry gives it a label. An entry can also give a description and a default value. A number input can have a min and a max. A text input can have a list of options or options from a table. The dialog shows them all, and the server refuses a value outside the bounds or the options. A button cannot supply a value. Name each parameter so a player can read it: roll, not r.

schemas/keeper.json
    {
      "id": "check_lore",
      "name": "Lore check",
      "source": "rules/check-lore.ts",
      "inputs": { "roll": { "label": "Roll", "min": 1, "max": 100 } }
    }
  1. Line 24 The entry for the roll input. The dialog shows the label and the server enforces min and max.

A min and a max do not stop a number with decimals. If the rule needs a whole number, it checks that itself, and a scenario step proves it. A scenario step outside the bounds is refused.

rules/check-lore.ts
export function check_lore(self: ElementRef, roll: number): void {
  if (math.floor(roll) === roll) {
    const lore: number = element.var(self, "lore") as number;
    ledger.set(self, "last_target", lore);
    ledger.set(self, "last_roll", roll);
  }
}
  1. Line 1 The roll is an input the player types.
  2. Line 2 Ignore a number with decimals. The input bounds already keep it between 1 and 100.
  3. Lines 4-5 Write the target and the roll.

A text input can instead offer a list of choices from a rulebook table. The Rulebook tables page shows how.

A text input can instead pick one of the Framework's stored fields by name, with a fields list. The Writing rules page shows an action that uses it.

A number input can ask the server to roll it. Add a roll key with one dice expression, such as "roll": "1d100" or "roll": "2d6+1". The dialog then shows a Roll button in place of a field the player types in. The server rolls and the dialog shows the total, which the player cannot change. Only a number parameter can be rolled. The compiler refuses a roll together with a defaultValue, and refuses a dice expression the server does not accept.

A rolled input is spent once: the action takes the roll when it runs, and a new roll voids the earlier unspent ones for that input. A roll is bound to its campaign and expires after 15 minutes. The Layouts and sheets page shows what the table sees.

Action log line

An action can post a result line to the table log. Add a log template to the action's entry in the schema, for example "log": "{name} takes {amount} damage, defense {defense}". The server fills each {name} placeholder as plain text after the action has been saved. A browser never sends the line.

A placeholder may name an input of the action, or a number, text or true/false field of the same Framework. The compiler refuses a hidden input, a health field (hp, current_hp, hit_points, temp_hp, current_health, max_health, hitPoints, and the health and condition fields of the combat block), a name that does not exist, a template longer than 200 characters, more than 16 placeholders, a stray brace and a control character. When an input and a field share a name, the input wins. The values come from after the action ran, so a field the action wrote shows its new value. A calculated field is not recalculated, so it shows the value it had before the action.

A player who acts on their own character sees the line in the same audience as their dice rolls: everyone, only them, or them and the GMs. In every other case, including a GM acting on any character, the line reaches only the campaign members who can read that character. If the audience is everyone but a member cannot read the character, the line goes as a private message to the members who can. The table shows the line under a Game label, so nobody mistakes it for a player's message.

Content presets

A preset is a named set of input values, such as a ready-made character. Fabled checks each preset like a scenario, so every value must fit its input. A game can hold up to 512 presets per Framework.

schemas/keeper.json
  "content": [
    {
      "id": "mira-scout",
      "name": "Mira the Scout",
      "parameters": {
        "name": "Mira",
        "max_hp": 8,
        "calling": "Scout",
        "lamp": "lamp"
      }
    }
  ]
  1. Lines 47-48 A preset has an id and a name.
  2. Lines 49-54 The input values the preset fills in. They must fit the input bounds.

Sections

Sections group inputs into steps of the creation wizard. When a Framework declares sections, every input must be listed in one of them, hidden inputs included.

schemas/keeper.json
  "sections": [
    {
      "id": "basics",
      "title": "Basics",
      "inputs": ["name", "max_hp", "calling", "lamp"]
    }
  ],
  1. Lines 25-27 A section has an id, a title and the inputs it shows. Every input must be in one section.
Input lamp is not assigned to any section.

Child Frameworks

A child Framework is a part of a bigger Element, such as the Lamp that a Keeper carries. The child has its own schema, constructor, Frame and scenario.

schemas/keeper.json
  "childFrameworks": { "lamp": "lamp" },
  1. Line 22 Maps the child slot lamp to the Framework with the id lamp.
schemas/lamp.json
{
  "id": "lamp",
  "name": "Lamp",
  "elementType": "item",
  "constructor": "rules/lamp.ts",
  "inputs": {
    "name": { "label": "Name" },
    "oil": { "label": "Oil", "defaultValue": 5 }
  },
  "frames": [{ "id": "card", "source": "layouts/lamp.json", "default": true }]
}
  1. Lines 6-9 The inputs of the child. A child needs a name too.
  2. Line 10 The child has its own Frame.

The parent maps a slot name to a Framework id in childFrameworks. A hidden input holds the reference. The constructor returns the child's inputs next to its own values.

rules/keeper.ts
export function construct(
  name: string,
  max_hp: number,
  calling: string,
  lamp: FrameworkRef,
): {
  values: { name: string; max_hp: number; hp: number; calling: string; guard: number };
  children: { lamp: { framework: FrameworkRef; params: { name: string; oil: number } } };
} {
  return {
    values: { name: name, max_hp: max_hp, hp: max_hp, calling: calling, guard: 0 },
    children: { lamp: { framework: lamp, params: { name: "Lantern", oil: 5 } } },
  };
}
  1. Line 5 lamp is a FrameworkRef. The platform turns the Framework id from the input into one.
  2. Lines 6-9 The return type lists the stored values and the child slots.
  3. Line 11 The starting values of the Keeper.
  4. Line 12 The child slot: which Framework to build and the inputs to give it.

Every Framework, children included, must return a name.

must define the required 'name' value, normally as a string constructor field.

A slot that points at a Framework outside the game fails with:

Invalid child Framework constraint: lamp.

Advanced fields

FieldPurpose
listsContent a character gains during play.
contentChoicesChoice points where the player picks one entry of a content Framework.
slotsPickers for repeatable choices.
poolsShared pools that number inputs are drawn from.
modifiersNumbers that the platform derives from the effects of content.
effectsHow a content Framework names a modifier and an amount.
levellingMoving a character up or down one level after creation.
rebuildWhat a rebuild does to the play state, such as a level change.
combatThe roles that the tabletop's combat tools read.
catalogueContent records kept outside the source and added in chunks.
tablesShared data tables.
playReadsDerived values that the tabletop reads from stored attributes.
rulesAttacksAttacks that every character has.
optionsFromTakes an input's options from another source.
rich, richTextMarks a text input as rich text.

Next