Docs

World reference

The tw global and friends — finding and spawning objects, zones, snap points, physics casts, persistent storage, grid, hands, lighting, music, timers, vectors.

On this page

Everything table-wide lives on the tw global, with a few sibling globals for subsystems: Grid, Hands, Lighting, Music, Turns, Wait, JSON, Vector, Color. This page covers all of them except the player handle and Turns and the UI builders.

Anywhere a function takes an object you may pass a handle, a numeric id or a guid string.

Finding objects

tw.getObject(id)                     -- handle or nil (numeric id)
tw.getObjectFromGUID("deck1")        -- by string id / alias
tw.getObjects()                      -- every object on the table
tw.getObjectsWithTag("die")          -- objects carrying a tag
tw.getObjectsWithAnyTags({"a", "b"}) -- at least one of the tags
tw.getObjectsWithAllTags({"a", "b"}) -- all of the tags
tw.distance(a, b)                    -- metres between two objects; -1 if missing

Spawning

tw.spawnObject({ type = "die", position = {x=0, y=0.3, z=0} })       -- returns the handle
tw.spawnObject({ type = "card", code = 14, faceUp = true })           -- ace of diamonds
tw.spawnObject({ type = "deck" })                                     -- full 52-card deck

type is one of die, chip, coin, deck, card, counter, clock, note, tablet (an unknown string falls back to a d6 die). code and faceUp apply to cards; position defaults to {0, 0.3, 0}.

