Cookbook
Worked examples for the things people actually write. Each one is complete and runs as-is.
A HUD element
Text and a panel in the corner. render2d shapes are batched, so a handful of them per frame costs nothing measurable.
local m = module {
name = 'Coords',
category = 'render',
}
local size = m:setting('number', 'Text size', 9, 6, 16)
local tint = m:setting('color', 'Colour', 0xFF22D3EE)
m:on('render2d', function()
local x, y, z = player.pos()
if not x then return end
local text = ('%.1f %.1f %.1f'):format(x, y, z)
local w = render2d.textWidth(text, size:get())
render2d.roundedRect(8, 8, w + 16, size:get() + 12, 6, 0x99000000)
render2d.text(text, 16, 14, size:get(), tint:get())
end)Highlighting entities
A box around every player, drawn in the world. Note the entity.isValid check is unnecessary here because the handles come from a query made in this same frame; it becomes necessary the moment you keep one.
local m = module {
name = 'Player Boxes',
category = 'render',
}
local fill = m:setting('color', 'Fill', 0x302563EB)
local outline = m:setting('color', 'Outline', 0xFF3B82F6)
m:on('render3d', function()
for _, e in ipairs(entities.players()) do
if not e:isFriend() then
render3d.entityBox(e, fill:get(), outline:get())
end
end
end)0 for outline-only boxes.Reacting to health
Combining a world-space query with a screen-space overlay: render3d.worldToScreen is callable from a render2d handler, which is where you usually want the answer.
local m = module {
name = 'Low Health Tags',
category = 'render',
}
local threshold = m:setting('number', 'Show under', 12, 1, 20)
m:on('render2d', function()
for _, e in ipairs(entities.players()) do
local hp = e:health()
if hp and hp < threshold:get() then
local x, y, z = e:pos()
local sx, sy = render3d.worldToScreen(x, y + 2.2, z)
if sx then
local label = ('%.0f'):format(hp)
local w = render2d.textWidth(label, 9)
render2d.text(label, sx - w / 2, sy, 9, 0xFFFF3B30)
end
end
end
end)Changing movement
The move event hands you the delta about to be applied and takes a replacement. Scripts compose: if two movement scripts are on, the second sees what the first returned.
local m = module {
name = 'Sprint Boost',
category = 'movement',
}
local factor = m:setting('number', 'Multiplier', 1.15, 1, 2)
m:on('move', function(x, y, z)
if not player.isSprinting() or not player.isOnGround() then return end
return x * factor:get(), y, z * factor:get()
end)Doing something on an interval
There is no sleep and no timer: a script cannot block the game thread. Compare timestamps instead.
local m = module {
name = 'Announce',
category = 'misc',
}
local every = m:setting('number', 'Seconds', 30, 5, 300)
local message = m:setting('string', 'Message', 'gg')
local lastSent = 0
m:on('enable', function()
-- Reset on enable, so switching the module on does not immediately fire.
lastSent = util.time()
end)
m:on('tick', function()
local now = util.time()
if now - lastSent < every:get() * 1000 then return end
lastSent = now
player.sendChat(message:get())
end)Patterns worth copying
Bail early on nil. Every getter returns nil with no world. One if not x then return end at the top of a handler is cheaper than a nil check on every line, and it is what stops a script throwing on the title screen.
Reset in the enable handler. Anything you accumulate across ticks should be cleared there. A module switched off and on again otherwise resumes mid-thought.
Do not keep entity handles. They go stale. If you must keep one across ticks, guard every use with e:isValid().
Query once, use many times. entities.players() walks the world; calling it three times in one handler walks it three times. Hoist it into a local.