Map Editor Documentation

Capability map

What you want a map to do and the node that does it, every entry checked against the live catalogue, plus an honest list of what is not possible yet.

Start here · Page 23 of 49

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 toHere
Triggers built from an event, conditions and actionsThe same three parts, same order. Triggers
An object editorData 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, hashtablesAll three, with declared types
Hand-written scriptJavaScript and TypeScript in Scripts (F11), through the public scripting API
A small cap on the whole map fileMap JSON, scripts and embedded assets share 4,194,304 bytes; hosted art is fetched separately
A trigger stopping mid-run with no explanationA 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 diagnoseNondeterminism 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 toHereNotes
Map initializationmapInitFires once, after every global initialiser
Time: elapsed game time is N secondsatTickOne exact tick
Time: every N seconds of game timeperiodicPlus an offsetTicks so two waves can interleave
Time: expiration of a timertimerExpires
Unit: a unit enters a regionunitEntersRegionUnits already inside when the trigger arms do not count
Unit: a unit leaves a regionunitLeavesRegionAlso fires when a unit dies or is removed inside the region
Unit: a unit diesunitDiesEvent unit is the corpse's identity, other unit is the killer
Unit: a unit is attackedunitAttackedEvent value is the damage, other unit the attacker. See the damage note below
Unit: a unit finishes training a unitunitTrained
Unit: a unit finishes constructionbuildingComplete
Unit: a unit gains a levelheroLevelsEvent value is the new level
Unit: a unit acquires an itemitemPickedUp
Player: player types a chat messageplayerChatsAll-chat only. Team chat and whispers never fire it
Player: player leaves the gameplayerLeavesDistinct from elimination; neither implies the other
Player: player defeatplayerEliminated
Player: research finishedupgradeCompleteEvent text is the upgrade id
Dialog: a dialog button is clickeddialogButtonEvent value is the button index
Game: custom eventcustom, raised with raiseEventFires later in the same tick, with a capped cascade
Player: player skips a cinematiccutsceneFinishedEvent value distinguishes played out, skipped, replaced and stopped
Keyboard: player presses a keyplayerKeyPressDown and up, through lockstep
(new here)variableChangesFires when a global is written with a different value
(new here)projectileHitsFires when a projectile lands or a bomb detonates
A unit takes damageunitDamagedFires after damage lands, from every source
A unit is about to take damageunitDamagingFires before it lands, and setEventDamage can change the number
Starts the effect of an abilityabilityCastStart, abilityCastEffect, abilityCastFinished, abilityCastCancelledThe four points of a cast, with eventSpellId and eventTargetPoint
Issues an orderorderIssuedFires for player, trigger and script orders
UI: a frame is clickeduiFrameClickedYour 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 downcityLotBuilt, cityLotLevelled, cityLotBurntMaps with city rules. Event value is the lot id. Build a city builder
(new here) A city reaches a tier; the city's year turnscityTierReached, cityLeanSeasonThe 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 testHere
Integer / Real / String comparisoneq, ne, lt, le, gt, ge
And, Or, Not (multiple conditions)and, or, not. Both and and or short-circuit
Unit is aliveunitProp with alive
Unit is a herounitProp with isHero
Unit type comparisonunitProp with typeId
Unit's life / manaunitProp with hp, maxHp, hpPercent, mana, maxMana
Player is an ally of playerisAllied, or isEnemy
Player slot status is playingplayerAlive
Player slot is a userplayerIsHuman
Point is in regionpointInRegion
Boolean comparisonA 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 tilecityStat, cityCoverageAt

Actions

