Server Entity API

Methods available on the entity object passed to server-side Lua hooks — movement, health, animation, mounts, and player utilities.

Most server-side hooks hand your script an entity object: the mob in a mob update hook, the player in an item-use or chat-command hook, the rider and mount in mount hooks, and so on. This page lists everything you can do with one.

An entity can be a player, a mob, a mount, or a projectile. Methods that only make sense for one kind are safe to call on any entity — on the wrong kind they quietly do nothing and return nil, 0, false, or an empty table as noted below. Your script never errors just because it got a player where it expected a mob.

You receive entity objects from:

  • Mob hooks (scripts.server-update, scripts.server-on-create) — the mob itself
  • Item hooks (server-on-primary-use) — the player using the item
  • Chat commands — the player who typed the command (see lua-chat-commands.md)
  • Interactable interaction hooks — the interacting player
  • Mount hooks (server-on-mounted / server-on-dismounted) — the rider and the mount
  • NPC dialogue hooks — the player and the NPC (see lua-npc-dialogue.md)
  • entity:get_closest_player(), entity:get_entity_by_id(...), and the globals spawn_mob_at(...) / spawn_mount(...)

Persistent Script State: lua_data

Every entity carries a lua_data table your scripts can freely read and write. It persists for the lifetime of the entity, across every hook call, so it is the standard place to keep per-entity state (timers, AI mode, counters):

-- /mobs/my_mob/scripts/server_update.lua
return function(entity, delta_ns)
    local data = entity.lua_data
    data.time_alive = (data.time_alive or 0) + delta_ns * 1e-9
end

entity.lua_data returns the live table — changes to its fields stick immediately. Assigning a whole new table (entity.lua_data = {}) replaces everything.


Identity

MethodReturnsDescription
get_id()numberThe entity’s runtime id, stable for its lifetime. This is the id returned by get_riders(), and the one to store in lua_data to refer back to another entity later.
get_name()stringThe entity’s type identifier (e.g. "Creation:zombie"), not its display name. Use set_display_name / the display shown above the entity for the player-facing name.
get_type()stringThe entity category: "Player", "Mob", "Mount", "Projectile", "ItemDrop", or "WieldedItem". Handy for branching on what you were handed.
get_entity_by_id(id)entity or nilResolve an id (from get_riders, get_id, a stored lua_data value) back to an entity object in this entity’s world. Returns nil if no such entity is loaded. This is how you turn the ids from get_riders() into entities you can drive.

Position and Movement

Positions are world coordinates in block units (1.0 = one block).

MethodReturnsDescription
get_x()numberThe entity’s current X position.
get_y()numberThe entity’s current Y position.
get_location()locationThe entity’s position as a location object with read-only .x, .y, and .world fields. This is a snapshot — changing it does not move the entity.
set_location(x, y)—Teleport the entity to (x, y) in its current world. Unlike setting velocity, this snaps the position instantly and re-indexes the entity in the spatial map right away, so mob AI targeting and range checks see the new position on the same tick. The move is also staged for the clients — it goes out with the next tick’s movement broadcast, so a scripted move is seen, including on a static mob that no physics step would ever move by itself.
get_velocity()number, numberTwo return values: the current X and Y velocity.
set_x_velocity(v)—Set the horizontal velocity directly.
set_y_velocity(v)—Set the vertical velocity directly.
set_max_x_velocity(v)—Cap the entity’s horizontal speed.
set_applying_x_velocity(bool)—Turn the entity’s own horizontal propulsion on or off.
set_applying_y_velocity(bool)—Turn vertical propulsion on or off (e.g. for flying or climbing behaviour).
set_flying(bool)—Toggle gravity-exempt (flying) movement — e.g. a boss that takes off mid-fight. While flying, the entity ignores gravity, holds its altitude, and takes no fall damage; entering flight zeroes its vertical velocity so it doesn’t inherit fall momentum. mob_path_to on a flying mob routes it through the air (around obstacles) rather than walking, so you can path a flyer the same way you path a grounded mob. You can still steer it directly with the velocity setters if you prefer.
is_flying()booleanWhether the entity is currently in flying (gravity-exempt) mode.
set_phasing(bool)—Toggle terrain-phasing (burrowing) movement — e.g. a boss that dives into the ground for one phase and surfaces for another. Phasing is a superset of flying: the entity is gravity-exempt and passes through solid blocks (it still collides with entities/static objects), and mob_path_to routes it straight through terrain toward the target. set_phasing(false) returns it to normal grounded movement.
is_phasing()booleanWhether the entity is currently in phasing (through-terrain) mode.
apply_impulse(vx, vy, duration_ms?)—Shove the entity by a velocity impulse (vx, vy) — knockback. The push fades out over duration_ms (optional, default ~250 ms) rather than cutting off, so a single call reads as a shove. See the note below on how this differs from set_x_velocity.
pull_toward(x, y, strength, duration_ms?)—Drag the entity toward the point (x, y) at strength (a velocity magnitude) — the convenience form of apply_impulse that computes the direction for you. A negative strength pushes away from the point instead. Call it every server-update tick (or pass a longer duration_ms) to sustain a pull; a boss dragging the player toward itself is player:pull_toward(entity:get_x(), entity:get_y(), strength).
grab(anchor_x, anchor_y, duration_ms)—Player only. Pin this player at (anchor_x, anchor_y) for duration_ms, locking its movement input — a grab / hold / stow. A boss typically calls it after a pull_toward, passing its own position, so the player is hauled in and held at the maw: local p = entity:get_closest_player(); if p then p:pull_toward(entity:get_x(), entity:get_y(), 40); p:grab(entity:get_x(), entity:get_y(), 1500) end. The hold is server-authoritative (the player can’t walk out), but the player can struggle free early by mashing movement keys. No-op on a non-player. Call again to extend or re-anchor an active hold; capped at 30 s.
is_grabbed()booleanWhether this entity is a player currently held by a grab. Lets a boss gate follow-up behaviour on the hold still being in effect (e.g. keep chewing until the player breaks loose). False for a non-player.

