The Forge was designed from the ground up to be programmable by modern AI models: Claude 3.7, ChatGPT, Cursor, Copilot, and local LLMs. Because maps, trigger logic, and unit balance are represented as pure, deterministic JSON rather than arbitrary code, an LLM can author complete game modes without runtime errors or syntax hallucinations.
Map Draft JSON / Context
│
▼
Prompt LLM with Schema & Mode Goals
│
▼
Pure JSON AST (TriggerScript + DefOverrides)
│
▼
Import into The Forge (Ctrl + Enter to Play)The AI-First RTS Workflow
Building custom game modes with AI follows a three-step cycle:
-
Export Your Map Context: In The Forge, open your map and click Export Context or copy the map's draft JSON from the editor menu. This gives the LLM your regions (
x, y, w, h), spawns, player seats, and starting entities. -
Prompt Your Model: Feed the exported context along with the Trigger AST and DefOverride schema provided below (or link directly to
/llms.txtand/llms-full.txt). Ask the model to generate aTriggerScriptor aDefOverrideSetfor your desired mode (Tower Defense, King of the Hill, Boss Arena, Auto-Battler). -
Import & Test in 1 Click: Paste the returned JSON into The Forge's Trigger or Data module. Press Ctrl + Enter (or Ctrl + F9) to immediately boot the real lockstep engine and playtest your mode.
Determinism & Totality: Why There Is No JS Eval
Traditional RTS modding environments (Warcraft III JASS/Lua, StarCraft II Galaxy) suffer from desyncs when user code interacts with floating-point variance, unstable object iteration, or unhandled exceptions.
Shards of Stone solves this fundamentally:
1. Pure JSON Abstract Syntax Tree (AST)
Triggers are stored as a closed union of typed data nodes (TriggerScript).
There is no eval(), no Function(), and no script engine bytecode. The
simulation runs on a deterministic tick loop (20 ticks per second) where
every client evaluates identical nodes in identical array order.
2. Guaranteed Multiplayer Lockstep
- Bit-exact math: All distance calculations use integer math or bit-exact
IEEE-754 routines (
detHypot). Implementation-defined functions likeMath.sinorMath.hypotare banned from the simulation. - Sorted groups: Unit groups are sorted
number[]entity arrays, neverSetinstances (whose iteration depends on insertion history). - Fixed execution order: Per-tick trigger events evaluate in a deterministic five-phase cycle, and actions run strictly in authored array order.
3. Total Evaluation (Zero Throws)
Every single expression and action in the AST is total. Asking for properties of an entity that died one tick earlier never throws a null pointer exception:
- Missing or dead unit $\rightarrow$ returns
0,false, or"" - Division or modulo by zero $\rightarrow$ returns
0(neverInfinityorNaN) - Undeclared variable or out-of-bounds array $\rightarrow$ zero-value of the type
- Unbounded loops $\rightarrow$ strictly capped by
LOOP_CAP(10,000) andCASCADE_CAP(64)
Because no trigger can throw, an imperfect AI-generated script degrades gracefully instead of terminating the match or desyncing peer clients.
Complete Schema Specifications
1. TriggerScript Schema
A map's trigger file has the following root structure:
interface TriggerScript {
version: 1;
vars: VarDecl[]; // Global variables and state counters
regions: RegionDef[]; // Axis-aligned bounding boxes
triggers: TriggerDef[];// Event-Condition-Action definitions
}
interface VarDecl {
name: string;
type: 'int' | 'real' | 'bool' | 'string' | 'unit' | 'unitGroup' | 'point' | 'player' | 'region' | 'timer';
array?: boolean;
arraySize?: number; // Up to 8192
init?: Expr; // Initial value evaluated at mapInit
}
interface RegionDef {
id: string; // e.g. "lane_spawn", "hill_center", "boss_lair"
x: number; // Top-left tile X
y: number; // Top-left tile Y
w: number; // Width in tiles (>= 1)
h: number; // Height in tiles (>= 1)
name?: string;
}
interface TriggerDef {
id: string; // Unique identifier
name?: string;
enabled?: boolean; // Default true
runOnce?: boolean; // Default false
events: TriggerEvent[];
conditions?: Expr[]; // ANDed expressions
actions: Action[]; // Evaluated sequentially
}2. Events (TriggerEvent)
Event Tag k | Key Parameters | Description |
|---|---|---|
mapInit | {} | Fires once on tick 0 after variable initialization. |
periodic | everyTicks, offsetTicks? | Fires every N ticks (20 ticks = 1 second). |
atTick | tick | Fires at a precise simulation tick. |
unitEntersRegion | region, filter? | Fired when a unit steps into a named region. |
unitLeavesRegion | region, filter? | Fired when a unit exits a named region. |
unitDies | filter? | Fired when a matching unit is killed. |
unitTrained | filter? | Fired when training completes at a building. |
buildingComplete | filter? | Fired when a construction completes. |
timerExpires | timer | Fired when a named countdown reaches 0. |
dialogButton | dialogId?, buttonIndex? | Fired when a player clicks a dialog option. |
custom | name | Raised imperatively via raiseEvent. |
3. Expressions (Expr)
Expressions evaluate to integers, floats, booleans, strings, points {x, y}, or unit lists:
- Literals:
{ k: 'int', v: 10 },{ k: 'real', v: 1.5 },{ k: 'bool', v: true },{ k: 'str', v: "Wave" },{ k: 'point', x: Expr, y: Expr },{ k: 'nullUnit' }. - Variables:
{ k: 'var', name: 'wave_num', index?: Expr }. - Arithmetic:
add,sub,mul,div(safe /0),mod,min,max,clamp,trunc,abs. - Logic & Comparison:
eq,ne,lt,le,gt,ge,and,or,not. - Clock & Random:
{ k: 'tick' },{ k: 'rand', min: Expr, max: Expr }(deterministic PRNG). - Unit & Region:
{ k: 'unitProp', unit: Expr, prop: 'hp' | 'maxHp' | 'x' | 'y' | 'alive' | 'owner' },{ k: 'unitsInRegion', region: 'hill', filter?: UnitFilter },{ k: 'groupCount', group: Expr },{ k: 'distance', a: Expr, b: Expr }. - Event Context:
{ k: 'eventUnit' }(unit entering/dying),{ k: 'eventOtherUnit' }(attacker/killer),{ k: 'eventPlayer' },{ k: 'eventValue' }.
4. Actions (Action)
Actions mutate simulation state or emit presentation updates:
- Control Flow:
{ k: 'if', cond, then, else },{ k: 'while', cond, body },{ k: 'forEachUnit', group, varName, body },{ k: 'wait', ticks: Expr },{ k: 'setVar', name, value },{ k: 'addVar', name, delta }. - Spawning & Unit Control:
createUnits:{ k: 'createUnits', unitTypeId: 'goblin_brawler', count: { k: 'int', v: 5 }, player: { k: 'int', v: 1 }, at: { k: 'regionCenter', region: 'spawn' }, intoVar: 'last_wave' }killUnit,removeUnit,setUnitHp,setUnitOwner,moveUnitorder:{ k: 'order', units: Expr, order: 'attack_move' | 'move' | 'attack' | 'hold', to?: Expr, target?: Expr }
- UI & HUD:
showText: Displays formatted on-screen notification to specified players.setCounter: Shows a persistent label and value on the player HUD.setLeaderboardRow: Updates rows in the top-right score table.ping: Renders a map beacon at a given tile coordinate.playSound: Plays audio cues (soundId).
- Economy & Victory:
setResource,addResource,setBuildableRegionvictory:{ k: 'victory', player: Expr }defeat:{ k: 'defeat', player: Expr }
Data Overrides (DefOverrideSet)
The Data module allows AI creators to balance existing units or introduce cloned custom archetypes without changing engine source code:
interface DefOverrideSet {
units?: DefOverride[];
buildings?: DefOverride[];
items?: DefOverride[];
spells?: DefOverride[];
}
interface DefOverride {
id: string; // ID to patch or new unique ID
baseId?: string; // If present, clones this base archetype
patch: Record<string, any>; // Overridden fields
}Example: Cloning a Boss Unit
{
"units": [
{
"id": "boss_magma_colossus",
"baseId": "dwarf_steam_tank",
"patch": {
"name": "Magma Colossus",
"hp": 4500,
"damage": 120,
"armor": 12,
"speed": 1.2,
"renderScale": 1.8,
"cost": { "gold": 0, "lumber": 0, "stone": 0, "oil": 0 }
}
}
]
}Copy-Paste AI Recipes & Prompt Templates
Copy these templates into your conversation with Claude, ChatGPT, or Cursor:
Recipe 1: Tower Defense Wave Generator
Prompt Template:
You are an expert RTS level designer for Shards of Stone.
Create a deterministic TriggerScript for a Tower Defense game mode on a 64x64 map.
Requirements:
1. Variables:
- lives: int, initial value 20
- current_wave: int, initial value 1
- max_waves: int, initial value 10
2. Regions:
- "spawn": x: 4, y: 32, w: 4, h: 4
- "waypoint_1": x: 32, y: 32, w: 4, h: 4
- "waypoint_2": x: 32, y: 12, w: 4, h: 4
- "goal": x: 60, y: 12, w: 4, h: 4
- "build_zone": x: 10, y: 10, w: 48, h: 48
3. Triggers:
- mapInit: Lock Player 0 building to "build_zone" via setBuildableRegion. Display initial Lives counter (20) and Wave counter (1).
- Periodic Wave Spawner: Every 600 ticks (30 seconds), if current_wave <= max_waves, spawn (current_wave * 4) "goblin_brawler" units for player 1 (Creep/Hostile) at region "spawn", and order them to attack_move towards "waypoint_1". Show text "Wave incoming!".
- Waypoint Triggers: When player 1 units enter "waypoint_1", order them to "waypoint_2". When entering "waypoint_2", order them to "goal".
- Leak Detection: When player 1 units enter "goal", kill them, subtract 1 from lives, update counter. If lives <= 0, trigger defeat for player 0.
- Victory Check: When current_wave > max_waves and groupCount(unitsOfPlayer(1)) == 0, trigger victory for player 0.
Output pure JSON matching the TriggerScript interface.Generated JSON Output:
{
"version": 1,
"vars": [
{ "name": "lives", "type": "int", "init": { "k": "int", "v": 20 } },
{ "name": "current_wave", "type": "int", "init": { "k": "int", "v": 1 } },
{ "name": "max_waves", "type": "int", "init": { "k": "int", "v": 10 } }
],
"regions": [
{ "id": "spawn", "x": 4, "y": 32, "w": 4, "h": 4, "name": "Creep Spawn" },
{ "id": "waypoint_1", "x": 32, "y": 32, "w": 4, "h": 4, "name": "Corner 1" },
{ "id": "waypoint_2", "x": 32, "y": 12, "w": 4, "h": 4, "name": "Corner 2" },
{ "id": "goal", "x": 60, "y": 12, "w": 4, "h": 4, "name": "Player Base" },
{ "id": "build_zone", "x": 10, "y": 10, "w": 48, "h": 48, "name": "Buildable Grounds" }
],
"triggers": [
{
"id": "td_init",
"name": "Map Initialisation",
"runOnce": true,
"events": [{ "k": "mapInit" }],
"actions": [
{
"k": "setBuildableRegion",
"player": { "k": "int", "v": 0 },
"regions": ["build_zone"]
},
{
"k": "setCounter",
"id": "lives_hud",
"label": { "parts": [{ "t": "lit", "s": "Castle Lives: " }] },
"value": { "k": "var", "name": "lives" }
},
{
"k": "setCounter",
"id": "wave_hud",
"label": { "parts": [{ "t": "lit", "s": "Wave: " }] },
"value": { "k": "var", "name": "current_wave" }
},
{
"k": "showText",
"players": null,
"text": { "parts": [{ "t": "lit", "s": "Defend the pass! Towers may only be built in the designated build zone." }] },
"durationTicks": 160
}
]
},
{
"id": "td_spawn_wave",
"name": "Spawn Wave",
"events": [{ "k": "periodic", "everyTicks": 600, "offsetTicks": 100 }],
"conditions": [
{ "k": "le", "a": { "k": "var", "name": "current_wave" }, "b": { "k": "var", "name": "max_waves" } }
],
"actions": [
{
"k": "createUnits",
"unitTypeId": "goblin_brawler",
"count": { "k": "mul", "a": { "k": "var", "name": "current_wave" }, "b": { "k": "int", "v": 3 } },
"player": { "k": "int", "v": 1 },
"at": { "k": "regionCenter", "region": "spawn" }
},
{
"k": "order",
"units": { "k": "unitsInRegion", "region": "spawn", "filter": { "players": [1] } },
"order": "attack_move",
"to": { "k": "regionCenter", "region": "waypoint_1" }
},
{
"k": "showText",
"players": null,
"text": {
"parts": [
{ "t": "lit", "s": "Wave " },
{ "t": "expr", "e": { "k": "var", "name": "current_wave" } },
{ "t": "lit", "s": " has arrived!" }
]
},
"durationTicks": 120
},
{
"k": "addVar",
"name": "current_wave",
"delta": { "k": "int", "v": 1 }
},
{
"k": "setCounter",
"id": "wave_hud",
"label": { "parts": [{ "t": "lit", "s": "Wave: " }] },
"value": { "k": "var", "name": "current_wave" }
}
]
},
{
"id": "td_wp1",
"name": "Waypoint 1 Transition",
"events": [{ "k": "unitEntersRegion", "region": "waypoint_1", "filter": { "players": [1] } }],
"actions": [
{
"k": "order",
"units": { "k": "eventUnit" },
"order": "attack_move",
"to": { "k": "regionCenter", "region": "waypoint_2" }
}
]
},
{
"id": "td_wp2",
"name": "Waypoint 2 Transition",
"events": [{ "k": "unitEntersRegion", "region": "waypoint_2", "filter": { "players": [1] } }],
"actions": [
{
"k": "order",
"units": { "k": "eventUnit" },
"order": "attack_move",
"to": { "k": "regionCenter", "region": "goal" }
}
]
},
{
"id": "td_leak",
"name": "Goal Breach",
"events": [{ "k": "unitEntersRegion", "region": "goal", "filter": { "players": [1] } }],
"actions": [
{ "k": "removeUnit", "unit": { "k": "eventUnit" } },
{ "k": "addVar", "name": "lives", "delta": { "k": "int", "v": -1 } },
{
"k": "setCounter",
"id": "lives_hud",
"label": { "parts": [{ "t": "lit", "s": "Castle Lives: " }] },
"value": { "k": "var", "name": "lives" }
},
{
"k": "if",
"cond": { "k": "le", "a": { "k": "var", "name": "lives" }, "b": { "k": "int", "v": 0 } },
"then": [
{ "k": "defeat", "player": { "k": "int", "v": 0 } }
]
}
]
}
]
}Recipe 2: King of the Hill / Control Point
Prompt Template:
Write a TriggerScript for a 2-player King of the Hill (KotH) match.
Requirements:
1. Region: "hill_center" at x: 44, y: 44, w: 8, h: 8.
2. Variables:
- score_p0: int (initial 0)
- score_p1: int (initial 0)
- target_score: int (500)
3. HUD:
- Leaderboard showing "King of the Hill" with current scores for Player 0 and Player 1.
4. Periodic Scoring (every 20 ticks = 1 second):
- Count alive Player 0 units in "hill_center" vs alive Player 1 units.
- If Player 0 has more units, add 1 to score_p0. If Player 1 has more, add 1 to score_p1.
- Update leaderboard rows.
- Check if either player reached target_score. Award victory to that player.Recipe 3: Hero Boss Arena Encounter
Prompt Template:
Write a TriggerScript and DefOverrideSet for an epic Boss Arena:
1. Data Overrides:
- Clone "dwarf_steam_tank" as "dungeon_golem_boss": Name "Ancient Iron Golem", HP 6000, Damage 150, Armor 15, Speed 1.4, renderScale 2.0.
2. Trigger Script:
- mapInit: Spawn "dungeon_golem_boss" at region "boss_arena" for player 1. Store ID in variable "boss_unit".
- Boss Phase 2 Transition: Event unitAttacked or periodic checking hpPercent of "boss_unit". When HP drops below 50%:
* Show announcement text: "The Ancient Iron Golem activates Overclock protocol!"
* Spawn 4 "dwarf_miner" repair units around the boss.
* Ping boss location on minimap in bright crimson (#FF2222).
- Boss Defeat: Event unitDies filtered on "boss_unit":
* Drop legendary relic item at boss death point.
* Grant victory to Player 0.Custom Assets & Rules: 3D Models, Sprites, Audio & Loading Screens
When prompting an LLM to build a rich mod, you can instruct it to hook into the engine's asset registry and custom map rules:
1. Visual & Audio Asset Keys in defOverrides
modelId: The 3D GLB model key loaded in Three.js mode. Examples:"dwarf_cannon_tower","dwarf_steam_tank","goblin_war_wagon","dwarf_gyrocopter","dwarf_tower".spriteKey: The 2D isometric sprite sheet key for classic canvas rendering.portraitKey: The animated selection portrait or unit card displayed in the HUD.icon: The 128×128 UI icon for training and building queues.playSound: Trigger action key for spatial audio events (e.g."goblin_bomb_lobber_attack","dwarf_steam_cannon_attack","horn_alert").
2. Map-Level Presentation & Rules
Include these in the root of your custom map JSON:
{
"name": "Custom Tower Defense",
"meta": {
"loadingScreen": "/assets/artwork/splash_mountain_hold.png"
},
"rules": {
"standardVictory": false,
"noStartingUnits": true,
"disableAi": true,
"allowedBuildings": [
"dwarf_tower",
"custom_spire_tower",
"custom_flak_tower",
"goblin_tower"
]
}
}meta.loadingScreen: Displays a custom 16:9 cinematic painting during match loading and pre-game staging.rules.disableAi: Set totrueto disable AI worker production and autonomous base-building, allowing computer players to act purely as scripted creep waves or bosses.rules.allowedBuildings: Whitelist array of building IDs. Filters the worker build menu to only show permitted structures (e.g. multi-faction defensive towers).
Recipe 4: Multi-Faction Tower Defense with Custom 3D Models & Loading Art
Prompt Template:
Write a complete SerializedMap JSON for an open-arena Tower Defense map.
Requirements:
1. Meta & Rules:
- meta.loadingScreen: "/assets/artwork/splash_mountain_hold.png"
- rules.disableAi: true (scripted creeps only, no enemy workers)
- rules.standardVictory: false, rules.noStartingUnits: true
- rules.allowedBuildings: ["dwarf_tower", "custom_spire_tower", "goblin_tower"]
2. Data Overrides:
- Clone "dwarf_tower" as "custom_spire_tower":
* Name: "Arcane Runic Spire"
* modelId: "dwarf_cannon_tower" (reuses the 3D cannon tower mesh)
* spriteKey: "dwarf_tower", portraitKey: "dwarf_tower"
* damage: 65, attackRange: 8, attackSpeed: 1.5, cost: { "gold": 120, "lumber": 30 }
* requires: [] (no barracks requirement)
- Clone "goblin_peon" as "td_scout": hp 45, speed 3.5.
- Clone "goblin_brawler" as "td_brawler": hp 120, speed 2.2, armor 2.
3. Triggers:
- Preparation phase: 30-second cooldown timer before wave 1 (atTick: 600) with a visible HUD countdown counter.
- Spawns waves across an open mazing arena targeting the player's base.
- Leak detection trigger: decrements lives counter, plays explosion sound effect, and pings minimap.
- Defeat trigger if base is destroyed or lives reach 0.1-Click Import & Playtesting
- In The Forge (Map Editor), switch to the Triggers tab (
F8) or Data tab (F9). - Paste the JSON into the script input or load it directly into your map's
.play.jsondraft. - Hit Ctrl + Enter or Ctrl + F9. The editor validates the AST and launches the full lockstep engine in under 500 milliseconds.
- If any node contains an unknown unit ID or out-of-bounds region, the Validation sidebar immediately highlights the JSON path with zero guesswork.