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:
--!strictlocal 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, meaning 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#
world.build:allow(id, { wall = 10, floor = 4, roof = 0 })world.build:allow(id, { wall = -1 }) -- unlimitedworld.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 |
Careful
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#
--!strictlocal 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 0end 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#
events:listen("world.collapse", function(e) -- a piece lost its support and came downend)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#
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#
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#
--!strictlocal 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 0end 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#
world:on_wake(function(elapsed) local cycles = math.floor(elapsed / REGROW) -- arithmetic, never a loop: `elapsed` can be a monthend)See also#
- world.build has every verb
- Terrain and blocks · Saving data
Updated 04 September 2026