Skip to content

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.

swoftlang
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

KeyTypeRequiredMeaning
type:string literalyesbase EntityType"ZOMBIE", "zombie", and "minecraft:zombie" all work
name:String expressionnonameplate, always visible; ${mob.health} re-renders on damage
health:Number expressionnomax health
damage:Number expressionnomelee hit damage
speed:Number expressionnomovement speed attribute
ai:enumnomelee (chase + attack + stroll), passive (stroll), none (statue)
viewable:booleannoauto-viewable at spawn; false spawns it hidden for per-viewer control — see per-viewer entities
tags { ... }blocknotyped per-entity state — see Tags
drops { ... }blocknorolled on death; see drops
on_spawn { ... }statementsnobinds mob
on_death { ... }statementsnobinds mob, killer (optional<Player>)
on_attack { ... }statementsnobinds mob, victim (Player)
on_hit(attacker) { ... }statementsnothe 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:

swoftlang
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 name

Spawning and despawning

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

swoftlang
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

PropertyTypeAccess
healthDoubleread/write — writing re-renders the nameplate
max_healthDoubleread-only
nameStringread/write
locationLocationread/write — writing teleports
typeStringread-only — the base entity type
custom_idStringread-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:

swoftlang
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):

swoftlang
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 attackeron_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:

swoftlang
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:

swoftlang
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

Global counterparts to the per-declaration handlers, typed in the event table and fired for every custom mob:

EventCancellableProperties on event
MobSpawnnomob
MobDeathnomob, killer (optional<Player>)
MobDamageyesmob, damage (rw), attacker (optional<Player>), cancelled (rw)
swoftlang
persistent kills for Player: Integer = 0

event MobDeath {
    execute {
        if event.killer exists {
            set kills for event.killer to (kills for event.killer) + 1
            send "<gold>${event.mob.custom_id} down — ${kills for event.killer} kills" to event.killer
        }
    }
}

event MobDamage {
    execute {
        if event.mob.custom_id is "lost_sheep" {
            cancel event
        }
    }
}

Per-declaration vs global

on_death in the declaration is the right place for behavior that belongs to the mob (drops flavor, death cries). The MobDeath event is the right place for systems that span all mobs — kill counters, quest progress, leaderboards.