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:
basedeclarations — static default values for stats.calcdeclarations — computed stats defined by a formula expression.mechanicdeclarations — 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:
basedeclarations define default values.calcdeclarations 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:
stringintegerdecimalboolresource<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.34true,falsenull- 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:
if ... then ... else ...||&&??==,!=<,<=,>,>=+,-*,/,%- unary
!, unary- - 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
listliteral,+becomesconcat. - If any operand is a
stringliteral,+becomesconcat. - If all operands are
numericliterals,+becomesadd. - 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):
| Argument | Type | Description |
|---|---|---|
name | string | Human-readable name for the mechanic (documentation only). |
description | string | Description of what this mechanic does (documentation only). |
event_names | string | Comma-separated event names that trigger this mechanic. |
calculated_event_names | formula → string[] | Formula returning additional event names. Appended to event_names. Use eventName() here if targeting another resource's mechanic. |
revert_event_names | string | Comma-separated event names that cause this mechanic to revert its reversible effects (e.g. setStat). |
calculated_revert_event_names | formula → string[] | Formula returning additional revert event names. Appended to revert_event_names. |
revert_on_remove | formula → bool | If 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.
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.