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:
| 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, and it covers every interaction — dialogue, mounting, and taming feeds alike. |
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. The mob’s display-name is drawn under the bar instead of as a nametag over the mob; with no display-name the bar is shown alone. |
raises-music-intensity | Client | true | Client-only. Whether this mob losing health near the player pushes the music toward its tension and combat layers — set false for animals players hunt. See music-config.md. |
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). |
extra-attacks | Server | (none) | More named attacks for behavior.melee-attacks to choose between. A map from attack name to a wielded-item-data block, the same block unarmed-attack takes: { bite: { wielded-item-data: { ... } } }. The name is what a melee-attacks entry’s animation refers to. Every extra attack deals the unarmed-attack’s damage (a mob has one damage value), so it needs an unarmed-attack, and its attacks differ in reach, timing and hitbox only. |
AoE zones are not declared here. Define them in aoe-zone-config.md and trigger them by name from a mob’s Lua or a
phased_bossability — zones are registered globally per mod, and any entity can trigger any zone.
Give a boss’s melee a tell. An
unarmed-attackwhose animation frames are all damaging connects the moment it starts, so there is nothing to react to. Mark the leading framesdamaging: falseto 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 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
static: 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. |
collidable | false | Whether the mob is solid: players and other entities collide with it instead of passing through, exactly as they would a block. Off by default, which is right for anything that moves under its own power — you walk through mobs, you do not bump into them. Turn it on for a barrier mob: a wall or gate a player has to break rather than walk past. A solid mob also stops projectiles, so it blocks a line of fire as well as a path. It does not block melee reach: a swing whose hitbox extends past the barrier still lands on whatever is on the other side. |
static | false | Fixed-prop movement: the mob is gravity-exempt (never falls) and AI-exempt (never pathfinds), so it stays exactly where it is spawned. It does not make the mob solid on its own — set collidable: true for that. 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 — or for a boss that fights from a fixed spot. A static mob ignores knockback and every other push, so it never drifts off its spot. Takes precedence over flying/phases-terrain. There is no runtime toggle; it is a spawn-time property. |
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(...).
Breakable component objects (the
staticprop pattern). A boss whose vulnerability is gated on the player destroying discrete objects — soul-lanterns, void-cores, junction-nodes — builds those objects asstatic: truemobs. 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 withspawn_mob_at("YourMod:component", { x = …, y = …, world = … }), stores the returnedwrapper:get_id(), and each tick checks survivors: a component is gone onceentity:get_entity_by_id(id)returnsnil(there is nois_alive—nilis 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.
| 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-damage | Server | Once per landed hit, right after damage is applied |
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.
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
Behavior (declarative AI: behavior)
A mob may declare a built-in AI archetype instead of a scripts.server-update Lua loop; the engine
then drives its whole detect → stalk → chase → attack → leash/despawn cycle. A mob with a behavior
block needs no AI script. server-update stays available for bespoke behaviour (e.g. bosses).
behavior:
archetype: ground_melee
detection-range: 24.0
leash-range: 40.0
stalk:
range: 12.0
speed-mult: 0.45
min-time-ms: 700
max-time-ms: 1400
wander:
enabled: true
radius: 10.0
pause-min-ms: 1500
pause-max-ms: 4000
despawn:
at-dawn: true
min-player-distance: 32.0
melee-attacks:
- animation: slash
range: 4.0
cooldown-ms: 1095
| Field | Default | Behavior |
|---|---|---|
archetype | (required) | The built-in state machine to run — ground_melee (population melee), phased_boss (grounded boss; see Phased boss), flyer (aerial diver), or resident (stays home and reacts to nobody; see Resident). A missing or unknown archetype skips the whole mob (a loud load error), like a bad spawn-rules. |
detection-range | 20.0 | Aggro on sight within this many blocks. |
leash-range | 40.0 | De-aggro and return once the target is beyond this many blocks (hysteresis with detection-range). |
stalk | (none) | Optional slow, telegraphed approach before the first lunge. Omit to commit to the chase immediately. See Stalk. |
wander | (none) | Optional idle roaming when the mob has no target. Omit to hold position. See Wander. |
despawn | (none) | Optional self-removal conditions. Omit and the behaviour never despawns the mob. See Despawn. |
melee-attacks | (none) | Priority-ordered list of melee attacks (animation, range, cooldown-ms, and an optional combo and wind-up-tint). Each time the mob is ready to swing, it takes the first entry whose range covers the target and swings that attack, then waits that entry’s cooldown-ms before picking again (one cooldown shared by every entry). So list point-blank attacks first. animation names the attack: the unarmed-attack’s own entity-animation, or a key under extra-attacks. An entry that names neither swings whichever attack was used last. range is measured between the mob’s and the player’s positions, not from the edge of the hitbox, so on a wide mob a short range can fall inside its own body and never fire. |
Locomotion animation is automatic: as a mob walks, jumps, or idles, the engine plays its walk / jump
/ idle sprite strips (declared in the mob texture config) with no
scripting — the attack swing takes priority, and the locomotion loop resumes when it finishes.
A mob that dies plays its death strip once before it is removed; a tamed mob that is downed (see
Taming) plays its downed strip instead, and leaves at once if it has none.
Attack combos
An attack may carry a combo block: the same attack swung several times in quick succession before its
ordinary cooldown starts. It is the melee layer’s difficulty lever — from-difficulty is the lowest
tier that sees the extra swings, so one entry can be a single strike on the easy tiers and a double on
the hard ones, with no second attack entry (which the priority order would pick on every tier).
melee-attacks:
- animation: slam
range: 12.0
cooldown-ms: 2200
combo:
swings: 2 # swings per commitment, counting the first
gap-ms: 1200 # delay between them
from-difficulty: forsaken # and on Condemned; a single swing below
| Field | Default | Behavior |
|---|---|---|
swings | 2 | How many times the attack swings per commitment, counting the first. Fewer than 2 is not a combo: the block is ignored with a warning. |
gap-ms | 500 | Delay between swings — the window the player dodges the follow-up in. Set it past the attack’s own animation, or the swings overlap and read as one. |
from-difficulty | (every tier) | The lowest difficulty the extra swings appear on: dormant, awakened, forsaken, condemned. Below it the attack is a single swing. An unrecognised name falls back to the default tier (awakened), never the harshest. |
A combo whose next swing can no longer reach its target — the player stepped out — is dropped, and the attack’s full cooldown starts instead, so a mob never swings at air. Losing the target entirely ends it the same way. Flyers use the same rule on their dive pass.
Attack wind-up tints
An attack may carry a wind-up-tint block: a colour pulsed over the mob as each swing starts,
fading over its own length. It is the tell for a sprite whose animation has no readable wind-up frames
— a stand-in for art, not a replacement for it.
melee-attacks:
- animation: slam
cooldown-ms: 2200
wind-up-tint:
color: [0.45, 0.18, 0.0] # r, g, b — 0..1, additive over the sprite
duration-ms: 700 # the swing's own wind-up, so the pulse ends as the blow lands
| Field | Default | Behavior |
|---|---|---|
color | (required) | Three numbers, [r, g, b], each 0..1 and clamped. Additive, so it brightens rather than recolours. Anything else — a missing channel, a non-number — fails the mob’s load with an error naming the block. |
duration-ms | (required) | How long the pulse takes to fade. Must be a positive whole number. |
Each swing of a combo pulses, so a double slam tells twice. The pulse is the same one
entity:tint sends from a script (see lua-server-entity.md),
and it adds to the player’s damage flash rather than being hidden by it.
Stalk
| Field | Default | Behavior |
|---|---|---|
range | (half detection-range) | Begin the slow creep once the target is within this many blocks. |
speed-mult | 0.4 | Movement-speed multiplier during the creep (the readable tell). |
min-time-ms | 800 | Minimum creep duration before the mob may strike. |
max-time-ms | 1600 | After this the mob commits to a full-speed lunge regardless. |
Wander
| Field | Default | Behavior |
|---|---|---|
enabled | true | Whether the mob roams when it has no target. |
radius | 8.0 | Roam within this many blocks of its spawn anchor. |
pause-min-ms / pause-max-ms | 1500 / 4000 | Random pause between roam destinations. |
Despawn
| Field | Default | Behavior |
|---|---|---|
at-dawn | false | Remove the mob when the world leaves night (its spawn window closes). |
min-player-distance | 24.0 | Never despawn while a player is within this many blocks — the mob waits until off-screen, then is deleted (no loot, no save), unlike a death. |
Resident (archetype: resident)
For a mob that lives somewhere and stays there: it roams within wander.radius of its anchor and
never targets anybody, whatever walks past or fights nearby. This is the archetype the settled
survivors use.
It takes only wander (plus the shared archetype). detection-range and leash-range are
accepted but unused, and stalk, despawn and melee-attacks are rejected at load — a resident
that could stalk or strike would be authoring that quietly does nothing.
behavior:
archetype: resident
wander:
enabled: true
radius: 10.0
pause-min-ms: 3000
pause-max-ms: 9000
Omit wander (or set enabled: false, or radius: 0) and the mob simply stands where it is.
For a survivor (below), the roam anchor is the bed they arrived at, remembered across reloads — not wherever they happen to be standing when the world loads.
Phased boss (archetype: phased_boss)
phased_boss is a superset of ground_melee: it chases and melees identically (the same
detection-range, leash-range, and melee-attacks fields apply — a boss’s slam is an ordinary melee
attack), layered with HP-threshold phases and cooldown-gated abilities (telegraphed AoE zones,
add spawns). stalk / wander / despawn are not valid here (declaring one is a loud load error).
Locomotion animation is automatic, exactly as for ground_melee.
behavior:
archetype: phased_boss
detection-range: 200.0 # a boss is usually summoned adjacent; a wide range = always closes
leash-range: 400.0
melee-attacks:
- animation: slam
range: 10.0
cooldown-ms: 1050
phase-thresholds: [0.5] # one split at 50% HP -> two phases (phase 1 above, phase 2 below)
abilities:
- type: aoe-zone # a telegraphed AoE ring, every 10 s, from the start
zone: "MyMod:ground_slam"
cooldown-ms: 10000
- type: spawn-adds # 1 minion every 5 s, but only from phase 2
mob: "MyMod:minion"
count: 1
radius: 5.0
cooldown-ms: 5000
from-phase: 2
animation: summon # optional body strip played on cast
| Field | Default | Behavior |
|---|---|---|
phase-thresholds | (none) | A list of HP fractions (each in 0–1) at which the boss advances a phase. N thresholds → N+1 phases (e.g. [0.66, 0.33] → 3 phases). Order-independent (sorted internally); values outside (0, 1) are dropped with a warning. Phases are one-way — a boss that heals does not revert. |
abilities | (none) | A list of boss abilities, each fired on its own cooldown-ms once its from-phase is reached. See Abilities. |
Boss abilities
Every ability entry shares these keys, plus the per-type keys below:
| Field | Default | Behavior |
|---|---|---|
type | (required) | aoe-zone or spawn-adds. A missing/unknown type drops just that ability (a warning), not the whole mob. |
cooldown-ms | 5000 | Fires this often. The first cast waits a full cooldown after the ability’s phase opens (no alpha-strike on spawn or on entering a phase). |
from-phase | 1 | The 1-indexed phase this ability becomes active in (and every phase after — the kit accumulates). 1 = active from the start. Abilities cannot vary by difficulty; the only difficulty lever in this archetype is a melee attack’s combo. |
animation | (none) | Optional body strip played on each cast (e.g. a summon flourish). |
type: aoe-zone — spawns a telegraphed AoE zone centered on the boss.
| Field | Default | Behavior |
|---|---|---|
zone | (required) | The fully-qualified aoe-zone id (e.g. "MyMod:ground_slam"). Missing → the ability is dropped. |
type: spawn-adds — spawns minions around the boss.
| Field | Default | Behavior |
|---|---|---|
mob | (required) | The fully-qualified mob id to spawn (e.g. "MyMod:minion"). Missing → the ability is dropped. The add is wired up exactly like a naturally-spawned mob, so a scripted minion runs its own hooks. |
count | 1 | How many to spawn per cast. There is no cap on living adds: every cast spawns count more, however many are already alive, so the cooldown is what limits add pressure. |
radius | 5.0 | Spawns within this many blocks of the boss. |
Survivors (survivor)
A mob with a survivor block is one of the authored survivor cast: a unique character who exists
once per save, is found in the world, can be recruited, and then lives at the player’s home.
survivor:
losable: false
arrival-delay-ms: 600000
| Field | Default | Behavior |
|---|---|---|
losable | false | Whether this survivor can be killed in the world before they are recruited. A losable: false survivor is invulnerable everywhere. Any survivor becomes invulnerable once recruited, whether travelling or settled. |
arrival-delay-ms | 600000 | How long the off-screen journey home takes. 0 means they arrive as soon as there is a bed to arrive at (useful when testing). |
One of each, forever. The engine records every survivor’s state in the save, so a survivor is
placed once across every world and every chunk generation — a structure marker that rolls one who
is already placed, recruited, settled or lost spawns nothing, and so does spawn_mob_at.
Recruiting is a dialogue choice (action: { type: recruit }, see
dialogue-config.md) or the Lua reach npc:recruit_survivor(player). The survivor
then leaves the world, and after arrival-delay-ms reappears at the recruiting player’s respawn
anchor — but only when that anchor is in the home world. With no eligible bed they wait (the player
is told), and arrive once one exists.
State reaches dialogue as world-flags, so a conversation can branch on it with ordinary flag /
not-flag conditions:
| Flag | Set when |
|---|---|
survivor:<mob name>:recruited | They have been recruited (travelling or home) |
survivor:<mob name>:settled | They have arrived at the base |
survivor:<mob name>:lost | They died in the world, or a script called survivor_lose |
The mob name is fully namespaced, e.g. survivor:Creation:reference_survivor:recruited.
Rejected at load, because each would quietly break the rules above: a survivor with a vendor:
block that is also losable (losing them would take a mechanical capability with it), a survivor with
a behavior.despawn block (the one instance must wait to be found), a survivor in a hostile
spawn-group, and a survivor with enabled spawn-rules (each is hand-placed, never drawn from a
spawn pool).
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
min-distance-from-spawn: 500.0
min-depth-factor: 0.5
pack:
min: 1
max: 3
radius: 5.0
| 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. |
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. |
pack | (none) | Spawn a small cluster of the same mob around each natural spawn (a night pack, a herd). Omit to always spawn alone. See Pack spawning. |
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, 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:
| 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.
Pack spawning
pack clusters a few of the same mob around each natural spawn (a night pack, a herd). The leader
validates the full spawn-rules; its pack members use a lighter placement check (clearance + ground
only), so a pack reliably forms near an already-legal leader.
pack:
min: 1
max: 3
radius: 5.0
| Field | Default | Behavior |
|---|---|---|
min | 1 | Minimum group size, counting the leader. |
max | min | Maximum group size (inclusive). An inverted max is floored to min. |
radius | 4.0 | Members spawn within this many blocks of the leader. |
The world’s mob-limits cap still applies — a pack stops early rather than exceeding the group limit.
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 and storage 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
storage:
rows: 2
columns: 5
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. |
storage | (none) | A pack stash the rider can open, { rows, columns } — same shape, ceiling and open path as a mount’s. Omit it and the creature carries nothing. |
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. |
success-sound | (none) | A sound name, from your sound config, that the tamer hears when a feed completes the taming. It is namespaced with your mod name, like food-items. Leave it out and only the particle burst plays. |
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. The tamer sees a burst of particles at the mob, and hears success-sound if the mob has one.
Follow and stay. A tamed mob has a companion mode, follow or stay, and starts on follow. Its owner flips it with COMPANION_TOGGLE (Shift + right-click by default; see Control Configuration) while in interact range and not riding it. Nobody else can flip it. The mode is saved with the mob. Your server-update script reads it with entity:get_companion_mode() and decides what following and staying mean (see Companion behaviour).
The engine does one thing on its own: a mob on follow that falls too far behind teleports to its owner. If it is more than 24 blocks away and hasn’t got closer for 3 seconds, it is moved beside its owner onto solid ground, clear of terrain. If there is no clear spot (the owner is in mid-air or swimming, or boxed in), it waits and tries again. It never teleports on stay, while ridden, to an owner in another world, or from an area that is no longer loaded. Your script’s pathing is still how it normally moves.
When a tamed creature reaches 0 health:
| Difficulty | Result |
|---|---|
| Dormant | Downed; returns after 30 seconds |
| Awakened | Downed; returns after 2 minutes |
| Forsaken | Downed; returns after 10 minutes |
| Condemned | Dies like any other mob |
- A downed mob leaves the world without dying:
server-on-deathandclient-on-deathdo not run, itsdropstable does not roll, and it plays itsdownedsprite strip (notdeath) if its texture config declares one. - It returns to its owner’s bed in the home world, at full health, with its pack contents intact.
- It waits, losing nothing, while its owner is offline or has no bed in the home world.
- At base it cannot die, on any difficulty. Within 48 blocks of its owner’s bed in the home world, a tamed mob reaching 0 health is left at 1 health instead. This needs the owner online.
Taming and the pack. A tamed creature’s storage stash belongs to the player who tamed it: only
that player can open it, and only that player’s changes to it are accepted — the server refuses
everyone else’s silently, the way an owner-locked chest does. A rideable mob with no taming
block is a public animal, and its stash is open to anyone who can ride 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), 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, 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. 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 severedtrail-chainsegment leaves the segments behind it holding their positions; afixed-offsetcluster simply loses that part. Addregrow-ms: <milliseconds>to a part to make it come back that long after being severed (respawned at full health, re-slotted into the formation). Omitregrow-msand 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 itsregrow-ms, if any). Returnstrueifselfwas a live body part.part:set_active(active)—falsesevers (assever());trueforces a severed part to regrow on the next tick, ignoring any remainingregrow-mstimer.controller:regrow_part(index)— force-regrow the part at 1-basedindex(matchingget_body_parts()order) of this controller’s group, ignoring its timer. Returnstrueif 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
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
Last updated