You want toHereNotes
Unit: create N units at a pointcreateUnitsUp to 500 per action, and it can store the result in a group variable
Unit: killkillUnitNormal death path: corpse, bounty, unitDies
Unit: removeremoveUnitNo death, no corpse, no bounty
Unit: move unit instantlymoveUnit
Unit: set life / manasetUnitHp, setUnitMana
Unit: change ownersetUnitOwner
Unit: make unit invulnerablesetUnitInvulnerable
Unit: issue an orderorderApplied inside the tick, not through lockstep
Unit: add / remove abilityaddAbility, removeAbilityA unit with no caster slot gets one. Removing does not silence a cast already in flight
Unit: apply / remove a buffaddBuff, removeBuff, unitHasBuffRe-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 animationplayAnimation
Unit group: pick every unit in a groupforEachUnitThe group is snapshotted and sorted before the first iteration
Unit group: add / remove / clearaddUnitToGroup, removeUnitFromGroup, clearGroup
Player: add / set goldaddResource, setResource
Player: set alliancesetAlliance
Player: set technology researched / availablesetTechAllowedAvailability 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 ceilingsetPopCapA 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)revealAreaA 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 playerisPointVisibleToThat seat's own units and buildings; allied vision is not folded in
Game: victory / defeatvictory, defeatPer seat
Game: display text to playershowTextTemplated, resolved in the sim so every client agrees
Game: clear text messagesclearText
Quest: create / change a questsetQuest, clearQuests
Leaderboard: create, change, destroysetLeaderboardTitle, setLeaderboardRow, removeLeaderboardRow, clearLeaderboard
Dialog: create, add button, show, clearshowDialog, hideDialogOne action builds the whole dialog; buttons fire dialogButton
Camera: pan camera to pointcameraPan
Camera: lock camera to unitcameraLock, cameraUnlock
Camera: set camera boundscameraSetBounds
Camera: set zoom (distance to target)cameraSetZoom
Cinematic: shake cameracameraShake
Camera: reset to game cameracameraReset
Cinematic: fade out / fade infadeScreen
Cinematic: turn cinematic mode onplayCutscene, stopCutsceneAn authored scene, not a mode flag. Cutscenes
Sound: play a soundplaySoundAny of the 3,857 shipped effects, voice lines and critter calls, or a sound your map ships
Sound: play a sound at a pointplaySoundAtPanned 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 musicplayMusic, stopMusicAny track from the shipped soundtrack. stopMusic on its own cannot stop the automatic battle shuffler starting the next track
Environment: set time of daysetTimeOfDaySee the note below
Trigger: turn on / offenableTrigger
Trigger: run trigger (checking conditions)runTriggerRuns inline, in the current event context
Trigger: skip remaining actionsstopTrigger
Countdown timer: startstartTimerOne-shot or repeating
Countdown timer: pause / resumepauseTimer, resumeTimer
Countdown timer: destroystopTimer
Wait N secondswaitIn ticks, and the wait survives save and load
If / then / elseif
For each integer A from 1 to NforEachInt
Loop while a condition holdswhileWith an iteration cap, so it cannot hang a match
Set variablesetVar, addVar
Hashtable: save / load / flushhashtableSet, hashtableGet, hashtableHas, hashtableCount, hashtableRemove, hashtableClearTyped per table
Carry values from one session to the nextgenerateSaveCode, verifySaveCodeA tamper-evident code bound to this map, rather than a local file
Cinematic: ping minimapping
Call a function you declaredcallFunction, call, returnDeclared functions with typed parameters and a return type
Set the damage of a damage eventsetEventDamageOnly inside unitDamaging, where the number has not landed yet
Create an itemitemCreatePlaces an item on the ground
Give an item to a heroitemGiveStraight into the hero's inventory
Remove an itemitemRemoveBy type and count
Hero has an item / how manyheroHasItem, heroItemCountExpressions, so they read in a condition
Set a hero's experienceheroAddXpGrants XP, so a custom level curve is yours to write
Set a hero's levelheroSetLevelIncluding starting a hero above level 1
Revive a heroheroReviveOptionally at a chosen tile
Set a unit's statsetUnitPropDamage, 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 pointfloatingTextRises 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 pointplayEffectPlays a spell's visual with none of its gameplay: no damage, no mana, no cooldown, no caster
Multiboard: create / set an item / destroymultiboardSet, multiboardCell, multiboardRemoveA 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 windowtimerWindowShow, timerWindowHideA title beside the countdown. The clock is the timer itself, so the two cannot drift apart
Quest: mark a campaign objectivesetObjectiveThe 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 maptravelToZoneSends 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 plaguecityZone, cityPlaceService, citySetTax, cityDisasterThe 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 legendoverlayDefine, overlayFill, overlayClearPlayers switch it on in the HUD's overlay frame. View-only, any genre
Environment: change terrain typesetTerrainGrass, 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 wantHere
Integer / Real / String / Boolean constantint, real, str, bool. A bare 5, 1.5, "hi" or true also works
Arithmeticadd, sub, mul, div, mod, neg, abs
Math: square rootsqrt
Math: powerpow
Math: sin, cos, tansin, cos, tan
Math: asin, acos, atan2asin, acos, atan2
Math: exponential, natural logexp, log
Math: min, maxmin, max
Math: random numberrand for integers, randReal for reals
Truncate a real to an integertrunc, and also floor, ceil, round, sign, clamp
(new here)lerp, for blending two numbers
Point: centre of regionregionCenter
Point: random point in regionrandomPointInRegion
Point with polar offsetpolarOffset
Angle between pointsangleBetween
Distance between pointsdistance
Point with offsetoffsetPoint
X / Y of pointpointX, pointY
Point (x, y)point
Position of unitunitPoint
Convert integer / real to stringtoStr
Convert string to integer / realstrToInt, strToReal
String: substringsubStr
String: lengthstrLen
String: convert to upper / lower casestrUpper, strLower. ASCII only, on purpose
String: concatenateconcat, or add with a string on either side
Hash a string to an integerhashString
Find one string inside anotherstrFind
(new here)strReplace, strSplitPart, strSplitCount, strTrimAscii
Convert seconds to a timer stringformatTicks
Units in a regionunitsInRegion
Units within N range of a pointunitsInRadius
Units owned by playerunitsOfPlayer
Units of typeunitsOfType
Random N units from a groupgroupPickN
Random unit from a groupgroupRandom
Number of units in a groupgroupCount
Unit group: is a unit in itgroupHas
The nearest unit to a pointclosestUnit
Filter a group by a conditiongroupFilter
(new here)groupSortBy, groupAt, groupUnion, groupDiff, groupEmpty, unitsWithTag
Number of players matching a conditionplayerCount
Name of playerplayerName
Current gold / lumber of playerplayerResource
Food used / food capplayerPop, playerMaxPop
Player's teamplayerTeam
Player has researchedplayerHasUpgrade
Triggering unit / killing unit / triggering playereventUnit, eventOtherUnit, eventPlayer
Entered chat string, event damage, clicked button indexeventString, eventValue
Elapsed game timetick
Remaining time for timertimerRemaining, timerRunning
No unit / nullnullUnit
Variablevar
Call a user functioncall
(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. applyImpulse and setUnitKinematics give a unit a real velocity, and unitVelocity reads 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, destroyUIFrame and clearUIFrames build custom interface out of described frames, and setCounter / removeCounter put a labelled number on screen. uiFrameClicked reports a click on one of your own frames, arriving through lockstep like a dialog button.
  • Restricting where a seat may build. setBuildableRegion takes 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. generateSaveCode binds the code to your map, and verifySaveCode reads 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.city runs the city Cities of Stone plays (roads, zones, services, peoples, seasons), rules.city.tuning gives 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.

MissingWhat it means in practiceNearest thing today
Granting a research mid-matchYou 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 grantForbid the rest and let the seat research the one you want
Taking vision awayrevealArea adds vision; nothing removes it, so there is no way to blind a seat. The fog refcount has no representation for a negative revealerLeave the area unrevealed, or end the reveal by letting its timer run out
Changing cliff height or pathing blockers mid-matchsetTerrain changes a tile's ground (grass, dirt, shallow water), but cliff levels, ramps and blockers are fixed once the match startsAuthor 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:

  1. No registering events at runtime. A trigger's events are declared, not registered, so the initialisation trigger other editors need is unnecessary.
  2. No cleanup. There is no location to destroy and no group to release. unitsInRegion returns a value, not a handle.
  3. players: null means 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.
  4. Every duration is in ticks. In this ordinary 20 Hz RTS example, 600 ticks is thirty seconds.

Where to go next