Mob Configuration

Register AI-driven mob entities — creatures, bosses, mounts, and NPCs — in your mod's catalog.

Mobs are AI-driven entities — neutral/hostile creatures, bosses, mounts, NPCs. This is the catalog file that registers them. Server and client each load their own copy of this file independently (some fields are server-only, some client-only — see Fields).

When several mobs share the same field clusters (a health block, a drop table), you can factor that shape into a preset and give each entry a preset: key instead of repeating it — see Config Presets.


File Location

Register the file in your mod’s manifest (mods/<YourMod>/<YourMod>.yaml):

mob-config: mob-config.yaml

Then create the file it points to, relative to your mod’s folder:

mods/<YourMod>/mob-config.yaml

The file is a single list of mob entries:

mobs:
  - name: my_mob
    spawn-group: Neutral
    hitbox:
      width: 2.0
      height: 3.0

Names are automatically prefixed with your mod name unless you write one already containing a colon.


Minimal Example

mobs:
  - name: my_mob
    hitbox:
      width: 2.0
      height: 3.0

Fields

A mob entry groups its settings into a few blocks. The top-level keys:

FieldSideDefaultDescription
nameBoth(required)The mob’s identifier.
hitboxServer(required){ width, height } in blocks.
damage-hitboxServer(none)Separate hitbox used only for dealing/receiving combat damage, if different from the physical hitbox.
spawn-groupServer"Neutral"Which natural-spawn pool this mob belongs to. A free-form grouping label used only for spawn pooling and server mob-limits — it has no built-in behavior of its own (spawn-group: Hostile does not make a mob hostile, and Boss does not make a boss; the boss healthbar is the separate display-boss-healthbar flag, and all behaviour comes from your Lua). Two built-in labels (Neutral, Npc) always exist; any other value is registered automatically. Matched case-insensitively against the spawn-group in server_config.yaml’s mob-limits (Hostile, hostile, and HOSTILE are one group).
interact-rangeBoth10.0How many blocks away this mob can be interacted with from. The same key items and interactables use, and it covers every interaction — dialogue, mounting, and taming feeds alike.
display-nameServer(none)Name shown in UI. Authored server-side and synced to clients — the client ignores its own copy of this field.
movementServer(all defaults)Movement/physics block — see Movement.
shaderClient(none)Client-only. Overrides the render shader for this mob (namespaced automatically with your mod name).
draw-priorityClient0Client-only. Draw order.
display-boss-healthbarClientfalseClient-only. Shows a boss-style healthbar instead of the normal one.
scriptsBoth(none)Lua hooks — see Scripts.
healthServersee health-config.mdSame shape as the standalone health config.
oxygenServersee oxygen-config.mdSame shape as the standalone oxygen config.
drop-tableServer(none)Items dropped on death — see Shared Blocks → drop-table (same shared block).
spawn-rulesServer(none)Natural-spawning conditions — see Natural Spawning. Omitting this key entirely excludes the mob from natural spawning (it can still be spawned via Lua or structures).
mountBoth(none)Makes this mob rideable — see Mount below.
unarmed-attackBoth(none)The mob’s natural attack when holding nothing — same shape as an item’s melee weapon config (damage plus a wielded-item-data block, whose optional hitbox sets the swing’s hit area).

AoE zones are not declared here. Define them in aoe-zone-config.md and trigger them by name from a mob’s Lua — zones are registered globally per mod, and any entity can trigger any zone.

Give a boss’s melee a tell. An unarmed-attack whose animation frames are all damaging connects the moment it starts, so there is nothing to react to. Mark the leading frames damaging: false to make them a pre-swing wind-up — see wind-ups. Ordinary mobs are usually fine without one; anything a player is meant to read and dodge needs it.

Movement

Server-only. All fields optional; omit the whole block to take every default.

Why this block’s words differ from the mount: block’s. A rideable mob has two movement blocks, and they deliberately use different vocabulary: movement: here uses walk-speed / jump-velocity / jump-acceleration, while mount: uses max-speed / jump-strength. That is not an inconsistency to reconcile — they drive two different systems. The movement: fields are engine physics: the pathfinder and jump code apply them directly, so walk-speed really is a velocity and jump-velocity really is the launch impulse. The mount: fields are inputs to a Lua steering script — the engine only stores them and hands them back through get_max_speed() / get_jump_strength(); nothing moves unless the mount’s server-update reads them. So a mob wandering on its own AI obeys movement:; the same mob being ridden obeys whatever its mount script does with max-speed/jump-strength.

