The scripting API

A user script runs against the open document, from Tools ▸ User scripts. It is ordinary JavaScript or TypeScript, and everything it can reach is listed on this page — this is the whole surface, generated from the same declaration that installs it.

Scripts run in an interpreter written in JavaScript, so a script has no network, no page, and no filesystem: not because they were taken away, but because nothing exists inside that sandbox except what appears below. The only effect a script can have is on your map.

API version 1 — MIT licensed, so you can develop against it in your own editor.
A script, in full
// Line up every crate on the Props layer to a whole tile.
const props = map.layer("Props");
if (!props) throw new Error("no Props layer");

let moved = 0;
for (const placement of props.placements) {
  if (placement.type?.name !== "Crate") continue;
  const x = Math.round(placement.x);
  const y = Math.round(placement.y);
  if (x === placement.x && y === placement.y) continue;
  placement.move(x, y);
  moved++;
}
console.log(`snapped ${moved} crates`);

// The whole run is one undo step: one Ctrl-Z puts all of it back. If this
// throws part-way, nothing it already changed is kept.

Globals

Already defined when your script starts. Everything else is reached by drilling into these.

mapMapThe open document.
selectionSelectionShorthand for map.selection.
paramsobjectThis script's configuration, as filled in on the run dialog.
eventsEventsListen for what the user does. Only useful to a script set to "stays running".

Objects

Each is a live view of the document, not a copy: read a property twice and the second read sees any change in between.

Map8 members

The open document. The root of everything a script can reach.

Layer14 members

One layer. A tile layer also has tile methods; ask `kind` first.

Placement19 members

One placed entity. A live view: read a property and you get the document's current value, not a snapshot from when you found it.

Tile7 members

One painted cell.

EntityType3 members

A type from the project. Read-only — a script edits the map, not the project that defines what can go on it.

Selection3 members

What the user had selected when the script started.

Events4 members

What the user is doing, as it happens. Registering a listener keeps a script alive after its code ends — so only a script set to "stays running" can usefully use this, and it runs until you stop it.

Events

A script set to stays running can listen for what you do and react to it. Registering a listener is what keeps it alive — a script whose code has ended and which is listening for nothing is finished, whatever its mode says.

// Keep every new crate on whole tiles, as they are placed.
events.on("placementsAdded", (event) => {
  for (const placement of event.placements) {
    if (placement.type?.name !== "Crate") continue;
    placement.move(Math.round(placement.x), Math.round(placement.y));
  }
});

// Or wait for one thing and carry on:
const saved = await events.once("documentSaved");
console.log(saved.ok ? "saved" : "save failed");
  • Your changes, not everyone's. In a shared session a collaborator's edits do not fire your listeners, because your script would then be making changes under your name in response to their typing. Pass { includeRemote: true } if that is what you want.
  • One event per frame. Dragging forty placements delivers one placementsChanged with forty placements in it, not forty events.
  • One event, one undo step. Everything a handler changes is a single ⌘Z, sitting beside your own steps. If it throws part-way, nothing it changed is kept.
  • Stoppable, always. Anything running is listed in the status bar, with what it has done and a Stop. Nothing re-arms itself: reload the page and it is gone.
selectionChangedThe selection changed. Fires for an empty selection too, so a script can react to a deselection.
placementsPlacement[]Everything now selected.
tilesTile[]Tile cells now selected.
placementsAddedOne or more placements were added to the document.
placementsPlacement[]The new placements.
placementsChangedExisting placements moved, were resized, rotated, or had a property set.
placementsPlacement[]The placements that changed.
fieldsstring[]Which fields changed across the batch — "x", "y", "width", "height", "angle", "properties", "points".
placementsRemovedPlacements were deleted. Ids rather than objects, because there is nothing left to hand you.
idsstring[]Ids of what was deleted.
tilesPaintedCells on a tile layer were painted or erased.
layerLayerThe layer that changed.
cells{ x: number; y: number }[]The cells that changed, painted and erased alike.
layerChangedA layer was renamed, hidden, shown, locked, or unlocked.
layerLayerThe layer.
documentSavedA save finished. A script cannot save — this is how it hears that one happened.
okbooleanFalse if the save failed.

What a script may spend

Exceeding any of these stops the script, names which one it was, and undoes whatever it was part-way through. Nothing it had already finished is kept either: a run is one undoable step, and so is one event.

You do not have to take a script's word for what it does. Set the speed to Crawl before pressing Run and it advances a few steps a frame, with the line it is on highlighted and the log filling as it goes; Pause, Step and Stop are beside it. Slowing a script down does not give it a bigger allowance — the clock only counts time it actually spends running.

One run5,000,000 steps · 5s · 20,000 changes · 200 log lines
Totals for the whole run. The step count is the real stop for an endless loop — a clock alone cannot interrupt one.
Each event500,000 steps · 250ms · 500 changes · 50 log lines
Per delivery, not per lifetime: a script that has been running for an hour is not closer to any of these than one that just started.
Event rate60 events per second
The one that catches a loop — a handler whose changes trigger the event it is listening for. Ordinary editing cannot reach it, because a whole drag is one event.

What a script cannot do

Worth knowing before you write one, because none of these fail in an obvious way if you assume otherwise.

  • Reach the network, disk, or page. There is no fetch, document or require. A script cannot load anything or send anything anywhere.
  • Run forever. Execution is metered in interpreter steps, so an endless loop is stopped mid-loop rather than hanging the editor.
  • Use timers. setTimeout and setInterval are removed: a script that scheduled work could never be said to have finished.
  • Change the project. Entity types, tilesets and settings are read-only. A script edits the map.
  • Run on anyone else's machine. In a collaborative session the script runs only where it was started; everyone else receives the resulting edits as ordinary changes, under the name of whoever ran it.

Stale handles

Objects are views, so one can outlive the thing it points at — you keep a Placement, and it is deleted (by you, or by someone else in the session). Reading .exists is always safe and returns false; reading anything else throws an error naming the id. That is deliberate — silently returning nothing would let a script carry on editing what is no longer there.