Appearance
Terrain and blocks
Two separate systems that live alongside each other.
world.terrain | world.blocks | |
|---|---|---|
| Shape | smooth relief | one-metre cubes |
| Good for | hills, valleys, riverbeds, building pads | walls, mines, structures, anything with a face |
| Coordinates | world metres | whole block coordinates |
| Reading | free, public | free, public |
Putting something on the ground
luau
local ground = world.terrain:height(x, z)
world:spawn({ model = "crate", pos = { x = x, y = ground + 0.5, z = z } })height never fails: a flat world reads 0, and so does nonsense input, so you can use it straight in maths.
It answers for the terrain, not for whatever is standing on it. For "what is under this point, including objects", cast a ray downward:
luau
local hit = world:raycast({
from = { x = x, y = 60, z = z },
dir = { x = 0, y = -1, z = 0 },
})Digging with the brushes
Four brushes, all with the same shape:
luau
world.terrain:raise(x, z, radius, strength)
world.terrain:lower(x, z, radius, strength)
world.terrain:flatten(x, z, radius, strength)
world.terrain:smooth(x, z, radius, strength)radius defaults to 4 (range 0.5–64), strength to 1 (range 0–8).
flatten pulls the area toward the height at the brush centre, not toward zero — put the centre where you want the floor.
A crater
luau
local function crater(cx: number, cz: number)
world.terrain:lower(cx, cz, 8, 4)
world.terrain:raise(cx, cz, 12, 0.6) -- the rim thrown up
world.terrain:smooth(cx, cz, 14, 1) -- soften the join
endThree strokes is three effects. A hundred strokes is not — spread those over a timer:
luau
local queue = {}
local i = 0
self:on_timer(0.1, function()
for _ = 1, 4 do
i += 1
local s = queue[i]
if not s then return end
world.terrain:raise(s.x, s.z, s.r, s.h)
end
end)Blocks: always look the id up by name
luau
local STONE = world.blocks:find("Stone")
if not STONE then
print("this world has no Stone in its palette")
return
endNever write the number. The ids belong to the creator's palette. Reordering it re-points every hard-coded id at a different material, silently, and the world starts building itself out of glass. A name is the one handle a script and a person share.
Reading a cell
luau
world.blocks:get(x, y, z) -- the type id; 0 is air, and so is outside the world
world.blocks:solid(x, y, z) -- can a body walk through it?
world.blocks:name(id) -- the other directionsolid is not the same as get() ~= 0 — a tuft of grass is a block you walk through. Use solid for "can I stand here", get for "what is here".
Breaking and placing what you are looking at
luau
self:on_action("mine", { key = "e" }, function(p, shot)
local eye, dir = shot.eye, shot.dir
local hit = world.blocks:raycast(eye.x, eye.y, eye.z, dir.x, dir.y, dir.z, 6)
if not hit then return end
local kind = world.blocks:name(hit.block) or "something"
world.blocks:set(hit.x, hit.y, hit.z, nil)
print(p.name .. " broke " .. kind)
end)Building on the face you hit uses the place_* fields — the same cell a player's own right click would use:
luau
world.blocks:set(hit.place_x, hit.place_y, hit.place_z, STONE)This raycast takes loose numbers, not a table. It is the one call in the API that does: origin and direction are six arguments, not { from =, dir = }.
Building a structure
luau
--!strict
local self = require("self")
local world = require("world")
local function room(x: number, y: number, z: number, w: number, h: number, d: number, b: number)
world.blocks:fill(x, y, z, x + w, y, z + d, b) -- floor
world.blocks:fill(x, y + h, z, x + w, y + h, z + d, b) -- roof
world.blocks:fill(x, y, z, x + w, y + h, z, b) -- walls
world.blocks:fill(x, y, z + d, x + w, y + h, z + d, b)
world.blocks:fill(x, y, z, x, y + h, z + d, b)
world.blocks:fill(x + w, y, z, x + w, y + h, z + d, b)
end
world:on_start(function()
local STONE = world.blocks:find("Stone")
if STONE then
room(0, 0, 0, 8, 4, 10, STONE)
end
end)Six fills is six effects — inside the ceiling. A fill that covers too many cells is refused whole, with nil, "box too big", said now rather than done halfway.
A pillar that grows one block per click
luau
self:on_touch(function()
local here = self:position()
local bx, bz = math.floor(here.x), math.floor(here.z)
for y = math.floor(here.y), math.floor(here.y) + 12 do
if world.blocks:get(bx, y, bz) == 0 then
world.blocks:set(bx, y, bz, STONE)
return
end
end
end)The loop only reads, which is free. It places exactly one block, so the whole click costs one effect.
Reacting to what players do
luau
local events = require("events")
events:listen("world.block", function(e)
-- somebody placed or broke a block
end)previous arrives as a number — world.blocks:name(previous) is what turns it into a sentence.
Permissions
Writing terrain in a social world is the world owner's scripts only; in an experience, any script. Writing blocks needs the block permission in either.
Reading both is always free and always public — everyone standing there is already looking at it.
