Deep Delve: Survivors is one hero alone in a cave while the swarm grows around them. It asks one question for fifteen minutes, which is whether you are still alive, and it never asks anything else.
Play it
What it is made of
| Piece | What the file says |
|---|---|
| Map | 48 tiles square, deep_cave biome, mapType of dungeon |
| Capabilities | The file retains legacy render-target metadata; The Forge and custom matches now use 3D |
| Seat 0 | user, dwarf, team 0. Starts on 100 gold and owns both placed entities |
| Seat 1 | computer, ratmen, team 1. Every spawned horror belongs to it |
| Rules | disableAi: true, standardVictory: false, noStartingUnits: true, allowedBuildings empty |
| Placed | 2 entities, both seat 0's: the Sanctum building with author id delve_sanctum, and one delve_hero with author id player_hero |
| Script | 9 triggers, 5 variables, 5 regions |
| Data | 6 unit overrides, and no buildings block at all |
allowedBuildings is an empty list, not an absent field. Combined with
noStartingUnits, that is how the map guarantees the player never gets a base:
there is nothing to place, nothing is handed out, and seat 0 starts and ends
with the two things the map placed by hand.
Both of those carry an author id, and nothing reads either one. There is no
entityByAid anywhere in this map's triggers. That is not a fault, it is just
worth knowing about author ids: they cost nothing to set, and they do nothing at
all until a trigger looks one up.
The capabilities block in this older map records the render target it was
authored for. It is preserved as legacy metadata: The Forge and custom matches
now use 3D. The four regions named
spawn_north, spawn_east, spawn_south and spawn_west ring the arena, and
arena_center sits in the middle of it.
How it works
| Trigger | Fires on | What it does |
|---|---|---|
t_init | mapInit, run once | Sets the leaderboard title and four rows, shows the briefing dialog, prints the opening line |
t_close_briefing | dialogButton for delve_briefing, run once | One hideDialog |
t_clock | periodic, every 20 ticks | Adds 1 to time_survived, redraws the timer row, counts surge_countdown down while it is above 0 |
t_wave_surge | periodic, every 600 ticks | Raises wave, resets surge_countdown to 30, announces the surge, and runs four createUnits calls, one per spawn region |
t_boss_encounter | atTick 6000, run once | Spawns one delve_broodmother at the centre of spawn_north and orders it in |
t_boss_final | atTick 12000, run once | Spawns one delve_titan at the centre of spawn_south and orders it in |
t_creep_kill | unitDies filtered to enemyOf seat 0 | Adds 1 to kills, pays 15 gold, redraws the kills row |
t_hero_dies | unitDies filtered to unitTypeIds of delve_hero and ofPlayer 0, run once | defeat for seat 0, and a line of text |
t_victory_condition | atTick 18000, run once | victory for seat 0, and a line of text |
The wave is one periodic trigger and four spawn regions. Every 600 ticks,
t_wave_surge calls createUnits at randomPointInRegion for each of the four
edge regions, then follows each call with an order of attack_move at
regionCenter of arena_center. The counts are expressions rather than
constants: the north and east calls each ask for 6 + wave * 2 gnawers, the
south call for 4 + wave skitterers, and the west call for 2 + wave / 2
sentries, truncated. Nothing anywhere holds a table of waves. The escalation is
arithmetic on one variable, which is why the trigger is thirteen actions long
instead of ninety.
randomPointInRegion, rather than regionCenter, is what makes a swarm read as
a swarm. A dozen units spawned at one point arrive as a single clump and get
stuck on each other; the same dozen spread across the 36 by 3 north band arrive
along a front. The two bosses do use regionCenter, because one unit has
nothing to spread out from.
The bosses are atTick, not milestones. t_boss_encounter at tick 6000 and
t_boss_final at tick 12000 are five and ten minutes in, on a clock that never
pauses. They do not check the wave number, or the kill count, or whether the
previous boss is dead. Two atTick triggers marked run once is the entire
mechanism, and it is worth preferring whenever a beat should land at a known
time rather than after a known amount of progress.
How the match ends
The win is t_victory_condition, and it has no condition at all. The
trigger has an atTick event set to tick 18000, no conditions array, and a
victory for seat 0. Reaching tick 18000 is the win, because reaching tick
18000 means the defeat trigger never fired:
{
"id": "t_victory_condition",
"name": "Victory Condition at 15 Minutes",
"runOnce": true,
"events": [{ "k": "atTick", "tick": 18000 }],
"actions": [{ "k": "victory", "player": { "k": "int", "v": 0 } }]
}At 20 ticks per second, tick 18000 is 15 minutes. time_survived exists on the
leaderboard so the player can watch the number climb, but no rule reads it.
The loss is t_hero_dies, which is the other half of the same idea. Its
event is unitDies with a filter of unitTypeIds set to ["delve_hero"] and
ofPlayer set to 0, and its only outcome action is defeat for seat 0. The
filter is on the event rather than in a condition, so the trigger body never
runs for any of the hundreds of other deaths on this map.
Both outcomes land on seat 0, which is the seat the player is watching from, so both stop the match. Seat 1 is never told anything, and on a map with exactly one human seat that is enough. Add a second human seat and it stops being enough, for the reason Your First Win Condition sets out.
Run it yourself
npx tsx packages/modkit/src/cli.ts play public/maps/deep-delve-showcase.json \
--ticks 2400 --seed 1map public/maps/deep-delve-showcase.json (seed 1)
seats 0:dwarf/t0 (human, idle unless --drive) 1:ratmen/t1
ended no — still running after 2400 ticks (120s)
vars time_survived=120, wave=3, kills=62, surge_countdown=1, spawned=[127,128,129]
commands 0 issued by --drive
fires Survival Clock ×120, Bounty & Kills ×62, Wave Surge Progression ×3, Initialization ×1
caps 0
broken 0
errors 0Two minutes in, three surges have landed and 62 horrors are dead, which is the
hero defending itself with no player at the keyboard. The fires line is the
useful part: the one-second clock at 120 matches time_survived=120, the surge
trigger at 3 matches wave=3, and Bounty & Kills at 62 matches kills=62,
which is the filtered death event matching on every horror that died.
Validation is worth running too, because it finds something real here:
npx tsx packages/modkit/src/cli.ts validate public/maps/deep-delve-showcase.jsonPASS public/maps/deep-delve-showcase.json [Full Map (SerializedMap)] 0 error(s), 1 warning(s)
WARNING perf-uncapped-periodic-spawn $.triggers.triggers[3].actions[5]: Trigger "Wave Surge Progression" fires every 600 ticks (30s) and creates units with nothing to stop it: it has no conditions, does not run once, never disables itself, and the spawn is not inside an if. The unit count rises for the whole match. Add a condition that caps the population, set the trigger to run once, or have it disable itself when the wave is done.That warning is correct, and this map lives with it: the hard stop at tick 18000 means the unit count cannot rise forever, because the match is over first. A map without a fixed end should take the advice.
One honest gap, unlike the survival, tower defence, castle fight and hero arena showcases: this map has no dedicated outcome test in the repository. Nothing drives a seat to the 15 minute win or to the hero's death and asserts what happened. The endings are readable in the JSON and have been played, but they are not pinned.
What to take from it
- A survive-for-N-minutes rule needs no counter. One
atTicktrigger with no conditions, marked run once, holding avictory. Writing a variable, aperiodicthat increments it and agecomparison gets the same result with three more things to get wrong. - Filter the event, not the body.
unitDiestakesunitTypeIds,ofPlayerandenemyOf.t_hero_diesandt_creep_killare both one filtered event with a short body, and neither pays for the deaths it does not care about. - Spread a wave with
randomPointInRegion. Four thin edge regions, each spawn followed by anorderofattack_moveatregionCenter, gives pressure from every side with no pathing setup at all. - Scale with arithmetic on the wave number.
add,mul,divandtruncover onewavevariable replaces a hand-written table and never runs out of waves. - Keep legacy capability metadata intact. Older maps may carry render-target
declarations in
meta.capabilities; leave those fields in place when editing an existing map unless you have deliberately updated its presentation.
Changing it
This map has no generator. Nothing in tools/ writes
public/maps/deep-delve-showcase.json; the only references anywhere in the
repository are test fixtures, the featured list and the perf fixture list. It is
hand-authored, pretty-printed JSON, which is why it reads so differently from
the generated showcases.
So you have two routes, and the first is the better one:
- Open it in The Forge. File → Import from AI / JSON…, paste the file,
and you get the terrain painter, the trigger editor and Test Map (
Ctrl+F9). - Edit the JSON. Fine for a trigger, risky for terrain. The terrain layers
are run-length encoded strings, and a hand-edited one can be the wrong shape
and still load, because the decoder is lenient on purpose. Both this map and
the tanks showcase once shipped a broken one: this file stored 47 rows for a
48 tile map, so its last row loaded as grass in a solid rock cave. The sweep
in
src/game/map/mapValidation.terrainEncoding.test.tsnow runs every shipped map through the same rule, and the codes to know arelayer-row-count,layer-row-widthandlayer-malformed-run.
Either way, validate before you play:
npx tsx packages/modkit/src/cli.ts validate public/maps/deep-delve-showcase.jsonWhere to go next
- Walkthrough: Survival is the same genre with building, a day and night cycle, and a counted win.
- Walkthrough: Tanks of Stone is the other hand-authored showcase, and it drives its enemies from a trigger.
- The Genre Cookbook collects the shapes these walkthroughs read in full.
- Your First Win Condition covers the counter-and-outcome shape this map deliberately skips.
- Triggers and the Trigger Reference cover every node named above.
- Validation and Tooling explains the warning codes and how to run the checks.