scmJS docs

Programs

Everything inside program(() => { … }) runs in the game:

program(() => {
  let wave = 0;
  let alarm = false;
  while (true) {
    if (bring(P1, units.AnyUnit, locations.Beacon, ">=", 1) && !alarm) {
      alarm = true;
      displayText("They are coming.");
    }
    if (alarm) {
      createUnit(P8, units.ZergZergling, 4, locations.Spawn);
      wave += 1;
    }
    if (wave >= 10) defeat();
    sleep(seconds(20));
  }
}, { owner: P1 });

A program runs every frame, from where it left off. Its body runs until it reaches a sleep() or its end, all within one frame of the game, and the next frame it carries on from there. A body that ends stops for good. So the loop above is a game loop: it looks at the beacon, sends a wave if the alarm is up, and sleeps twenty seconds.

Variables hold numbers, booleans, texts and units of the game, and a let p = { lives: 3, gold: 0 } is a variable per field. They live in the game while the map is played and take nothing from the map: no death counters, no switches, no triggers in the list. Numbers are whole and signed, as in TypeScript: a - b is below zero when b is larger, while (i >= 0) ends, and a number below zero is shown with its minus sign. A number runs from −2 147 483 648 to 2 147 483 647 and wraps at either end. A u8 or u16 variable (let lives: u8 = 3) stays within 0 … 255 or 0 … 65 535 and stops at both ends, which is what lives and cooldowns want; a u32 runs from 0 to 4 294 967 295, for bit masks and hashes, and is kept apart from a plain number in arithmetic unless u32(x) or i32(x) says which is meant. +, -, *, / and % work between variables, with Math.min, Math.max, Math.abs and clamp(), and so do the bitwise &, |, ^, <<, >> and >>>; division is whole and towards zero, and dividing by a variable that is 0 gives 0. Where the game takes nothing below zero — hit points, an amount, a unit count — a number below zero goes in as 0.

Arrays hold numbers, booleans, texts, units, records or other arrays: let hp = [10, 20, 30], new Array(12).fill(3), an index that is a constant or a variable (hp[i] += 7), .length, for (const x of xs), fill, includes and indexOf. An array that something pushes to grows — const queue: number[] = []; queue.push(x); queue.pop() ?? 0 — with no size to declare: the cells come from a pool the map's programs share, and the workspace's Settings view (Ctrl+,) sets how large it is, a setting kept in the map. An index past the end reads 0 and stores nothing, and Simulate says where. An array of records — let waves = [{ count: 4, delay: 2 }], waves[i].count += 1, waves.push({ … }) — hands out its records by reference, as TypeScript does, and an array of units (const squad: Unit[] = [], squad.push(u)) may be kept across a sleep(). A record in an array may hold a unit, a text, an array of its own (squads[i].members.push(u)) or another record. An array of arrays is a grid — let grid = [[0, 0, 0], [0, 0, 0]], grid[y][x] += 1 — or rows that each grow (buckets[i].push(v)), and const names: string[] = [] is an array of texts. A list made outside the program — const price = [50, 100, 150], or a wave table of records — can be looked up with a variable: price[level], waves[wave].count.

The array methods that take a function are there, written as in TypeScript: forEach, some, every, find, findIndex, reduce, map, filter, sort and reverse, and chains of them. filter over the units of the game is how to keep some of them:

program(() => {
  while (true) {
    // The three most hurt units at the base get 20 hit points, every two seconds.
    const hurt = unitsAt(locations.Base, { owner: CurrentPlayer }).filter((u) => u.hp < u.maxHp);
    hurt.sort((a, b) => a.hp - b.hp);
    for (const u of hurt.slice(0, 3)) u.heal(20);
    sleep(seconds(2));
  }
}, { owner: AllPlayers });

The function is written into the loop the method becomes, so it costs nothing and sees the program's variables — and for the same reason it cannot be kept in a variable: write it where it is used, or name a function. sort wants its function ((a, b) => a - b), runs within the frame, and is nothing for dozens of items and felt for hundreds every frame; the end of the line says sorts in the frame. slice, concat, toSorted, toReversed and Array.from make copies. Patterns and spread work as well: const { x, y } = mouse(P1), for (const { count, delay } of waves), [a, b] = [b, a], [...xs, 7], waves.push({ ...w, count: 9 }).