movement:
  walk-speed: 5.0
  jump-acceleration: 100.0
  jump-velocity: 10.0
  max-velocity: { x: 50.0, y: 50.0 }
  complex-pathing: false
  flying: false
  phases-terrain: false
  static: false
  fly-speed: 5.0
FieldDefaultDescription
walk-speed5.0The ground speed the pathfinder actually drives this mob at. This is the one to change to make a mob faster or slower.
jump-acceleration100.0Sustained upward acceleration applied while a jump is held, on top of the initial jump-velocity impulse. The same key the player config uses.
jump-velocity10.0The upward velocity impulse applied on the jump frame — the initial launch speed, not a cap. The same key the player config uses.
max-velocity{x: 50.0, y: 50.0}Hard { x, y } cap the physics engine clamps any movement to (knockback, falling, being pushed) — not the mob’s walking speed. Leave it well above walk-speed. Either axis may be omitted and defaults independently to 50.0 (the same convention as a mount’s and the player’s max-velocity).
complex-pathingfalseWhether this mob uses the more expensive pathfinding mode. (Flying mobs always use it, regardless of this setting.)
flyingfalseGravity-exempt movement: the mob ignores gravity, holds its altitude, and takes no fall damage. mob_path_to routes it through the air (straight-line flight around obstacles) instead of walking/jumping. Can also be toggled at runtime with entity:set_flying(true).
phases-terrainfalseTerrain-phasing (burrowing) movement: everything flying does, plus the mob passes through solid blocks and its pathfinder routes straight through terrain toward its target (it still collides with entities and static objects). Use it for burrowing/ghost mobs. Takes precedence over flying when both are set. Can be toggled at runtime with entity:set_phasing(true). Uses fly-speed.
staticfalseFixed-prop movement: the mob is gravity-exempt (never falls) and AI-exempt (never pathfinds), so it stays exactly where it is spawned. It stays solid — blocks and entities still collide with it. Everything else about a mob still works (health, taking damage, dying, loot), it just doesn’t move. Use it for spawned, breakable component objects — boss-linked lanterns/cores/nodes a player destroys. Takes precedence over flying/phases-terrain. There is no runtime toggle; it is a spawn-time property.
fly-speedwalk-speedSpeed (blocks/sec) the flight pathfinder drives a flying or phasing mob at — the aerial analog of walk-speed. Used when flying: true or phases-terrain: true; defaults to walk-speed when omitted.

Setting flying: true is all a mob needs to fly — the engine handles the rest automatically: it pathfinds through the air around obstacles, and the flyer gets natural aerial motion (a gentle idle hover bob, organic weaving, easing out of a hover, and banking/tilting toward its heading). None of that motion is configurable per-mob today; drive attack patterns from a server-update script using the velocity setters and mob_path_to.

Setting phases-terrain: true likewise handles the rest automatically: the mob is gravity-exempt like a flyer and its pathfinder tunnels straight through solid blocks toward its target, so a burrowing mob will dig through rock to reach a player. It gets the same aerial motion treatment as a flyer. A burrowing boss whose segments should dive and surface on cue can flip phasing per-phase from a server-update script with entity:set_phasing(...).

Breakable component objects (the static prop pattern). A boss whose vulnerability is gated on the player destroying discrete objects — soul-lanterns, void-cores, junction-nodes — builds those objects as static: true mobs. A static mob is a full damageable entity that simply holds its spot, so you get health, damage, death, and loot for free without it wandering or falling. The boss script spawns each component with spawn_mob_at("YourMod:component", { x = …, y = …, world = … }), stores the returned wrapper:get_id(), and each tick checks survivors: a component is gone once entity:get_entity_by_id(id) returns nil (there is no is_alivenil is the dead test). Break-faster-than-it-restores loops (re-spawn a destroyed component on a timer) and “expose the boss once all are broken” gates (entity:set_invulnerable(false)) both fall out of that.

Scripts

Lua hooks live under a single scripts: block. Each value is the path to a Lua file, relative to your mod’s folder:

scripts:
  server-update: /mobs/my_mob/scripts/server_update.lua
  client-on-interact: /mobs/my_mob/scripts/client_interact.lua

Each script file returns its function — there is no function name to declare:

-- /mobs/my_mob/scripts/server_update.lua
local ai = require("MyMod.mobs.my_mob_ai")

return function(entity, delta_time_ns)
  -- ...
end

If you need to point at a file another way, the long form is a mapping with a path field (server-update: { path: /mobs/my_mob/scripts/server_update.lua }); the shorthand string above is equivalent.

