Dicey Table

Script Editor

Opening a scene script — from the Hierarchy's SCRIPT section, or the asset explorer — opens it as a tab hosting a full Monaco editor, the same engine behind VS Code.

<!-- screenshot: editor/item-editors/script.png -->

See the tab strip conventions — dirty dots, closing, cycling and mounted-but-hidden tabs apply here too, with one addition specific to this editor: closing a tab with unsaved script changes asks you to confirm before discarding them, in addition to the tab strip's own dirty dot.

This is Table Scripting — TypeScript, authored in the editor, reaching the table through the world, globalEvents and refObject globals. It is a different surface from Mod Scripting (JavaScript, hosted in a mod's GitHub repository, reaching the table through api); the two share no globals and are documented separately. See Scripting API for the full reference.

DTS autocomplete#

The editor is seeded with the DiceyTable Table Scripting API's ambient type declarations (DICEYTABLE_SCRIPT_API_DTS, from packages/shared/src/scripting.ts — the single source of truth both this editor and the published reference read from), so world., globalEvents. and refObject. autocomplete, hover for docs, and type-check exactly like a real TypeScript project. The compiler target is ES2020 with no DOM library — table scripts run in a sandbox with no document or window, so referencing either is a type error here too, catching a whole class of mistake before you ever run the script.

refObject is typed for the script's own kind. The declarations load in two pieces — the surface itself, plus a one-line declare const refObject the editor swaps per script (refObjectDeclarationDts, same file). A script created from a deck's SCRIPT section records refKind: "deck", so refObject. offers DeckObject's members; a global script keeps the generic ObjectHandle. See Object Types.

Because an ambient global can only be declared once for the whole language service, the visible script tab is the one that declares it: switching tabs re-applies the typing for the tab you switched to. Two script tabs open on different kinds can therefore show stale squiggles in the background one until you look at it.

Values that exist in your project#

A script reaches the table through strings - a tag, an entity name, a seat variable - and TypeScript types every one of them as plain string, so a typo compiles and then quietly matches nothing. The editor closes that gap from the scene you have open.

Suggestions. Start a string where a project value belongs and the list offers the ones that exist, most-used first, each with how many things match:

You type It suggests
{ tag: " , tags: [" , object.tags.includes(" tags - queen 2 on the board - 1 in the project
x.name === " , switch (x.name) { case " entity names and zone names
world.getObjectById(" , objectIds: [" entity ids, shown with the entity's name
presetId: " the standard preset ids
world.getSeatVariable(" / setSeatVariable(" the scene's declared seat variables
seat: " , world.getSeatZones(" seat colours
zoneName: " , point.label === " zone names, snap point labels

On the board counts entities placed in the scene. In the project counts what the project owns but has not placed: prefabs, and tags a zone's tag filter already names. Hovering a value shows the same counts and a few of the entities that carry it.

Warnings. A string in one of those positions that matches nothing gets a warning underline - the same as a compiler warning - and a row in the Problems bar, as you type, without saving. When something close exists the message says so (Did you mean "queen"?).

It is a warning and never an error, because a script can legitimately name something that only appears at run time. Tags you pass to world.spawnObject({ tags: [...] }) are understood as created by the script and never flagged. Anything else can be silenced from the lightbulb (Ctrl/Cmd+.), or by writing the comment yourself:

Comment Silences
// dt-ignore-next-line the line below it
// dt-ignore at the end of a line that line
// dt-known "promoted" "ghost" those values, anywhere in the script

Comparisons against .id are completed but never warned about: zone, snap-point and menu-item ids share the member name, and an entity spawned at run time has an id the editor cannot know.

The check is a fast scan of the text, not a full parse, so that it keeps working while a line is half-typed. It recognises the shapes in the table above; a value that reaches the API through a variable (const t = "queen"; getAllObjects({ tag: t })) is neither completed nor checked.

Drag a piece onto the script#

Drag an entity from the Hierarchy, or a preset, model or file from the asset explorer, and drop it on the code. An orange caret shows where it will land. What is written depends on where you drop it:

  • Inside a string - just the value that string wants: the entity's id in getObjectById(""), its name in a name comparison, its first tag in a tag: string.
  • Inside an async function - const whiteQueen = await world.getObjectById("..."); under a doc comment naming the entity and its tags.
  • Anywhere else - a small async function getWhiteQueen() helper. A table script cannot await at file level, so a bare await there would be a compile error on a line you did not write.

Several selected entities drop as one reference each, or as one helper returning the ones still on the table. A preset or a project model drops as a ready world.spawnObject({...}) call; any other file drops as its repo-relative path. Generated names never collide with a name already in the script.

A dropped entity is referenced by id, which is exact but breaks if that entity is deleted and re-made. For a reference an author can re-point without touching code, declare a script variable instead.

Snippets sidebar#

The sidebar on the right of the code holds common patterns in groups - table events, this entity, finding entities, spawning and moving, cards and decks, players and zones, saved data and timing, right-click actions. Click one to insert it on its own line at the cursor; Tab steps through the parts you are meant to rename, and repeated names edit together. Every snippet opens with a doc comment that says what it does and the thing most likely to surprise (handlers run on the host only; handles are snapshots; saved data is a string).

The filter box searches titles and descriptions. The Snippets button in the editor header hides the sidebar; the choice is remembered in this browser. Without the sidebar, typing dt- in the code lists the same snippets as completions (dt-deal, dt-find-tag, ...).

Snippets marked Needs a script attached to an entity read refObject, which a scene script does not have.

Transpile on save#

There is no separate "compile" step — Ctrl/Cmd+S (or the Save button) does three things at once:

  1. Runs the TypeScript worker's syntactic and semantic checks against your source.
  2. Emits JavaScript, even if there are type errors — TypeScript's own default behavior, so a script with cosmetic type problems can still run. A syntax error, unlike a type error, produces no output at all.
  3. Saves both the original source and the compiled JavaScript onto the script record; the sandbox host runs the compiled output.

The Problems panel below the editor lists every diagnostic with its line and column; clicking one jumps the cursor there. Compile diagnostics are as of the last save; the project-value warnings in the same list are live. No problems and a successful compile shows a plain confirmation instead.

What's not here#

Mod scripts (JavaScript, in a mod's GitHub repository) do not open in this editor and do not get this DTS autocomplete — mod-author Monaco autocomplete for the mod API is a tracked follow-up, not shipped today. A mod's .js entry file opens through the plain-file editor instead, with syntax highlighting but no DiceyTable-specific type checking.

See also#