Map Editor Documentation

Your First Custom Unit

Cloning a shipped unit into one of your own in the Data module, making it trainable, finding real ids instead of guessing them, and the did-you-mean suggestion that validates cleanly while giving you the wrong unit.

Start here · Page 32 of 49

Your map can change what things are, for the length of one match. A crossbowman with more range. A farm that costs nothing. Or, which is what this page does, a brand new unit built from an existing one.

The learning path so far:

  1. Your First Map.
  2. Your First Win Condition.
  3. Your first custom unit (this page).
  4. Your First Mod.
  5. Publish and Iterate.

Fifteen minutes. Nothing you do here leaves your map: a custom unit belongs to the match, not to the game.

Patch or clone, and why it matters

There are two moves, and picking the wrong one is the most common first mistake.

A patch changes a unit that already exists. Patch dwarf_crossbowman and every crossbowman in the match has the new numbers, on every seat, for both sides.

A clone makes a new unit from an existing one. It copies everything, applies your changes, and leaves the original alone. The new id can be placed in the editor and named by triggers like any other unit.

If you want "the dwarves' crossbowmen are stronger on my map", patch. If you want "there is a new unit called the Marksman", clone. Most first custom units are clones.

Step 1: open the Data module

Press F7. The Data module lists the six things a map can change: units, buildings, items, spells, research upgrades and heroes.

The left column is the entries your map has made so far, which starts empty. The button you want is + Clone a def.

Step 2: clone something close to what you want

Pick a base that already behaves the way you want, and change the numbers. Cloning a crossbowman into an archer is easy. Cloning a worker into a dragon is a long afternoon, because everything you do not change comes from the base: movement, attack behaviour, build options, sounds, footprint and art.

Give the clone an id of its own. Ids use lowercase letters, digits and underscores. Use a prefix that is obviously yours, so it cannot collide with a shipped id or with a mod's:

myvault_marksman        good
marksman                risky
dwarf_crossbowman_2     confusing

Then change the fields you care about. For units the combat fields are hp, damage, armor, range, attackSpeed and speed. Buildings use attackDamage and attackRange instead, which is a genuine difference and not a typo in this page.

Step 3: give it a way into the match

A clone is not automatically buildable. Nothing produces it until you say so. There are two ways in:

Train it. Add it to a building's production list. A plain array replaces the list, which would remove everything else the barracks makes, so use the $append operator instead.

Spawn it. A trigger can create it directly with a Create units action, which is what wave-based maps do. Nothing needs to produce it at all in that case.

The second is how most custom genres work. A tower defence never trains its creeps; a trigger makes them.

Step 4: check it before you play it

Press F10 for the Validation module.

The validator checks patch field names against the fields the definition really has. A misspelled field is a warning that names the closest real one, so rnage comes back as "did you mean range?". It stays a warning because the entry still installs and the unknown field is simply ignored, which is worth knowing: a typo in a field name does not stop your map, it just quietly does nothing.

Then test it. Press Ctrl+F9, build or spawn the unit, and look at it. Numbers in a panel are not the same as a unit that feels right.

How to find an id, and why you must not guess

This is the single largest source of first-attempt failures, and it is worth more care than it looks.

The game ships 248 units and 492 buildings. You will not remember the ids, and the names in the game are not the ids.

From a checkout of the repository, ask:

npx tsx packages/modkit/src/cli.ts catalog --kind units --ids-only --json
npx tsx packages/modkit/src/cli.ts catalog --kind buildings --ids-only --json

Without --json you get a count and nothing else, which is exactly the situation that makes people guess.

In the editor, the Data module's clone picker lists every real id, and the entity palette in the Terrain module does the same for anything placeable.

The did-you-mean is not a spell checker

Here is the trap, in full, because it has cost real time.

goblin_grunt does not exist. Write it and the nearest name is goblin_peon, which is a worker. Take that and your map validates with zero errors while your raider wave is made of peasants. The melee unit you actually wanted is goblin_brawler.

The validator now says so itself. Because this cost real time, the unit-type message carries the role of whatever it suggests, and when that role cannot fight it also lists the same race's fighters:

