scmJS docs

Coming from eudplib

This page is for map makers who already write EUD maps with eudplib, in Python or in epScript, and build them with euddraft. A TrigScript program does the same kind of work: code that runs inside StarCraft: Remastered, reading and writing the game's memory. Most of what you know carries over, and this page says where each idea went.

Which one to use#

TrigScript suits you when you want to work in one place: the code is checked against the map's own names as you type, Simulate and tests try it without starting the game, and saving the map builds it, in the browser or the desktop app. Its language is TypeScript, with arrays that grow, classes, closures, try / catch and async, and the same script can make ordinary triggers for the classic game beside the programs.

eudplib stays the better fit when a map depends on an euddraft plugin or a community library that TrigScript has nothing like, when you want euddraft's map protection, or when you already have a build you are happy with. TrigScript does not read eudplib or epScript code: moving a map over means writing its code again.

The same ideas, side by side#

eudplib, epScript TrigScript
EUDVariable(), var x = 0; let x = 0; inside a program() (Variables)
PVariable, getcurpl() / setcurpl() A per-player program, { owner: AllPlayers }: every variable is per player, and CurrentPlayer is that player (Owners)
onPluginStart() The start of a program's body, before its loop
beforeTriggerExec(), afterTriggerExec() A loop in the program: while (true) { …; await nextFrame(); } (How a program runs)
EUDIf(), EUDWhile(), if, while in epScript if, while, for, switch, with conditions of the game where a boolean goes (Control flow)
DoActions(…), an action as a statement An action as a statement: setDeaths(P1, units.TerranMarine, "set", 0);
@EUDFunc, function in epScript function, called or written out by the compiler (Functions)
EUDFuncPtr A function kept in a variable, and closures (Functions as values)
EUDArray, EUDVArray number[], fixed or growing, with push, map, filter, sort and the rest (Arrays)
foreach (cunit : EUDLoopCUnit()), CUnit for (const u of unitsOf(P1)), u.hp, u.order(…) (Units)
f_dwread_epd, SetMemory, MemoryEPD stats() and memory.* for the named fields; memory() and setMemory() for any address (The game's tables and memory)
StringBuffer, f_sprintf A template literal: displayText(`Wave ${wave}`) (Text)
The MSQC plugin (keys and the mouse) keyPressed, keyHeld, clicked, mouse, with nothing to configure (Keys, the mouse and chat)
The chatEvent plugin chatted(player, "-spawn {n} {what:unit}") and on.chat
The .eds file and an euddraft run Saving the map

One difference shows up everywhere: a unit's fields. eudplib reads them as the game keeps them, so hit points are in 256ths and an owner is a number. TrigScript's fields are in the units the game shows (u.hp is whole hit points, u.owner a player), and only the fields that have been seen working in Remastered are there.

A counter of lives, in epScript as a euddraft plugin, then in TrigScript:

var lives = 3;

function beforeTriggerExec() {
    if (Deaths(P1, AtLeast, 1, "Terran Marine")) {
        lives -= 1;
        SetDeaths(P1, SetTo, 0, "Terran Marine");
        if (lives == 0) {
            setcurpl(P1);
            DisplayText("Out of lives.");
        }
    }
}
program(() => {
  let lives = 3;
  while (true) {
    if (deaths(P1, units.TerranMarine, ">=", 1)) {
      lives -= 1;
      setDeaths(P1, units.TerranMarine, "set", 0);
      if (lives == 0) displayText("Out of lives.");
    }
    sleep(frames(1));
  }
});

The program runs as Player 1, so displayText is already Player 1's. Its loop is the beforeTriggerExec of the epScript, and the sleep gives the frame back.

A loop over units: Player 1's Marines under 20 hit points healed to 40.

function beforeTriggerExec() {
    foreach (cunit : EUDLoopCUnit()) {
        if (cunit.owner == P1 && cunit.unitType == $U("Terran Marine") && cunit.hp < 20 * 256) {
            cunit.hp = 40 * 256;
        }
    }
}
program(() => {
  while (true) {
    for (const u of unitsOf(P1, { type: units.TerranMarine })) if (u.hp < 20) u.hp = 40;
    sleep(frames(1));
  }
});

The epScript examples here were built with euddraft 0.11 and eudplib 0.81.

Speed and size#

We built the same test maps both ways, with TrigScript's own build and with eudplib, and ran them on the same machine. The figures are from October 2026, and they are rounded:

Neither kind of map plays on StarCraft 1.16.1: that version refuses writes past the end of the deaths table, which such a map makes from its first frame. A script of trigger() calls alone, with no program, plays on every version.

What does not carry over#

scmJS 0.8.5 · Generated from the repository. The source is under the MIT license, which does not cover what ATTRIBUTION.md lists. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.