Tables keyed by an id of the game are arrays with a cell for every id, so a key of the game is one read: const bounty: Record<UnitType, number> = { [units.ZergZergling]: 5 } and bounty[u.type], new Map<UnitType, number>() with get (?? 0 for a key never set), set, has, delete, clear and size, new Set<UnitType>() with add and the same; for (const [key, value] of lost) goes through the keys that are there. The keys are unit types, players, locations, switches, weapons, upgrades or technologies.

A Map and a Set over any number, or over units, are for keys that are no id of the game: new Map<number, number>() for a tile packed into one number, new Map<Unit, number>() for a cooldown a unit, new Set<Unit>() for the units already dealt with. They go through their keys in the order the keys went in, as JavaScript's do. A key is looked for, a few steps where a table keyed by ids of the game is one read, and the values are numbers or booleans. A unit that died stays an entry until it is deleted; in a loop over the map it reads as no unit, which is the moment to delete it.

Control flow is what it says. if/else, while, do, for, switch, break, continue and c ? a : b all work. Conditions go in an if or a while; actions stand as statements. A loop runs all its rounds at once, within the frame, which has one consequence to keep in mind: a loop that never ends and never sleeps would freeze the game. The editor refuses one and says where to put sleep(frames(1)). A for whose bounds are known when the script is applied is unrolled, and the editor says so at the end of the line.

Time is sleep. sleep(seconds(15)) gives the frame back and carries on that much later, while other programs and the map's triggers go on. frames(n) is the game's own clock, a second is twenty-four frames at Fastest, and minutes() is there too. Something that runs on its own clock is another program — one program per concurrent activity. The game's own wait() is allowed but is a different thing: it stalls every trigger of that player, so use it for a short pause (a text, then a sound) and sleep to pass time.

Edges. if (rose(bring(…))) is true on the frame the condition becomes true and not again until it has been false in between; once(…) is true the first time only. random() is a coin toss, and random(n) a whole number from 0 to n − 1.

Reads: what the game holds is a value. Every condition that compares a quantity is also a read of it when the comparison and the amount are left out, and a read goes wherever a number goes:

program(() => {
  let price = 50;
  while (true) {
    if (minerals(CurrentPlayer) >= price * 2 && bring(CurrentPlayer, units.AnyUnit, locations.Beacon) >= 1) {
      setResources(CurrentPlayer, "subtract", price, "ore");
      createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Spawn);
      price += deaths(CurrentPlayer, units.TerranMarine);
    }
    sleep(seconds(1));
  }
}, { owner: AllPlayers });

deaths(p, unit), kill(p, unit), bring(p, unit, location), command(p, unit), accumulate(p, resource), score(p, kind), countdownTimer() and elapsedTime() all read this way, and the common ones have plainer names: minerals(p), gas(p), countUnits(p, unit, location?), kills(p, unit), countdown(), elapsed() — those two in the game's own seconds, which at Fastest pass about one and a half times as fast as the seconds of sleep. A read means what its condition means — a force's minerals are the force's sum, units.Men counts what Bring counts — and it is taken when the line runs, so let ore = minerals(P1) keeps the number and minerals(P1) written twice reads twice. What to read — the player, the unit, the location — is fixed when the script is applied. About the players themselves there are race(p) (compare it with races.Zerg, .Terran, .Protoss), slot(p) (slots.Human, .Computer, .Empty), isHuman(p), hasLeft(p) and supply(p, "used" | "max" | "provided") as the top bar shows it.

The units on the map are objects. A loop runs over the ones that match, a pick finds one, and a unit has properties and things it can be told:

program(() => {
  while (true) {
    // Everything Player 2 has on the hill is worn down to half health.
    for (const u of unitsAt(locations.Hill, { owner: P2 })) u.hp = u.maxHp / 2;

    // The Marine nearest the beacon is sent to the base; nobody else moves.
    const scout = nearest(units.TerranMarine, locations.Beacon, { owner: P1 });
    if (scout) scout.order("move", locations.Base);

    sleep(seconds(5));
  }
});

