Building characters
Character creation and levelling up are mostly choices: a class, a playbook, skills, a fighting style, cantrips, a subclass at level 3. The packs declare them, the engine says which are waiting for an answer and which options are eligible, and your app decides how to ask.
Three ways to create a character
Section titled “Three ways to create a character”Every game can be created the same three ways, and the pack says which it supports.
The ruleset lists its creation starts in ruleset.creation:
"creation": [ { "id": "classless", "action": "mork-borg-core:create-character", "params": { "classless": true }, "random": true, "pack": "mork-borg-core" }, { "id": "class", "action": "mork-borg-core:create-character", "random": true, "pack": "mork-borg-core" }]Name each start with creation.<id>.name in its pack’s text. For a new character, make the file
with creating: true, then offer:
| Mode | Offer it for | Run |
|---|---|---|
| Random | a start with random |
the start’s action, policy { dice: auto, choices: auto, allocations: auto } |
| Guided | every start | the same, policy { dice: pause, choices: pause, allocations: pause } |
| Manual | always | nothing: the player fills in the blank sheet |
const character = { specVersion: '0.1.0', id: newId(), packs, creating: true };const start = ruleset.creation[0];let run = startAction(ruleset, character, { action: { id: start.action }, params: start.params, policy: { dice: 'pause', choices: 'pause', allocations: 'pause' }, // guided});// Put each pause to the player and continueAction with the answer, as in Rolling dice.A guided run is a wizard: each pause is a question. Besides dice and choices, creation uses
allocations (needs.kind: 'allocate'): spend a budget across fields (FitD’s four action
dots, 5E point buy) or hand out a list of values (5E’s standard array, Fate’s skill pyramid).
needs.allocate lists the fields, and the budget or the values. Each field has its computed
value and its input value, before effects, so you can show “8 + 7 point buy + 2 racial”; on
a budget it also has its limit and costs, the cost of each amount from 0, so the points
left are budget less the sum of costs[amount] (5E’s point buy: [0, 1, 2, 3, 4, 5, 7, 9]).
Answer with { amounts }, a map of field ID to amount:
continueAction(ruleset, run, { amounts: { 'fitd-scoundrel:ratings.prowl': 2, 'fitd-scoundrel:ratings.survey': 1, 'fitd-scoundrel:ratings.command': 1, },});A creation action can also ask the picks the content it adds brings (5E’s skills and fighting
style, a Mothership class’s skills, FitD’s Veteran), one choice at a time. Such a pause carries
needs.choice: the choice’s path, owner and id, and its min, max and picked so far,
for “pick 2 of 6”. Name the question with the choice’s own text,
<owner>.choices.<id>.prompt, and the options as you name listOptions results. #done ends
the whole step, not just this choice: every choice still pending is left for later, so label it
“Skip the rest” (actions.options.done.pending.name).
The finished run proposes the changes that build the character, ending with finishCreation.
Applying them is the player’s confirmation. finishCreation clears creating, unless something
is still missing: then the batch fails with creation-incomplete, and the result’s issues
list what’s missing (a pending choice as too-few-picks). Apply the other changes on their own,
let the player resolve what’s listed (often the picks the added class or race brings), and apply
{ type: 'finishCreation' } when they’re done. A character made by hand finishes the same way.
While creating is true, the pack’s creation-only rules apply as ordinary rules issues (FitD:
no action above 2; Fate: the skill pyramid), so a manual builder shows them like any other.
Two kinds of input
Section titled “Two kinds of input”- Numbers the player supplies (ability scores, rolled hit points):
setthem. On a sheet filled in by hand, the engine checks only that the value fits the field (a whole number from 1 to 30), plus any creation-only constraint the pack has. A pack’s creation action may offer generation methods (5E’s offers 4d6 drop lowest, point buy and the standard array); your app can offer others (“my GM said 18”). - Content choices (a class, skills, a feat): go through the engine, as below.
Input fields, with their types and bounds, are in ruleset.fields (input: true). The pack’s
display hints often give an input widget too (Showing a sheet).
Starting content
Section titled “Starting content”Some content is added, not chosen. In 5E, a class is a copy in the character’s features, with
its level as state:
applyChanges(ruleset, character, [ { type: 'addItem', entry: { id: 'fighter', item: '5e-srd-5.1:classes.fighter', state: { level: 1, startingClass: true }, }, },]);What to offer here (every class) is a catalog, not a choice; see
Listing content. Other games declare their starting
picks as rules-level choices instead: a new FitD scoundrel has a pending special-abilities and vice
right away (and an optional playbook when the table has its own).
Which content a game adds and which it chooses is the pack’s design. An any-game builder works
from pending alone, and adds a “add content” screen per content type for the rest.
Pending choices
Section titled “Pending choices”After every change, computeSheet lists the choices that apply now and still need picks:
"pending": [ { "path": { "entry": "fighter", "choice": ["fighting-style"] }, "min": 1, "max": 1, "picked": 0 }, { "path": { "entry": "fighter", "choice": ["skills"] }, "min": 2, "max": 2, "picked": 0 }]pathsays where the choice lives: on an entry (entry), or at the rules level (noentry), then the choice ID. A nested choice continues the path through the picked option:{ choice: ['playbook', 'homebrew:playbooks.bruiser', 'special-ability'] }is the special ability of a table’s own playbook (the Bruiser offitd-ash.yaml), under the playbook choice.min,maxandpickedsay how many picks it needs and has.- 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.
The prompt text comes from the choice’s owner (Text and translation).
Choices can depend on level (“at level 3”), so new ones appear as the character grows. A choice
with required: false in the pack is never pending; your app can still offer it.
Every choice, answered or not
Section titled “Every choice, answered or not”A wizard with Previous and Next needs the answered steps too. sheet.choices lists every choice
that applies, in the same order as pending, answered or not:
"choices": [ { "path": { "entry": "class", "choice": ["bonus-skills"] }, "owner": "mothership-1e:classes.marine", "id": "bonus-skills", "min": 1, "max": 1, "picks": ["trained"], "open": false }, { "path": { "entry": "class", "choice": ["bonus-skills", "trained", "trained"] }, "owner": "mothership-1e:classes.marine.choices.bonus-skills.options.trained", "id": "trained", "min": 2, "max": 2, "picks": ["mothership-1e:skills.computers"], "open": true }]picksare the options picked so far;openis true for the entries that are inpending.ownerandidname the prompt:<owner>.choices.<id>.promptin the owner’s pack (classes.marine.choices.bonus-skills.options.trained.choices.trained.prompt). A slot hasslot: { choice, number }: its prompt is underslot.choice, with{slot}filled in.- Inline options (the Marine’s “two Trained skills”) and picks that are references only (a
bard’s instruments) are listed here, though
activationsdoesn’t list them.
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
content that asks it).
Listing options
Section titled “Listing options”const result = listOptions(ruleset, character, { entry: 'fighter', choice: ['fighting-style'] });// { options: [{ option, eligible, picked, recommended, reasons }, …], min: 1, max: 1 }listOptions returns every option, including ineligible ones:
eligibleis false when picking it would cause rules issues, andreasonslists them. They’re exactly the issuesapplyChangeswould report if the player picked it anyway, so “needs Strength 13” reads the same in the picker and on the sheet.pickedmarks options already picked (useful when re-opening a choice to change it).recommendedmarks the option the pack recommends, if any.
Whether to hide, grey out or allow ineligible options is your call. A strict builder hides them; a table that bends rules shows them with their reasons.
Options are content IDs (translate with <id>.name), inline option IDs (key
<owner>.choices.<choice>.options.<id>.name), or plain values. A value may have text under the
same key; without it, name the value yourself (a content ID by its item’s name, a field ID by
the field’s). A value choice can depend on the character: 5E’s Expertise lists every skill and
marks the ones the character isn’t proficient in ineligible, and a FitD scoundrel’s crew dots
come from the linked crew sheet, so pass the links to listOptions too.
Some content choices pick a reference rather than something the character has: a bard’s three instruments are proficiencies, not instruments carried, so they add no actions or effects of their own.
An unknown path returns { errors: [{ code: 'unknown-choice' }] }.
Picking
Section titled “Picking”applyChanges(ruleset, character, [ { type: 'choose', path: { entry: 'fighter', choice: ['skills'] }, option: 'skill.athletics' }, { type: 'choose', path: { entry: 'fighter', choice: ['skills'] }, option: 'skill.perception' }, { type: 'choose', path: { entry: 'fighter', choice: ['fighting-style'] }, option: '5e-srd-5.1:fighting-styles.dueling', },]);One choose change per pick. Picking an option can make new choices pending (a picked
playbook brings its special abilities), so compute the sheet again after each batch and keep
going until pending is empty, or the player stops.
unchoose removes a pick and every pick nested under it, in the same all-or-nothing change.
Changing a pick is unchoose then choose in one batch.
Levelling up
Section titled “Levelling up”A level-up is ordinary changes in one batch. In 5E it raises a class copy’s level state:
applyChanges(ruleset, character, [{ type: 'adjustState', entry: 'fighter', key: 'level', by: 1 }]);Then compute the sheet: at fighter level 3, martial-archetype becomes pending. Put the new
pending choices to the player, and apply the level change and the picks together if you want the
whole level-up to be one undo step. 5E’s level-up action does this for you: after the level and
hit points it asks the choices the new level brings, as a creation action does.
Multiclassing is another class copy (addItem with level 1). A multiclass prerequisite the
character doesn’t meet comes back as an issue, not an error:
{ "code": "prerequisite-not-met", "id": "5e-srd-5.1:classes.fighter/multiclass", "source": { "type": "item", "id": "5e-srd-5.1:classes.fighter", "entry": "fighter" }}Legality is checked against the current character only. If Strength later drops below 13, the same issue appears, as if the class had been taken illegally. The engine has no history to consult, and that’s on purpose.
Items and templates
Section titled “Items and templates”- Buying or finding gear is
addItemwith a new instanceid. Carried content goes toinventory, the rest tofeatures; the engine picks from the content type. - Equipping is
equip. Effects that only apply while equipped (when: self.equipped) follow. - Per-copy state (charges, attunement, a lit Flame Tongue) is
setState/adjustStateon the entry. The state keys are declared by the content type, the item and its templates. - Magic properties like +1 are templates applied to a copy (
applyTemplate). To offer them, list the templates in the ruleset (content of typetemplate) whoseappliesTomatches the item;applyChangesrejects one that doesn’t apply withtemplate-not-applicable. A template can set fields too (a rarity, a bonus, a Sun Blade’s radiant damage); to show a copy’s fields, read the ruleset’stemplateFieldsand merge each template’sfieldsover the base item’s in the copy’s order, as the engine does foritem.