Damage and destruction#
Things that can be broken, and things that break them.
Making something breakable#
An object is damageable only if it has a max_health.
self:set({ max_health = 100, health = 100 })Without it, damage and heal refuse with this object has no max_health. That is how a world says which props are breakable and which are scenery, otherwise every prop could be deleted by any script that could reach it.
Hurting itself#
--!strictlocal self = require("self") self:set({ max_health = 60 }) self:on_touch(function(p) local left = self:damage(20, { by = p.id }) if not left then return end if left <= 0 then print("felled") else self:set({ transparency = 1 - (left / 60) }) endend)by credits somebody, for a scoreboard or a kill feed.
Tip
The number you get back is a prediction. The subtraction happens on the server, because two axes hitting the same tree on the same tick would otherwise both read the same health and the tree would take one hit for two swings.
What you receive is what your blow would leave, which is exactly what you need to decide whether you just felled it. For the authoritative outcome, listen for world.destroyed.
Hurting something else#
local world = require("world")local objects = require("objects") local hit = world:raycast({ from = shot.eye, dir = shot.dir, ignore_player = p.id }) if hit and hit.what == "object" then local obj = objects:get(hit.object) if obj then obj:damage(25, { by = p.id }) endendobjects:damage answers true, not the remaining health, because no script holds a mirror of another object's life.
Reacting to a destruction#
--!strictlocal events = require("events")local world = require("world") events:listen("world.destroyed", function(e) if type(e) ~= "table" then return end world:spawn({ shape = "sphere", pos = { x = e.x, y = e.y, z = e.z }, scale = 0.4, color = "#8c5a2b", collide = false, life = 4, })end)This is the authoritative event. Use it for the loot, the score and the sound, not the prediction from damage.
Careful
world.destroyed is not world.collapse. Destroyed is about damage, meaning health reached zero. Collapse is about support: a building piece lost what was holding it up and came down. A wall can do either, and they usually deserve different reactions.
A tree that drops something#
--!strictlocal self = require("self")local world = require("world")local players = require("players")local events = require("events") self:set({ max_health = 80 }) self:on_action("chop", { key = "e" }, function(p) local who = players:get(p.id) if not who then return end if self:position():dist(who.pos) > 3 then return end local left = self:damage(20, { by = p.id }) if not left then return end self:play_sound("chop", { pitch = { 0.9, 1.1 }, range = 30 }) if left > 0 then return end local here = self:position() for i = 1, 3 do world:spawn({ model = "log", pos = { x = here.x + i * 0.4, y = here.y + 1, z = here.z }, life = 120, }) endend)Three spawns plus a damage plus a sound is five effects, comfortably inside the eight-per-event ceiling. Ten logs would not be.
Player health#
Players do not have health the way objects do. What a hit costs is your game's rule. Keep it in one place:
--!strictlocal self = require("self")local players = require("players")local events = require("events") local SPAWN = { x = 0, y = 1, z = 0 }local MAX = 100 local hp = {} players:on_enter(function(p) hp[p.id] = MAX end)players:on_leave(function(p) hp[p.id] = nil end) local function show(id: string) local who = players:get(id) if who then who:update_hud("vitals", { hp = hp[id] or 0 }) endend events:listen("combat:hit", function(e) if type(e) ~= "table" then return end local id = tostring(e.target) local left = (hp[id] or MAX) - (tonumber(e.amount) or 0) if left > 0 then hp[id] = left show(id) return end hp[id] = MAX show(id) local ok, why = players:teleport(id, SPAWN) if not ok then print("could not respawn: " .. why) end events:emit("combat:down", { who = id, by = e.by })end)Check what teleport answers. Moving somebody else needs Act on other people's avatars, which the world's owner holds and may grant. The symptom of not checking is not a crash. It is a respawn that silently never happens.
Regenerating#
self:on_timer(2, function() for id, v in pairs(hp) do if v < MAX then hp[id] = math.min(MAX, v + 3) show(id) end endend)Mending an object#
self:heal(15)Same rules and the same prediction as damage.
See also#
Updated 11 October 2026