Custom Mobs
A mob block declares a reusable mob type: base entity, live nameplate, combat numbers, AI mode, a drop table, and handlers for its lifecycle. Spawning one is a single statement.
mob CryptGhoul {
type: "ZOMBIE"
name: "<red>Crypt Ghoul <green>${mob.health}<red>❤"
health: 200
damage: 25
speed: 0.3
ai: melee
drops {
item "ROTTEN_FLESH" chance 0.5 amount 2
}
on_death {
if killer exists {
send "<green>You slew the Crypt Ghoul!" to killer
}
}
}
command "ghoul" {
execute {
spawn mob CryptGhoul at location(10, 64, 20) as m
set m.health to m.max_health / 2
send "<gray>Spawned ${m.custom_id} at half health." to sender
}
}Type name vs id
A mob declaration has two identities, and they do different jobs:
- The type name (
CryptGhoul) is the Capitalized compile-time handle. It is a real nominal type — you write it inspawn mob CryptGhoul, inis a CryptGhoul, and anywhere a type is expected.CryptGhoulis a subtype ofMob(CryptGhoul <: Mob), so a spawnedCryptGhoulis usable anywhere aMobis. - The
id:string is the runtime key: persistence, the string-id APIs (all_mobs("crypt_ghoul"), drop tables, saved tags), and any external reference. It is optional and defaults tosnake_caseof the type name —CryptGhoul→"crypt_ghoul". Writeid:only to pin a key that differs from the name.
mob CryptGhoul {
// id: defaults to "crypt_ghoul"
type: "ZOMBIE"
health: 40
}
command "count-ghouls" {
execute {
set n to 0
loop all_mobs() as m {
if m is a CryptGhoul { // narrows m to CryptGhoul in the block
set n to n + 1
}
}
send "<gray>${n} crypt ghouls abroad" to sender
}
}spawn mob CryptGhoul as m gives m the static type CryptGhoul; a spawn mob by id <stringExpr> still exists for data-driven spawns, and yields the base Mob because the id isn't known until runtime.
Breaking change from the string form
The old mob "zombie" { } string-keyed form is gone. A mob is now declared by a Capitalized type name, with the string moved into the optional id: field: mob Zombie { id: "zombie" … } (or drop id: when snake_case of the name already matches). Spawns change with it — spawn mob "zombie" becomes spawn mob Zombie. String-id call sites (all_mobs("…"), drops, persisted tags) keep their string: they reference id:, not the type name.
Declaration keys
| Key | Type | Required | Meaning |
|---|---|---|---|
id: | string literal | no | the wire/persistence id; defaults to snake_case of the type name — see Type name vs id |
type: | string literal | yes | base EntityType — "ZOMBIE", "zombie", and "minecraft:zombie" all work |
name: | String expression | no | nameplate, always visible; ${mob.health} re-renders on damage |
health: | Number expression | no | max health |
damage: | Number expression | no | melee hit damage |
speed: | Number expression | no | movement speed attribute |
ai: | enum | no | melee (chase + attack + stroll), passive (stroll), none (statue) |
viewable: | boolean | no | auto-viewable at spawn; false spawns it hidden for per-viewer control — see per-viewer entities |
tags { ... } | block | no | typed per-entity state — see Tags |
drops { ... } | block | no | rolled on death; see drops |
on_spawn { ... } | statements | no | binds mob |
on_death { ... } | statements | no | binds mob, killer (Optional<Player>) |
on_attack { ... } | statements | no | binds mob, victim (Player) |
on_hit { ... } | statements | no | the mob is damaged; binds mob and attacker (Optional<Player>) |
The type: is validated against the real EntityType registry of the pinned Minestom snapshot — all 149 constants, with a suggestion when you're close:
e_mobtype.sw:2:11: error: unknown entity type 'RAT'; did you mean 'BAT'?Drops
One line per possible drop: item "<id>" chance <0..1> [amount <n>]. Ids resolve against custom items first, then vanilla materials — so a declared item and "ROTTEN_FLESH" sit in the same table:
item RottenScimitar {
material: "GOLDEN_SWORD"
name: "Rotten Scimitar"
rarity: uncommon
}
mob ArmedGhoul {
type: "ZOMBIE"
drops {
item "rotten_scimitar" chance 0.05 amount 1
item "ROTTEN_FLESH" chance 0.5 amount 2
}
}Every drop rolls independently on death. A literal id that is neither a declared custom item nor an ALL_CAPS vanilla material name is a compile error:
e_drop.sw:4:14: error: unknown item 'not_a_thing_xyz' in drops; declare it with 'item "not_a_thing_xyz" { }' or use an ALL_CAPS vanilla material nameSpawning and despawning
| Form | Effect |
|---|---|
spawn mob <MobType> at <location> [as <var>] | spawns one instance; as binds it as that mob type |
despawn <mob> | removes the entity |
all_mobs() | List<Mob> — every live custom mob |
all_mobs("id") | List<Mob> — live instances of one declaration |
Like give item, a literal id in spawn mob must name a declaration — unknown ids are compile errors, not silent no-ops at 2 a.m.
mob LostSheep {
type: "SHEEP"
name: "<yellow>Lost Sheep"
health: 20
ai: passive
}
command "cleanse" {
execute {
loop all_mobs("lost_sheep") as s {
despawn s
}
send "<green>The pasture is quiet." to sender
}
}The Mob value
| Property | Type | Access |
|---|---|---|
health | Double | read/write — writing re-renders the nameplate |
max_health | Double | read-only |
name | String | read/write |
location | Location | read/write — writing teleports |
type | String | read-only — the base entity type |
custom_id | String | read-only — the declaration id |
A Mob composes the Entity property table — it is an Entity, so every entity property (uuid, glowing, gravity, invisible, on_fire, silent, passengers, …) reads and writes on a Mob value too:
mob Sentry {
type: "IRON_GOLEM"
health: 100
}
command "sentry" {
execute {
// in_front_of(player, distance) is a Location: 'distance' blocks ahead of the eye line
spawn mob Sentry at in_front_of(sender, 3) as g
set g.glowing to true
set g.gravity to false
send "spawned ${g.uuid}" to sender
}
}Per-mob timers
A Mob carries a task registry: set mob.tasks.<id> to schedule every N ticks { } (bind it in on_spawn) attaches a named repeating task that auto-cancels when the mob despawns.
The on_hit handler
on_hit { ... } runs when the mob takes damage, inline in the declaration. It binds mob and attacker — an Optional<Player>, none when the damage has no player source (fall, fire, another mob):
mob Guardian {
type: "IRON_GOLEM"
name: "<gold>Guardian"
health: 100
on_hit {
if attacker exists {
send "<red>You struck the ${mob.custom_id}!" to attacker
}
set mob.velocity to velocity(0, 0.5, 0)
}
}It is the per-declaration twin of the global MobDamage event, which also now carries attacker — on_hit for one mob's knockback flavor, MobDamage for cross-mob systems.
Tags
mob.tags.<name> is freeform per-entity state — read, set, and delete arbitrary values on a live mob (or any Entity) without declaring a field. A read is Optional<Any> (the tag may be unset), a write stores, and set ... to none deletes:
mob Elite {
type: "ZOMBIE"
health: 40
}
command "promote" {
execute {
spawn mob Elite at location(0, 64, 0) as g
set g.tags.level to 5 // store
set lvl to g.tags.level otherwise 0 // read with a fallback
send "level ${lvl}" to sender
set g.tags.level to none // delete
}
}Because a tag read is Optional<Any>, narrow or otherwise it before use — using one directly is a compile error:
mob_tags_optional.sw:5:33: error: the left operand of '+' is Optional<Any> and may be missing; check it with 'if ... exists' or provide a fallback with 'otherwise'Typed tags
Declaring a tags { ... } block on the mob gives a tag a fixed type instead of the freeform Optional<Any>. Each entry is name: <Type> — a typed, indexable store that starts empty:
mob Zombie {
type: "ZOMBIE"
viewable: false
tags: { hits: Map<Player, Integer> } // typed, keyed by Player
on_hit {
if attacker exists {
set mob.tags.hits[attacker] to (mob.tags.hits[attacker] otherwise 0) + 1
}
}
}Now mob.tags.hits[attacker] is Optional<Integer> — the value type is known, but a key that was never written is still missing, so the otherwise 0 fallback stays required. This Map<Player, Integer> keyed by player is the backbone of the per-viewer zombie: one counter per attacker on a single mob. A typed tag and a freeform mob.tags.<other> can coexist on the same mob.
Mob events
The base Mob receiver is the global counterpart to the per-declaration handlers — its methods fire for every mob, and mob is the mob:
| Method | Cancellable | Bound variables |
|---|---|---|
on_spawn | no | — |
on_death | no | killer (Optional<Entity>) |
on_hit | no | attacker (Entity) |
persistent kills for Player: Integer = 0
Mob {
on_death {
if killer exists {
set k to player(killer.uuid)
if k exists {
set kills for k to (kills for k) + 1
send "<gold>${mob.custom_id} down — ${kills for k} kills" to k
}
}
}
}To veto damage, reach for the cancellable Entity.on_hit — it fires for every live entity and can cancel event:
Entity {
on_hit {
if entity.type is "SHEEP" {
cancel event // sheep take no damage
}
}
}Per-declaration vs global
on_death inside a mob <MobType> { } declaration is the right place for behavior that belongs to that one mob (drops flavor, death cries). The base Mob { on_death } is the right place for systems that span all mobs — kill counters, quest progress, leaderboards.