> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.trysulfur.net/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.trysulfur.net/_mcp/server.

# declare_mob reference

`declare_mob` registers a custom mob type. Like everything in Sulfur's plugin API it is a
**load-time capture**: the declaration is stored once when the module body runs; each spawned mob
gets a fresh per-mob AI referencing the shared frozen callables. The Starlark interpreter fires
**only inside a running goal's callbacks** — an idle declared mob makes zero Starlark calls per
tick.

## Signature

```python
declare_mob(
    name = "wanderer",           # unique mob name (duplicate = load error)
    base_type = "pig",           # an existing vanilla entity type (see the allowed list)
    attributes = {...},          # optional: attribute overrides (string -> number)
    goals = [goal(...), ...],    # optional: AI goals
    skills = [skill(...), ...],  # optional: declared skills — see the Skills page
)
```

### `base_type` — custom means custom *behavior*

A declared mob is a custom **behavior**, not a new wire entity type: `base_type` resolves to an
existing 26.2 entity, and the spawned mob uses that type's wire id, hitbox dimensions, and
attribute supplier — so an unmodified vanilla client renders it natively. An unknown `base_type`
is a loud load error.

Allowed values: `pig`, `cow`, `sheep`, `chicken`, `skeleton`, `creeper`, `spider`, `zombie`,
`cat`, `witch`, `villager`, `silverfish`, `wolf`, `husk`, `mooshroom`, `rabbit`, `enderman`,
`fox`, `endermite`, `turtle`, `ocelot`, `pillager`, `vindicator`, `evoker`, `ravager`,
`iron_golem`, `sulfur_cube`, `happy_ghast`.

### `attributes`

A dict of attribute overrides applied on top of the base type's real attribute supplier. Friendly
names are aliased to registry keys; any attribute the entity's supplier registers can be
overridden:

| Name             | Meaning                                                                  |
| ---------------- | ------------------------------------------------------------------------ |
| `max_health`     | Max health (mob spawns at full health).                                  |
| `movement_speed` | Movement speed; also scales the declared mob's walk pace proportionally. |
| `follow_range`   | Bounds the target-goal scan box (hostiles).                              |
| `attack_damage`  | Melee damage dealt by `Mob.doHurtTarget`.                                |
| `armor`          | Folds into the victim-side armor curve.                                  |

Non-string keys and non-numeric values are load errors. An attribute the base type doesn't have is
skipped at spawn (the base type itself was validated at load).

## `goal(...)`

```python
goal(
    priority,                          # int — lower runs first (the vanilla goal-selector slot)
    flags,                             # list of control flags: "MOVE", "LOOK", "JUMP", "TARGET"
    tick = None,                       # callable(entity, world, nav) — runs each tick while the goal is running
    can_use = None,                    # callable -> truthy — may the goal start? (default: always)
    start = None,                      # callable — fires on the not-running -> running edge
    stop = None,                       # callable — fires on the running -> not-running edge
    can_continue = None,               # callable -> truthy — may it keep running? (default: can_use)
    requires_update_every_tick = False,# bool — tick on the every-tick path (jar goals that need it)
    kind = None,                       # string — route to a Go-native jar-ported goal instead of callbacks
    avoid_type = None,                 # string — required iff kind == "avoid_entity" (the class to flee)
)
```

Rules, all enforced at load:

* `flags` must be from the closed set `MOVE`, `LOOK`, `JUMP`, `TARGET` (the exact four vanilla
  `Goal$Flag` values). Goals claiming `TARGET` are arbitrated by the independent target selector,
  exactly as vanilla routes `HurtByTargetGoal`/`NearestAttackableTargetGoal`.
* A callback goal needs **at least one** of `tick` / `start` / `can_use`.
* A `kind=` goal must supply **no** callbacks and no `requires_update_every_tick` — the Go-native
  goal owns the behavior and its cadence. Its declared flags must match the native goal's own flag
  set.
* `avoid_type` is required for `kind = "avoid_entity"` and forbidden everywhere else.

All callbacks receive the three handles `(entity, world, nav)` — see
[Handles & capabilities](/plugins/handles-and-capabilities). Callbacks run on fresh step-budgeted
threads with error isolation: a failing `can_use` counts as `False`, a failing `tick` is logged
and skipped, and the tick loop survives.

### `kind=` — Go-native goals

Some vanilla goals are the same class for every mob (combat targeting, melee chase, sun-flee, …)
with nothing per-mob to express in a script — and re-expressing their RNG in Starlark would risk
drift. A `kind=` goal declares only the priority + flags and routes to the verbatim Go port. The
mob's per-entity RNG stream is shared between native and scripted goals, so both stay in lockstep.

Available kinds include (each a 1:1 port of the named jar goal):