HookSideCalled
server-updateServerEvery tick
server-on-createServerWhen spawned
server-on-deathServerWhen it dies, just before it leaves the world
server-on-damageServerOnce per landed hit, right after damage is applied
server-on-dialogue-openServerWhen an NPC dialogue session opens with this mob
server-on-dialogue-choiceServerWhen a player picks an NPC dialogue choice
client-on-interactClientWhen a player interacts with it
client-updateClientEvery frame
client-on-createClientWhen received/loaded
client-on-deathClientWhen it dies
server-on-mountedServerWhen a rider mounts (requires a mount block)
server-on-dismountedServerWhen a rider dismounts (requires a mount block)
client-on-mountedClientWhen a rider mounts (requires a mount block)
client-on-dismountedClientWhen a rider dismounts (requires a mount block)

server-on-death receives the mob and runs before it is removed from the world, so the hook can still read its position, lua_data, and riders. The mounted/dismounted hooks receive (rider, mob), and mirror the standalone mount’s hooks of the same names — a rideable mob and a mount are scriptable the same way.

server-on-damage receives (mob, attacker_id, amount, damage_type) — the mob that was hit, the attacker’s entity id (or nil for environmental damage like fall or void), the damage actually applied after armor, and the damage-type name ("physical", "fire", "blast"). It fires once per landed hit, after the damage is applied, and only when real damage got through (a fully-evaded or zero-damage hit fires nothing). It is the clean way to make a boss react to being struck — interrupt an attack, stagger, or open a window — without polling get_health() deltas every tick. The hook is fire-and-forget (it does not block the mob’s update), so express the reaction as state you set on the mob: for a “hit the weak point to interrupt” fight, combine it with a body-group core sensor (so you know which part was struck) and toggle the boss’s own state or set_invulnerable from the hook.

-- /mobs/my_boss/scripts/server_on_damage.lua
return function(mob, attacker_id, amount, damage_type)
    -- stagger only while winding up, and only for a real player hit
    if mob.lua_data.state == "inhale" and attacker_id ~= nil then
        mob.lua_data.state = "staggered"
        mob.lua_data.stagger_ns = 0
    end
end

Natural Spawning

spawn-rules controls whether and where a mob spawns naturally. A mob with no spawn-rules key at all is never added to the natural-spawn pool — it can still be created via Lua or placed by a structure, but the natural spawner will never pick it.

The natural spawner only draws from mob types that have a matching entry in the server’s mob-limits (in server_config.yaml). If a mob has spawn-rules but its spawn-group isn’t listed there — commonly a typo in spawn-group — it silently never spawns. The server logs a warning at load naming any such group, so watch the log if a spawn isn’t appearing.

Why this looks nothing like tree/structure spawning. Mob natural spawning is a runtime system: the server periodically attempts spawns in loaded chunks, gated by these live-world spawn-rules (nearby players, time of day, current biome) and capped by mob-limits. That’s deliberately different from how trees, structures, and sky-island features are placed — those are worldgen placements, decided deterministically per chunk from a spacing grid plus a spawn-chance roll (see Tree Configuration). The two systems share almost no fields because they answer different questions (“is this spot currently safe to spawn on?” vs. “how densely should this be pre-placed?”), so don’t expect a mob-style spawn-rules block on a tree.

spawn-rules:
  enabled: true
  check-spawn-clearance: true
  must-spawn-on-ground: true
  valid-spawn-blocks:
    - MyMod:grass
  distance-from-player: 20.0
  biome-restrictions: [Forest, Plains]
  time-of-day: night_time
  min-distance-from-spawn: 500.0
  min-depth-factor: 0.5
FieldDefaultBehavior
enabledtrueWhether the mob participates in natural spawning. Set false to keep the rules on record while opting out (useful for temporarily disabling a spawn without deleting its configuration).
check-spawn-clearancefalseReject if the mob would overlap a block or entity.
must-spawn-on-groundfalseReject if nothing solid is directly below.
valid-spawn-blocks(none)If set, reject unless the block directly below matches one of these entries. Leaving it unset (or empty) allows any surface. See Valid spawn blocks.
distance-from-player(none)If set, reject if any player is closer than this many blocks.
biome-restrictions(none)If set, reject unless the mob’s current biome name is in this list.
time-of-day(none)If set, reject unless the current time of day matches. See Time of day.
min-distance-from-spawn(none)Spawn-anchored difficulty gradient (horizontal). If set, reject unless the mob is at least this many blocks from the world spawn origin (x=0) — keeps dangerous mobs out of the safe start so danger rises as you travel outward.
min-depth-factor(none)Spawn-anchored difficulty gradient (depth). If set, reject unless the normalized within-world depth here (0.0 at the top, 1.0 at the world floor) is at least this — so danger rises as you dig. The depth reference is the world-builder preset’s world-floor-y (and world-ceiling-y on cave worlds); see world builder config.

