Skip to main content

RPG Script (Stats)

RPG Script (.rpgs) is a compact language for defining base and calculated stats. It compiles into the same JSON format the app uses internally.

Scope (current): stats + formulas only (StatFormulaComponent trees).
Not supported yet: authoring effects/mechanics via RPG Script.


File structure

A .rpgs file contains a list of stat declarations:

  • base declarations define default values.
  • calc declarations define a formula (an expression) that compiles into a formula JSON tree.

Declarations

Base stat

Syntax:

base <type> <id>(<meta args>) = <literal>;

Example:

base integer level(name="Level") = 1;
base string character_name(name="Name") = "";
base bool is_npc(name="NPC?") = false;

Important: base defaults are literal-only (numbers, strings, bool, null, list literals).

Types

Supported type forms:

  • string
  • integer
  • decimal
  • bool
  • resource<resource_type> (e.g: resource<spell>)
  • Arrays: T[] (e.g. integer[], resource<spell>[])

Examples:

base resource<spell> favorite_spell(name="Favorite Spell") = null;
base resource<spell>[] prepared_spells(name="Prepared Spells") = [];

Comments

  • Line comments: // ...
  • Block comments: /* ... */

Expressions

Calculated stat expressions compile into StatFormulaComponent JSON.

Literals

  • "text"
  • 123, 12.34
  • true, false
  • null
  • List literals: [1, 2, 3], ["a", "b"], []

Identifiers

An identifier is treated as a stat reference:

strength + proficiency_bonus

Compiles as

{
"type": "add",
"components": {
"type": "list",
"components": [
{
"type": "stat",
"stat": "strength"
},
{
"type": "stat",
"stat": "proficiency_bonus"
},
]
}
}

As with regular RPG JSON, Identifiers may include . and ? inside the name. Names starting with $ are reserved for lambda params / special variables

Operator precedence

From lowest → highest:

  1. if ... then ... else ...
  2. ||
  3. &&
  4. ??
  5. ==, !=
  6. <, <=, >, >=
  7. +, -
  8. *, /, %
  9. unary !, unary -
  10. grouping (...), calls, literals

Special expressions

Null coalescing: ??

a ?? b

Compiles into:

{
"type": "defaultIfNull",
"components": <a>,
"default_components": <b>
}

If/then/else

if condition then x else y

This is lowered to a single when component with clauses, including a final true fallback.

When expression

when {
cond1 -> value1,
cond2 -> value2,
else -> fallback
}

This compiles to:

{
"type": "when",
"clauses": [
{ "condition": <cond1>, "components": <value1> },
{ "condition": <cond2>, "components": <value2> },
{ "condition": { "type":"constant","value_type":"bool","value":true }, "components": <fallback> }
]
}

Function / component calls

A call compiles to a component JSON object:

contains(haystack=spells, needle="Fireball")

Compiles to:

{ "type": "contains", "haystack": <...>, "needle": <...> }

Positional first argument sugar

The first argument may be passed without a name — it becomes components = <expr>:

add( [1, 2, 3] )          // sugar for add(components=[1,2,3])

After the first argument, all arguments must be named.

Lambdas (map / reduce / filter / sortBy / findFirst / flatMap)

Lambdas are only valid as arguments to supported components.

Lambda forms: (x, y) => expr, (z) => expr2

Operator behavior notes

+ chooses between add and concat

  • If any operand is a list literal, + becomes concat.
  • If any operand is a string literal, + becomes concat.
  • If all operands are numeric literals, + becomes add.
  • Otherwise (stats / calls / mixed), it defaults to numeric semantics (add) and the evaluator decides validity.

- becomes n-ary subtract

  a - b - c

subtract(list(a,b,c))

*, &&, || are flattened to n-ary

a * b * c     -> multiply(list(a,b,c))
a && b && c -> and(list(a,b,c))
a || b || c -> or(list(a,b,c))

Practical examples

Proficiency bonus (rounded up)

calc integer proficiency_bonus(name="Proficiency Bonus", abbreviation="PB") =
1 + divide(components=[level, 4], rounding_method="up");

Conditional stat (if → when)

calc integer speed =
if has_armor then 25 else 30;

Defaulting null

calc string title = nickname ?? character_name;

Complex example

calc integer classes_hp_mods(name = "Classes HP mods") =
max(
[
0,
(level * max(
map(
classes,
mapper = ($class) => add(
map(
($class.class.selected_archetype?.actual_hp_modifiers ?? $class.class.actual_hp_modifiers),
mapper = ($mapValue) => metaStat(meta_stat = concat([$mapValue, "_modifier"])),
),
),
),
)),
],
);