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#
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:
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:
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 to 64), strength to 1 (range 0 to 8).
flatten pulls the area toward the height at the brush centre, not toward zero, so put the centre where you want the floor.
A crater#
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 joinendThree strokes is three effects. A hundred strokes is not, so spread those over a timer:
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) endend)Blocks: always look the id up by name#
local STONE = world.blocks:find("Stone")if not STONE then print("this world has no Stone in its palette") returnendCareful
Never 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#
world.blocks:get(x, y, z) -- the type id; 0 is air, and so is outside the worldworld.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#
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:
world.blocks:set(hit.place_x, hit.place_y, hit.place_z, STONE)Careful
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#
--!strictlocal 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) endend)Six fills is six effects, which is 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#
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 endend)The loop only reads, which is free. It places exactly one block, so the whole click costs one effect.
Reacting to what players do#
local events = require("events") events:listen("world.block", function(e) -- somebody placed or broke a blockend)previous arrives as a number, and world.blocks:name(previous) is what turns it into a sentence.
Permissions#
Writing terrain needs Terraform and writing blocks needs Dig and build with blocks. The world's owner holds both; anybody else holds what the owner granted them. See Permissions.
Reading both is always free and always public, because everyone standing there is already looking at it.
See also#
Updated 11 October 2026