Making mods
New units, buildings and upgrade levels in one JSON file: the format, how to test a mod in the game, and how to publish it to the registry.
Steel Tide takes mods. A mod adds units, buildings and upgrade levels — described in one JSON file, with sprite sheets beside it if you draw some — and the game lists them under Settings → Mods, next to the official registry where everyone's mods are published. A mod cannot change the rules, the interface or the units the game ships with: it adds, and everything it adds is switched off with it. That is what keeps a mod safe to install and a save safe to load.
The whole reference is on this page, generated from the same tables the game validates a mod against. If you would rather hand the job to a coding agent, the brief at the bottom tells it everything it needs.
What a mod is
A folder:
my-mod/
mod.json the manifest: the mod's identity, its defs, and the sheets they draw with
sprites/*.png optional art
README.md optional
mod.json names the mod, then lists its defs — each a unit, a building or an upgrade level — and, if the mod draws its own art, the sheets. Every field a def can carry is a field of the game's own unit definition, so anything you can read off a wiki page here is something a mod can set.
Your first mod in five minutes
The dead-simple path is extends: start from a vanilla def and override what you like. This is a complete, working mod — a heavier Bison for the late game:
{
"format": "steel-tide-mod",
"v": 1,
"id": "bison-ii",
"name": [
"Bison II",
"野牛 II"
],
"version": "1.0.0",
"author": "you",
"description": [
"A heavier Bison for the late game.",
"后期用的重型野牛。"
],
"defs": [
{
"id": "bison2",
"extends": "mbt",
"name": [
"Bison II",
"野牛 II"
],
"desc": [
"Thicker plate, a bigger gun, and a bigger bill.",
"更厚的装甲、更大的炮,也更贵。"
],
"tier": 3,
"cost": 520,
"hp": 1100,
"speed": 52,
"requires": [
"radar"
],
"weapons": [
{
"id": "cannon",
"cls": "cannon",
"dmg": 90,
"reload": 2,
"range": 5,
"splash": 12,
"turret": true,
"muzzleOffset": 23
}
],
"aiWeight": 2
}
]
}- Save that as
mod.jsoninside a folder calledbison-ii. - In the game, open Settings → Mods and press Open folder… (or zip the folder and press Upload file…). The game says what it loaded — or lists what is wrong, with the path into
mod.jsonfor each thing. - Start a skirmish. The Bison II is in the war factory's list from level 2, because that is where the Bison is; it draws with the Bison's art; and the AI fields it too, since the def carries
aiWeight.
extends copies everything — art, weapons, cost, where it is built — and each field you name replaces the copy. Leave weapons out to keep the base's guns; write your own list to replace them.
Testing
- Open folder… is the edit loop. In Chrome and Edge the game keeps the folder open, so after you edit
mod.jsonyou press Reload on the mod's row and the change is in. Elsewhere, pick the folder again. - Upload file… takes a zip of the folder or a
.steel-tide-modfile (the manifest with the sheets embedded — what the converter hands out). - Add from URL… and
?mod=take a mod served over the web:npx serve --cors my-modand then openhttps://play.steelti.de/?mod=http://localhost:3000/— the mod is fetched again at every boot, so a page reload picks up an edit. - Developer Tools → Showcase (developer mode: five taps on the version line) places every unit and building on one map, yours included, so you can see the art and watch the guns without building anything.
- The console (backquote) has
give <id>for any def.
Mods apply to skirmish and the campaign. A networked match is always unmodded — the server's roster is the vanilla one. A save remembers the mods it was made with and will not load without them.
Going deeper
A mod with its own art, a defended building, and a fourth level for the power plant line:
{
"format": "steel-tide-mod",
"v": 1,
"id": "ironworks",
"name": [
"Ironworks",
"铁工厂"
],
"version": "1.0.0",
"author": "you",
"description": [
"A hover tank, a bunker and a fusion plant.",
"一辆悬浮坦克、一座碉堡和一座聚变电站。"
],
"license": "CC-BY-4.0",
"defs": [
{
"id": "ironworks-hover",
"name": [
"Skimmer Hover Tank",
"掠行悬浮坦克"
],
"desc": [
"Fast, thin-skinned, rockets.",
"快、皮薄、打火箭。"
],
"kind": "unit",
"domain": "ground",
"tier": 2,
"cost": 260,
"hp": 320,
"armor": "light",
"speed": 110,
"turnRate": 5,
"radius": 9,
"trail": "tire",
"weapons": [
{
"id": "pods",
"cls": "rocket",
"dmg": 16,
"reload": 2.2,
"range": 4.5,
"burst": 4,
"burstDelay": 0.1,
"splash": 10,
"spread": 14
}
],
"producedBy": [
"factory2",
"factory3"
],
"aiWeight": 1
},
{
"id": "ironworks-bunker",
"name": [
"Bunker",
"碉堡"
],
"desc": [
"A gun pit that takes a beating.",
"扛打的火力点。"
],
"kind": "building",
"cost": 380,
"hp": 1600,
"fw": 2,
"fh": 2,
"power": -3,
"vision": 8,
"weapons": [
{
"id": "gun",
"cls": "autocannon",
"dmg": 22,
"reload": 0.5,
"range": 5,
"turret": false,
"targets": [
"ground",
"ship"
]
}
]
},
{
"id": "ironworks-fusion",
"name": [
"Fusion Plant",
"聚变电站"
],
"desc": [
"The plant line's fourth level.",
"发电厂线的第四级。"
],
"extends": "power3",
"upgradeOf": "power3",
"cost": 2200,
"hp": 2600,
"power": 500,
"upgradeCost": 1100,
"upgradeTime": 60,
"requires": [
"radar",
"reactor"
]
}
],
"sprites": [
{
"key": "u.ironworks-hover",
"file": "sprites/u.ironworks-hover.png",
"frames": 1,
"rotated": true,
"fw": 24,
"fh": 26
},
{
"key": "u.ironworks-bunker",
"file": "sprites/u.ironworks-bunker.png",
"frames": 1
}
]
}Units of measure
A tile is 32 world pixels. speed and projectile speed are world px/s; range, minRange, vision, sonar, stealth, detect, repairRange and interceptRange are tiles; radius, splash, spread and muzzleOffset are world px; every time is seconds. For scale, the Bison Battle Tank is cost 280, hp 620, speed 60, radius 10, with a cannon of dmg 60 every 1.8 s at range 4.6; the Recon Buggy is cost 60, hp 150, speed 120; a War Factory is 3×3 tiles, hp 1500, power −8.
Where things are built
- A unit joins the production list of the buildings in
producedBy. Left out, it joins its domain's line from its tier up: a tier-2 ground unit appears at the level-2 and level-3 war factory, a tier-1 ship at every naval yard. Withextends, it is built wherever the base is. - A building is placed by the units in
builtBy— the engineer, unless you say otherwise. A building with aproduceslist is a factory; a unit that names a plain building inproducedByturns it into one. - An upgrade level is a building with
upgradeOf: the named building gains an Upgrade button that turns it into this def, atupgradeCostoverupgradeTime, and this def is never placed directly. A building has one next level, soupgradeOfmay name a vanilla building at the end of its line (power3,factory3,extractor3,gatling,cannonturret2,samsite,interceptor2,radar,repairtower,reactor,nukesilo) or another def of your mod. requireslists buildings that must stand before something can be built, the way Tier 3 needs the Radar Station.
Art
A def draws with the atlas key in sprite (its body) and turretSprite (a rotating part). Three ways to fill them:
- Nothing. A def with no sheet is drawn as a plain placeholder in the faction's colour, sized to the def — a mod plays before its art exists.
- Borrowed.
"sprite": "u.mbt", "turretSprite": "tur.mbt"draws with the Bison's art;extendsinherits the base's. - Your own. A
spritesentry per sheet: a PNG (WebP and JPEG work too),framesanimation frames left to right in one strip, evenly spaced, no gaps. The key isu.<id>for a body andtur.<id>for a turret, and a mod may not take a vanilla key.
Hulls, turrets and everything that turns are one image facing up with "rotated": true — the game bakes the 24 headings. fw/fh are the in-game size of that image in world px (a tank hull is about 24×24; draw it at 2–4× that and the loader resamples). pivotX/pivotY put the pivot on the turret ring.
Buildings are a strip of frames, not rotated, drawn top-down with a slight southern tilt: the footprint is the bottom fw×32 by fh×32 px of the frame and anything above overhangs the terrain behind. Width, height and anchor are sized from the def's footprint for you.
Team colour is magenta: paint faction-coloured parts in #FF66FF, #FF00FF and #990099 and nothing else in magenta; the game recolours them per player. Generated sheets are cleaned on load — backgrounds removed, frames registered, static pixels frozen — so a transparent background is best but not required.
The AI
AI opponents build a modded unit at its aiWeight, on the scale the vanilla roster is weighed on (a Bison is 3, a scout car 1), once its requires are met. Leave it out and they never build it. Buildings and upgrade levels are yours alone.
Reference
Anything not in these tables is ignored with a warning; a number outside its range is an error.
mod.json
| field | type | required | default | meaning |
|---|---|---|---|---|
| format | string | yes | always "steel-tide-mod" | |
| v | integer 1–1 | yes | format version, 1 | |
| id | id | yes | the mod's id: lower case, letters, digits, `-` — its folder in the registry | |
| name | text | yes | the name shown in the mod list | |
| version | string | yes | e.g. `1.0.0`; the game offers an update when the registry's is newer | |
| author | string | who made it | ||
| description | text | one or two sentences for the list | ||
| homepage | string | a link: a repository, a thread | ||
| license | string | CC-BY-4.0 | the mod's licence (SPDX id) | |
| minGame | string | the oldest game version it is written for | ||
| defs | def[] | yes | the units, buildings and upgrade levels | |
| sprites | sheet[] | the sheets the defs draw with (see below) | ||
| files | { path: dataURL } | single-file form only: the sheets, embedded as data URLs by path |
name, description and a def's name/desc are text: a string (used for both languages), ["English", "中文"], or { "en": "…", "zh": "…" }.
A def (defs[])
| field | type | required | default | meaning |
|---|---|---|---|---|
| id | id | yes | unique across every mod and the vanilla roster; prefix a generic word with your mod's id | |
| name | text | yes | as the HUD shows it | |
| desc | text | "" | the tooltip line | |
| extends | id | a vanilla def id (or an earlier def of this mod) to copy, then override field by field; inherits its art and where it is built | ||
| kind | unit | building | yes | a unit or a building (required unless `extends` says) | |
| domain | ground | ship | air | units only | ground | where it moves; a submarine is a `ship` with `underwater` |
| tier | integer 1–3 | 1 | the factory level it appears at, and the badge | |
| cost | number 0–99999 | yes | metal | |
| buildTime | number 0–3600 | cost ÷ 14 | seconds at full power | |
| pop | integer 0–50 | 1 for a unit, 0 for a building | population it counts for | |
| hp | number 1–1000000 | yes | hit points | |
| armor | light | medium | heavy | ship | sub | air | structure | by domain | the armour class weapons are multiplied against | |
| speed | number 0–1000 | units only | 60 | world px/s (a tile is 32) |
| turnRate | number 0–50 | units only | 3.5 | rad/s |
| vision | number 0–64 | 8 | sight, in tiles | |
| radius | number 1–200 | 9, or the footprint | collision radius, world px | |
| weapons | weapon[] | [] | the weapons (see below); an empty list is unarmed | |
| fw | integer 1–8 | buildings only | 2 | footprint width, tiles |
| fh | integer 1–8 | buildings only | 2 | footprint height, tiles |
| producedBy | id[] | units only | the line for its domain, from its tier up | the buildings whose production list it joins (vanilla or this mod's) |
| produces | id[] | buildings only | a factory: the units it builds | |
| builtBy | id[] | buildings only | ["engineer"] | the builder units that may place it |
| builds | id[] | units only | a builder unit: the buildings it can construct | |
| buildRate | number 0–10000 | units only | a builder unit: hp of work per second | |
| power | number -10000–10000 | 0 | positive produces, negative draws | |
| metalRate | number 0–1000 | buildings only | metal per second (an extractor) | |
| needsDeposit | boolean | buildings only | must stand on a deposit | |
| repairRange | number 0–64 | buildings only | a repair aura, tiles | |
| repairRate | number 0–10000 | buildings only | hp per second per target | |
| repairTargets | integer 1–50 | buildings only | targets served at once | |
| upgradeOf | id | buildings only | the building this is the next level of; that one gains the upgrade button | |
| upgradeCost | number 0–99999 | buildings only | cost − the source's cost | with `upgradeOf`: the upgrade's price |
| upgradeTime | number 0–3600 | buildings only | buildTime | with `upgradeOf`: seconds |
| requires | id[] | building ids that must stand before it can be built | ||
| nukeCapacity | integer 0–10 | a launcher: warheads it holds | ||
| nukeCost | number 0–99999 | a launcher: metal per warhead | ||
| nukeTime | number 0–3600 | a launcher: seconds per warhead | ||
| interceptRange | number 0–64 | point defence: reach in tiles | ||
| interceptMag | integer 1–200 | point defence: rounds ready | ||
| interceptReload | number 0.05–600 | point defence: seconds per round replaced | ||
| interceptMuzzleOffset | number 0–200 | point defence: launcher length, world px | ||
| transportCap | integer 1–50 | units only | a transport: hold, in cargo weight | |
| landsForCargo | boolean | units only | a cargo plane that touches down to load | |
| cargoWeight | number 0–50 | units only | pop | how much of a hold it takes |
| underwater | boolean | units only | a submarine: seen only by sonar | |
| sonar | number 0–64 | units only | sonar range, tiles | |
| stealth | number 0–64 | units only | seen only within this many tiles of an enemy | |
| detect | number 0–64 | buildings only | reveals stealth within this many tiles | |
| hovers | boolean | units only | an aircraft that hovers instead of orbiting | |
| altitude | number 0–64 | units only | 12 | an aircraft's drawn height, px |
| fireOnMove | boolean | units only | keeps shooting on a plain move | |
| trail | tread | tire | wake | units only | by domain | the mark it leaves |
| sprite | string | this mod's u.<id> sheet, else the base's art | the body's atlas key: one of this mod's sheets, or a vanilla key to borrow its art | |
| turretSprite | string | this mod's tur.<id> sheet, else the base's (when its art is kept) | the rotating part's key, if any | |
| aliases | string[] | other names the console's `give` accepts | ||
| aiWeight | number 0–10 | units only | 0 | how readily the AI builds it — a Bison is 3, a scout car 1; 0 never |
Defaults with no extends: a unit is domain: "ground", tier: 1, pop: 1, speed: 60, turnRate: 3.5, vision: 8, radius: 9, armour by domain (ground medium, ship ship, air air), a tread trail on land and a wake at sea; a building is fw: 2, fh: 2, armor: "structure", power: 0, pop: 0. buildTime is cost ÷ 14 seconds.
A weapon (defs[].weapons[])
| field | type | required | default | meaning |
|---|---|---|---|---|
| id | string | w1, w2… | a name for the weapon | |
| cls | mg | autocannon | cannon | at | he | rocket | navgun | ashm | torpedo | aa | yes | what it was built to kill — picks its row of the armour matrix | |
| dmg | number 0–100000 | yes | damage per hit | |
| reload | number 0.05–600 | yes | seconds between shots or bursts | |
| range | number 0.5–64 | yes | tiles | |
| minRange | number 0–64 | tiles it cannot fire inside | ||
| projectile | bullet | shell | missile | rocket | bomb | torpedo | flak | by class | the round drawn | |
| speed | number 1–5000 | by projectile | round speed, world px/s | |
| targets | (ground | ship | sub | air)[] | by class | what it may fire at: ground, ship, sub, air | |
| mult | { armour: number } | overrides of the class row, by armour class | ||
| splash | number 0–500 | blast radius, world px | ||
| burst | integer 1–32 | shots per burst | ||
| burstDelay | number 0–5 | seconds between the shots of a burst | ||
| homing | boolean | the round tracks its target | ||
| interceptable | boolean | point defence may shoot it down | ||
| arc | boolean | a ballistic arc (artillery) | ||
| turret | boolean | true when the def has a turretSprite | fired from the rotating part | |
| muzzleOffset | number 0–200 | pivot to muzzle, world px | ||
| spread | number 0–200 | inaccuracy at full range, world px | ||
| friendlyFire | boolean | the blast hurts your own side too | ||
| sound | mg | autocannon | cannon | missile | flak | arty | rocket | torpedo | bomb | by class | the firing sound |
Damage is dmg × the armour matrix cell for the weapon's class against the target's armour, with mult overriding single cells. The counter web explains the classes; the matrix:
| Weapon | Light | Medium | Heavy | Structure | Ship | Submarine | Aircraft |
|---|---|---|---|---|---|---|---|
| ×1.50 | ×0.80 | ×0.40 | ×0.35 | — | — | — | |
| ×1.50 | ×1 | ×0.50 | ×0.50 | ×0.70 | — | — | |
| ×0.60 | ×1.30 | ×1 | ×0.80 | ×0.90 | — | — | |
| ×0.60 | ×1.30 | ×1.10 | ×1 | ×0.90 | — | — | |
| ×0.40 | ×1.10 | ×1.80 | ×0.70 | ×1 | — | — | |
| — | — | — | — | — | — | ×1 | |
| — | — | — | — | — | — | ×1 | |
| ×1.20 | ×1 | ×0.75 | ×1.60 | ×1 | — | — | |
| ×1.20 | ×1 | ×0.75 | ×1.60 | ×1 | — | — | |
| ×1.20 | ×1 | ×0.60 | ×1.60 | ×1 | — | — | |
| ×1.50 | ×1 | ×0.50 | ×0.50 | ×0.80 | — | — | |
| — | — | — | — | ×1.60 | — | — | |
| ×1 | ×1 | ×0.75 | ×0.75 | ×1 | — | ×1 | |
| ×1 | ×1.10 | ×0.80 | ×0.80 | ×1 | ×1.90 | — | |
| — | — | — | — | ×1.30 | ×1.20 | — | |
| ×1.20 | ×1 | ×1 | ×1.60 | ×1.20 | — | — | |
| ×1.20 | ×1 | ×0.75 | ×1.60 | — | — | — | |
| — | — | — | — | — | — | ×1 | |
| ×0.60 | ×1.10 | ×1.80 | ×0.80 | ×1 | — | — | |
| ×1.40 | ×1.40 | ×0.90 | ×0.70 | ×0.90 | — | — | |
| — | — | — | — | ×1.70 | — | — | |
| ×1.20 | ×1.20 | ×1 | ×1.60 | ×1.10 | — | — | |
| ×1.20 | ×1.20 | ×1.10 | ×1 | ×1 | — | — | |
| ×1 | ×0.90 | ×0.60 | ×1.60 | ×1 | — | — | |
| — | — | — | — | ×0.90 | ×1.80 | — | |
| ×1.50 | ×0.80 | ×0.40 | ×0.30 | ×0.60 | — | ×0.70 | |
| ×1.50 | ×0.90 | ×0.50 | ×0.30 | ×0.70 | — | ×0.80 | |
| ×0.60 | ×1.30 | ×1.10 | ×0.60 | ×1 | — | — | |
| ×0.60 | ×1.30 | ×1.20 | ×0.60 | ×1.10 | — | — | |
| — | — | — | — | — | — | ×1 | |
| — | — | — | — | — | — | ×1 |
A sheet (sprites[])
| field | type | required | default | meaning |
|---|---|---|---|---|
| key | string | yes | `u.<id>` for a body, `tur.<id>` for a rotating part; never a vanilla key | |
| file | string | yes | the image, relative to mod.json (PNG, WebP or JPEG) | |
| frames | integer 1–64 | 1 | animation frames, left to right in one strip | |
| fw | number 4–512 | the footprint (a building) or the image | in-game frame width, world px | |
| fh | number 4–512 | in-game frame height, world px | ||
| rotated | boolean | one up-facing image; the game bakes the 24 headings (hulls, turrets) | ||
| pivotX | number 0–1 | 0.5 | rotation pivot, as a fraction of the frame | |
| pivotY | number 0–1 | 0.5 | rotation pivot, as a fraction of the frame | |
| anchorY | number 0–512 | px from the top to the footprint centre (tall buildings) | ||
| mount | [x, y] | a body: where its turret sits, `[fx, fy]` | ||
| fps | number 0–60 | animation speed | ||
| teams | boolean | true | recolour magenta per faction | |
| ss | integer 1–4 | supersample factor; omit to let the game choose | ||
| fitFootprint | boolean | scale the drawn content to fill the frame | ||
| animRegion | [x0, y0, x1, y1] | where the animation lives, `[x0, y0, x1, y1]` fractions; the rest is frozen | ||
| stabilize | boolean | true | re-align drifting frames | |
| freezeStatic | boolean | median-freeze pixels that barely change | ||
| stripBg | boolean | force background removal on or off | ||
| bgMinLuma | number 0–255 | lightest colour still taken as background | ||
| artifactCleanup | boolean | sweep specks left by background removal |
Publishing
The registry is the public repository rivertwilight/steel-tide-mods: every mod is a folder under mods/<id>/, and the game and this site list what is there.
- Fork it and add your folder as
mods/<id>/— the folder name is the mod'sid. - Run
node tools/check.mjs mods/<id>. It prints every error the game would, and checks the folder against every other published mod: ids are global, so prefix a generic word with your mod's id (ironworks-bunker, notbunker). - Open a pull request. CI runs the same check; once merged, the index is rebuilt and the mod appears in the game's registry list and on the mods page.
- Bump
versionfor every change. The game offers the update to whoever has the mod.
A mod is published under the licence its manifest names (CC-BY-4.0 unless you say otherwise). Only ship art you have the right to.
Converting a Rusted Warfare mod
The converter takes a Rusted Warfare custom-unit mod — a .rwmod, a zip, or the unit folder — and gives back a Steel Tide mod: the .ini files become defs, the images become sheets, green team colour becomes magenta. Prices, hit points, speeds and ranges are scaled so a Rusted Warfare tank lands near a Bison (or converted unit for unit, if you prefer). It runs in the browser; nothing is uploaded. Look the manifest over afterwards: effects, animations, shields and energy have no counterpart here.
For coding agents
The registry ships a skill — the whole format, how to test a mod and how to publish it, in the form Claude Code, Cursor, Codex and the other agents read. Install it with skills:
npx skills add rivertwilight/steel-tide-mods --skill steel-tide-mods-guideline
Then ask your agent for a mod. Working inside a clone of the registry, an agent finds the same brief as AGENTS.md, and it is served as plain text at steelti.de/mods/AGENTS.md for anything that reads a URL.
