In the summer of 2002, Blizzard released Warcraft III: Reign of Chaos. Packed inside that retail disc was the Warcraft III World Editor, a piece of software that accidentally altered the history of competitive video games. Over the following decade, custom map creators used its trigger editor to invent the multiplayer online battle arena with Defense of the Ancients, popularised tower defense as a standalone subgenre, and laid the mechanical seeds that would eventually blossom into auto battlers.

Yet beneath that explosion of community creativity sat an architectural house of cards: JASS, or Just Another Scripting Syntax. JASS was an imperative, interpreted language designed in the late 1990s. It was fast enough for a single CPU core, but it asked map authors to act as manual memory managers and fragile lockstep police.
When Adam and I set out to build a fully moddable real-time strategy game for modern browsers, we studied JASS with deep admiration and immediate dread. We had already spent weeks reading two decades-old map formats to import terrain from older titles. But running legacy procedural scripting inside modern web browsers does not just fail to scale; it breaks multiplayer lockstep determinism at the fundamental compiler level.
Here is why we abandoned procedural code for a closed, deterministic JSON Abstract Syntax Tree, and how that decision made browser RTS maps both leak-proof and AI-native.
The three architectural demons of JASS
To understand why web RTS modding requires a new foundation, you must examine the three structural flaws that haunted every custom map tournament in 2005.
1. The handle leak plague
In JASS, game entities (units, locations, groups, timers, and condition expressions) are represented as opaque integer handles pointing into a global engine table. The engine tracked these with an internal reference counter, but the scripting language required manual pointer destruction:
local location p = GetRectCenter(gg_rct_Spawn)
call CreateNUnitsAtLoc(5, 'ogbr', Player(1), p, 270.0)
// If you forget this exact line, 32 bytes leak permanently in memory:
call RemoveLocation(p)
set p = nullNotice the final two lines. Calling RemoveLocation freed the spatial
coordinate inside the engine, while assigning null cleared the variable
reference on the VM stack. If a map author omitted RemoveLocation, the handle
slot remained occupied forever. If they omitted set p = null, the engine
reference counter refused to recycle the slot.
In a custom Tower Defense map spawning 60 creeps every five seconds, a single missing cleanup line leaked thousands of handles per minute. After twenty minutes of play, the global handle table exceeded 300,000 entries. Pathfinding slowed down, garbage collection stalled, and the client crashed with a fatal out-of-memory error.
2. Out-of-sync desyncs
Multiplayer RTS games run on lockstep simulation: each client receives only player inputs, running an identical simulation frame-by-frame on local hardware. If two computers compute even one pixel of movement differently, the game states diverge and the match instantly drops into an Out-of-Sync error. We documented our lockstep test suite in how lockstep determinism is tested, and custom scripts are where that verification usually breaks.
JASS made desyncs horrifyingly easy to trigger:
- Calling
GetLocalPlayer()allowed authors to execute code only on one participant's computer (useful for showing private UI text). But if that block accidentally created a unit handle, read a random number, or touched pathfinding, the local simulation diverged and dropped all other players. - Unit group iteration in JASS was unsorted. Depending on memory layout and
CPU cache order, two clients iterating through
GetUnitsInRectcould process targets in different sequences. If an action damaged the closest target first, differing iteration order meant different units survived.
3. The 50GB binary distribution barrier
A custom map was packaged inside an MPQ archive: a proprietary binary container holding raw terrain files, sound clips, and compiled bytecode. Playing a new map required launching a heavyweight native desktop client, sitting in a Battle.net chat room, and enduring a peer-to-peer download throttled to kilobytes per second. If the host disconnected, everyone was kicked back to the menu.
Why Lua and eval(js) break browser lockstep
When designing our browser engine, the obvious first thought was to embed a lightweight scripting language. WebAssembly can compile Lua in an afternoon, or we could have let modders write raw JavaScript callbacks executed inside Web Workers, following our approach to running simulation in a Web Worker.
Both paths lead to immediate multiplayer disaster on the open web.
The first issue is cross-engine floating-point variance. When three players join a match, one might be running Google Chrome on Windows (V8 engine), another Safari on an Apple M3 laptop (JavaScriptCore), and the third Firefox on Linux (SpiderMonkey).
Different JavaScript JIT compilers implement transcendental math functions differently:
// On Chrome V8 (x86-64):
Math.hypot(3.14159265, 2.71828182) // 4.154881079361849
// On a slightly older Safari JIT or ARM platform:
Math.hypot(3.14159265, 2.71828182) // 4.1548810793618485That tiny discrepancy in the final bit of mantissa is invisible in a web page, but in an RTS collision system, it means a goblin clips a rock on one machine and slides past it on another. Five ticks later, the entire match has desynced.
The second issue is object iteration stability. In JavaScript, iterating over
a Set or keys of a Record depends on insertion history. If client A creates
unit 12 before unit 14, while client B receives the network packet in a split
tick boundary, the iteration order diverges.
Finally, arbitrary code execution in the browser is a security hazard. Letting
untrusted custom maps run arbitrary JavaScript means sandboxing fetch,
localStorage, the DOM, and WebSockets. One sandbox bypass allows a map to
steal cookies or mine cryptocurrency in the background.
The solution: a closed, deterministic JSON AST
Instead of executing procedural scripts, our game engine evaluates triggers as
pure, declarative data. We defined a strict TypeScript schema called
TriggerScript.
A trigger is not a script file. It is an abstract syntax tree stored as clean JSON:
{
"id": "spawn_wave_periodic",
"events": [
{ "k": "periodic", "everyTicks": 200 }
],
"conditions": [
{ "k": "lt", "a": { "k": "var", "name": "wave" }, "b": { "k": "int", "v": 20 } }
],
"actions": [
{
"k": "createUnits",
"unitTypeId": "goblin_brawler",
"count": { "k": "int", "v": 5 },
"player": { "k": "int", "v": 1 },
"at": { "k": "regionCenter", "region": "spawn" },
"intoVar": "active_wave"
},
{
"k": "order",
"units": { "k": "var", "name": "active_wave" },
"order": "attack_move",
"to": { "k": "regionCenter", "region": "goal" }
},
{ "k": "addVar", "name": "wave", "delta": { "k": "int", "v": 1 } }
]
}This structure produces several radical benefits:
- Zero eval, zero bytecode: The simulation loop walks the node tree at each tick. The JSON is inert data, which means it cannot touch the browser DOM, read cookies, or execute infinite un-capped CPU loops.
- Canonical sorted evaluation: When an action queries units in a region, the engine returns a sorted array of numeric entity IDs. Every client on earth iterates the exact same entities in the exact same order.
- Integer and bit-exact spatial math: Spatial queries do not rely on
hardware-dependent float routines. We use integer grid coordinates and a
deterministic distance function (
detHypot) that computes identical values on every CPU architecture.