Forced movement vs. setting velocity. set_x_velocity / set_y_velocity replace the entity’s velocity and only work on server-driven entities (mobs, mounts, projectiles) — on a player they do nothing, because the player’s own client is authoritative over its position. apply_impulse / pull_toward are the way to move a player from a script: the push is sent to the player’s client and folded into its movement for the duration. They also work on mobs (added straight to velocity). The push is capped at the target’s own maximum velocity, so it accelerates hard but never launches faster than the entity could normally move; use a longer duration_ms or a repeated pull_toward for a sustained drag. Typical use: local p = entity:get_closest_player(); if p then p:apply_impulse(dir_x * 40, dir_y * 40) end.

Pathfinding (Mobs Only)

MethodReturnsDescription
mob_path_to(x, y, time_seconds, max_checks, delay_ms)—Ask a mob to pathfind to (x, y). time_seconds is how long the traversal should take, max_checks caps how much searching the pathfinder may do, and delay_ms is the minimum time between path requests. The path is computed over the following ticks, not instantly. The call is ignored if the mob is already following a path or if delay_ms has not elapsed since the last request — so it is safe to call every update tick. Does nothing on non-mobs.
stop_pathing()—Cancel the current path and any queued path movement.

Collision

MethodReturnsDescription
is_collidable()booleanWhether the entity currently collides with the world.
toggle_collidable()—Flip the collidable flag.

Appearance and Actions

MethodReturnsDescription
animate(name, reverse)—Play the named animation on every client that can see the entity. reverse (boolean) plays it backwards.
set_state(state)—Set the entity’s state string (drives state-dependent visuals defined in texture configs).
set_display_name(name)—Change the name shown above the entity.
primary_use(x, y)—Make the entity perform its primary-use action aimed at world position (x, y) — for mobs this triggers their attack behaviour. Clients are notified so the use animation plays.
tint(r, g, b, duration_ms)—Pulse an additive colour over the entity’s sprite for every client that can see it, fading to nothing over duration_ms. Channels are 0..1 and clamp; a non-finite one raises an error. Use it as a wind-up tell: give it the attack’s telegraph length so the pulse ends as the blow lands.
hold_tint(r, g, b)—The same colour, held steady until clear_tint. Use it for a window that lasts — a boss’s vulnerable phase.
clear_tint()—Clear both the pulse and the hold. The client’s own damage flash is unaffected.

Tints and the damage flash

A tint is additive and applied after lighting, so it brightens the sprite rather than recolouring it — a dark colour is a subtle wash, 1.0 on a channel is as strong as it gets.

The pulse, the hold and the client’s own red damage flash are three separate slots that add together, so:

  • a player landing hits on the entity still sees a wind-up pulse — their own damage flash cannot hide it;
  • a held tint is still there when a damage flash fades, instead of being wiped by it.

A tint lives on the client’s copy of the entity, so an entity that leaves view and streams back in arrives untinted, and a player who joins mid-hold does not see it. That suits a tell that lasts a moment; it is not state to rely on.

A mob’s archetype attacks can pulse the same way without a script — see wind-up-tint in mob-config.md.

Health