unitsAt(location, filter?), unitsOf(player, filter?) and allUnits(filter?) are what a for…of runs over; first(filter?), nearest(type, location, filter?) and randomUnit(filter?) give one unit, or null when nothing matches — so the if (scout) is required, and the editor says so when it is missing. A filter is { type, owner, at }; units.Men, units.Buildings and units.Factories work as a type. A unit has hp, shields and energy in whole points, maxHp and maxShields, owner, type, x, y, kills, cooldown, resources, the spell timers (stim, lockdown, stasis, …) and invincible, burrowed, cloaked, hallucinated, underAttack. Hit points, shields, energy, kills, the cooldown, the timers and invincible can be written; the position, the owner and the rest are read only, and the editor marks a write to one as you type. A unit can be told order("move" | "patrol" | "attack", location), give(player), kill(), remove(), damage(n), heal(n) — or { percent: 50 } — and locate(location), which centres a location on the unit so that the ordinary actions can happen where it stands.

A variable can keep a unit across a sleep. Units die, and the game hands a dead unit's place to the next one made, so every use checks that the unit is still the same one: once it is gone its numbers read 0 and nothing written or told to it has any effect, and if (u) asks whether it is still there. A loop over units runs within one frame, so a sleep inside one is an error; to take units one at a time, find the next after each sleep. Each loop and each pick looks through all of the game's 1700 unit slots when its line runs, which is nothing a few times a second and worth a thought every frame: the editor writes scans units at the end of such a line.

stats() reaches the game's own tables: what a unit type costs, what a weapon does, which upgrades a player has.

program(() => {
  stats(units.TerranMarine).minerals = 25;
  stats(units.TerranMarine).speed = 6.5;               // pixels a frame; a Marine walks at 4
  stats(units.ZergZergling).name = "Dog";
  stats(weapons.GaussRifle).damage += 2;
  stats(P1).upgrades[upgrades.TerranInfantryWeapons] = 3;
  stats(P3).color = "teal";
});

A field reads as a number (or true / false) and takes = and +=. Unit types, weapons (weapons.), upgrades (upgrades.), technologies (techs.) and players each have their own fields, and the completion list after the dot is the list: only what was played and seen working in Remastered is offered, and the hover on each field says what it reaches — most unit-type fields apply to units made after the write, a weapon's to every unit using it, a colour at once. A write lasts for the game.

What the players do is there to read: a key, a mouse button, where the mouse is, and what a player types.

program(() => {
  while (true) {
    if (keyPressed(CurrentPlayer, "F8")) createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Anywhere);

    if (clicked(CurrentPlayer, "right")) {
      const at = mouse(CurrentPlayer);
      displayText(`You clicked at ${at.x}, ${at.y}.`);
    }

    const m = chatted(CurrentPlayer, "-spawn {n} {what:unit}");
    if (m) createUnit(CurrentPlayer, m.what, m.n, locations.Anywhere);

    underMouse(CurrentPlayer, { owner: CurrentPlayer })?.heal(10);
    sleep(frames(1));
  }
}, { owner: AllPlayers });

A key, a click and a typed line are true on the one frame they arrive, so look for them in a loop that sleeps one frame at a time. keyPressed is true once per press — not while the key is held, and not while the player is typing a message. mouse(p) is the place on the map under the player's cursor, in pixels (32 to a tile); centerLocation(location, x, y) moves a location there, so that a unit can be created under the cursor, and underMouse(p) is the unit nearest the cursor, or null.

chatted(p, pattern) is null until the player sends a line that fits the pattern, and then holds what the pattern read out of it. The pattern's own words are matched exactly and the whole line has to fit; {n} reads a whole number, {what:unit} a unit type by its name (the rest of the line, so it comes last) and {kind:ore|gas} one of the listed words, as its place in the list. The editor knows the names in the pattern: after m. it offers n and what, and nothing else. A pattern starts with a word of its own, such as -spawn, so ordinary talk is left alone. A game played in single player has no chat, so try typed lines in a multiplayer game — hosting one alone is enough.

All of this reaches every player's computer in step, a few frames after it happens. It costs the map a little, and only when a program reads input: one free location among the first 63 (nine when the mouse is read; the script is told when there is no room), and the Valkyrie and Player 12, which carry the input between computers and must be left alone.

A text can hold the program's numbers. Write it as a template literal:

program(() => {
  let wave = 0;
  while (true) {
    wave += 1;
    displayText(`Wave ${wave}: you have ${minerals(CurrentPlayer)} ore, ${name(CurrentPlayer)}.`);
    print(`${color(P1)}${name(P1)}\x01 leads with ${kills(P1, units.AnyUnit)} kills`, { to: AllPlayers, position: "center" });
    sleep(seconds(30));
  }
}, { owner: AllPlayers });

