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).


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

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
  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.
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(...).

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


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

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, or a mapping that adds constraints on the placed block’s live state:

valid-spawn-blocks:
  - MyMod:grass              # any grass block
  - name: MyMod:water        # only water at a full fluid level
    fluid-level: 1.0
  - name: MyMod:oak_slab     # only a bottom half-slab
    block-shape:
      type: slab
      divisions: 2

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
      constrained: 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), and offset: { x, y } (default 0,0, used by fixed-offset).

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. (Severing/regrowing parts is planned for a later version.)

Reaching parts from Lua

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

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