Skip to main content

RPG Script

RPG Script (.rpgs) is a compact language for defining your system's stats, mechanics, and views. It compiles into the same JSON format the app uses internally, saving you from writing verbose raw JSON by hand.

RPG Script covers:

  • base declarations — static default values for stats.
  • calc declarations — computed stats defined by a formula expression.
  • mechanic declarations — event-driven effects (e.g. restore actions on turn start).
  • Views — UI components used inside mechanic pop-ups and other interactive elements.

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"])),
),
),
),
)),
],
);

Mechanics

A mechanic declaration defines an event-driven effect — something the app executes automatically when a specific event fires.

Syntax

mechanic <id>(<meta_args>) = <effect_expression>;

Meta arguments (all optional except where noted):

ArgumentTypeDescription
namestringHuman-readable name for the mechanic (documentation only).
descriptionstringDescription of what this mechanic does (documentation only).
event_namesstringComma-separated event names that trigger this mechanic.
calculated_event_namesformula → string[]Formula returning additional event names. Appended to event_names. Use eventName() here if targeting another resource's mechanic.
revert_event_namesstringComma-separated event names that cause this mechanic to revert its reversible effects (e.g. setStat).
calculated_revert_event_namesformula → string[]Formula returning additional revert event names. Appended to revert_event_names.
revert_on_removeformula → boolIf true, reversible effects are automatically undone when the resource owning this mechanic is removed from the character. Defaults to false.

The body after = is an Effect expression.

Event name scoping

When you write an event name directly as a string literal (e.g. event_names = "my_event"), the app automatically appends the resource instance's unique id at runtime, so only that specific instance reacts to it. This prevents unintended cross-instance triggering (e.g. casting one spell shouldn't fire all spells).

If you fire an event from a formula (e.g. in fireEvent) and want to target a specific instance's mechanic, use eventName(stat = "id", event_name = "my_event") to build the fully-qualified name.

Example: restore actions at turn start

mechanic reset_actions_on_turn_advanced(
name = "Reset actions on turn advanced",
event_names = "turnAdvanced",
) =
when {
$event?.combatant?.id? == id -> sequence(effects = [
setStat(stat = "actions_remaining", new_value = 3, aggregation_type = "set"),
setStat(stat = "reaction_available", new_value = 1, aggregation_type = "set"),
]),
};

See the Combat Events reference for the full list of events and their payloads, and the Mechanic reference for advanced usage including lifecycle events.


Views

Views are UI components used in pop-ups displayed by mechanics (via showPopUp) or defined directly in the system JSON for interactive character sheet elements, resources (display, edit, list, search), etc. They let you build rich, dynamic UI driven by stat formulas.

Every view has an id field. Inside mechanic bodies or other formulas, you can read input values from a view using $view.<view_id>.

Syntax

Views are called like functions inside effect expressions:

showPopUp(
pop_up_title = "Edit HP",
pop_up_view = column(children = [
stepper(
id = "hp_input",
label = "Current HP",
value = current_hp,
min = 0,
max = max_hp,
),
]),
display_save_button = true,
on_save = setStat(stat = "current_hp", new_value = $view.hp_input, aggregation_type = "set"),
)

View types

See the Views reference for the full list of available view types and their individual argument docs (text, stat, ticker, popUpButton, resourceArray, select, etc.).

Tip: Install the RPGScript VS Code Extension to get full IntelliSense, hover docs, and tab-stop snippets for every view type and its arguments directly in your editor.


Annotations

@party_visible

Place @party_visible before a base or calc declaration to mark that stat as visible to other party members during a live session. These stats appear on the party view screen so the GM and other players can see them.

@party_visible
base integer current_hp(name = "Current HP") = 10;

@party_visible
calc integer armor_class(name = "AC") = 10 + dexterity_modifier;

If no stats are marked @party_visible, the app falls back to the stats defined in character_indexed_stats.