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 "crypt_ghoul" {
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 "crypt_ghoul" 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
}
}Declaration keys
| Key | Type | Required | Meaning |
|---|---|---|---|
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(attacker) { ... } | statements | no | the mob is damaged; binds mob and the named 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 "rotten_scimitar" {
material: "GOLDEN_SWORD"
name: "Rotten Scimitar"
rarity: uncommon
}
mob "armed_ghoul" {
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 "id" at <location> [as <var>] | spawns one instance; as binds it as a Mob |
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 "lost_sheep" {
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(<name>) { ... } runs when the mob takes damage, inline in the declaration. It binds mob and an attacker parameter you name — 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(attacker) {
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(attacker) {
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 this is the mob:
| Method | Cancellable | Parameters |
|---|---|---|
on_spawn() | no | — |
on_death(killer) | no | killer (optional<Entity>) |
on_hit(attacker) | no | attacker (Entity) |
persistent kills for Player: Integer = 0
Mob {
on_death(killer) {
if killer exists {
set k to player(killer.uuid)
if k exists {
set kills for k to (kills for k) + 1
send "<gold>${this.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(attacker) {
if this.type is "SHEEP" {
cancel event // sheep take no damage
}
}
}Per-declaration vs global
on_death inside a mob "id" { } 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.