name(p) is the player's name and color(p) the colour code of their colour, filled in by the game. displayText shows the text to the current player, as it always has; print(text, { to, position }) shows it to someone else — a player, AllPlayers, a force — or, with position: "center", on the line in the middle of the screen where the game's own messages appear.

A text is a value. A string variable holds one, a function takes and returns one, a record has one for a field, and there is no length to declare:

program(() => {
  let shown = "";
  while (true) {
    const slain = kills(P1, units.AnyUnit);
    const rank = slain >= 50 ? "Veteran" : slain >= 10 ? "Soldier" : "Recruit";
    const line = `${rank}: ${slain} kills, ${Math.max(50 - slain, 0)} to go`;
    if (line != shown) { setMissionObjectives(line); shown = line; }
    sleep(seconds(1));
  }
});

Texts take +, +=, templates, the comparisons, length, s[i], slice, indexOf, includes, startsWith, endsWith, padStart, padEnd, repeat and for (const ch of s); a character is a character, so "저글링".length is 3. The objectives, a leaderboard's label, a transmission's line and a unit type's name (stats(units.ZergZergling).name = …) take a text the program made, up to 255 bytes; what is on the screen is the text as it was when the action ran, so run the action again when the text changes, as above. A made text holds 1 023 bytes and is cut off past that, which the game says once in red. split, replace, trim and parseInt are not there: keep a number beside the text it was made into.

Functions declared inside the program, or made with game() in any file, run in the game too. Arguments pass by value, they may return a number, a boolean, a text or a unit — function canAfford(price: number) { return gold >= price; } — and they may sleep. A function used once is written into the program where it is called; one used more than once is a single copy in the built map that every call runs, which keeps a script with helpers small. The end of the function's line says which it got (called ×3, inlined ×2), and hovering it says why: a function that sleeps, or whose parameter has to be known when the map is built (the player of setResources(p, …)), stays inlined. A function may call itself — fib(n - 1) + fib(n - 2), a flood fill over an array — as long as it does not sleep: each run has its own parameters and locals, as in TypeScript. It may go 1 024 calls deep unless the workspace's Settings says otherwise; a call past that stops the program, and the game says where. Thousands of such calls within one frame make the game pause, since each keeps its function's variables while it runs.

Classes are TypeScript's, declared inside the program: fields, a constructor, methods, get and set, static, private, extends with super.

program(() => {
  class Wave {
    left: number;
    constructor(public unit: UnitType, count: number) { this.left = count; }
    get done() { return this.left == 0; }
    send() {
      createUnit(P8, this.unit, 1, locations.Spawn);
      this.left--;
    }
  }
  const waves = [new Wave(units.ZergZergling, 12), new Wave(units.ZergHydralisk, 6)];
  for (const w of waves) {
    print(`${w.left} are on their way`, { to: AllPlayers });
    while (!w.done) { w.send(); sleep(seconds(1)); }
    sleep(seconds(30));
  }
  victory();
});

An instance is a record and a method a function handed the instance, so what holds for functions holds for methods. Which class an instance is, is settled when the script is applied — nothing of a class is left in the game — and the limits follow from that: an array of instances holds one class, a function can give back an instance only when every return gives the same one (return this, so calls chain, or return new Wave(…)), and a class a program writes to is declared inside it.

A program runs for its owner (Player 1 unless { owner: … } says otherwise), as that player, while that player is in the game. AllPlayers, a force or a list of players makes a per-player program: the same code runs for each of those players who is in the game, computers included, CurrentPlayer is that player, and every variable is per player, each with their own copy — which is how lives, scores and cooldowns are written once. let total = shared(0) is one value they all share.

Everything the body reads from outside is computed when the script is applied. A constant, a helper, a condition, an action: each is worked out once, when the script runs, and the editor underlines those parts with dots so the boundary is visible as you type. That is what lets a helper written outside the program supply actions inside it. It is also the one rule to keep in mind: a program variable cannot reach a condition, an action or a helper, because those were computed before the game started. The exceptions are the amount of setResources, setDeaths, setScore and setCountdownTimer, the unit count of createUnit, killUnitAt, removeUnitAt and giveUnits and an action's unit type, which can be variables or expressions over them, and a text: displayText and print take any text the program made, and so do the objectives, a leaderboard's label, a transmission and a unit type's name.