Map Editor Documentation

AI Creators & LLM Modding

How to use Claude, ChatGPT, Cursor, and other LLMs to design custom RTS game modes, triggers, unit overrides, and complete maps.

Page 13 of 13

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:

  1. 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.

  2. Prompt Your Model: Feed the exported context along with the Trigger AST and DefOverride schema provided below (or link directly to /llms.txt and /llms-full.txt). Ask the model to generate a TriggerScript or a DefOverrideSet for your desired mode (Tower Defense, King of the Hill, Boss Arena, Auto-Battler).

  3. 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 like Math.sin or Math.hypot are banned from the simulation.
  • Sorted groups: Unit groups are sorted number[] entity arrays, never Set instances (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 (never Infinity or NaN)
  • Undeclared variable or out-of-bounds array $\rightarrow$ zero-value of the type
  • Unbounded loops $\rightarrow$ strictly capped by LOOP_CAP (10,000) and CASCADE_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 kKey ParametersDescription
mapInit{}Fires once on tick 0 after variable initialization.
periodiceveryTicks, offsetTicks?Fires every N ticks (20 ticks = 1 second).
atTicktickFires at a precise simulation tick.
unitEntersRegionregion, filter?Fired when a unit steps into a named region.
unitLeavesRegionregion, filter?Fired when a unit exits a named region.
unitDiesfilter?Fired when a matching unit is killed.
unitTrainedfilter?Fired when training completes at a building.
buildingCompletefilter?Fired when a construction completes.
timerExpirestimerFired when a named countdown reaches 0.
dialogButtondialogId?, buttonIndex?Fired when a player clicks a dialog option.
customnameRaised 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, moveUnit
    • order: { 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, setBuildableRegion
    • victory: { 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 to true to 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

  1. In The Forge (Map Editor), switch to the Triggers tab (F8) or Data tab (F9).
  2. Paste the JSON into the script input or load it directly into your map's .play.json draft.
  3. Hit Ctrl + Enter or Ctrl + F9. The editor validates the AST and launches the full lockstep engine in under 500 milliseconds.
  4. If any node contains an unknown unit ID or out-of-bounds region, the Validation sidebar immediately highlights the JSON path with zero guesswork.