Code frameworks
Quickstart: your first code framework
Write a small game with one character, two actions and a sheet. Then check it and create a character in The Forge.
Before you start
- You need a paid plan to create a Game.
- Create a private game in The Forge. Choose CREATE YOUR OWN GAME, then Manual Editor, and create the game.
- Write the files in any editor, then paste them into the source editor, or type them there.
The game
Lantern Keepers has one character, the Keeper. A Keeper has a name, a maximum HP and the current HP. Two actions change the HP: Take damage and Heal. Heal never goes above the maximum.
Step 1: the manifest
The manifest names the project and lists every Framework and scenario file. Keep the projectId that the editor created for your game. The value in this example is only a placeholder.
{
"formatVersion": "fabled.game-project/v1",
"projectId": "game-your-game-id",
"projectKind": "GAME",
"compilerProfile": "native-game-v1",
"name": "Lantern Keepers",
"entryFramework": "keeper",
"frameworks": ["schemas/keeper.json"],
"scenarios": ["scenarios/new-keeper.json", "scenarios/heal.json"]
}- Line 3 Keep the projectId the editor made for your game. This one is a placeholder.
- Lines 4-5 GAME is a full game with a playable character. native-game-v1 is the only profile.
- Line 7 The Framework a player creates first.
- Lines 8-9 Every Framework file and every scenario file is listed here.
Step 2: the Framework
A Framework file declares what the player enters (inputs), what the Keeper can do (actions) and which Frame draws it. The constructor and each action point to a rules file.
{
"id": "keeper",
"name": "Keeper",
"elementType": "character",
"constructor": "rules/keeper.ts",
"inputs": {
"name": { "label": "Name" },
"max_hp": { "label": "Maximum HP", "defaultValue": 10 }
},
"actions": [
{ "id": "hurt", "name": "Take damage", "source": "rules/hurt.ts" },
{ "id": "heal", "name": "Heal", "source": "rules/heal.ts" }
],
"frames": [
{ "id": "sheet", "source": "layouts/keeper.json", "default": true }
]
}- Lines 2-5 Who the Framework is: its id, display name, the kind of Element it builds and the rules file that builds one.
- Lines 6-9 Inputs the player fills in. Each key matches a parameter of the constructor.
- Lines 10-13 Actions. Each one points to a rules file.
- Lines 14-16 The Frames that draw a Keeper. Exactly one is the default.
Step 3: the constructor
The constructor turns the inputs into the starting values. Its return type lists every stored field. A new Keeper starts at full HP.
export function construct(
name: string,
max_hp: number,
): { name: string; max_hp: number; hp: number } {
return { name: name, max_hp: max_hp, hp: max_hp };
}- Lines 1-4 The inputs, and the fields the constructor returns.
- Line 5 The starting values. A new Keeper starts at full HP.
Step 4: the actions
An action takes the Element first, then its own inputs. It changes values only through the ledger. Take damage adds a negative amount. When a player uses the button of an action with inputs, such as amount, Fabled asks for them in a dialog.
export function hurt(self: ElementRef, amount: number): void {
ledger.add(self, "hp", 0 - amount);
}- Line 2 ledger.add changes hp by an amount. Adding a negative number subtracts.
Heal reads two values, then sets the new HP. It does nothing when the Keeper is already at full HP, so that case writes nothing.
export function heal(self: ElementRef, amount: number): void {
const hp: number = element.var(self, "hp") as number;
const max_hp: number = element.var(self, "max_hp") as number;
if (hp < max_hp) {
ledger.set(self, "hp", math.min(max_hp, hp + amount));
}
}- Lines 2-3 Read the current and the maximum HP. element.var gives a value that you cast with as number.
- Line 4 Do nothing when the Keeper is already at full HP, so that case writes nothing.
- Line 5 Set hp to the smaller of the maximum and the healed value.
Step 5: the Frame
The Frame draws the sheet. Text shows values with {{element.attributes.name}} style bindings. Each button names an action alias that the data contract declares.
{
"version": "1.0",
"metadata": {
"name": "Keeper sheet",
"bindingMode": "bound",
"frameworkRef": "keeper",
"context": "BLADE",
"template": "standard-blade"
},
"dataContract": {
"requiredBindings": [
{ "binding": "element.attributes.name", "expectedType": "string" },
{ "binding": "element.attributes.hp", "expectedType": "number" },
{ "binding": "element.attributes.max_hp", "expectedType": "number" }
],
"optionalBindings": [],
"actions": [
{ "name": "hurt", "functionRef": "hurt" },
{ "name": "heal", "functionRef": "heal" }
],
"regions": []
},
"layout": {
"kind": "container",
"children": [
{ "kind": "text", "text": "{{element.attributes.name}}" },
{
"kind": "text",
"text": "HP: {{element.attributes.hp}} / {{element.attributes.max_hp}}"
},
{ "kind": "button", "label": "Take damage", "actionRef": "hurt" },
{ "kind": "button", "label": "Heal", "actionRef": "heal" }
]
}
}- Lines 3-9 metadata names the Frame and says which Framework it belongs to.
- Lines 10-22 dataContract lists the fields the Frame reads and the actions its buttons may call.
- Lines 23-34 layout is the tree that is drawn. Text shows fields with double braces, and buttons name an action alias.
Step 6: the scenarios
A scenario builds a Keeper and checks its starting values.
{
"id": "new-keeper",
"framework": "keeper",
"parameters": { "name": "Mira", "max_hp": 10 },
"expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 }
}- Line 4 The inputs to build the Keeper with.
- Line 5 Every value the constructor must return.
This scenario takes damage, heals past the maximum and heals again at full HP. Each step lists the exact writes the action must make. The last step lists none.
{
"id": "heal-after-damage",
"framework": "keeper",
"parameters": { "name": "Mira", "max_hp": 10 },
"expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 },
"actions": [
{
"action": "hurt",
"parameters": { "amount": 6 },
"expectedWrites": [{ "op": "ledger.add", "key": "hp", "value": -6 }]
},
{
"action": "heal",
"parameters": { "amount": 9 },
"expectedWrites": [{ "op": "ledger.set", "key": "hp", "value": 10 }]
},
{ "action": "heal", "parameters": { "amount": 3 }, "expectedWrites": [] }
]
}- Lines 7-11 Step one: take 6 damage. The action must make exactly one write, a ledger.add of -6.
- Lines 12-16 Step two: heal 9. The result is capped at the maximum, so hp is set to 10.
- Line 17 Step three: heal again at full HP. The guard skips the write, so no writes are expected.
Put it in The Forge
- Open your game in The Forge.
- In the Gameplay step, choose Advanced: source and versions to open the source editor.
- If the game has no project yet, choose Write it myself to start from editable examples. Do not choose Start building my game, which is the AI route. Replace the example files with the files above: use Add file for new paths and Remove file for files you do not need. Keep the projectId that the editor put in the manifest.
- Choose Check and preview. Fix every problem listed, then check again.
- Choose Save project.
- Choose Prepare my game. This creates private Frameworks, Frames and content from your saved version.
- Choose Create a character in this game and try Take damage and Heal on the sheet.
Break it on purpose
Change the field that Heal writes from hp to mana. The compiler stops and names the problem.
export function heal(self: ElementRef, amount: number): void {
const hp: number = element.var(self, "hp") as number;
const max_hp: number = element.var(self, "max_hp") as number;
if (hp < max_hp) {
ledger.set(self, "mana", math.min(max_hp, hp + amount));
}
}The check reports:
Action "keeper.heal" calls ledger.set(self, "mana", ...), and "mana" is not declared on this schema. Declared scalar fields: hp, max_hp, name.Set the name back to hp and the check passes. Rules can only write fields that the constructor returns.