MethodReturnsDescription
get_health()numberCurrent health.
get_max_health()numberMaximum health.
heal(amount)—Restore amount health, capped at max health. Healing always applies — it is not blocked by invincibility frames or damage reduction.
damage(amount, attacker_id, damage_type)—Deal amount damage to the entity — the counterpart to heal, and the way to script entity→entity damage without an AoE zone. attacker_id is optional (pass the attacker’s get_id() to credit the hit for aggro/kill purposes, or omit it). damage_type is an optional type name — "physical" (the default when omitted), "fire", or "blast" (case-insensitive); an unknown name raises a Lua error. The hit routes through the victim’s normal mitigation (general armor, per-type resistance for the given type, and the minimum-damage floor), isn’t blocked by per-attacker invincibility frames but still respects the victim’s global post-hit window. If it drops the entity to zero health, death (drops, on_death hooks) is handled on the same tick, exactly as any other kill.
set_invulnerable(bool)—Turn a transient armor state on or off. While true, every damage source is rejected — melee, projectiles, AoE zones, scripted damage(), and environmental (fall/oxygen) — the entity simply takes no damage. This is the primitive for armored / weak-point bosses: keep the boss invulnerable while its body is armored, then set_invulnerable(false) to open a vulnerable window (and re-arm it after). Distinct from the permanent takes-damage config flag and from invincibility frames; it is runtime-only and not saved, so a boss that reloads starts vulnerable — re-arm it from your server-update hook. heal() still works while invulnerable.
is_invulnerable()booleanWhether the entity is currently in the transient invulnerable state set by set_invulnerable.
despawn()—Mobs only. Remove the mob at the end of its next update, with no loot and no death hook. This is how an encounter resets itself — a lair boss that nobody is fighting removes itself so its lair can start a fresh fight. Does nothing on any other entity.

Inventory

MethodReturnsDescription
set_inventory_size(rows, columns)—Resize the entity’s inventory grid.
has_item(name, count?)booleanWhether the entity’s inventory holds at least count (default 1) of the named item. Use for scripted key/toll gates.
remove_item(name, count?)numberRemove up to count (default 1) of the named item, returning how many were actually removed. Broadcasts the change so the client stays in sync — use it to consume a key or pay a cost.
give_item(name, count?)numberAdd count (default 1) of the named item to the inventory, returning how many did not fit (0 on a full pickup). Broadcasts the change so the client stays in sync — use it to hand out a quest reward or starter gear. Errors if the item name is unregistered.

Capabilities

Persistent per-player unlocks — the Metroidvania ability set checked by sealed barriers. Capabilities are free-form string ids you choose; grant them from progression scripts (a boss death, a consumed item, a mount, an interactable). They are saved with the player and carry across world transfers. Players only.

MethodReturnsDescription
has_capability(name)booleanWhether the player holds the named capability. false for non-players.
grant_capability(name)booleanGrant the capability. Returns true if it was newly granted (was not already held).
revoke_capability(name)booleanRemove the capability. Returns true if it was present and removed.
-- A shrine that permanently grants an ability. Block interact handlers receive
-- (world, x, y, layer, actor_entity_id, actor); `actor` is the interacting player.
return function(world, x, y, layer, actor_entity_id, actor)
    if actor and actor:grant_capability("storm_ward") then
        actor:send_message("You feel the storm's blessing.")
    end
end

Milestones

Grant a one-shot event Almanac milestone to a player from a progression script — a boss death, a world arrival, a story beat. An event milestone is a milestone whose config declares metric: event (no threshold); the id you pass is that milestone’s own id (namespaced, e.g. Creation:felled_torvastus). Like every milestone its reward is cosmetic / title / lore / waypoint only — never power — and the player sees the “milestone reached” banner. Players only.

MethodReturnsDescription
grant_milestone_event(id)booleanGrant the named event milestone. Returns true only on a new grant — a re-grant (a re-kill, a re-visit) is a no-op, so first-time-only needs no guard of your own. Returns false (and warns in the server log) if id is not a declared event milestone.

Because the milestone is granted to the entity you call it on, a mob’s death hook — which is handed only the dying entity — reaches the players who should earn it via entity:get_players_in_radius:

-- A boss's server-on-death hook: credit every player who was present at the kill.
return function(entity)
    for _, player in ipairs(entity:get_players_in_radius(600)) do
        player:grant_milestone_event("Creation:felled_torvastus")
    end
end

Recording a cleared run

MethodReturnsDescription
grant_run_cleared()booleanRecord that this player’s run cleared the game. Returns false only when the entity has no world context yet. Players only.
grant_ea_cleared()booleanRecord that this player beat the last boss of the Early Access build. A separate record from grant_run_cleared: it unlocks the Nightmare Modifiers and nothing else, and it carries no difficulty tier. Returns false only when the entity has no world context yet. Players only.

