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. |
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.
damagehealapplyConditionremoveConditionadjustResourcesetFlaglearnLoregrantItemremoveItemadjustReputationsetDispositionadjustCurrencymoveemitnoiseifrepeatforEachletEffects 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
[ { "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": { "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
1d20 | One twenty sided die. |
2d6+3 | Two six sided dice plus three. |
4d6kh3 | Roll four, keep the highest three. |
2d20kh1 | Advantage. Not an engine special case, just notation. |
2d20kl1 | Disadvantage. |
1d8+1d4-1 | Several 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"?