Time of day

time-of-day takes a single keyword, a group keyword expanding to several, or a list of keywords:

KindAccepted values
Single phasemidnight, dawn, morning, noon, afternoon, dusk, evening, night
Groupday_time (dawn → afternoon), night_time (dusk → midnight), all
time-of-day: night_time          # a group
time-of-day: [dawn, dusk]        # a list of single phases

A value outside these lists is warned about and the restriction is dropped, so the mob spawns at any time — check the log if a night-only mob appears at noon.

Valid spawn blocks

Each entry is either a bare block name, a #tag reference (every block carrying that tag), or a mapping that adds constraints on the placed block’s live state:

valid-spawn-blocks:
  - MyMod:grass              # any grass block
  - "#soil"                  # any block tagged `soil`, from any mod
  - name: MyMod:water        # only water at a full fluid level
    fluid-level: 1.0
  - name: "#slab"            # any slab-tagged block, only when a bottom half-slab
    block-shape:
      type: slab
      divisions: 2

A #tag expands to one match per block carrying it, so - "#soil" is shorthand for listing every soil block by name; a name: "#tag" mapping applies its constraints to every one of them. A #tag that no block carries fails the entry at load (rather than silently allowing any block).

Only the properties you write are compared — everything else is left free, so - MyMod:grass matches every grass block whatever state it is in. The mapping form accepts:

FieldDescription
name(required) The block to match, mod-prefixed like any other reference.
stateBlock state index, 0255 — the value a script sets via block.state.
fluid-levelFluid fill level, compared exactly. Write 1.0 for a full fluid block.
collidableWhether the placed block collides.
facingup, down, left or right.
block-shapeThe same block as block-config.yaml takes — {type, divisions, count, side}.

Two entries may name the same block with different constraints (full water and half-full water), and both are kept.

A malformed entry — an unknown key, a missing name, an unresolvable block, or a bad value — fails the whole mob rather than being skipped. Dropping an entry would quietly widen the rule: lose them all and the list reads as empty, which means “spawn anywhere” instead of the narrow list you wrote.


Mount

Adding a mount: block makes this mob rideable. Its max-speed, jump-strength, and seats are the same fields a standalone mount declares — just nested under mount: here (a mob is a creature first, so its rideability lives in its own block, kept separate from the mob’s own movement), rather than at the top level (where the whole entry is the mount). seats uses the exact same shape; a mob’s mount: block additionally supports taming (below), which standalone mounts don’t need.

mount:
  max-speed: 80.0
  jump-strength: 12.0
  seats:
    - x-offset: 0.0
      y-offset: 2.5
      rider-hidden: false
  taming:
    enabled: true
    taming-threshold: 100.0
    food-items:
      - item: MyMod:berries
        progress: 25.0
FieldDefaultDescription
max-speed80.0A steering value for the mob’s script, not applied by the engine — read it with entity:get_max_speed() in the mob’s update script to decide how fast to move while ridden.
jump-strength12.0Same as max-speed — read via entity:get_jump_strength() and applied by your script.
seats(required — at least one)Same shape as a mount’s seats.
taming(none)If present with enabled: true, the mob must be tamed before it can be ridden — see below. If omitted (or enabled: false), the mob is rideable as soon as it exists, same as a mount.

Like mounts, a ridden mob does not move on its own — its update script reads the rider’s input (entity:get_rider_input_x() / get_rider_input_y()) together with get_max_speed() / get_jump_strength() and sets the mob’s velocity. See mount-config.md’s Steering a mount for the pattern.

The mount’s server-on-mounted / server-on-dismounted hooks live in the mob’s scripts: block with every other hook, not inside mount:.

Taming

FieldDefaultDescription
enabledfalseWhether taming is required at all.
taming-threshold100.0Total progress needed to tame the mob.
food-items(none)Items that add progress when fed to the mob — each entry is { item, progress }. item is required; progress is optional and defaults to 25.0 if omitted.

Players interact with an untamed mob to feed it; each interaction adds the matching food item’s progress toward taming-threshold. Once reached, the mob becomes rideable and remembers who tamed it.


