scmJS docs

Writing triggers

A trigger is one call:

trigger(AllPlayers, [
  bring(CurrentPlayer, units.AnyUnit, locations.Beacon, ">=", 1),
], [
  displayText("You found it!"),
  preserve(),
]);

trigger(players, conditions, actions, options?) records one trigger. players is a player or a list of them; conditions and actions are lists of what the condition and action functions return. The options are the execution flags by name: { preserve: true } is the same as the preserve() action, and disabled, ignoreGameEnd and the rest are there too.

Every condition and action the game has is a function named after StarEdit's, in camel case: bring, deaths, command, accumulate, elapsedTime, switchIs (its real name is a reserved word), createUnit, displayText, setResources, moveUnit, order, runAiScript, victory. The arguments come in the order the Trigger Editor shows them, and the ones the editor offers as a list are short words: ">=", "<=", "==" for a comparison; "set", "add", "subtract" for a modifier; "set", "clear", "toggle", "randomize" for a switch; "ore", "gas", "oreAndGas" for a resource; "move", "patrol", "attack" for an order. A unit count is a number or "All". StarEdit's own labels ("At least") are accepted as well. not(condition) is the opposite of a condition where one condition can say it: not(bring(…, ">=", 1)) is "at most 0". Hover any of them in the editor for its arguments.

Names come from the map. Each display name becomes an identifier — Terran Marine is units.TerranMarine, Terran Siege Tank (Tank Mode) is units.TerranSiegeTankTankMode — and the display name itself still works as an index, units["Terran Marine"]. P1 … P12, CurrentPlayer and AllPlayers are constants, and the other groups are under players: players.Force1, players.Foes, players.Allies. A raw number works wherever a name does, which is how an EUD player or an odd unit id gets in.

The script is a program that runs when it is applied, and everything TypeScript offers at that moment is fair game. A loop makes the same trigger for several players; a function returns a list of actions; a table holds the numbers; a template string builds the text:

function reinforce(p: Player) {
  return trigger(p, [deaths(p, units.TerranMarine, ">=", 10)], [
    createUnit(p, units.TerranSiegeTankTankMode, 1, locations.Spawn),
    setDeaths(p, units.TerranMarine, "subtract", 10),
    displayText("Reinforcements have arrived."),
  ], { preserve: true });
}

for (const p of [P1, P2, P3, P4]) reinforce(p);

Nested lists are flattened and false, null and undefined entries are skipped, so a helper can return a list of actions and a condition can be written hardMode && bring(…). What the script records is what the map gets, and the order of the trigger() calls is the order of the triggers.

A condition is a value here — the game tests it later — so if (bring(…)) outside a program does not do what it looks like, and the editor says so and where the test belongs: in a trigger's conditions, or in an if inside a program.

A script can be several files. import { x } from "./name" brings in another file of the map's script; nothing else can be imported. The library is available as globals, so no import is needed, and also as the module "trigscript" for anyone who prefers import { trigger, bring } from "trigscript".

hyperTriggers(P8) anywhere in the script emits the classic three preserved triggers of sixty-two waits, so the whole trigger list runs every frame instead of every two seconds. Give them to a player whose other triggers never wait — a computer slot, usually — since a Wait stalls every trigger of that player. A map with a program does not need them: its build already makes the list run every frame.