Skip to main content

Code frameworks

Lists of content

Let a character gain content in play: declare a list, change it with actions, and show it on the sheet.

What a list is

A list holds content that a character gains in play, such as a pack of trinkets. The content is another Framework, and its presets are the entries. A list lives in the ledger and is not a constructor field. Only actions change it.

OptionMeaning
labelThe name of the list.
ofThe id of the content Framework.
maxHow many entries the list holds, from 1 to 128.
cardThe Frame that draws an entry as a card. It must be a Frame of the content Framework.
summaryUp to 4 fields of the entry to show on its row.
effects, caps, distinctByEntries that change modifiers, and limits on them. See Pools and modifiers.

Row lists, which add cells to each row, exist and are not covered yet.

Declare a list

Add the list to the Framework that owns it. The content Framework holds the entries as presets. The Grow Lantern tutorial builds the Trinket Framework step by step.

schemas/keeper.json
  "lists": {
    "pack": {
      "label": "Pack",
      "of": "trinket",
      "max": 2,
      "card": "trinket-card",
      "summary": ["weight"]
    }
  }
  1. Line 45 The content Framework whose presets are the entries.
  2. Line 46 The list holds at most this many entries.
  3. Line 47 The Frame that draws an entry.
  4. Line 48 Fields to show on an entry's row. A summary field needs a closed domain: options, a minimum and maximum, or true and false.

Add and remove

A list changes only through actions. Take the entry as a ContentRef input and call list.add with an offer.

rules/pack-trinket.ts
export function pack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.add(self, "pack", trinket, offer.where({ weight: { lte: 3 } }));
}
  1. Line 1 The entry the player picks.
  2. Line 2 list.add checks the offer first. This offer allows entries that weigh 3 or less.

Remove an entry with list.remove.

rules/unpack-trinket.ts
export function unpack_trinket(self: ElementRef, trinket: ContentRef): void {
  list.remove(self, "pack", trinket);
}
  1. Line 2 Removes the picked entry. It is refused if the list does not hold it.

When a player uses the button, Fabled asks them to pick one of the entries that the offer allows. Use list.count to stop an add by a rule of your own, as the Writing rules page shows.

Offers

offer.where takes conditions on the fields of the entry. You write the conditions as literals, so an offer cannot read the character. To depend on the character, test it with if before list.add.

OperatorMeaningExample
eqThe field equals the value.{"slot":{"eq":"Ring"}}
neThe field does not equal the value.{"slot":{"ne":"Ring"}}
gteThe number is at least the value.{"weight":{"gte":2}}
lteThe number is at most the value.{"weight":{"lte":3}}
inThe field equals one of the values. Up to 64 values.{"slot":{"in":["Ring","Cloak"]}}
hasThe field is a comma separated text that holds one of the values.{"tags":{"has":["fire"]}}

Every condition of an offer must hold, and an offer names at most 6 fields. A field that the entry does not have never matches.

Show it on the sheet

A Frame that shows a list uses version 2.0. Declare a region in the dataContract, then draw it. A region can also give each entry up to 4 buttons, each running an action on that entry.

layouts/keeper.json
      {
        "kind": "region",
        "id": "pack_list",
        "listBinding": "{{element.lists.pack}}",
        "emptyText": "Your pack is empty.",
        "itemActions": [{ "actionRef": "unpack_trinket", "label": "Unpack" }]
      }
  1. Line 46 Shown while the list is empty.
  2. Line 47 A button on each entry. The action must take only the entry.

Test it

Test each refusal that the offer and the size can give. The steps below add, refuse three ways, remove, and refuse a remove.

scenarios/pack.json
{
  "id": "pack-trinkets",
  "framework": "keeper",
  "parameters": {
    "name": "Mira",
    "max_hp": 10,
    "calling": "Guard",
    "lamp": "lamp"
  },
  "expectedValues": {
    "name": "Mira",
    "max_hp": 10,
    "hp": 10,
    "calling": "Guard",
    "guard": 3
  },
  "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } },
  "actions": [
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [{ "op": "list.add", "key": "pack", "value": "rope" }]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "ALREADY_IN_LIST",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "anvil" },
      "expectedRefusal": "NOT_OFFERED",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "candle" },
      "expectedWrites": [
        { "op": "list.add", "key": "pack", "value": "candle" }
      ]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "bell" },
      "expectedRefusal": "LIST_FULL",
      "expectedWrites": []
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [
        { "op": "list.remove", "key": "pack", "value": "rope" }
      ]
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "NOT_IN_LIST",
      "expectedWrites": []
    }
  ],
  "expectedLists": { "pack": ["candle"] }
}
  1. Lines 19-24 Accepted: the rope is added.
  2. Lines 25-31 Refused: the pack already holds the rope.
  3. Lines 32-38 Refused: the offer does not allow the anvil.
  4. Lines 47-53 Refused: the pack holds 2 entries, which is its maximum.
  5. Lines 54-61 Removed: the rope leaves the pack.
  6. Lines 62-68 Refused: the rope is not in the pack any more.
  7. Line 70 The pack ends with the candle only.

A scenario can also start with a list that already holds entries. Name them in lists, by preset id, and check them with expectedLists.

scenarios/seeded.json
{
  "id": "seeded-pack",
  "framework": "keeper",
  "parameters": {
    "name": "Mira",
    "max_hp": 10,
    "calling": "Guard",
    "lamp": "lamp"
  },
  "expectedValues": {
    "name": "Mira",
    "max_hp": 10,
    "hp": 10,
    "calling": "Guard",
    "guard": 3
  },
  "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } },
  "lists": { "pack": ["rope"] },
  "expectedLists": { "pack": ["rope"] }
}
  1. Line 18 The list the Element starts with.
  2. Line 19 The list after the scenario.

SAME_NAME and LIST_CAP come from the limits of a list that applies effects. The Pools and modifiers page proves them.