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 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 in spawn mob CryptGhoul, in is a CryptGhoul, and anywhere a type is expected. CryptGhoul is a subtype of Mob (CryptGhoul <: Mob), so a spawned CryptGhoul is usable anywhere a Mob is.
  • 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 to snake_case of the type name — CryptGhoul"crypt_ghoul". Write id: only to pin a key that differs from the name.
swoftlang
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

KeyTypeRequiredMeaning
id:string literalnothe wire/persistence id; defaults to snake_case of the type name — see Type name vs id
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 { ... }statementsnothe 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:

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

Spawning and despawning

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

swoftlang
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

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 { ... } 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):

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

MethodCancellableBound variables
on_spawnno
on_deathnokiller (optional<Entity>)
on_hitnoattacker (Entity)
swoftlang
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:

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