Unlike a milestone this is not stored in the world save: it is written to the player’s own cross-save profile, so it still counts on the next seed. That is what the record is for — the post-clear content that unlocks on “you have cleared the game” has to see a clear that happened in a different save.

The tier recorded is the run’s difficulty floor — the lowest tier it ever ran at, not the tier it ended on — and the server reads it itself, so there is no argument to pass and no way for a script to claim a harder clear than the run earned. Calling it again is harmless: the profile keeps the harshest clear and ignores anything softer, so a re-kill neither adds nor downgrades.

Like the milestone grant, a death hook reaches its players through entity:get_players_in_radius:

-- The finale's server-on-death hook: everyone present cleared the game.
return function(entity)
    for _, player in ipairs(entity:get_players_in_radius(600)) do
        player:grant_run_cleared()
    end
end

Players

MethodReturnsDescription
get_closest_player()entity or nilThe nearest player in the same world, or nil if there is none. Works from any entity.
get_players_in_radius(radius)table of entitiesEvery online player within radius blocks of this entity, in its world — empty if there are none. The entity-side counterpart of world:get_players_in_radius, for hooks that are handed only the entity (a mob’s server-on-death or server-update). Works from any entity.
send_message(text)—Send a private chat message to this entity’s player. In a chat-command hook this replies to whoever ran the command; elsewhere it messages the player entity directly. Does nothing for non-player entities. The text is shown exactly as written.
send_localized_message(key, args?)—As send_message, but key is looked up in the game’s string table (assets/lang/<locale>.yaml) in the player’s own language. args is an optional table of { name = value } placeholders (strings or numbers) filling the text’s {name} holes: player:send_localized_message("interactable.bed.waiting", { resting = 1, present = 3 }). A key the table does not have is shown as written.
set_custom_spawn(x, y)—Set the player’s respawn point to (x, y) in their current world (e.g. a bed). Players only.
clear_custom_spawn()—Remove the custom respawn point, reverting to the world’s default spawn. Players only.
transfer_to_world(world_name)—Move the player to another world, arriving at that world’s spawn. Players only. The destination world is created (with its registered type) on first visit. The transfer runs on the next server tick.
transfer_to_world(world_name, x, y)—As above, but arrive at exactly world position (x, y) (the same coordinates the debug overlay uses; placed as given — make sure your exit is open space, not inside terrain). Players only.

Mounts, Riders, and Taming

These work on mounts and on mobs configured as rideable. On anything else they return the neutral value listed.

MethodReturnsDescription
is_mountable()booleanWhether this entity can carry riders.
get_seat_count()numberTotal number of seats (0 if not mountable).
get_riders()tableA list (1-indexed) of the entity IDs currently riding this entity. Empty seats are skipped, so the list is compacted — index does not equal seat number. Empty table if none.
get_mount_type()string or nilThe mount type name, or nil for anything that is not a mount.
mount_entity(rider, seat_index)—Put rider (another entity object) onto this entity. seat_index is 0-based; pass nil to use the first free seat. Ignored if the seat is taken or no seat is free. All clients are updated.
dismount()—Remove this entity (a rider) from whatever it is riding. Ignored if it is not riding anything. All clients are updated. Note: this does not fire the mount’s server-on-dismounted hook.
get_mounted_on()number or nilThe entity ID of the mount this entity is riding, or nil if not riding.
get_rider_input_x()numberThe current horizontal steering input from this mount’s rider (0.0 when unridden or not a mount). Use in a mount/mob update hook to drive movement.
get_rider_input_y()numberThe rider’s vertical input.
get_max_speed()numberThe max-speed value from this entity’s mount/mount config (0.0 if not a mount). The engine does not apply it — read it in your update hook to decide how fast to drive the mount.
get_jump_strength()numberThe jump-strength value from this entity’s mount/mount config (0.0 if not a mount). Also engine-inert — read it and apply the jump yourself.
get_taming_progress()numberCurrent taming progress of a tameable mob (0.0 otherwise).
set_taming_progress(p)—Set the taming progress. Does not itself complete taming.
is_tamed()booleanWhether the mob has been tamed by someone.
get_owner()entity or nilThe player who tamed this mob, when they are in the same world. nil for an untamed mob or an owner who is offline or elsewhere.
get_companion_mode()string or nilA tamed mob’s companion mode, "follow" or "stay". nil if the mob is not tamed. Its owner flips it with the COMPANION_TOGGLE control; a newly tamed mob starts on "follow".
set_companion_mode(mode)booleanSet the companion mode to "follow" or "stay". Returns false (and changes nothing) for anything but a tamed mob. Saved with the mob, and all clients are updated. Any other string is an error.

