A trigger is an event, some conditions and a list of actions: when something
happens, if these things are true, do this. The Trigger module (F6) builds
triggers from pickers rather than from typed code. Every node has a known shape,
so an action that needs a region offers you the regions that exist and nothing
else.
EVENT every 600 ticks
CONDITION wave counter < 20
ACTIONS create 8 goblin_brawler at region "spawn_north" for player 5
order units in region "spawn_north" to attack-move to "lane_end"
add 1 to wave counter
show text to all players: "Wave {wave counter}"The game runs at 20 ticks per second, and every duration in a trigger is in ticks: 600 ticks is thirty seconds.
This page explains how the system fits together. Every node, field by field, is in the Trigger Reference, which is generated from the same catalogue the validator uses, so it cannot drift from what the game accepts.
The module
The Trigger module has six tabs: triggers, variables, regions, timers, functions and hashtables.
On the triggers tab, + Trigger adds one. A selected trigger shows a name
(editor-only; players never see it), Enabled and Run once checkboxes, and
four sections: Events, Conditions (all must hold), Locals and
Actions. The add buttons open a searchable picker grouped by category.
Triggers and sandboxed scripts
Triggers are typed JSON nodes. Their variables, arrays, loops, functions and hashtables remain useful for maps authored visually.
The Scripts module adds JavaScript and TypeScript through a pinned QuickJS interpreter. Author code receives the sos API, seeded randomness and persistent storage. Clocks, raw network/DOM access, imports and generated code are unavailable. Local maps can use the bounded, permissioned creator API bridge and per-game player saves. Fixed memory and instruction budgets bound each script. Read Your first sandboxed script for an example and the supported calls.
RTS matches keep their normal deterministic lockstep. A creator can explicitly select local simulation and a higher tick rate, with a prominent warning that multiplayer and deterministic replay become unavailable.
Events
An event is what makes a trigger run. The families:
- Time. Map initialisation, every N ticks, at an exact tick, a named timer expiring.
- Regions. A unit entering or leaving a region.
- Units. A unit dying, being trained, being attacked, a building completing, a hero levelling, an item being picked up, a projectile hitting.
- Players. A player being eliminated, finishing research, typing a chat line, pressing a key, clicking a trigger dialog button.
- Script. A global variable changing, a custom event raised by another trigger, a cutscene finishing.
A few details matter when you rely on them:
- Unit dies matches its filter against the unit as it was when it died, so a filter on owner or type still works on a unit that no longer exists.
- Projectile hits takes an optional
tagthat matches the shooter's authored tag, the spell id, the shooter's unit type or the projectile type. - Player chats matches
exact,prefixorcontains. It fires only for all-chat lines from a player with a seat: team chat, whispers and spectators' chat never reach a trigger. It works in single player and in multiplayer; the chat line travels as a normal game command. A line longer than 256 characters never reaches a trigger: it is dropped, not cut short. - Player presses key fires on key down and key up (event value 1 or 0).
Only keys some trigger listens for are sent at all, auto-repeat is ignored,
keys typed into a text box are ignored, and presses are capped at 20 per
second. It does not swallow the key: a map that listens for
astill letsado whatever the game hotkey does.
Inside the actions, the event context expressions read what happened: the triggering unit, the other unit (the killer, the attacker or the shooter), the triggering player, the event value (damage, button index, level) and the event text (the chat line, the key, the upgrade id).
Conditions and expressions
Conditions are expressions that must all be true. Expressions cover literals, arithmetic, comparison and logic, the current tick, a synchronised random number, player resources and population, unit properties, groups of units (in a region, of a player, of a type, with a tag), distances and points, timers, counters, text operations, and hashtable and function results.
Two conveniences worth knowing if you ever edit the JSON directly:
- Literal shorthand. Wherever a field takes an expression, a bare number,
string or boolean is accepted:
5means{ "k": "int", "v": 5 }. A bare string is always text. To read a variable, write{ "k": "var", "name": "lives" }. - A bare string is also accepted wherever a text template is expected.
Variables, locals and arrays
Global variables are declared on the variables tab with a type (int,
real, bool, string, unit, unitGroup, point, player, region or
timer), an optional initial value and an optional array size (up to 8192).
Locals belong to one run of one trigger. Each time the trigger fires it
gets fresh copies, so two waves spawning at once cannot overwrite each other's
bookkeeping. Locals survive a wait and a save and reload. A loop variable you
did not declare becomes a local automatically. Local arrays are capped at 1024
elements.
Conditions see globals only, because they are checked before a run exists.
Functions
A function is a named list of actions with typed parameters (up to 16),
optional locals, and an optional return type. Call it as an action
(callFunction, optionally storing the result) or inside an expression
(call). Functions live in the script's functions list:
{
"version": 1,
"vars": [],
"regions": [],
"functions": [
{
"name": "waveSize",
"params": [{ "name": "wave", "type": "int" }],
"returns": "int",
"actions": [
{ "k": "return", "value": { "k": "add", "a": 4, "b": { "k": "mul", "a": { "k": "var", "name": "wave" }, "b": 2 } } }
]
}
],
"triggers": []
}Three rules, all enforced by the validator:
- No
waitinside a function. A function can be called in the middle of an expression, and an expression cannot be paused halfway. A function that needs to happen later should start a timer or raise a custom event. - No
stopTriggerinside a function. Usereturn. - Recursion is allowed up to 32 calls deep. Past that the call returns the zero value of its type, identically on every machine, rather than crashing.
Hashtables
A hashtable stores values under two keys:
(unit id, "kills"), ("wave", 7). Keys are integers or strings, every value
is converted to the table's declared type, and a table holds up to 16,384
entries (64 tables per map). Writing a new key into a full table is dropped;
overwriting an existing one always works. Reading a missing key gives the zero
value.
Actions
- Control flow. If/then/else, while (at most 10,000 iterations), for each unit, for each integer, wait, run or enable another trigger, raise a custom event, return.
- Units. Create (up to 500 at once), remove, kill, set health or mana, change owner, make invulnerable, teleport, group membership, orders (move, attack-move, attack, stop, hold, patrol, follow).
- Economy and diplomacy. Set or add resources, change alliances, restrict where a player may build.
- Outcome. Victory and defeat.
- Timers. Start, pause, resume, stop.
- Interface. Text banners, counters, a leaderboard, dialogs with buttons,
minimap pings (a fading ring on the minimap), sounds, quest log entries, and
custom HUD frames with a title, text and a progress bar. A frame never takes
mouse clicks and never covers more than 40% of the screen's width or height;
its title is cut at 120 characters and its text at 2,000. Its colours
(
progressColorand the style's background, border and text colours) must be hex,#rgb,#rrggbbor#rrggbbaa. The validator reports anything else, such asred, as an error, and the game draws it in the default colour. - Camera. Pan, set zoom, lock to a unit or point, unlock, confine to a region, shake, reset. Each is shown only to the players you name and never affects the game itself.
- Cutscenes. Play and stop a scene; see Cutscenes.
- Physics. Give a unit a velocity with friction and bounce, for knockback and sliding mechanics.
- Save codes. Generate a code from named values; read one back with an expression. See below.
playSound accepts any sound the game ships and any custom sound the map or
its mod registers. A custom sound is an https:// URL or a data: URI; an
http:// URL is a validation error. Sounds are presentation only: a player
whose browser cannot decode one hears silence and plays the identical match.
Which seat a victory actually ends the match for
This one catches people out, so it is worth stating plainly.
Outside multiplayer, a victory or defeat action ends the match only for the
seat the player is watching from. A victory on any other seat is not an error
and not a no-op: it records that the seat won, and the match carries on. If you
test a single-player map with victory on the enemy seat, the trigger will run,
the validator will report nothing, and the match will simply keep going.
So a single-player map that is won by beating seat 1 does not say "seat 1 loses". It says the player's own seat wins, or it says "seat 1 is defeated" and "seat 0 wins", together:
victory player: 0 // the seat the player watches from — this ends it
defeat player: 1 // optional, and only affects seat 1's own statePairing the two is the habit worth having, because it is also what makes the map
behave the same way in multiplayer, where a victory resolves for the winning
team rather than for one watcher. The bundled arena starter template is
written that way and is the one to copy.
Two consequences follow, and neither is reported anywhere today:
- A result never names the beneficiary of a loss. A defeat reports no winner, so asserting a specific winner on the losing side never passes.
- Turning standard victory off means nothing else can end the match either, so a map with only a non-watched-seat victory runs until it hits a tick limit.
Save codes
Classic RPG maps let players type -save, write down a code, and -load
it next time. The same pattern works here: a player chats trigger with
pattern -save calls Generate save code with the values to keep, and
shows the code; a -load trigger reads it back with Read save code.
A code looks like SOS-9ABC-DEFG-…. It is tied to the map through the script's
saveCodeSalt, which defaults to map: followed by the map name. Two things
follow from that:
- Set
saveCodeSaltyourself before you publish if you might ever rename the map. Renaming a map without it invalidates every code players have saved. - A code cannot be edited by hand and does not load on a different map. It is not cryptographically secure: the key ships inside the map, so anyone willing to read the map file can forge one. That is the same bar classic save codes met, and it is the honest ceiling for a code the game has to verify offline.
Determinism rules you can see
Several parts of the design exist so two machines running the same trigger get the same result, and they show up in the editor:
- Arrays, triggers and actions run in the order you authored them.
- Per-tick events fire in a fixed order, so "a unit died" and "a timer expired" on the same tick always resolve the same way round.
- Unit groups are sorted lists, never sets.
- A loop over units takes a snapshot before it starts, so creating units inside the loop does not change what it iterates.
waitsuspends the trigger as data (a resume tick and a position), which is what lets a waiting trigger survive a save and reload.- Events raised by events drain with a hard cap of 64, so a cycle stops rather than hanging the match.
Every node is total at runtime: asking for a unit that has died yields zero or false, dividing by zero yields zero. A node that threw an error on one machine and not another would split the match, so a mistake degrades instead.
That runtime forgiveness is not an excuse for sloppy data, and the validator is strict about it. An unknown node, a misspelled field, an undeclared variable, a region, unit type, item, upgrade or sound id that does not exist: each is an error with a JSON path and, where something is close, a "did you mean". See Validation & Tooling.
Regions, points and cameras
A region is a named rectangle of tiles. Draw one with the Region tool in
the Terrain module; it appears in the Trigger module's region list straight
away. Regions are the anchor for most triggers: something entering one,
something being created in one, counting what is inside one.
Points are single named tiles. Cameras are named views for cutscenes.
Seeing what your triggers did
In a Test Map match, the Trigger Debug panel (Ctrl+Shift+F9) lists every
trigger that fired, every condition that came out false, each action that ran,
waits, cap hits, and every place a trigger read something that was not there.
Its Variables tab shows each global's current value. See
Testing.
Building a Tower Defense map
- Waves are one trigger each, on an at tick event (tick 600 is thirty seconds in). Each creates the wave's units into a unit group variable and gives that group a single move order to the goal region.
- Move, not attack-move. Attack-moving units stop to fight whatever they pass, towers included. When the showcase's creeps attack-moved, its boss outranged the towers, destroyed them from outside their reach and then destroyed the base, so the match was lost with every life intact.
- The path is the maze the player builds. A move order routes around the towers by itself, so no waypoint triggers are needed.
- Leaks are an enter-region trigger on the goal region: remove the unit and subtract a life.
- Lives are an integer variable and a counter.
- Buildable areas are the restrict-building action, so towers only go where you want them.
- Ending the match is a defeat action, which requires standard victory to be turned off in Players & Teams.
The bundled tower-defense-showcase map is built exactly this way.
Building a hero arena
Three things carry it: independent hero instances, so several players can each have their own copy of the same hero (see Units & Properties); the map rule that allows the same hero to be picked more than once; and per-map hero changes in Data Overrides. Hero selection is a dialog, a dialog-button event and a create-units action.
Interface actions and multiplayer
Text, counters and leaderboards are resolved inside the simulation and stored with the rest of the trigger state, so they are part of the checksum. "The counter said 12 on one machine and 11 on another" is caught as a desync rather than passing as a cosmetic difference. What reaches the screen is filtered to the local player, so a message sent to player 3 is not shown to player 4.
Dialog clicks, chat lines and key presses travel back as normal game commands, so they arrive on the same tick for everyone.
Next: Trigger Reference.