Appearance
Building and survival
Letting players raise structures that hold each other up, and the rules of a world where that matters.
Turning building on
Three lines is a working set:
luau
--!strict
local world = require("world")
local players = require("players")
world:on_start(function()
world.build:define("wall", { from = "WoodWall", kind = "wall", material = "wood" })
world.build:define("floor", { from = "WoodFloor", kind = "floor", material = "wood" })
world.build:define("beam", { from = "WoodBeam", kind = "beam", material = "wood" })
end)
players:on_enter(function(p)
world.build:allow(p.id, { wall = -1, floor = -1, beam = -1 })
end)from is the display name of an object you already built with the ordinary editor. The region snapshots its whole subtree — parts, properties, contents and scripts — so a door that opens keeps opening on every copy anybody raises, with the builder as its owner.
Keep prototypes somewhere out of the way, or delete them: the catalogue is saved with the world and outlives them.
The allowance is a count, not a recipe
luau
world.build:allow(id, { wall = 10, floor = 4, roof = 0 })
world.build:allow(id, { wall = -1 }) -- unlimited
world.build:allow(id, {}) -- closes the bar| Count | The player sees |
|---|---|
-1 | the piece, no number |
n | the piece, with n left |
0 | the piece, greyed out — how somebody learns it exists before affording it |
| absent | nothing; the piece is not in their bar |
The engine does not know what wood is. It does not know what a wall costs or whether the player is holding a hammer — those are your game's questions, and every game answers them differently.
What the engine does is show the number and refuse the eleventh wall without a round trip. You decide when to hand out more.
Charging for a piece
luau
--!strict
local self = require("self")
local events = require("events")
local world = require("world")
local COST = { wall = 4, floor = 3, beam = 2 }
local function wood_of(id: string): number
return tonumber(self.store:get("wood." .. id) or "0") or 0
end
local function allowance(id: string)
local wood = wood_of(id)
world.build:allow(id, {
wall = math.floor(wood / COST.wall),
floor = math.floor(wood / COST.floor),
beam = math.floor(wood / COST.beam),
})
end
events:listen("world.build", function(e)
if type(e) ~= "table" then return end
local id = tostring(e.player_uuid)
local price = COST[tostring(e.piece)] or 0
self.store:set("wood." .. id, tostring(math.max(0, wood_of(id) - price)))
allowance(id)
end)Re-issuing the whole allowance after each build is what keeps the bar honest: the numbers the player sees are always what they can actually afford.
What holds a structure up
Every standing piece carries a support from 1 (on the ground) to 0 (nothing holding it), computed from the joints.
Sideways costs more than upward. That single asymmetry is the whole mechanic: a stone tower goes up comfortably, a wooden walkway sags after a few spans and needs a pillar.
material | up | out |
|---|---|---|
"straw" | 4 pieces | 2 |
"wood" (default) | 8 | 5 |
"stone" | 16 | 8 |
"iron" | 32 | 12 |
When support reaches zero the piece comes down, and so does whatever it was holding. Nobody can raise a piece that would fall immediately — the placement is refused.
Choosing materials is how you pace a world: straw is a shelter, wood is a house, stone is a keep. A player who wants a bridge has to find iron or build pillars.
Reacting to a collapse
luau
events:listen("world.collapse", function(e)
-- a piece lost its support and came down
end)Different from world.destroyed, which is about damage. A wall can do either, and they usually deserve different reactions — a collapse is a consequence of the structure, a destruction is somebody's doing.
Reading how strained something is
luau
local support, band = world.build:support(id)| Band | Support | Reads as |
|---|---|---|
0 | ≥ 0.75 | solid |
1 | ≥ 0.5 | fine |
2 | ≥ 0.25 | strained |
3 | < 0.25 | about to go |
The band travels so your HUD and the built-in support view can never disagree about what a colour means.
Placing a structure from a script
luau
world.build:place("wall", {
pos = { x = 4, y = 0, z = -2 },
rot = { x = 0, y = 90, z = 0 },
})A starter shelter at spawn, a quest structure, a ruin the world begins with.
The structural rules still apply — a script cannot hang a wall in mid-air either, because the support solve would knock it down on the same tick. For something that should go anywhere at all, use world:spawn.
Resources that come back
luau
--!strict
local self = require("self")
local world = require("world")
local REGROW = 900
self:set({ max_health = 60 })
local function harvested_at(): number
return tonumber(self.store:get("cut") or "0") or 0
end
self:on_touch(function(p)
local cut = harvested_at()
if cut > 0 then
if world:clock() - cut < REGROW then
return -- still bare
end
self.store:set("cut", "0")
self:set({ transparency = 0, collide = true })
return
end
events:emit("wood:credit", { who = p.id, amount = 8 })
self.store:set("cut", tostring(world:clock()))
self:set({ transparency = 0.85, collide = false })
end)The timestamp is what makes this survive: world:clock() keeps running while the world is off, so a tree cut on Friday has grown back by Monday. That is the only version of time a visitor recognises.
Catching up after a long sleep
luau
world:on_wake(function(elapsed)
local cycles = math.floor(elapsed / REGROW)
-- arithmetic, never a loop: `elapsed` can be a month
end)See also
- world.build — every verb
- Terrain and blocks · Saving data