Companion behaviour

The engine stores the companion mode, and moving to follow, holding a spot, and choosing what to fight are all your mob’s server-update script. The one exception is catching up: a companion on "follow" that falls far behind is teleported beside its owner by the engine (see Mob Configuration → Taming), so your script only needs to handle the ordinary walk. These two methods work on any entity and answer the targeting half:

MethodReturnsDescription
get_last_attacker(max_age_seconds)entity or nilThe entity that last landed a hit on this one, if that was within max_age_seconds and it is still in the world.
get_last_target(max_age_seconds)entity or nilThe entity this one last landed a hit on, under the same conditions.

A hit counts only if it did damage: one stopped by friendly fire or absorbed by invulnerability frames is not recorded. Hits are not saved, so a reload forgets every fight. For the usual companion behaviour, check entity:get_last_attacker(n) first, then owner:get_last_attacker(n) and owner:get_last_target(n), and skip the owner and the companion itself. The base game’s glorbo does this in scripts/mobs/glorbo_ai.lua.

Survivors

npc:recruit_survivor(player)

Recruits this NPC on behalf of player: they leave the world and travel to that player’s home, arriving after their configured delay. The scripted counterpart of the recruit dialogue choice — for an NPC whose conversation is Lua rather than a declarative tree, or for a rescue beat that ends in recruitment with no conversation at all.

Requires this mob to have a survivor: block (see mob-config.md). Returns whether the request was queued, not whether it succeeded: the engine enacts it on the next tick and refuses it there if that survivor is already recruited, settled or lost.

The NPC is removed from the world as part of recruiting, so treat the call as the end of anything the script was doing with it.

-- server-on-dialogue-choice: the player agreed to lead them out
if choice_id == "come_with_me" then
    npc:recruit_survivor(player)
    return { type = "close" }
end

Area-of-Effect Zones

MethodReturnsDescription
create_aoe_zone(zone_name, x, y)—Spawn the AoE zone named zone_name (as defined in an aoe-zone-config.md file, qualified as "ModName:zone_name") at world position (x, y), owned by this entity. Clients are notified automatically. Logs a warning and does nothing if the zone name is unknown.
create_aoe_zone_following(zone_name, target_entity_id)—Like create_aoe_zone, but the zone tracks target_entity_id — its whole area (telegraph, geometry, damage, force) follows that entity each tick — or only until the strike, if the zone sets lock-on-active: true (see aoe-zone-config.md). Spawns at the target’s current position; if the target leaves the world the zone stays at its last-known spot. Does nothing (with a warning) if the zone name is unknown or the target isn’t currently in the world. Use it for a player-following mark or an anchored beam.

Example — A Mob That Chases the Nearest Player

-- a mob's scripts.server-update
return function(entity, delta_ns)
    local player = entity:get_closest_player()
    if player == nil then
        entity:stop_pathing()
        return
    end

    local target = player:get_location()
    -- Safe to call every tick: re-path requests are rate-limited by delay_ms.
    entity:mob_path_to(target.x, target.y, 2.0, 200, 500)
end

Notes

  • Timing: update hooks receive delta_ns in nanoseconds. Convert with delta_ns * 1e-9 for seconds.
  • Replication: animate, primary_use, mount_entity, dismount, create_aoe_zone, and send_message notify clients immediately. set_location re-indexes the entity on the server at once and stages the move for the next movement broadcast — before 2026-09-18 it did only the first half, and the “regular entity sync” it was documented to rely on could not carry it: a position is broadcast by the entity’s own update noticing that it changed during that update, which a write from a script outside it never satisfies. Plain state setters (velocity, state, display name, collision) take effect on the server and reach clients through the regular entity sync.
  • Long-running scripts: each hook call has a server-side time budget (a few seconds). Keep per-tick work small; spread heavy work across ticks using lua_data.
  • Numeric inputs are validated: methods that take coordinates, velocities, or amounts (set_x_velocity, set_y_velocity, set_max_x_velocity, set_location, heal, damage, set_taming_progress, set_custom_spawn, create_aoe_zone, primary_use, and the x/y/time_seconds of mob_path_to) raise a Lua error if passed a non-finite number (NaN or infinity — e.g. from 0/0 or math.huge), so a bad value fails loudly instead of corrupting entity state. set_inventory_size clamps each dimension to a sane maximum (256), and mob_path_to’s max_checks is clamped to 100000, so oversized values are bounded rather than allocating or searching without limit.

Last updated