tw.spawnTemplate spawns anything the object library holds — custom models, skins, filled bags, whole sets — by template id or gallery name, built exactly as the library window builds it (skin, contents, snap points and the template's script included; a set template puts its whole pile down and returns the first piece):

tw.spawnTemplate("goblin", { position = {x=0.3, y=0.2, z=0} })   -- handle or nil
tw.spawnTemplate({ template = "Leather bag" })                    -- by gallery name

And anything that exists (or existed) on the table can be respawned from a snapshot:

local json = template:getJSON()                    -- serialize any live object
tw.spawnObjectJSON({ json = json, position = {x=0.3, y=0.2, z=0} })
tw.spawnObjectData({ data = template:getData() })  -- same, from a Lua table
tw.spawnTablet({ position = {x=0, y=0.1, z=0}, document = { ... } })  -- tablet + UI document
tw.destroyObject(obj)

Snapshots carry everything: kind, skin, contents, physics properties, script with its vars. Keep a serialized template in tw.setTable (or an infinite bag) and stamp out copies at runtime.

Groups, copy and paste

tw.group({a, b, c})        -- weld objects into one rigid assembly; returns group id (0 = none)
tw.ungroup(obj)            -- dissolve the group obj belongs to
tw.copy({a, b})            -- clipboard snapshot (full fidelity, offsets kept)
tw.paste({ position = {x=0, y=0.3, z=0} })  -- fresh copies at position + each offset; handles

clone / reload / copy / paste are full clones: kind, contents, transform, physics, the visual skin and the script with its data. Copying a welded group pastes a re-welded group.

Physics casts

tw.cast({ origin = {x=0, y=1, z=0}, direction = {x=0, y=-1, z=0} })          -- ray
tw.cast({ shape = "sphere", origin = p, size = 0.1 })                        -- radius
tw.cast({ shape = "box", origin = p, size = {x=0.4, y=0.2, z=0.4} })         -- full size

Returns a list of hits { object, point, distance }, sorted nearest first (empty if nothing). The ray reports every object it crosses within maxDistance (default 100 m). Sphere and box are real collider-vs-shape overlap tests; for them point is the object's centre.

-- what is directly below me?
local p = self:getPosition()
for _, hit in ipairs(tw.cast({ origin = {x=p.x, y=p.y + 1, z=p.z}, direction = {x=0, y=-1, z=0} })) do
  print("hit:", hit.object:kind(), hit.distance)
end

Persistent storage

Table-wide state, stored in the table's database — survives restarts, saves and rollbacks:

tw.store("round", 3)              -- values round-trip through JSON, tables included
tw.get("round")                   -- 3 (nil if absent)
tw.del("round")

tw.setTable("scores", { alice = 10, bob = 7 })   -- whole-table shared state
tw.getTable("scores")             -- an independent copy; setTable again to write back
tw.setTable("scores", nil)        -- clear

Per-object equivalents (obj:setVar, obj:setTable) live on the object handle.

Zones

A scripting zone is an invisible box the engine tracks every frame. Enter/leave events fire on the script that created it. Zones are ephemeral — recreate them in onLoad.

function onLoad()
  discard = tw.createZone({ position = {x=0, y=0.1, z=0}, scale = {x=0.4, y=0.4, z=0.4} })
end
function onObjectEnterZone(zone, obj)
  print(obj:kind(), "entered;", #zone:getObjects(), "inside")
end
function onObjectLeaveZone(zone, obj) print(obj:kind(), "left") end

tw.createZone options:

Key Meaning
position, scale box centre and full size (metres)
tags list of tag names — only matching objects are tracked
layout a table of layout options — makes it a layout zone (below)
owner, hidden hidden zone: contents visible only to owner; hidden = true hides them from others entirely, "masked" shows grey silhouettes

Zone handle methods:

zone:id()
zone:getPosition()  / zone:setPosition(v)
zone:getScale()     / zone:setScale(v)
zone:getRotation()  / zone:setRotation(45)     -- yaw degrees; containment follows
zone:getObjects()                              -- handles inside right now
zone:setHidden(ownerId, "masked")              -- re-aim the hidden mode later
zone:getOptions()   / zone:setOptions(tbl)     -- read/patch layout options
zone:layout()                                  -- force an immediate re-arrange
zone:destroy()

Layout zones

With a layout table the zone keeps its contained (un-held) objects arranged automatically — drop cards in and they tidy into a row.

tw.createZone({ position = {x=0, y=0.1, z=-0.5}, scale = {x=1, y=0.3, z=0.3},
                tags = {"card"},
                layout = { spacing = 0.09, perRow = 0, alternate = false } })

Layout options (any subset; setting one implies layout = true):

Key Meaning
spacing gap between cells, metres
perRow columns along X; 0 = a single row
direction "right", "left", "forward", "back" — growth direction
alternate snake rows (boolean)
facing "keep", "up", "down" — force orientation on entry
hspread, vspread per-axis spacing overrides, metres
combine meld loose cards into decks
maxPerGroup split melded decks into piles of at most N
sort "none", "name", "value", "custom" — custom calls your groupSort(zone, objects) which returns the reordered list

Snap points

Table-global snap points: absolute positions a dropped object is pulled to. Applied by the server on release — within range (XZ metres) the object jumps to the point and adopts its yaw. Ephemeral like zones — declare in onLoad.

tw.setSnapPoints({
  { position = {x = 0.25, y = 0.02, z = 0.15} },
  { position = {x = -0.25, y = 0.02, z = 0.15},
    rotation = 90, range = 0.08, tags = {"meeple"} },
})
tw.getSnapPoints()    / tw.clearSnapPoints()

Per-object (object-relative) snap points exist too — obj:setSnapPoints in the objects reference.

Effects and sound

local id = tw.effect({ preset = "confetti", position = {x=0, y=0.2, z=0} })
tw.stopEffect(id)

tw.playSound({ asset = "assets/bell.ogg", position = {x=1, y=0, z=-2},
               volume = 0.6, rate = 1.2 })       -- one-shot, spatialised
tw.playSound({ asset = "assets/ding.ogg" })      -- non-spatial

obj:playSound and obj:createEffect attach to an object instead — see the objects reference. Sound assets live in the object library; paths are relative (assets/clack.ogg).

Music — shared and synchronised

One authoritative track for everyone; the server derives playback position from its clock, so late joiners start at the right spot.

Music.play({ asset = "assets/ambient.ogg", volume = 0.4, loop = true })
Music.pause()   / Music.resume()   / Music.stop()
Music.seek(30000)          -- milliseconds
Music.setVolume(0.25)      / Music.setRate(1.0)

-- Ambience layers over the main track:
Music.layer({ asset = "assets/rain.ogg", loop = true, gain = 0.3 })
Music.setLayer({ id = 1, gain = 0.5, playing = true })
Music.stopLayer(1)         / Music.stopLayers()
Music.setDuck(0.5)         -- lower layers while the main track plays

Music.pause("all") / Music.resume("layers") scope the action to the track, the layers or both.

Grid, hands and lighting

Grid.set({ type = "hex", size = 0.05, color = "#334455", visible = true, snap = true })
Grid.setType("rect")  / Grid.setSnapping(true) / Grid.setVisible(false)
Grid.get()

Hands.setEnabled(true)          / Hands.isEnabled()
Hands.setHiding("reverse")      -- "default" | "reverse" | "disable"
Hands.getHiding()

Lighting.set({ env = "night", exposure = 0.9,
               sun = { intensity = 2, azimuth = 45, elevation = 30 } })
Lighting.setEnv("studio")       -- "studio" | "soft" | "night"
Lighting.setExposure(1.1)       / Lighting.setSun({ color = "#ffe0c0" })
Lighting.get()

All three patch only the keys you pass, are server-authoritative and persist with the table.

Console output

print("value", 42)          -- browser console, prefixed with the script's object id
tw.log("same thing")        / tw.logError("bad state")
tw.broadcastToAll("Go!")    -- a line to every player (alias tw.printToAll)
tw.printToColor("psst", 2)  -- a line to player 2 (alias tw.broadcastToColor)

Note the argument order of printToColor: message first, player second.

tw.stringColorToRGB("red")  -- seat-palette name -> {r=, g=, b=} in 0..1

Wait — timers

Wait.time(fn, seconds)             -- run fn once after a delay
Wait.time(fn, seconds, reps)       -- repeat reps times (reps < 0 = forever)
Wait.frames(fn, n)                 -- run fn after n server frames
Wait.condition(fn, cond)           -- run fn when cond() first returns true
Wait.condition(fn, cond, timeout, onTimeout)
local id = Wait.time(fn, 5)
Wait.stop(id)                      / Wait.stopAll()   -- scoped to the calling script

Timers are dropped when their script reloads — re-arm them in onLoad.

Vector

Vector(x, y, z) or Vector({x=, y=, z=}); every vector the API returns already has these methods. Operators +, -, == and * (scalar or componentwise) work.

local a = Vector(1, 0, 0) + Vector(0, 0, 1)
a:magnitude()     / a:sqrMagnitude()
a:normalized()    -- a copy; a:normalize() mutates in place
a:dot(b)          / a:cross(b)
a:distance(b)     / a:lerp(b, t)
a:scale(2)        / a:copy()
local x, y, z = a:get()
Vector.distance(a, b)   / Vector.between(a, b)

Color

local c = Color(1, 0, 0)          -- r, g, b, a (a defaults to 1)
Color.Red  Color.Green  Color.Blue  Color.White  Color.Black  Color.Yellow
c:lerp(other, t)   / c:toHex()    -- "RRGGBBAA"
c:copy()

JSON

local s = JSON.encode({ name = "deck", cards = {1, 2, 3} })
local t = JSON.decode(s)          -- t.name, t.cards[1]; nil on parse error

View as Markdown