Most people who build a custom game arrive with a design already in their head. This page maps the things a map usually needs onto the nodes that do them, points out what this engine does that you might not expect, and lists what genuinely is not here yet.
Every node id on this page is checked against the live node catalogue by a test
in the engine repository, capabilityMap.test.ts. If a node named here stopped
existing, that test would fail rather than this page quietly lying to you. The
"Not possible yet" list is checked the other way round: the
test fails when a capability listed there appears, so the list cannot silently
go stale either.
The short version
If you have used a trigger editor before, three things will be familiar: the event / condition / action shape of a trigger, an object editor (here: data overrides), and regions. You can also write sandboxed JavaScript or TypeScript, combine it with JSON triggers, and reuse the public arcade controllers. Build a custom game explains which layer to use. Some native engine capabilities remain unavailable to downloaded maps.
| If you are used to | Here |
|---|---|
| Triggers built from an event, conditions and actions | The same three parts, same order. Triggers |
| An object editor | Data overrides: patch any shipped unit, building, hero, item, spell or upgrade |
| Regions (rects) | Regions, declared once in the script and referenced by id |
| Variables, arrays, hashtables | All three, with declared types |
| Hand-written script | JavaScript and TypeScript in Scripts (F11), through the public scripting API |
| A small cap on the whole map file | Map JSON, scripts and embedded assets share 4,194,304 bytes; hosted art is fetched separately |
| A trigger stopping mid-run with no explanation | A per-tick budget that reports the trigger, the tick and the budget it blew |
| Handle leaks (locations, groups, effects) | Nothing to leak. There are no handles to destroy |
| Desyncs you could not diagnose | Nondeterminism is not expressible in the node set. Determinism for authors |
In ordinary RTS lockstep, a tick is one twentieth of a second. An "every 2 seconds" event is everyTicks: 40. Explicit local profiles can change that rate; see Timing and multiplayer.
Events
The event list is thinner than a mature editor's in the unit and ability areas, and broader everywhere else.
| You want to react to | Here | Notes |
|---|---|---|
| Map initialization | mapInit | Fires once, after every global initialiser |
| Time: elapsed game time is N seconds | atTick | One exact tick |
| Time: every N seconds of game time | periodic | Plus an offsetTicks so two waves can interleave |
| Time: expiration of a timer | timerExpires | |
| Unit: a unit enters a region | unitEntersRegion | Units already inside when the trigger arms do not count |
| Unit: a unit leaves a region | unitLeavesRegion | Also fires when a unit dies or is removed inside the region |
| Unit: a unit dies | unitDies | Event unit is the corpse's identity, other unit is the killer |
| Unit: a unit is attacked | unitAttacked | Event value is the damage, other unit the attacker. See the damage note below |
| Unit: a unit finishes training a unit | unitTrained | |
| Unit: a unit finishes construction | buildingComplete | |
| Unit: a unit gains a level | heroLevels | Event value is the new level |
| Unit: a unit acquires an item | itemPickedUp | |
| Player: player types a chat message | playerChats | All-chat only. Team chat and whispers never fire it |
| Player: player leaves the game | playerLeaves | Distinct from elimination; neither implies the other |
| Player: player defeat | playerEliminated | |
| Player: research finished | upgradeComplete | Event text is the upgrade id |
| Dialog: a dialog button is clicked | dialogButton | Event value is the button index |
| Game: custom event | custom, raised with raiseEvent | Fires later in the same tick, with a capped cascade |
| Player: player skips a cinematic | cutsceneFinished | Event value distinguishes played out, skipped, replaced and stopped |
| Keyboard: player presses a key | playerKeyPress | Down and up, through lockstep |
| (new here) | variableChanges | Fires when a global is written with a different value |
| (new here) | projectileHits | Fires when a projectile lands or a bomb detonates |
| A unit takes damage | unitDamaged | Fires after damage lands, from every source |
| A unit is about to take damage | unitDamaging | Fires before it lands, and setEventDamage can change the number |
| Starts the effect of an ability | abilityCastStart, abilityCastEffect, abilityCastFinished, abilityCastCancelled | The four points of a cast, with eventSpellId and eventTargetPoint |
| Issues an order | orderIssued | Fires for player, trigger and script orders |
| UI: a frame is clicked | uiFrameClicked | Your own createUIFrame frames. Arrives through lockstep, and a click only counts on a frame that seat can really see right now |
| (new here) A city lot is built, levels up or burns down | cityLotBuilt, cityLotLevelled, cityLotBurnt | Maps with city rules. Event value is the lot id. Build a city builder |
| (new here) A city reaches a tier; the city's year turns | cityTierReached, cityLeanSeason | The lean season's warning, its start and its end |
About damage detection. Custom-map authors reach for "a unit takes damage" as a matter of reflex, because half the custom-spell tradition is built on it, and both halves of that idiom now work here.
unitDamaged fires after damage lands, from every source: melee, projectile,
spell, burn, thorns, cleave and the rest. unitDamaging fires before it
lands, and inside it setEventDamage changes the number — so custom armour
types, damage reduction auras and reflect are all expressible. Read the amount
with eventDamage, the element with eventDamageType, and which door it came
through with eventDamageKind.
Two rules worth knowing before you build on it. wait is illegal inside
unitDamaging, because the damage has to resolve in the same tick. And damage
caused by a damage hook stops firing hooks after two levels, so a reflect that
reflects a reflect terminates instead of looping.
unitAttacked still exists and still fires for attacks specifically, which is
the cheaper event when an attack is all you care about.
Conditions
Most trigger editors split conditions into a small set of comparisons and a large set of boolean queries. Here, a condition is just an expression that evaluates to a boolean, so comparisons and queries are the same thing.
| You want to test | Here |
|---|---|
| Integer / Real / String comparison | eq, ne, lt, le, gt, ge |
| And, Or, Not (multiple conditions) | and, or, not. Both and and or short-circuit |
| Unit is alive | unitProp with alive |
| Unit is a hero | unitProp with isHero |
| Unit type comparison | unitProp with typeId |
| Unit's life / mana | unitProp with hp, maxHp, hpPercent, mana, maxMana |
| Player is an ally of player | isAllied, or isEnemy |
| Player slot status is playing | playerAlive |
| Player slot is a user | playerIsHuman |
| Point is in region | pointInRegion |
| Boolean comparison | A boolean expression on its own, or the bool literal |
| (new here) A city's population, tier, money, demand or problems; a service's reach at a tile | cityStat, cityCoverageAt |
Actions
| You want to | Here | Notes |
|---|---|---|
| Unit: create N units at a point | createUnits | Up to 500 per action, and it can store the result in a group variable |
| Unit: kill | killUnit | Normal death path: corpse, bounty, unitDies |
| Unit: remove | removeUnit | No death, no corpse, no bounty |
| Unit: move unit instantly | moveUnit | |
| Unit: set life / mana | setUnitHp, setUnitMana | |
| Unit: change owner | setUnitOwner | |
| Unit: make unit invulnerable | setUnitInvulnerable | |
| Unit: issue an order | order | Applied inside the tick, not through lockstep |
| Unit: add / remove ability | addAbility, removeAbility | A unit with no caster slot gets one. Removing does not silence a cast already in flight |
| Unit: apply / remove a buff | addBuff, removeBuff, unitHasBuff | Re-using a buff id refreshes it rather than stacking. Buffs are not carried through a save, exactly like spell-applied ones |
| Animation: play unit's animation | playAnimation | |
| Unit group: pick every unit in a group | forEachUnit | The group is snapshotted and sorted before the first iteration |
| Unit group: add / remove / clear | addUnitToGroup, removeUnitFromGroup, clearGroup | |
| Player: add / set gold | addResource, setResource | |
| Player: set alliance | setAlliance | |
| Player: set technology researched / available | setTechAllowed | Availability only: it forbids or re-allows a research for one seat. It does not GRANT one, and it does not take away a research already finished |
| Player: set food cap ceiling | setPopCap | A separate number from the cap farms provide, so losing a farm never eats into it. Leave the value out to clear it |
| Visibility: create a fog modifier (visible) | revealArea | A timed disc, like the Reveal spell. The ground stays explored afterwards, and teammates see through it. There is no permanent modifier and no "masked" one |
| Boolean: is point visible to player | isPointVisibleTo | That seat's own units and buildings; allied vision is not folded in |
| Game: victory / defeat | victory, defeat | Per seat |
| Game: display text to player | showText | Templated, resolved in the sim so every client agrees |
| Game: clear text messages | clearText | |
| Quest: create / change a quest | setQuest, clearQuests | |
| Leaderboard: create, change, destroy | setLeaderboardTitle, setLeaderboardRow, removeLeaderboardRow, clearLeaderboard | |
| Dialog: create, add button, show, clear | showDialog, hideDialog | One action builds the whole dialog; buttons fire dialogButton |
| Camera: pan camera to point | cameraPan | |
| Camera: lock camera to unit | cameraLock, cameraUnlock | |
| Camera: set camera bounds | cameraSetBounds | |
| Camera: set zoom (distance to target) | cameraSetZoom | |
| Cinematic: shake camera | cameraShake | |
| Camera: reset to game camera | cameraReset | |
| Cinematic: fade out / fade in | fadeScreen | |
| Cinematic: turn cinematic mode on | playCutscene, stopCutscene | An authored scene, not a mode flag. Cutscenes |
| Sound: play a sound | playSound | Any of the 3,857 shipped effects, voice lines and critter calls, or a sound your map ships |
| Sound: play a sound at a point | playSoundAt | Panned and quietened by distance, and hidden by fog of war. The first play of a sound the client has not loaded is silent, so play it once early |
| Sound: play / stop music | playMusic, stopMusic | Any track from the shipped soundtrack. stopMusic on its own cannot stop the automatic battle shuffler starting the next track |
| Environment: set time of day | setTimeOfDay | See the note below |
| Trigger: turn on / off | enableTrigger | |
| Trigger: run trigger (checking conditions) | runTrigger | Runs inline, in the current event context |
| Trigger: skip remaining actions | stopTrigger | |
| Countdown timer: start | startTimer | One-shot or repeating |
| Countdown timer: pause / resume | pauseTimer, resumeTimer | |
| Countdown timer: destroy | stopTimer | |
| Wait N seconds | wait | In ticks, and the wait survives save and load |
| If / then / else | if | |
| For each integer A from 1 to N | forEachInt | |
| Loop while a condition holds | while | With an iteration cap, so it cannot hang a match |
| Set variable | setVar, addVar | |
| Hashtable: save / load / flush | hashtableSet, hashtableGet, hashtableHas, hashtableCount, hashtableRemove, hashtableClear | Typed per table |
| Carry values from one session to the next | generateSaveCode, verifySaveCode | A tamper-evident code bound to this map, rather than a local file |
| Cinematic: ping minimap | ping | |
| Call a function you declared | callFunction, call, return | Declared functions with typed parameters and a return type |
| Set the damage of a damage event | setEventDamage | Only inside unitDamaging, where the number has not landed yet |
| Create an item | itemCreate | Places an item on the ground |
| Give an item to a hero | itemGive | Straight into the hero's inventory |
| Remove an item | itemRemove | By type and count |
| Hero has an item / how many | heroHasItem, heroItemCount | Expressions, so they read in a condition |
| Set a hero's experience | heroAddXp | Grants XP, so a custom level curve is yours to write |
| Set a hero's level | heroSetLevel | Including starting a hero above level 1 |
| Revive a hero | heroRevive | Optionally at a chosen tile |
| Set a unit's stat | setUnitProp | Damage, armour, magic resist, move speed, attack speed, range, max HP, max mana, mana regen and sight, on one unit at runtime |
| Floating text: create over a unit or point | floatingText | Rises and fades, and hidden by fog of war — a word over an enemy hero would report where the enemy hero is |
| Special effect: create at a point | playEffect | Plays a spell's visual with none of its gameplay: no damage, no mana, no cooldown, no caster |
| Multiboard: create / set an item / destroy | multiboardSet, multiboardCell, multiboardRemove | A titled grid of text. Rows and columns are 0-based, and setting the board again replaces it rather than merging |
| Countdown timer: create a timer window | timerWindowShow, timerWindowHide | A title beside the countdown. The clock is the timer itself, so the two cannot drift apart |
| Quest: mark a campaign objective | setObjective | The campaign's own mission objectives. setQuest is the quest log every mode has; this is the list that decides whether a mission is won |
| Load a different map | travelToZone | Sends the match to another zone in the map's chain, carrying the state the author nominated. Single-player only |
| (new here) City: zone, place a service, set tax, raise a fire or plague | cityZone, cityPlaceService, citySetTax, cityDisaster | The HUD's own city commands, so the city's prices and refusals apply. Build a city builder |
| (new here) Draw a map layer: a value per tile through a colour ramp, with a legend | overlayDefine, overlayFill, overlayClear | Players switch it on in the HUD's overlay frame. View-only, any genre |
| Environment: change terrain type | setTerrain | Grass, dirt or shallow water over a region. Trees, ore, rock, deep water, lava and cliffs are left alone, and land and water never swap under a building or a ship |
| (comments) | comment |
Time of day works, with one catch. setTimeOfDay takes a progress from 0
to 1 across the day and a speed multiplier, where speed: 0 holds that time
of day instead of pausing nothing. It works from a trigger and from the
editor's Environment panel, which now reaches a running match: set the time and
a cycle speed of frozen, save, and the map plays at that hour. The panel does not
preview it in the editor viewport, and the Tileset panel still does not reach a
match at all.
Values, maths and text
Values, the parts of a trigger line you fill in, are expressions here. This is the area that grew most recently.
| You want | Here |
|---|---|
| Integer / Real / String / Boolean constant | int, real, str, bool. A bare 5, 1.5, "hi" or true also works |
| Arithmetic | add, sub, mul, div, mod, neg, abs |
| Math: square root | sqrt |
| Math: power | pow |
| Math: sin, cos, tan | sin, cos, tan |
| Math: asin, acos, atan2 | asin, acos, atan2 |
| Math: exponential, natural log | exp, log |
| Math: min, max | min, max |
| Math: random number | rand for integers, randReal for reals |
| Truncate a real to an integer | trunc, and also floor, ceil, round, sign, clamp |
| (new here) | lerp, for blending two numbers |
| Point: centre of region | regionCenter |
| Point: random point in region | randomPointInRegion |
| Point with polar offset | polarOffset |
| Angle between points | angleBetween |
| Distance between points | distance |
| Point with offset | offsetPoint |
| X / Y of point | pointX, pointY |
| Point (x, y) | point |
| Position of unit | unitPoint |
| Convert integer / real to string | toStr |
| Convert string to integer / real | strToInt, strToReal |
| String: substring | subStr |
| String: length | strLen |
| String: convert to upper / lower case | strUpper, strLower. ASCII only, on purpose |
| String: concatenate | concat, or add with a string on either side |
| Hash a string to an integer | hashString |
| Find one string inside another | strFind |
| (new here) | strReplace, strSplitPart, strSplitCount, strTrimAscii |
| Convert seconds to a timer string | formatTicks |
| Units in a region | unitsInRegion |
| Units within N range of a point | unitsInRadius |
| Units owned by player | unitsOfPlayer |
| Units of type | unitsOfType |
| Random N units from a group | groupPickN |
| Random unit from a group | groupRandom |
| Number of units in a group | groupCount |
| Unit group: is a unit in it | groupHas |
| The nearest unit to a point | closestUnit |
| Filter a group by a condition | groupFilter |
| (new here) | groupSortBy, groupAt, groupUnion, groupDiff, groupEmpty, unitsWithTag |
| Number of players matching a condition | playerCount |
| Name of player | playerName |
| Current gold / lumber of player | playerResource |
| Food used / food cap | playerPop, playerMaxPop |
| Player's team | playerTeam |
| Player has researched | playerHasUpgrade |
| Triggering unit / killing unit / triggering player | eventUnit, eventOtherUnit, eventPlayer |
| Entered chat string, event damage, clicked button index | eventString, eventValue |
| Elapsed game time | tick |
| Remaining time for timer | timerRemaining, timerRunning |
| No unit / null | nullUnit |
| Variable | var |
| Call a user function | call |
| (new here) | entityByAid, to reach a unit you placed in the editor by its authored id |
| (new here) | triggerFireCount, counter, unitVelocity |
Things you might not expect to be here
Worth knowing they are here, because they change how you would solve a problem you previously solved the long way round.
- Physics.
applyImpulseandsetUnitKinematicsgive a unit a real velocity, andunitVelocityreads it back. Knockbacks, launches and bouncing projectiles are a single action rather than a periodic timer that moves a unit by hand. - Declarative HUD frames.
createUIFrame,updateUIFrame,destroyUIFrameandclearUIFramesbuild custom interface out of described frames, andsetCounter/removeCounterput a labelled number on screen.uiFrameClickedreports a click on one of your own frames, arriving through lockstep like a dialog button. - Restricting where a seat may build.
setBuildableRegiontakes a list of regions, an empty list for nowhere, or null to restore the default. - Group algebra. Union, difference, sorting and "pick N" are nodes, not loops you write.
- Save codes that cannot be forged.
generateSaveCodebinds the code to your map, andverifySaveCodereads a field back out or rejects it. - Headless testing. You can run your map against the AI a thousand times and assert the outcome. Testing.
- A city builder.
rules.cityruns the city Cities of Stone plays (roads, zones, services, peoples, seasons),rules.city.tuninggives it your own numbers, and scripts and triggers read and drive it. Overlays (your own map layers) and runtime terrain work on any map. Build a city builder.
Not possible yet
This is the honest list. Everything in it is something an experienced map author would reach for and not find today. Several are being built right now, so check the date on this page against the Trigger Reference, which is generated and always current: if a capability below has landed, it will be in the reference and this row will have been deleted.
| Missing | What it means in practice | Nearest thing today |
|---|---|---|
| Granting a research mid-match | You can forbid and re-allow a research with setTechAllowed, but nothing hands a seat one it has not paid for. A map's per-seat starting tech list is the only grant | Forbid the rest and let the seat research the one you want |
| Taking vision away | revealArea adds vision; nothing removes it, so there is no way to blind a seat. The fog refcount has no representation for a negative revealer | Leave the area unrevealed, or end the reveal by letting its timer run out |
| Changing cliff height or pathing blockers mid-match | setTerrain changes a tile's ground (grass, dirt, shallow water), but cliff levels, ramps and blockers are fixed once the match starts | Author the heights before the match. Terrain and height |
Scripts can combine the exposed operations into new mechanics: shields, custom damage rules, wave logic, scoring and scripted encounters. The boundary is the public API. There is no arbitrary native system or new native spell-effect registration, direct renderer access or general rigid-body API. Existing spell visuals and trigger actions are available through sos.triggers.run; see Current boundaries.
A worked example
The classic "every 30 seconds, spawn a wave at a region and announce it, until wave 20" as a whole trigger script:
{
"version": 1,
"vars": [{ "name": "wave", "type": "int" }],
"regions": [{ "id": "spawn_north", "x": 2, "y": 2, "w": 4, "h": 4 }],
"triggers": [
{
"id": "waves",
"events": [{ "k": "periodic", "everyTicks": 600 }],
"conditions": [{ "k": "lt", "a": { "k": "var", "name": "wave" }, "b": 20 }],
"actions": [
{ "k": "addVar", "name": "wave", "delta": 1 },
{
"k": "createUnits",
"unitTypeId": "goblin_brawler",
"count": 8,
"player": 5,
"at": { "k": "regionCenter", "region": "spawn_north" }
},
{
"k": "order",
"units": { "k": "unitsInRegion", "region": "spawn_north" },
"order": "attack_move",
"to": { "k": "regionCenter", "region": "spawn_north" }
},
{ "k": "showText", "players": null, "text": "A wave is coming." }
]
}
]
}Four things worth naming if you have written this in another editor:
- No registering events at runtime. A trigger's events are declared, not registered, so the initialisation trigger other editors need is unnecessary.
- No cleanup. There is no location to destroy and no group to release.
unitsInRegionreturns a value, not a handle. players: nullmeans every seat. A list means those seats. Seat ids are not 0 and 1: a lobby seats up to eight, and creeps sit above the playable range. Never hardcode a seat you did not place. Players and teams.- Every duration is in ticks. In this ordinary 20 Hz RTS example, 600 ticks is thirty seconds.
Where to go next
- Triggers for how the system fits together.
- Trigger reference for every node, field by field. Generated from the catalogue, so it is never out of date.
- Determinism for authors for why the maths nodes exist instead of you doing your own.
- Performance and limits for what a map may spend per tick.
- Troubleshooting by error code when the validator refuses something.