Multi-Part Bodies (body)

A body: block turns a mob into a multi-part boss: one controller (this mob — the head or core) plus a set of part mobs that the engine spawns, positions, and coordinates as one logical boss. Each part is itself an ordinary mob entry (its own hitbox, texture, etc.), so parts are hit and rendered like any mob — the body: block just links them. It is shape-agnostic: the same block expresses a trailing worm, a fixed cluster, or a fully script-driven arrangement.

# A trailing worm (segments follow the head)
- name: deep_serpent
  spawn-group: Boss
  hitbox: { width: 4.0, height: 4.0 }
  health: { max: 500.0, takes-damage: true }
  display-boss-healthbar: true
  body:
    formation: trail-chain
    part-spacing: 2.0
    damage-model: shared
    parts:
      - name: serpent_segment
        count: 12
FieldDefaultDescription
formationtrail-chainHow parts are positioned each tick — trail-chain, fixed-offset, or script (below).
part-spacing2.0(trail-chain only) Blocks between each part along the trail. A single distance along the chain — not the {x, y} grid spacing that trees and structures use.
crumb-step0.25(trail-chain only) How often the head drops a trail point. Smaller = smoother trailing, slightly more work; the default is fine.
damage-modelsharedshared (one health pool) or per-part (below).
parts(required — at least one)The parts to spawn. Each entry: name (required, a mob name), count (default 1, how many to spawn), offset: { x, y } (default 0,0, used by fixed-offset), and regrow-ms (optional, per-part only — see below).

Formations

  • trail-chain — parts trail the controller in a chain, each part-spacing blocks behind the previous one along the controller’s recent path. The worm case. count is the chain length.
  • fixed-offset — each part holds its offset from the controller (a rigid cluster). Give each part its own offset; the x offset is mirrored automatically when the boss faces left, so parts stay on the correct side as it turns.
  • script — the engine does not position parts; a Lua update script places them with entity:set_location(...) for bespoke shapes or movement. Use get_body_parts() (below) to reach them.

Damage models

  • shared — every part is a damage sensor: hits it takes are forwarded into the controller’s single health pool, so the boss healthbar (put display-boss-healthbar: true on the controller) reads as one pool no matter which part is struck. Part health values in config are ignored for shared parts.
  • per-part — parts keep their own health and are damaged independently; the boss dies when the controller dies. When a part’s health runs out it is severed rather than killed: its entity is removed (no collision, no rendering, no loot, no death hook), leaving a gap in the formation. A severed trail-chain segment leaves the segments behind it holding their positions; a fixed-offset cluster simply loses that part. Add regrow-ms: <milliseconds> to a part to make it come back that long after being severed (respawned at full health, re-slotted into the formation). Omit regrow-ms and a severed part stays gone for the rest of the fight. This is the “dismantle the boss piece by piece” model.

Reaching and mutating parts from Lua

In the controller’s scripts, entity:get_body_parts() returns its live part entities (spawn order — head→tail for a chain; severed parts are omitted); on a part, entity:get_body_controller() returns the head/core. Use these to, e.g., fire an attack from a specific segment.

For the per-part model, parts can also be severed and regrown from script:

  • part:sever() — sever this part now (honoring its regrow-ms, if any). Returns true if self was a live body part.
  • part:set_active(active)false severs (as sever()); true forces a severed part to regrow on the next tick, ignoring any remaining regrow-ms timer.
  • controller:regrow_part(index) — force-regrow the part at 1-based index (matching get_body_parts() order) of this controller’s group, ignoring its timer. Returns true if that part was severed.

Not persisted. A body-group boss and its parts are runtime-only: they are not saved, so summon/spawn them fresh (e.g. via a summon item) rather than relying on them surviving a world reload. This keeps autosave from leaving orphaned parts on disk.


Complete Example

mobs:
  - name: forest_wolf
    spawn-group: Hostile
    hitbox:
      width: 2.0
      height: 2.0
    movement:
      walk-speed: 8.0
    health:
      max: 20.0
      regen-per-second: 0
      takes-damage: true
    scripts:
      server-update: /mobs/forest_wolf/scripts/server_update.lua
      client-update: /mobs/forest_wolf/scripts/client_update.lua
    spawn-rules:
      check-spawn-clearance: true
      must-spawn-on-ground: true
      valid-spawn-blocks:
        - MyMod:grass
      time-of-day: night_time
    drop-table:
      drops:
        - item: MyMod:wolf_pelt
          min-amount: 1
          max-amount: 2
          chance: 0.6

Last updated