Dicey Table

Choosing a surface

You are writing code for a DiceyTable table. There are two places to put it, they share no objects, and each one can do things the other cannot. Pick before you start.

  • Table Scripting — TypeScript, written in the editor, saved inside the scene document. You get world, globalEvents and, in an object script, refObject. It runs on the host.
  • Mod Scripting — one JavaScript file in your mod's GitHub repository. You get api, handed to setup(api, manifest). It is gated by capabilities you declare in the manifest.

The decision table#

Find the thing you want to do. If it is on this list, the answer is not a preference.

You want to Surface Because
Move, rotate or reposition an entity from code Table handle.setPosition / setRotation. Mod scripting has no transform method at all.
Remove an entity from the table Table handle.destroy(). delete is withheld from mods.
React to one specific entity Table refObject.onPickedUp, onRolled, onDropped — entity-scoped delegates. Mod hooks are table-wide.
Post a chat line as the table Table world.broadcast.
Pace a sequence, or poll Table world.wait, globalEvents.onTick.
React to chat, or to turn start and end Table onChatMessage, onTurnStarted, onTurnEnded.
Draw a panel, button, checkbox or text on the table Mod api.setUiElement. Table scripting has no UI surface.
Play a sound, or change an entity's sound Mod api.playSound, api.setObjectSound.
Read the contents of a deck or bag Mod api.getContainerContents.
Read what is in a seat's hand Mod api.getHandObjects.
Know which seat or team this player is in Mod api.getMySeat, api.getMyTeam — answered per peer.
Filter entities by several tags at once Mod api.listObjects({ tags, match }). world.getAllObjects takes one tag.
split or combine a deck from code Mod api.objectAction(id, "split"). Table scripting declares both and exposes no method for either.

Two rows are worth reading twice. A mod cannot move or delete anything. It can flip, rotate, lock, shuffle, draw, deal, split, combine and roll — and that is the complete list. If your game needs a piece to end up somewhere specific, it needs a table script.

What each surface is really for#

Table Scripting is for rules. It runs on one peer, holds live handles, and reacts to entity events with entity-scoped delegates. It is the natural home for "when this die settles, add its face to the score", "when a card lands in this zone, flip it", "at the start of each turn, deal one".

Mod Scripting is for the table around the rules. It runs with a declared capability list, answers per-peer questions, and owns the two surfaces table scripting has none of — sound and on-table UI. It is the natural home for "show each player a score panel only they can see", "play a shuffle sound the game ships itself", "read the deck to build a draft display".

Nothing stops a mod shipping both: an editor-authored scene with scene scripts inside it, and an entry.script JavaScript file beside it. They will not be able to call each other — they are different frames on different surfaces with no shared memory — but they can both act on the same table and observe each other's effects through the snapshot.

What you give up either way#

Table Scripting Mod Scripting
Authored in The editor's script editor, with full API autocomplete Any editor. The API declarations exist and power this reference; wiring them into an editor for autocomplete has not been done.
Distributed as Part of the scene document inside your mod A .js file in your mod's repository
Language TypeScript, transpiled on save JavaScript, run verbatim
Runs on The host only Gated per method — see Host authority
Security model A three-type intent allowlist, no capabilities 11 declared capabilities, plus the same intent-level checks
Object model Live ObjectHandles with methods and per-entity events Plain snapshot data plus api.objectAction(id, action)
Errors surface as Script-console diagnostics with the script name and phase Mod runner diagnostics
Actions available 13 in the type union, 9 with a method 10, all callable

Both surfaces are scanned by the same five static patterns, and both run in the same kind of opaque-origin iframe. See Sandbox limits.

Names that look shared and are not#

The two surfaces were designed with different vocabularies on purpose, and the near-misses are the easiest way to write code that silently never runs:

Table Scripting Mod Scripting Relationship
globalEvents.onTurnStarted api.on("onTurnStart", …) None. Different names, different payloads.
globalEvents.onObjectDropped api.on("onObjectDropped", …) Same name, different payload — the mod hook fires off an event-log line whose message starts "moved ", and carries the log event plus a snapshot, not a handle.
globalEvents.onCardDrawn api.on("onCardDrawn", …) Same name, different payload, same caveat.
globalEvents.onZoneEnter / onZoneLeave api.on("onZoneEnter", …) / "onZoneLeave" Same names, and for once the same four-string payload. The mod side needs read-world on top of subscribe-events, and both are host-only.
globalEvents.onTriggerEnter / onTriggerLeave api.on("onTriggerEnter", …) / "onTriggerLeave" Same names, and the same five-field payload. Same rule as the zone pair: the mod side needs read-world on top of subscribe-events, and both are host-only.
world.getAllObjects({ kind, tag }) api.listObjects({ kind, tag, tags, match }) Different filters. Do not assume parity.
ObjectData.name TableObjectState.label The same underlying value — the slug — under two names.

world does not exist in a mod's runtime, and api does not exist in a table script's. Pasting code between them does not produce an error you can see in the editor; it produces a script that throws on its first call.

The two surface values#

The surface badge says which declaration a member comes from. There is no third value and nothing belongs to both.

table-script#

Declared in the table-scripting declarations (packages/shared/src/scripting.ts). Reachable through world, globalEvents or refObject.

mod#

Declared in the mod-scripting declarations (packages/shared/src/modScripting.ts). Reachable through api, or through the exports.setup entry point.

A type used by both surfaces is documented once, on the surface whose reference page owns it, and cross-linked from the other.

See also#

  • Host authority — which peer each surface runs on.
  • Sandbox limits — the language subset and the scanner, which apply to both.
  • Action vocabularies — the 13 and the 10, side by side.
  • Anatomy of a mod — where each of these files sits in a repository.
  • Known limitations — where the two surfaces are asymmetric by accident rather than by design.
  • The mod api object — Surface B's 25 methods, each with its capability.
  • Mod hooks and capabilities — Surface B's hook events and its 12 capability slugs.
  • Types — Surface A's Vec3, ObjectKind, SpawnObjectOptions, PlayerInfo and TurnInfo.
  • setup.json — what a mod places before either surface runs.
  • Limits and caps — the numeric ceilings both surfaces hit.
  • Scripting API — the reference index for both surfaces, once you have chosen.
  • Events — the globalEvents delegates, which exist only on the table-scripting side.
  • world — the global a mod does not have.