The DSL

Data cannot express behaviour, so one small JSON language covers everything that needs logic.

One language for ability effects, item procs, trap triggers, dialogue gates, loot rules, and quest objectives. It is sandboxed: a world you downloaded cannot run code. Chance goes through the seeded dice, so it replays. It is plain JSON, so the editor can render and check it without executing it.

Reading values

Everything readable goes through one mechanism.

{ "ref": "actor.attr.might" }
{ "ref": "actor.inventory.rope", "else": 0 }

There is no hasFlag, hasItem, or hasCondition operator. Those are ordinary paths.

Is a flag set?flags.met_vess
How many ropes?actor.inventory.rope
Is it burning?actor.conditions.burning
Faction standing?reputation.wardens
Quest status?quests.the_mill_door.status
Known lore?lore.the_drowned_bell
What day is it?world.day
Your own data?flags.morale. An entry's extra bag is not in scope.
A missing path is an error
It does not read as zero. Add else where a path is genuinely optional. Five namespaces are open and read as null instead: flags, quests, memory, reputation, lore.

Expressions

An expression produces a value.

refaddsubmuldivmodnegminmaxfloorceilroundabsclamprollcondconcatlengthlist
// floor((might - 10) / 2), the attribute modifier as content defines it
{ "floor": { "div": [ { "sub": [ { "ref": "actor.attr.might" }, 10 ] }, 2 ] } }

{ "roll": "2d6+3" }

{ "cond": { "gte": [ { "ref": "actor.res.hp" }, 10 ] },
  "then": "steady", "else": "failing" }

Predicates

A predicate produces a yes or a no.

allanynoteqnegtgteltlteinchanceexiststest
{ "all": [
    { "gte": [ { "ref": "actor.attr.might" }, 14 ] },
    { "test": { "ref": "flags.met_vess" } },
    { "not": { "exists": "actor.conditions.frightened" } }
] }

all on an empty list is true. any is false.

Effects

An effect is a thing that happens. Nineteen of them, including control flow.

damagehealapplyConditionremoveConditionadjustResourcesetFlaglearnLoregrantItemremoveItemadjustReputationsetDispositionadjustCurrencymoveemitnoiseifrepeatforEachlet

Effects do not write state. They produce a list of intents and the engine applies each one. The dead take no damage, resources stop at their limits, immune creatures shrug off conditions, and an unknown id is refused.

[
  { "damage": { "target": { "ref": "target.id" },
                "amount": { "add": [ { "roll": "2d6" }, { "ref": "actor.mod.might" } ] },
                "damageType": "fire" } },
  { "applyCondition": { "target": { "ref": "target.id" },
                        "condition": "burning", "duration": 3 } }
]

Control flow

Do something to every party membereffects
[ { "forEach": { "in": { "ref": "party" }, "as": "member",
               "do": [ { "heal": { "target": { "ref": "member.id" }, "amount": 5 } } ] } } ]

party is the only list the engine puts in scope. To hit every enemy, set the ability's targeting to allEnemies and the engine loops for you, running your effects once per target.

Repeat a fixed number of timeseffects
[ { "repeat": { "times": 3,
              "do": [ { "damage": { "target": { "ref": "target.id" },
                                    "amount": { "roll": "1d4" } } } ] } } ]

index is bound inside the loop.

repeat and forEach expose index. Bindings stay inside the loop. repeat stops at ten thousand iterations. Worked examples of all of this are on writing formulas.

Rules

A predicate and the effects it gates. Used by ancestry traits and item procs.

{ "when": { "test": { "ref": "flags.moonlit" } },
  "then": [ { "applyCondition": { "target": { "ref": "actor.id" },
                                  "condition": "emboldened" } } ] }

Dice notation

1d20One twenty sided die.
2d6+3Two six sided dice plus three.
4d6kh3Roll four, keep the highest three.
2d20kh1Advantage. Not an engine special case, just notation.
2d20kl1Disadvantage.
1d8+1d4-1Several terms.

Notation is checked at load.

Errors

Every failure carries a path and, where it can, a suggestion.

"fgte" is not a valid predicate operator (at content.abilities.2.when.fgte)
  did you mean "gte"?