Skip to content

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
end

objects: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.

See also

Hungrit scripting documentation.