scmJS docs

Examples

Each of these is a complete main.ts. They assume a map with locations named Beacon, Spawn, Base, Hill and Shop; use your own names, and the editor completes them.

Starting resources and a welcome. The simplest trigger: no preserve, so it runs once, and CurrentPlayer means each player in turn.

trigger(AllPlayers, [always()], [
  setResources(CurrentPlayer, "set", 500, "ore"),
  setResources(CurrentPlayer, "set", 100, "gas"),
  displayText("Hold the hill for five minutes to win."),
]);

Hyper triggers. One line, and the whole trigger list runs every frame. Player 8 is the computer slot here, and nothing else in the script makes that player wait. For a map of plain triggers; a map with a program runs every frame already.

hyperTriggers(P8);

Something for every player. A helper returns the trigger; a loop calls it. Death counts are the game's own counters, and subtracting ten after the reward makes the trigger fire again at the next ten, which preserve: true allows.

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);

A shop. Bring a civilian to the shop with enough minerals, and four marines appear at the spawn. The civilian is moved out first, so the trigger does not fire again on the next cycle, and price is an ordinary constant used twice.

const price = 150;

trigger(AllPlayers, [
  bring(CurrentPlayer, units.TerranCivilian, locations.Shop, ">=", 1),
  accumulate(CurrentPlayer, ">=", price, "ore"),
], [
  moveUnit(CurrentPlayer, units.TerranCivilian, 1, locations.Shop, locations.Spawn),
  setResources(CurrentPlayer, "subtract", price, "ore"),
  createUnit(CurrentPlayer, units.TerranMarine, 4, locations.Spawn),
  displayText(`Four marines for ${price} minerals.`),
], { preserve: true });

Waves from a table. The table is ordinary data; the program walks it, one wave every forty-five seconds, then waits for the last attacker to die. for … of over a list known when the script is applied is unrolled, so w.unit and w.n are plain values in each copy. The program runs as Player 1, so victory() is Player 1's; a team needs a trigger() for the others. This one, and every example below with a program, makes a map for StarCraft: Remastered.

const waves = [
  { unit: units.ZergZergling, n: 8 },
  { unit: units.ZergHydralisk, n: 6 },
  { unit: units.ZergUltralisk, n: 2 },
];

program(() => {
  displayText("The first wave arrives in thirty seconds.");
  sleep(seconds(30));
  for (const w of waves) {
    createUnit(P8, w.unit, w.n, locations.Spawn);
    order(P8, w.unit, locations.Spawn, locations.Base, "attack");
    minimapPing(locations.Spawn);
    sleep(seconds(45));
  }
  while (command(P8, units.AnyUnit, ">=", 1)) {
    sleep(seconds(2));
  }
  displayText("The last wave is broken.");
  victory();
});

Lives, per player. One program, run for every player: lives is a separate counter for each. When the hero dies the game's death count for it goes to 1; the program resets that count, takes a life, and either brings the hero back or ends the game for that player. The sleep(frames(1)) at the end of the loop is what makes it a game loop: look once a frame.

program(() => {
  let lives: u8 = 3;
  while (true) {
    if (deaths(CurrentPlayer, units.JimRaynorMarine, ">=", 1)) {
      setDeaths(CurrentPlayer, units.JimRaynorMarine, "set", 0);
      lives -= 1;
      if (lives == 0) {
        displayText("No lives left.");
        defeat();
      } else {
        createUnit(CurrentPlayer, units.JimRaynorMarine, 1, locations.Spawn);
        displayText("Your hero returns.");
      }
    }
    sleep(frames(1));
  }
}, { owner: AllPlayers });

King of the hill. A point a second for holding the hill alone, shown on a leaderboard, and a win at a hundred. points is a variable of the program and the amount of setScore follows it; players.Foes is "anyone at war with the current player", so the check is written once for everyone.

trigger(AllPlayers, [always()], [leaderboardPoints("Hill", "custom")]);

program(() => {
  let points: u16 = 0;
  while (true) {
    if (bring(CurrentPlayer, units.AnyUnit, locations.Hill, ">=", 1)
        && !bring(players.Foes, units.AnyUnit, locations.Hill, ">=", 1)) {
      points += 1;
      setScore(CurrentPlayer, "set", points, "custom");
      if (points >= 100) {
        displayText("The hill is yours.");
        victory();
      }
    }
    sleep(seconds(1));
  }
}, { owner: AllPlayers });

Random events. Every two minutes, a coin toss decides which of two things happens. random() is the coin; else is the other side of it.

program(() => {
  while (true) {
    sleep(minutes(2));
    if (random()) {
      displayText("Reinforcements pour from the nydus canal.");
      createUnit(P8, units.ZergZergling, 12, locations.Spawn);
    } else {
      displayText("A supply drop: 100 minerals for everyone.");
      setResources(AllPlayers, "add", 100, "ore");
    }
  }
});

A beacon that opens a gate, once. rose() fires on the frame the condition becomes true, once() only the first time it does, so this runs exactly once however long the unit stands there. Two things happen on their own clocks, so there are two programs.

program(() => {
  while (true) {
    if (once(bring(P1, units.AnyUnit, locations.Beacon, ">=", 1))) {
      displayText("The gate opens.");
      killUnitAt(P12, units.LeftUpperLevelDoor, "All", locations.Base);
      setSwitch(switches.Switch1, "set");
    }
    sleep(frames(1));
  }
});

program(() => {
  while (true) {
    if (switchIs(switches.Switch1, "set")) {
      createUnit(P8, units.ZergHydralisk, 2, locations.Spawn);
      sleep(seconds(30));
    } else {
      sleep(seconds(1));
    }
  }
});

A heal that each Marine gets once every ten seconds. A Map keyed by units keeps a number for each of them; a unit that has died reads as no unit in the loop over the map, which is where its entry is deleted.

program(() => {
  const wait = new Map<Unit, number>();
  while (true) {
    for (const u of unitsAt(locations.Hill, { owner: P1, type: units.TerranMarine })) {
      if (!wait.has(u)) { u.heal({ percent: 100 }); wait.set(u, 10); }
    }
    for (const [u, left] of wait) {
      if (!u || left <= 1) wait.delete(u); else wait.set(u, left - 1);
    }
    sleep(seconds(1));
  }
});

A map that already has triggers. Open TrigScript and press Import map triggers: every hand-made trigger comes back as a trigger() call in its order, and applying the script replaces the whole list with what it makes. From there a repeated trigger becomes a loop, a number used in ten places becomes a constant, and the rest stays as it was.