Appearance
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.
luau
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
luau
--!strict
local 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) })
end
end)by credits somebody, for a scoreboard or a kill feed.
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 — exactly what you need to decide whether you just felled it. For the authoritative outcome, listen for world.destroyed.
Hurting something else
luau
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 })
end
endobjects:damage answers true, not the remaining health — no script holds a mirror of another object's life.
Reacting to a destruction
luau
--!strict
local 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.
world.destroyed is not world.collapse. Destroyed is about damage — 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
luau
--!strict
local 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,
})
end
end)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:
luau
--!strict
local 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 })
end
end
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. It needs the player's body permission, which they grant at the door and may refuse. The symptom of not checking is not a crash — it is a respawn that silently never happens.
Regenerating
luau
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
end
end)Mending an object
luau
self:heal(15)Same rules and the same prediction as damage.
