Map Editor Documentation

Triggers

Events, conditions and actions; why there is no scripting language; and how Tower Defense and DOTA maps are built from the same node set.

Page 8 of 12

A trigger is an event, some conditions and a list of actions. The Trigger module builds them from pickers rather than from text: every node has a known shape, so an action that takes a unit group offers you the unit groups 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"
           set wave counter = wave counter + 1
           display text to all players: "Wave {wave counter}"

Why there is no scripting language

There is no JavaScript in a map, not even in a sandbox. This is the decision most likely to surprise anyone coming from another editor, so it is worth being explicit about the reason.

The simulation is lockstep. Every client runs the entire match locally and compares hashes; nothing streams game state from a host. That means any difference in behaviour between two clients is a desync, and a desync ends the match for everyone. A script engine's Math implementations, object iteration order and floating-point printing differ between browsers and between versions of the same browser. Running downloaded user scripts inside a lockstep simulation is a way for a map to break a match on some machines and not others, and nobody can debug that in the field.

So the artifact is a closed set of typed nodes instead. The campaign's 48 missions are built from the same kind of data, which is a reasonable indication of the ceiling. If a text language for triggers ever ships, it will compile to these nodes and never to JavaScript.

What you get

Events. Map initialisation, periodic, at a specific tick, timer expiry, a unit entering or leaving a region, a unit dying, a unit being trained, a building completing, a unit being attacked, a hero levelling, an item being picked up, a player being eliminated, an upgrade completing, a variable changing, a dialog button being pressed, and custom events a trigger raises itself.

Expressions. Literals, variables, arithmetic, the current tick, a seeded random number, player resources and population, unit properties, the units in a region, the units carrying a tag, the entity with a given authored id, distance between two things, whether two players are allied, and the context of the event that fired.

Actions. Control flow including a wait, creating and removing units, issuing orders, changing resources and alliances, restricting where a player may build, declaring victory or defeat, starting and stopping timers, and the user interface set: text, counters, leaderboards, dialogs, pings, cutscenes and sound.

Every node is total. Asking for a unit that has died yields zero or false, not an error. That is not politeness: a node that threw on one client and not another would fork the simulation between the client that threw and the client that did not, so a map with a mistake in it degrades rather than desyncing.

Determinism rules you can see

Several parts of the design exist so that two clients running the same trigger produce the same result, and they show up in the editor:

  • Arrays run in the order you authored them, always.
  • Per-tick events fire in a fixed five-phase 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. Iterating a set in insertion order makes the result depend on spawn order.
  • A loop over units takes a snapshot before it starts, so creating units inside the loop does not change what the loop iterates.
  • wait suspends the trigger as data (a resume tick and a position in the action list), which is what lets a waiting trigger survive a save and reload.
  • An event that raises another event drains to a stable state with a hard cap, so a cycle stops rather than hanging the match.

Trigger-issued orders go through the same command path player orders use, but are applied inside the tick rather than through the network layer. They are identical on every client by construction, and a single wave spawn would exceed the per-turn command limit if it were sent over the wire.

Regions, points and cameras

Regions are rectangles or groups of rectangles, drawn in the Terrain module on the Regions layer. They 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 and for moving a player's camera during a scenario.

Building a Tower Defense map

The engine work beyond the trigger set is small, which means the map does most of it:

  • Waves are a periodic event, a create-units action and an attack-move order.
  • Lanes are a chain of enter-region triggers, each issuing the next waypoint. There is no new pathing involved.
  • Lives are an integer variable and a counter on the interface.
  • Buildable areas are a per-player restriction set by an action, so towers can only go where you want them.
  • Ending the match is a defeat action, which requires standard victory to be turned off in Players & Teams.

Building a DOTA-style map

Three things carry it. Independent hero instances, so five 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 action, a dialog-button event and a create-units action. Respawn timers reuse the same hero progress mechanism a campaign uses, keyed per instance rather than per hero definition.

Interface actions

Text, counters and leaderboards are resolved inside the simulation and stored with the rest of the trigger state, which means they are hashed. "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 interface is a message filtered to the local player, so a message sent to player 3 is not rendered for player 4.

Dialog button presses travel back as a normal command, so they arrive on the same tick for everyone.

Next: Data Overrides.