Totality: the mathematical rule of zero throws
The most common bug in custom maps is the null pointer crash. A trigger fires
when a projectile hits a hero, but the hero was killed by another unit in the
preceding frame. In traditional languages, calling hero.getHealth() throws a
null reference error, crashing the thread.
In our trigger engine, every single expression and action is total. In formal mathematics, a total function is defined for all possible inputs: it never enters an undefined state and never throws an exception.
Our evaluator enforces totality through four simple rules:
- Missing entity safety: If a trigger requests properties for an entity
that has been destroyed, the expression evaluator yields
0,false, ornullUnit. It never throws. - Division by zero safety: In the
divandmodexpression nodes, division by zero yields0. It never producesNaNorInfinity, which would poison subsequent physics ticks. - Bounded execution: Loops are strictly capped at 10,000 iterations, and event cascades cannot exceed 64 deep. An accidental infinite loop written by an author simply stops executing and logs a diagnostic warning.
- Zero allocations: Units in the engine are stored as flat numeric indices
in Entity Component System arrays. There is no handle table, no
RemoveLocation, and no manual pointer destruction. Ephemeral data is recycled through object pools.
Because the system cannot throw, an imperfect custom map continues to run smoothly rather than abruptly ending the match for all eight players.
AI as the new level designer
The transition from procedural code to structured JSON AST makes possible something Warcraft III modders could only dream of: reliable prompt-to-map generation with modern large language models.
When you ask Claude 3.7 Sonnet / Claude 4, GPT-4o / GPT-5, or Cursor to write a custom game mode in JASS or Lua,
the model struggles. It hallucinates Blizzard native function signatures that
do not exist, forgets the RemoveLocation boilerplate, and outputs code that
fails to compile in WorldEdit.
In contrast, our engine publishes its complete schema through /llms.txt and
/docs/map-editor/ai-creators. A large language model understands JSON schemas
natively:
Prompt:
"Create a Tower Defense wave controller in Shards of Stone TriggerScript.
Spawn 6 goblin brawlers at region 'spawn_lane' every 20 seconds.
When a brawler reaches region 'temple_gate', decrement variable 'player_lives'
by 1 and remove the unit. If lives reach 0, trigger defeat for player 0."Because the output is validated against a typed schema, the model produces syntactically valid logic on the first attempt. The author simply clicks Import in The Forge, presses Ctrl + Enter, and is immediately playtesting their custom game mode in the browser.
The in-browser Forge
A modern modding ecosystem cannot rely on legacy desktop software. The entire authoring pipeline for Shards of Stone lives in the browser at /map-editor.
The Forge combines:
- Dual-mode editing: smooth switching between a 2D tile editing grid and a 3D Three.js perspective view.
- Terrain height sculpting, ramp painting, water, lava, and doodad placement.
- Unit and building placement with customized starting inventory and orders.
- Per-map balance overrides: tweaking damage, health, and gold costs without touching global engine files.
- Trigger creation with visual event-condition-action graphs or raw JSON AST editing.
Because maps are compact JSON files under 200KB, they can be shared via a URL or stored in local browser storage. There are no 50GB downloads, no third-party patching tools, and no lobby waiting rooms.
The open web takes the mantle
Warcraft III proved that player-created custom maps can birth entire genres. The open web provides the distribution network that classic RTS modding always deserved: frictionless links, instant loading, cross-platform play, and a safe runtime.
By abandoning procedural scripting for a deterministic, total JSON AST, we eliminated handle leaks, made multiplayer lockstep permanent, and turned AI assistants into collaborative level designers.
You can inspect the complete schema in our AI Creator documentation, explore the architecture breakdown on our /modding hub, build your own battlefield in /map-editor, or jump straight into the browser skirmish at /play today.




