You have a map. This page turns it into a game: something that can be won and can be lost, and that says so.
The learning path so far:
- Your First Map: terrain, gold, a region, one trigger.
- Your first win condition (this page).
- Your First Custom Unit.
- Your First Mod.
- Publish and Iterate.
It takes about twenty minutes, and you will finish with a map that ends properly whichever way the match goes.
First: you may not need a trigger at all
Open the Players module (F8) and look at the Rules block. There is a
victory preset picker there, and it already covers a lot of ground:
| Preset | What wins the match |
|---|---|
| Conquest | Last team standing, and nothing else |
| Standard | Conquest, or a held Wonder, or every Relic banked |
| Wonder | Conquest, or holding a completed Wonder for the hold length |
| Relics | Conquest, or banking and holding every Relic on the map |
| Regicide | Conquest, plus: a team whose King dies loses outright |
| King of the Hill | Conquest, or holding the central neutral Monument |
| Score | A time limit, and the highest-scoring living team wins |
| Domination | Capture and hold control points to build score |
| Capture the Flag | Carry the neutral flag home |
Some of these need the map to contain the thing they are about. King of the Hill needs a central Monument, Capture the Flag needs a flag site, Relics need relics. If a preset describes your idea, set it, place what it needs, and you are done. No triggers.
The rest of this page is for the other case: the rule you want is yours, and no preset says it.
Step 1: turn off the standard win check
This is the one that catches everyone, so do it before anything else.
In Players (F8), under Rules, find Standard victory and turn it
off.
Standard victory is the engine's own last-team-standing check. It is on by default, and it is exactly right for a normal skirmish. It is wrong for everything else, because it runs all the time and does not know about your rule. On a one-seat map it is worse than wrong: "one player left alive" is true on tick 1, so the match declares victory before the player has moved. The validator refuses to publish a one-seat map with standard victory still on, and that rule exists because the failure is instant, total and completely unexplained the first time you see it.
Turning it off means the engine will never end the match. From here on, ending the match is your job, and if you never write the trigger the match never ends.
While you are in that block, two neighbours matter:
- No starting units. Turn it on when you do not want the engine handing every seat a town hall and workers behind your back. A tower defence or an escape map wants this on. A skirmish with an objective wants it off.
- Min and max players. Set the real range. This is what the lobby will let people choose.
Step 2: give the rule a number
Nearly every win condition in this engine is the same shape: a number, a rule that moves it, and a rule that reads it.
Lives that count down. Waves survived. Seconds held. Kills banked. Crystals delivered. Pick the number your game is about, and make it a variable.
In Triggers (F6), open the variables tab and add one:
- Name:
lives - Type:
int - Initial value:
20
That variable is not just bookkeeping. It is how you will test the map later, because an automated run can read it back and tell you the objective made progress rather than just that the match ended. A win condition with no number behind it is very hard to check.
Step 3: the trigger that moves the number
Add a trigger that changes the variable when the thing happens. For a defence map, that is "something reached the goal":
- + Trigger, name it
Leak. - Events: Unit enters a region, region
goal. - Actions: Add to variable,
lives, delta-1. - Actions: Kill unit, so the leaker does not sit in the region forever.
Leave Run once unticked. This one should fire every time.
Two habits worth forming now, because they are the difference between a map that runs well and one that stutters:
- Prefer an event to a poll. "A unit entered this region" costs nothing until it happens. "Is anything standing near the goal" asks the question twenty times a second forever.
- If you must poll, poll on a timer. An Every N ticks event at 20 ticks
runs once a second. A player cannot tell it from a per-tick check, and it
costs a twentieth as much. Group scans are charged per unit walked, against a
budget of
500,000expression evaluations per tick, so a scan on a busy map is not free.
Step 4: the trigger that ends the match
Add a second trigger that reads the number and calls it:
- + Trigger, name it
Defeat, and tick Run once. - Events: Every N ticks, every
20. - Conditions: A ≤ B, with A the variable
livesand B the number0. - Actions: Show text to everyone,
The gate has fallen. - Actions: Defeat, player
0.
Run once matters here. Without it the trigger keeps firing every second after the game is over.
Then write the winning side too, as its own trigger: Every N ticks,
condition wave ≥ the last wave, actions Show text and Victory,
player 0.
Step 5: the rule about seats, which is not obvious
A victory or defeat action resolves for the seat the player is watching from. Outside multiplayer, a victory declared on any other seat fires, does nothing visible, and the match carries on.
It is not an error. Nothing is logged. The trigger runs to the end, the "Objective complete" line appears in the debug panel, and the match simply does not stop.
Measured on the melee-objective starter template, which wins by holding the
centre shrine. The shipped map ends with victory on seat 0:
ended victory — winner seat 0 at tick 381 (19s) [trigger]
fires Count shrine control ×20, Setup ×1, Objective complete ×1The identical map with that one action changed to seat 1, a computer seat, and nothing else touched:
ended no — still running after 900 ticks (45s)
fires Count shrine control ×45, Setup ×1, Objective complete ×1Same trigger, fired once, in both runs. One match ended and one did not.
So when the other side is supposed to win, do not declare victory on the other side and stop. Declare the losing human seat defeated as well:
- Victory, player
1 - Defeat, player
0
Played back, that reads as a defeat with no winner, which is what the player watching from seat 0 should see:
ended defeat — winner none at tick 381 (19s) [trigger]Pair every outcome both ways. It costs one extra action and it is the difference between a symmetric map with two endings and one with half of them missing.
Step 6: play both endings
Press Ctrl+F9 to test. Then do the part everybody skips: play the branch
you did not write the test for.
Let something through on purpose and watch the defeat. Then hold and watch the victory. A conditional outcome that has never executed is a guess, and the starter templates themselves shipped with one such branch that could not win until somebody played it.
If nothing happens, press Ctrl+Shift+F9 for Trigger Debug, which lists
every trigger that fired and every condition that failed. The usual answer is a
region drawn somewhere other than where you walked.
Step 7: check it without playing it
The map editor's Test Map is the honest check, but it needs you at the keyboard. For the check you can repeat after every edit, run the map headlessly from a checkout of the repository:
npx tsx packages/modkit/src/cli.ts play my-map.json \
--ticks 3000 --seed 1 \
--assert 'state=victory' --assert 'winner=0' --assert 'var:lives>0'The output tells you four things at once:
ended victory — winner seat 0 at tick 381 (19s) [trigger]
vars hold_ticks=400
fires Count shrine control ×20, Setup ×1, Objective complete ×1
caps 0
broken 0
errors 0The fires line is the fastest diagnostic in the toolkit. A trigger that
never fired prints nothing there, which is usually the whole answer.
Three things will mislead you the first time:
- A headless run has no player in it. Human seats sit idle unless you drive
them, so a build-to-survive map loses by default. That is correct, and it
is worth having: it proves the map is losable. The shipped tower defence
showcase does exactly this, ending in defeat at tick 2056 with
livesat-1, because nobody built a tower. To assert a win, either pre-place what a player would have built, or drive the seat. - One tick short reads like a dead trigger.
--ticks 600on the same showcase printsstill running after 600 ticks, which looks identical to a broken map. Read theendedline before you believe anything else, and raise--ticksbefore you start debugging. - A non-empty
capsline means real stutter. Overrunning a budget disables the offending trigger and reports it. It does not fail silently and with no name attached, but it does mean your map is spending more than its share.
The whole thing as JSON
If you would rather read it than click it, this is the trigger half of a lives-based defence, and it is the same document the editor writes:
{
"version": 1,
"vars": [
{ "name": "lives", "type": "int", "init": 20 }
],
"regions": [
{ "id": "goal", "name": "Goal", "x": 14, "y": 2, "w": 4, "h": 4 }
],
"triggers": [
{
"id": "t_leak",
"name": "Leak",
"events": [{ "k": "unitEntersRegion", "region": "goal" }],
"actions": [
{ "k": "addVar", "name": "lives", "delta": -1 },
{ "k": "killUnit", "unit": { "k": "eventUnit" } }
]
},
{
"id": "t_defeat",
"name": "Defeat",
"runOnce": true,
"events": [{ "k": "periodic", "everyTicks": 20 }],
"conditions": [
{ "k": "le", "a": { "k": "var", "name": "lives" }, "b": 0 }
],
"actions": [
{ "k": "showText", "players": null, "text": "The gate has fallen." },
{ "k": "defeat", "player": 0 }
]
}
]
}Note le, not lte. The comparison nodes are eq, ne, lt, le, gt and
ge, and there is no gte or lte. If you write one, the validator will
suggest gt, which is > where you meant >=. That suggestion validates
cleanly and shows up much later as a condition that fires a tick early, or
never. Read what a suggestion is, not just that there was one.
To check a trigger on its own without pasting it back into the map, validate the fragment against the map, so region ids and map-defined clones are in scope:
npx tsx tools/validate.ts my-trigger.json --against my-map.jsonWithout --against, every region id and every unit your map defines reads as
unknown and the report is noise.
Common first mistakes
| What you see | What it usually is |
|---|---|
| Victory on tick 1 | Standard victory still on, with one competing seat |
| The trigger fires and the match continues | The outcome is on a seat nobody is watching from. Pair it with a defeat on the watched seat |
| Nothing fires at all | The region is not where you think it is. Trigger Debug (Ctrl+Shift+F9) will say |
| The message repeats forever | Run once is unticked on the ending trigger |
| Headless says "still running" | --ticks is too low, or the map genuinely never ends. Check the ended line first |
| The map can only be lost | The winning branch was never played. Play it |
Where to go next
- Triggers is the full reference for events, conditions, variables, functions and outcomes.
- Players & Teams covers seats, teams, the rules block and one-player maps in detail.
- The Genre Cookbook has a worked recipe for each of the common shapes, with a command per recipe.
- Performance and limits explains every budget and what happens when you cross one.
Next: Your First Custom Unit.