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:
| Field | Side | Default | Description |
|---|---|---|---|
name | Both | (required) | The mob’s identifier. |
hitbox | Server | (required) | { width, height } in blocks. |
damage-hitbox | Server | (none) | Separate hitbox used only for dealing/receiving combat damage, if different from the physical hitbox. |
spawn-group | Server | "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-range | Both | 10.0 | How many blocks away this mob can be interacted with from. The same key items and interactables use. |
display-name | Server | (none) | Name shown in UI. Authored server-side and synced to clients — the client ignores its own copy of this field. |
movement | Server | (all defaults) | Movement/physics block — see Movement. |
shader | Client | (none) | Client-only. Overrides the render shader for this mob (namespaced automatically with your mod name). |
draw-priority | Client | 0 | Client-only. Draw order. |
display-boss-healthbar | Client | false | Client-only. Shows a boss-style healthbar instead of the normal one. |
scripts | Both | (none) | Lua hooks — see Scripts. |
health | Server | see health-config.md | Same shape as the standalone health config. |
oxygen | Server | see oxygen-config.md | Same shape as the standalone oxygen config. |
drop-table | Server | (none) | Items dropped on death — see Shared Blocks → drop-table (same shared block). |
spawn-rules | Server | (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). |
mount | Both | (none) | Makes this mob rideable — see Mount below. |
unarmed-attack | Both | (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 useswalk-speed/jump-velocity/jump-acceleration, whilemount:usesmax-speed/jump-strength. That is not an inconsistency to reconcile — they drive two different systems. Themovement:fields are engine physics: the pathfinder and jump code apply them directly, sowalk-speedreally is a velocity andjump-velocityreally is the launch impulse. Themount:fields are inputs to a Lua steering script — the engine only stores them and hands them back throughget_max_speed()/get_jump_strength(); nothing moves unless the mount’sserver-updatereads them. So a mob wandering on its own AI obeysmovement:; the same mob being ridden obeys whatever its mount script does withmax-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
| Field | Default | Description |
|---|---|---|
walk-speed | 5.0 | The ground speed the pathfinder actually drives this mob at. This is the one to change to make a mob faster or slower. |
jump-acceleration | 100.0 | Sustained upward acceleration applied while a jump is held, on top of the initial jump-velocity impulse. The same key the player config uses. |
jump-velocity | 10.0 | The 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-pathing | false | Whether this mob uses the more expensive pathfinding mode. (Flying mobs always use it, regardless of this setting.) |
flying | false | Gravity-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-terrain | false | Terrain-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-speed | walk-speed | Speed (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: trueis 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 aserver-updatescript using the velocity setters andmob_path_to.
Setting
phases-terrain: truelikewise 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 aserver-updatescript withentity: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.
| Hook | Side | Called |
|---|---|---|
server-update | Server | Every tick |
server-on-create | Server | When spawned |
server-on-death | Server | When it dies, just before it leaves the world |
server-on-dialogue-open | Server | When an NPC dialogue session opens with this mob |
server-on-dialogue-choice | Server | When a player picks an NPC dialogue choice |
client-on-interact | Client | When a player interacts with it |
client-update | Client | Every frame |
client-on-create | Client | When received/loaded |
client-on-death | Client | When it dies |
server-on-mounted | Server | When a rider mounts (requires a mount block) |
server-on-dismounted | Server | When a rider dismounts (requires a mount block) |
client-on-mounted | Client | When a rider mounts (requires a mount block) |
client-on-dismounted | Client | When 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 bymob-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 aspawn-chanceroll (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-stylespawn-rulesblock 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
| Field | Default | Behavior |
|---|---|---|
enabled | true | Whether 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-clearance | false | Reject if the mob would overlap a block or entity. |
must-spawn-on-ground | false | Reject 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:
| Kind | Accepted values |
|---|---|
| Single phase | midnight, dawn, morning, noon, afternoon, dusk, evening, night |
| Group | day_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:
| Field | Description |
|---|---|
name | (required) The block to match, mod-prefixed like any other reference. |
state | Block state index, 0–255 — the value a script sets via block.state. |
fluid-level | Fluid fill level, compared exactly. Write 1.0 for a full fluid block. |
collidable | Whether the placed block collides. |
facing | up, down, left or right. |
block-shape | The 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
| Field | Default | Description |
|---|---|---|
max-speed | 80.0 | A 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-strength | 12.0 | Same 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
| Field | Default | Description |
|---|---|---|
enabled | false | Whether taming is required at all. |
taming-threshold | 100.0 | Total 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
| Field | Default | Description |
|---|---|---|
formation | trail-chain | How parts are positioned each tick — trail-chain, fixed-offset, or script (below). |
part-spacing | 2.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-step | 0.25 | (trail-chain only) How often the head drops a trail point. Smaller = smoother trailing, slightly more work; the default is fine. |
damage-model | shared | shared (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, eachpart-spacingblocks behind the previous one along the controller’s recent path. The worm case.countis the chain length.fixed-offset— each part holds itsoffsetfrom the controller (a rigid cluster). Give each part its ownoffset; 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 Luaupdatescript places them withentity:set_location(...)for bespoke shapes or movement. Useget_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 (putdisplay-boss-healthbar: trueon 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
summonitem) 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