`nearest_attackable_target`, `hurt_by_target`, `melee_attack`, `ranged_bow_attack`,
`spider_attack`, `leap_at_target`, `avoid_entity`, `float`, `climb_on_powder_snow`,
`creeper_swell`, `witch_ranged_attack`, `restrict_sun`, `flee_sun`, `sit`, `follow_owner`,
`owner_hurt_by`, `owner_hurt`, `angry_player_target`, `skeleton_target`,
`enderman_look_for_player`, `enderman_freeze_when_looked_at`, `enderman_take_block`,
`enderman_leave_block`, `silverfish_merge_stone`, `silverfish_wake_friends`, `cube_float`,
`cube_random_direction`, `cube_keep_on_jumping`, `fox_faceplant`, `fox_stalk`, `fox_pounce`,
`fox_seek_shelter`, `fox_sleep`, `fox_perch_search`, `fox_defend_trusted`, `fox_land_target`,
`fox_search_items`, `turtle_goto_water`, `turtle_go_home`, `turtle_travel`, `turtle_lay_egg`,
`nearest_healable_raider_target`, `cat_relax_on_owner`, `cat_lie_on_bed`, `cat_sit_on_block`,
`long_distance_patrol`, `iron_golem_hostile_target`, `pillager_crossbow_attack`,
`evoker_casting_spell`, `evoker_summon_spell`, `evoker_attack_spell`, `evoker_wololo_spell`.

An unknown kind fails loudly — a hostile with a typo'd combat goal never boots silently disarmed.

## Example: a minimal wandering mob

The `wandermob` test plugin declares a pig-based mob with a single MOVE goal that keeps it walking
east via the real Go pathfinder:

```python title="wandermob/main.star"
# on_wander_tick fires while the MOVE goal runs. It receives (entity, world, nav) handles.
# If the mob has no active path it picks a nearby target and asks the Go nav to path there.
# The Go nav + physics then walk the mob (the goal SETS a target; it never moves the mob).
def on_wander_tick(entity, world, nav):
    if not nav.has_path():
        nav.path_to(entity.x + 8.0, entity.y, entity.z)

declare_mob(
    name = "wanderer",
    base_type = "pig",
    attributes = {
        "max_health": 12.0,
        "movement_speed": 0.25,
    },
    goals = [
        goal(priority = 6, flags = ["MOVE"], tick = on_wander_tick),
    ],
)
```

```toml title="wandermob/plugin.toml"
name = "wandermob"
version = "0.1.0"
entrypoint = "main.star"
runtime = "starlark"
capabilities = ["entities.read", "entities.write", "nav"]
```

## Example: a real vanilla mob (excerpt)

The bundled `vanilla_cat` plugin is a method-for-method port of the 26.2 jar's
`Cat.registerGoals`, mixing scripted passive goals with `kind=` natives:

```python title="plugins/mobs/vanilla_cat/main.star (goals excerpt)"
declare_mob(
    name = "vanilla_cat",
    base_type = "cat",
    # ...
    goals = [
        # @1 FloatGoal / TamableAnimalPanicGoal — scripted passive callbacks (shared port)
        # ...
        # @2 SitWhenOrderedToGoal [JUMP, MOVE] — Go-native
        goal(priority = 2, flags = ["JUMP", "MOVE"], kind = "sit"),
        # @3 CatRelaxOnOwnerGoal (no flags) — Go-native comfort goal
        goal(priority = 3, flags = [], kind = "cat_relax_on_owner"),
        # @4 CatTemptGoal(0.6, CAT_FOOD, true) [MOVE, LOOK] — scripted
        goal(
            priority = 4,
            flags = ["MOVE", "LOOK"],
            can_use = tempt_food_can_use,
            tick = tempt_food_tick,
            stop = tempt_food_stop,
            can_continue = tempt_food_continue,
        ),
        # @6 FollowOwnerGoal(1.0, 10.0, 5.0) [MOVE] — Go-native
        goal(priority = 6, flags = ["MOVE"], kind = "follow_owner"),
        # @10 BreedGoal(0.8) [MOVE, LOOK] — scripted
        goal(
            priority = 10,
            flags = ["MOVE", "LOOK"],
            can_use = breed_can_use,
            tick = breed_tick,
            stop = breed_stop,
            can_continue = breed_continue,
        ),
        # @11 WaterAvoidingRandomStrollGoal(0.8) [MOVE] — scripted
        goal(
            priority = 11,
            flags = ["MOVE"],
            can_use = stroll_can_use,
            stop = stroll_stop,
            can_continue = stroll_continue,
        ),
        # @12 LookAtPlayerGoal(Player, 10.0) [LOOK] — scripted
        goal(
            priority = 12,
            flags = ["LOOK"],
            can_use = look_can_use,
            start = look_start,
            tick = look_tick,
            stop = look_stop,
            can_continue = look_continue,
        ),
    ],
)
```

## How a declared mob ticks

1. **Spawn** builds a fresh entity from the base type (wire id, hitbox, attribute supplier),
   applies the declared attribute overrides, initializes health to max, and builds a fresh AI
   whose goals reference the shared frozen callables. Each mob's RNG is reseeded by entity id so
   two mobs of the same declaration wander independently.
2. **Arbitration** runs through the same goal selector as Go-native mobs: goals claim their
   control flags by priority; a scripted goal is not a bypass and not a parallel tick.
3. **Callbacks** fire only for a *running* goal (`can_use` is evaluated when the selector offers
   the goal its flags; `tick` only while it holds them). Per-goal, per-mob mutable state lives in
   the `get_state`/`set_state` scratch.
4. If the declaration carries `skills`, a per-mob skill runner is attached and the `spawn` trigger
   fires — see [Skills](/plugins/skills).