unknown unit type 'goblin_grunt' — did you mean 'goblin_peon'? NOTE: 'goblin_peon'
is a 'worker' unit. It does not fight — if you meant a fighter, the goblin_* ones
are goblin_brawler (melee), goblin_bomb_lobber (ranged), goblin_warg_rider
(heavy), goblin_demolisher (siege), goblin_shaman (caster).

The same shape used to catch trigger authors with comparison nodes: there is no gte or lte. The real operators are eq, ne, lt, le, gt and ge. Writing gte was answered with gt, which is > where you meant >=: a clean report, and a condition that fires a tick early or never. That one is fixed at the source. gte is now answered with ge, and the message tells you in the same breath not to take gt.

Read what the suggestion is, not just that there was one. Outside those two cases the validator is still matching strings, and it has no idea what you meant. catalog --kind units --ids-only prints every id, so there is no need to guess.

The whole thing as JSON

The Data module writes this. You can also write it by hand, and a mod is mostly this and nothing else.

A clone plus the append that makes it trainable:

{
  "units": [
    {
      "id": "myvault_marksman",
      "baseId": "dwarf_crossbowman",
      "patch": { "name": "Marksman", "range": 10, "hp": 60, "cost": { "gold": 140 } }
    }
  ],
  "buildings": [
    { "id": "dwarf_barracks", "patch": { "produces": { "$append": ["myvault_marksman"] } } }
  ]
}

A patch, for comparison, which changes every crossbowman in the match:

{
  "units": [
    { "id": "dwarf_crossbowman", "patch": { "range": 8, "cost": { "gold": 90 } } }
  ]
}

Nested objects merge, so "cost": { "gold": 90 } changes the gold price and leaves lumber, stone and oil alone. Plain arrays replace wholesale, which is why produces needed $append.

Four things that surprise people

A clone can be based on a clone. Up to 16 deep. Each one starts from its base as the match sees it, patch included, so a change near the top of a chain carries down.

A clone of a shipped unit does not pick up your patch to that unit. Patching dwarf_ironguard's armour changes every ironguard in the match, but not a clone built from it. If you want clones to follow a change, make the change on a clone and build the others from that.

Order does not matter. The entries are not applied top to bottom, so a clone may appear above its base.

A custom unit needs art or it borrows some. A clone keeps its base's model and sprite, which is usually what you want. Giving it a look of its own is a separate job, covered in Custom Units & Models.

Things a map cannot change about a unit

Worth knowing before you plan around them:

  • A unit's spells are a list you can edit, but the behaviour behind a spell is the engine's. See the spell sections of Data Overrides before you build a design on one.
  • Triggers CAN write a unit's live stats mid-match. setUnitProp writes damage, range, attackSpeed, maxHp, armor, magicResist, moveSpeed, maxMana, manaRegen and sightRadius on one unit; hp, mana, owner and invulnerability have had their own actions all along. This page used to say the opposite, and said it for as long as setUnitProp shipped.
  • Triggers CAN grant and remove abilities and buffs mid-match, with addAbility / removeAbility and addBuff / removeBuff. A buff may only modify a stat the combat code actually reads; the node reference lists which.
  • A trigger cannot change a unit's TYPE. There is no morph, transform or replace-unit action: a Footman does not become a Knight in place. Spawn the other unit and remove the first, and remember that everything holding the old unit in a variable or a group is now holding a corpse.

That last one is an honest gap rather than a hidden feature. The Capability map keeps the full list of what is here, what is not, and the nearest thing today, and a test checks both halves of it against the live catalogue in both directions.

Common first mistakes

What you seeWhat it usually is
The unit exists but nothing can build itA clone is not trainable until a building produces it, or a trigger creates it
The barracks now makes only your unitproduces was written as a plain array, which replaces. Use $append
Your change did nothingA misspelled field name. It is a warning, not an error. Read the Validation list
Every crossbowman changed, not just yoursThat was a patch. A new unit needs baseId
The wave spawned workersThe id came from a did-you-mean suggestion. Look it up in the catalogue
The new unit looks like the old oneExpected. A clone keeps its base's art until you give it some

Where to go next

  • Data Overrides is the full reference: merge rules, every list operator, spells, upgrades, heroes and what is checked.
  • Custom Units & Models covers giving your unit a 3D model of its own.
  • Asset Catalog is how to find any id the map can name.

Next: Your First Mod.