Make a creature
This guide adds a hostile creature to the flagship game with a mod: the
razorback, a boar-like animal that spots players, telegraphs a lunge at
them, flees when badly hurt, drops loot and spawns in a meadow. It is
registry data plus one asset file, modelled on the flagship’s boar
(game/flagship-game/data/). The finished mod is
tools/test-mods/guide-creature;
its manifest, guest code, data and asset files are shown below in full.
Each step links the contract for its fields, and the
capability map lists what
a mod can reach today.
What you’ll build
| Files | What they do |
|---|---|
data/archetypes/ | the components every razorback carries |
data/locomotion/, data/awareness/, data/brains/ | how it moves, notices and decides |
data/abilities/, effects/, tags/, presentation/, damage_types/ | the telegraphed lunge and its cooldown |
data/factions/ | whom it attacks |
data/items/, data/loot_tables/ | what it drops |
data/spawn_rules/, data/spawners/ | where it appears |
assets/guide-creature/animations/razorback.bind.ron | drives the boar’s animation graph from the razorback’s state |
Before you start
- Read the data stage: one directory per
registry under
data/, and everyidin your mod’s namespace. - Install the mod tool with
cargo install --path tools/cargo-mod --locked. Building needs thewasm32-unknown-unknowntarget andwasm-tools, and runscargo build -p <package name>, so the crate must build from the current directory (in this repository, as a workspace member).
1. The mod package
The manifest’s id is the namespace of every entry, so all keys below
start with guide-creature:.
[package]
id = "guide-creature"
name = "guide-creature-mod"
display_name = "Guide Creature"
version = "0.1.0"
side = "both"
kind = "game-module"
wasm = "guide_creature_mod.component.wasm"
data_format = 1
[assets]
dir = "assets"
[deps]
flagship-game = "0.1"
[build]
use_wasm_tools = true
data_format = 1is the registry-data format this build reads.side = "both": most of the mod’s registries are hashed into the join handshake, and clients compile its binding table.[deps]names the flagship, whose keys the data uses (the players’ faction, the boar model, a cue, a spawn category), and loads its data first.
A game module needs a wasm component; there is no data-only mod kind. This
guest registers nothing, so #[mod_entry] names no exports and the
component exports only manifold:core/plugin-lifecycle:
//! The example mod of the "Make a creature" guide
//! (`docs/guides/make-a-creature.md`). The creature lives in `data/` and
//! `assets/`; the guest only names the mod.
use manifold_mod_substrate::prelude::*;
#[derive(Default)]
#[mod_entry]
pub struct GuideCreatureMod;
impl Plugin for GuideCreatureMod {
const ID: &'static str = "guide-creature";
}
2. The archetype
An archetype lists the components every
instance carries, each written in the component’s own schema; () takes
its defaults.
// The razorback: a hostile creature built on the flagship boar's model.
Archetype(
id: "guide-creature:razorback",
persistence: Session,
spatial_layers: ["creature"],
bounds_radius: 0.6,
components: {
"manifold-sim:position": (),
"manifold-sim:velocity": (),
"manifold-sim:yaw": (),
"manifold-sim:pitch": (),
"manifold-sim:motion-mode": (),
"manifold-model:model": (asset: "flagship:creatures/boar"),
"manifold-anim:anim-setup": (bindings: "guide-creature:animations/razorback"),
"manifold-anim:hit-eval": (mode: Lazy),
"manifold-nav:locomotion": (profile: "guide-creature:razorback"),
"manifold-gameplay:stats": (base: { "manifold:max_health": 30.0 }),
"manifold-gameplay:damage-receiver": (death: (
loot: Some("guide-creature:entities/razorback"),
corpse: Linger(ticks: 40),
despawn: AfterCorpse,
)),
"manifold-gameplay:faction": (id: Some("guide-creature:razorbacks")),
"manifold-gameplay:abilities": (granted: ["guide-creature:lunge"]),
"manifold-ai:brain": (def: "guide-creature:razorback"),
"manifold-ai:ai-presentation": (),
},
variants: [
(id: "elder", when: Some(Chance(0.1)), priority: 10,
patch: { "manifold-gameplay:stats": (base: { "manifold:max_health": 60.0 }) }),
(id: "common"),
],
)
persistence: Session: not saved; the spawner makes new ones after a restart. Themanifold-sim:*components are the body;manifold-nav:locomotionmakes it an agent.manifold-model:modelnames the model andmanifold-anim:anim-setupthe binding table.hit-evalinLazymode builds the hit pose only when a hit query needs it.damage-receivermakes it damageable, rolls its loot at death and keeps the corpse for 40 ticks (one second).ai-presentationis the public brain state the binding table reads.variants: one spawn in ten is anelderwith twice the health. The variant withoutwhenis the fallback.
An archetype can inherit with parent: instead, merging its components
into the parent’s field by field. The razorback does not inherit from
flagship-game:boar, so changes to the boar do not reach it.
3. Movement
A locomotion profile sets the body that the integrator moves and the pathfinder plans for.
// How the razorback moves and which cells its paths may use.
LocomotionProfile(
id: "guide-creature:razorback",
class: "manifold:walker_small",
body: (width: 0.9, height: 0.9),
speed: (walk: 3.0, sprint: 6.5, swim: 1.2),
turn_rate_deg_per_s: 600.0,
malus: {
Water: 12,
DangerFire: 8,
},
)
manifold:walker_smallfits bodies up to 0.98 × 0.98 blocks; a larger body fails the data stage.- Omitted fields take their defaults: a 0.5 m step, a 1.1 m jump, a three-block drop, a gap jump derived from walking speed.
malusmakes water and cells beside fire costly. Cells the class cannot cross (closed doors, damaging fire, hazardous fluid) stay impassable; pricing one makes it passable. A profile that does less than its class gets a warning, because its reachability answers become searches.
4. Awareness
An awareness profile turns sightings into
knowledge. Seeing a player fills a meter; past promote, after the
reaction delay, the player is Engaged. Knowledge holds for hold
seconds, then decays. give_up is how long it ignores a target navigation
cannot reach.
// How fast the razorback notices a player and how long it stays engaged.
AwarenessProfile(
id: "guide-creature:razorback",
promote: { Engaged: 0.75 },
reaction: { Engaged: 0.15 },
hold: 6.0,
decay_per_s: 0.05,
give_up: 10.0,
)
5. The brain
An archetype brain is a fixed priority list of
built-in behaviours, evaluated every evaluate_every ticks and at once on
new knowledge, damage or a lost target.
// The razorback's brain: an archetype brain, a fixed priority list of
// built-in behaviours (0 is the highest priority).
Brain(
id: "guide-creature:razorback",
senses: [
Sight(radius: 24.0, lose_radius: Some(32.0),
cones: { Unaware: 60.0, Engaged: 110.0 },
interval_ticks: 10, targets: HostileTo),
Damage,
],
memories: ["manifold:last_hurt_by", "manifold:unreachable"],
awareness: Some("guide-creature:razorback"),
lod: Some("manifold:ambient_creature"),
// The binding table reads these by index: "graze" must stay at 1.
presentation_activities: ["idle", "graze", "alert", "charge", "flee"],
kind: Some(Archetype(evaluate_every: 10, behaviours: [
(name: "flee", priority: 0, controls: [Move, Look], presents: Some("flee"),
behaviour: FleeWhen(when: StatBelow("manifold:health", 0.2),
distance: 20.0, speed: 1.3)),
(name: "lunge", priority: 1, controls: [Move, Look, Attack], requires: [Reachable],
presents: Some("charge"),
behaviour: ChargeAttack(ability: "guide-creature:lunge", min_range: 3.0,
max_range: 9.0, recovery: 1.0)),
(name: "glare", priority: 2, controls: [Look], presents: Some("alert"),
behaviour: LookAtNoticed(min_state: Engaged, max_distance: 32.0)),
(name: "home", priority: 3, controls: [Move],
behaviour: ReturnHome()),
(name: "graze", priority: 4, controls: [Move, Look], presents: Some("graze"),
behaviour: Graze(duration: (3.0, 6.0), chance: 0.1)),
(name: "wander", priority: 5, controls: [Move],
behaviour: Wander(radius: 10.0, around: Home, pause: (2.0, 5.0))),
])),
)
senses: sight of targets its faction regards as hostile, within 24 blocks and a 60° half-cone (110° once engaged), through voxel line of sight. Damage engages the attacker at once.memoriesare engine kinds: the last attacker, and targets given up on.lod: full rate within 48 blocks of a player; dormant when far away and idle.presentswrites an index intopresentation_activities, which clients see asai-presentation.activity.- A behaviour starts when its control flags are free or held by a lower
priority, which it interrupts.
glare(Look) runs besidewander(Move). ChargeAttackcloses to betweenmin_rangeandmax_rangewith sight and a straight walkable lane, then requests the ability.requires: [Reachable]stops it when navigation proves the target unreachable.
The built-ins are Wander, Graze, LookAtNoticed, ChargeAttack,
FleeWhen and ReturnHome. For more, use a full brain: scored activities
that run state trees, as the flagship’s pet does.
6. The lunge
An ability is a chain of engine elements. The brain requests it; the gameplay framework runs it.
// The razorback's lunge: a telegraphed wind-up, then a dash that hits the
// first target in its path.
Ability(
id: "guide-creature:lunge",
tags: ["manifold:ability.attack"],
required: (none: ["manifold:status.debuff.stun"]),
blocked: (any: ["guide-creature:cooldown.lunge"]),
owned_tags: ["manifold:state.casting"],
cooldown: Some("guide-creature:cooldown.lunge"),
targeting: Target(range: 12.0, relation: Hostile, requires_sight: true),
interrupt: (on_tags: (any: ["manifold:status.debuff.stun"]), before_commit_only: true),
root: Serial([
Face(to: Target, turn_rate_deg_per_s: 720.0),
Parallel(until: All, children: [
SetPresentation(field: "guide-creature:lunge_phase", value: 1),
OneShot(play: "charge_windup"),
Telegraph(shape: Rect(width: 1.4, length: 9.0), anchor: CasterFacing,
lock: OnStart, ticks: 24),
]),
Commit,
SetPresentation(field: "guide-creature:lunge_phase", value: 2),
Parallel(until: First(0), children: [
Dash(speed: 14.0, ticks: 24, direction: Locked, stop: [Collision, HitTarget]),
SweepVolume(shape: Box(half: (0.6, 0.6, 0.9)), ticks: 24, hit: (
damage: Some((damage_type: "guide-creature:lunge", base: "5")),
max_targets: 1)),
]),
]),
)
- Activation checks
required,blockedand the target: a hostile within 12 m, in sight. - The wind-up sets
lunge_phaseto 1, plays the boar graph’scharge_windupone-shot and publishes a 9 × 1.4 m telegraph that clients fill over 24 ticks. Commitstarts the cooldown. The dash and the sweep run together until the dash ends on a wall, a hit or its 24 ticks. When the ability ends,lunge_phasereturns to its default.- A stun (
manifold:status.debuff.stun) refuses the lunge, and cancels a wind-up.
The cooldown is an effect that grants a tag, which blocked tests:
// The lunge's cooldown: it grants the tag the lunge is blocked by.
Effect(
id: "guide-creature:cooldown.lunge",
duration: For("3s"),
grant_tags: ["guide-creature:cooldown.lunge"],
stacking: Replace,
)
// Held while the lunge cools down; the lunge is blocked by it.
Tag(id: "guide-creature:cooldown.lunge")
lunge_phase is a public presentation field, and the damage type sets the
hit’s immunity window, knockback and cue:
// What the lunge shows: 0 idle, 1 wind-up, 2 lunging.
Field(id: "guide-creature:lunge_phase", kind: U8, default: 0)
// The lunge's hit: at most one per razorback in 10 ticks, with knockback.
DamageType(
id: "guide-creature:lunge",
immunity: (by: Owner, ticks: 10),
knockback: Some((horizontal: 7.0, vertical: 3.5, from: Attacker)),
hit_cue: Some("flagship-game:cue.hit.blunt"),
)
7. Factions and loot
A faction relation
is one-way unless the faction sets symmetric: true. The sight sense’s
HostileTo reads it.
// Razorbacks are hostile to the flagship's players. Players declare no
// relation to razorbacks, so they regard them as Neutral and may hunt them.
Faction(
id: "guide-creature:razorbacks",
relations: [("flagship-game:players", Hostile)],
)
A loot table rolls item kinds at death.
Without luck this one gives 2 to 5 bristles three times in four, otherwise
a boar tusk; quality raises the tusk’s weight with the killer’s luck. The
drops are reserved for the killer for 200 ticks.
// A drop with no model of its own: the ground shows the drop archetype's sack.
ItemKind(id: "guide-creature:bristle")
// What a razorback drops: bristles, or now and then one of the flagship's
// boar tusks.
LootTable(
id: "guide-creature:entities/razorback",
context: LootEntity,
pools: [
Pool(rolls: Const(1), entries: [
Item(item: "guide-creature:bristle", weight: 3,
functions: [SetCount(Uniform(min: Const(2), max: Const(5)))]),
Item(item: "flagship-game:boar_tusk", weight: 1, quality: 2),
]),
],
)
8. Model, graph and binding table
The razorback reuses the boar’s committed model (flagship:creatures/boar)
and graph (flagship:animations/boar) by reference. Authoring a model is
covered in model assets; its “Creatures
(profile-less graphs)” section lists the boar’s bones, clips and regions.
A binding table sets graph parameters from replicated state, on the server and every client. The mod ships its own so the boar graph follows the lunge field:
// guide-creature:animations/razorback drives the flagship boar's graph from
// the razorback's state; `charging` reads the lunge field.
(
graph: "flagship:animations/boar",
bindings: [
(param: "speed", source: Motion(PlanarSpeed)),
(param: "alert", source: Field("manifold-ai:ai-presentation", "awareness"), map: AtLeast(2.0)),
(param: "grazing", source: Field("manifold-ai:ai-presentation", "activity"), map: Equals(1.0)),
(param: "charging", source: Field("manifold-gameplay:presentation", "guide-creature:lunge_phase"), map: Equals(2.0)),
(param: "dead", source: Field("manifold-gameplay:presentation", "manifold:dead")),
(param: "look_yaw", source: View(YawOffset), map: Scale(57.2958)),
(param: "look_pitch", source: View(Pitch), map: Scale(57.2958)),
],
)
- The path gives the id:
assets/<ns>/animations/<path>.bind.ronis<ns>:animations/<path>. graphmust be the graph the model runs (MODEL504). Each source must be a public field of a component replicated at rate classCritical.grazingtestsactivity= 1, sograzestays at index 1 of the brain’spresentation_activities.
9. Where it spawns
A spawn rule says what may spawn where; a spawner places spawns inside a box of the world.
// Where a razorback may appear: on grass under open sky, at least 24 blocks
// from every player, in the flagship's ambient creature category.
SpawnRule(
id: "guide-creature:spawn/razorback",
archetype: "guide-creature:razorback",
category: "flagship-game:cat/creature",
weight: 10,
conditions: All([
Ext(atom: "manifold:surface_block", args: (block: "flagship:grass")),
Ext(atom: "manifold:sky_light", args: (at_least: 8)),
]),
density: (per_region: 3, category_per_region: 12),
distance: (min: 24.0, max: 64.0),
persistence: Ambient,
)
// A spawner region west of the flagship's world spawn: two razorbacks at
// most, a death respawned after a minute.
Spawner(
id: "guide-creature:spawner/west_meadow",
shape: Region(min: (-72, 48, 28), max: (-28, 84, 72)),
source: Rules(["guide-creature:spawn/razorback"]),
policy: Ambient,
max_alive: 2,
cadence: "4s",
respawn: (delay: "60s", catch_up_max: 1),
leash: Some(24),
)
conditionstest what the spawner measured at the cell: the block underfoot and its sky light.categoryshares the flagship’s ambient creature caps (ten per player).- Spawns land at least
distance.minfrom every player and never where a player could see them. - The spawner mirrors the flagship’s boar meadow across the world spawn and
assumes the same grassy terrain. It evaluates every 4 s while a client
subscribes to its region and respawns a death after 60 s.
leashis each creature’s home radius forReturnHome.
Test it
Validate offline with the flagship’s data loaded first:
cargo mod validate tools/test-mods/guide-creature/mod.toml --base game/flagship-game
Errors name the file, line and field. The offline stage does not run the
flagship’s code, so it also reports the three components the flagship’s
player archetype takes from that code (flagship-game:inventory,
carried-stack and game-mode) as unregistered
(issue #530); those errors
are not yours, nor is a MODEL208 warning in the engine’s rifle layer. Binding
tables compile only when the server first animates the creature.
Build and install. cargo mod publish builds the native component and
the browser bundle (it needs Node, the pinned jco and Chromium) into
guide-creature.mod. Native singleplayer refuses a second game module
(issue #528), so test on a
dedicated server. Put the archive in the server’s mods directory,
<data root>/<pack>/mods/; for the development server:
cargo mod publish --manifest tools/test-mods/guide-creature/mod.toml
mkdir -p target/dev-session/data/baseline/mods
cp tools/test-mods/guide-creature/guide-creature.mod target/dev-session/data/baseline/mods/
python scripts/dev-session.py server
The server loads the mod at boot and prints a RELAY_TICKET; a client that
lacks the mod receives it at join. In another terminal, join from a browser,
or from a native client with its own state directory and the ticket:
python scripts/dev-session.py browser --build-web
python scripts/dev-session.py native --state-dir ../manifold-client --ticket TICKET
A native client that shares the server’s state directory finds the mod in its own mods directory too: it loads that copy at boot and refuses the delivered one (issue #528).
Play. The commands below need Admin. On a dedicated server, that is a
signed-in client whose platform username is in the server’s saves/ops.ron
(target/dev-session/data/baseline/saves/ops.ron for the development
server); development identities (--username) are never Admin. Open the
console (Backquote), aim at a block, and run:
/entity spawn guide-creature:razorback at @look
/entity inspect @last
/ai why @last
/ai trace @last on
/ai overlay sight activity
/entity spawn guide-creature:razorback#elder at @look
/entity ability @last use guide-creature:lunge at @s
/entity kill @last
/entity def guide-creature:razorback
/entity spawner info guide-creature:spawner/west_meadow
- The server logs
anim (server): flagship:creatures/boar driven by guide-creature:animations/razorbackon the first spawn, or the binding error with its MODEL code. /entity inspectshows stats, effects, locomotion and brain (Shift+F4 opens a live inspector)./ai whysays why each behaviour is or is not running.- In its view, it engages after its reaction delay and lunges; out of view, it glares, then grazes and wanders. Killed, it drops bristles (the flagship’s loot sack) or a tusk.
/entity deflists each registry with that key and where it was declared;/entity spawner infoshows the spawner’s count, queue and rejected cells.
Limits
The capability map has the current status of each row below.
- No code. There is no WIT for entities, gameplay, AI or animation yet: only the built-in behaviours, senses, ability elements and loot functions. Guest behaviours, loot functions and damage hooks wait for their contract steps (how mods reach the engine).
- Movement and perception. Only walkers are built; flight, swimmers,
climbing and doors are design. The senses are sight, damage, and the
owner and item sensors a pet uses: no hearing, and no
SuspiciousorAlertstates (AI and navigation). - Spawning. Only
Regionspawners withRulesand theAmbientpolicy. Player-anchored spawners, herds, encounters, persistent creatures and biome conditions are later phases. - Loot. Players cannot pick up
guide-creature:bristle: the flagship inventory holds only blocks. Item instances and general pickup are design (items). - Hit feedback. Hit markers and damage numbers reach the client, but
no HUD draws them yet
(issue #532); watch
health with
/entity inspect. - Installing. Native singleplayer refuses a second game module, and a
native client that also has the mod installed refuses the delivered copy
(issue #528;
several game modules).
Test on a dedicated server, joining from a browser or from a native
client with its own
--state-dir. - Offline validation.
cargo mod validate --base game/flagship-gamereports the flagship’s guest-declared components as unregistered (issue #530). - Iteration. Data has no hot reload; restart the server.
See also
Archetypes ·
data stage ·
perception and brains ·
navigation ·
population ·
gameplay framework ·
model assets ·
animated characters (docs/architecture/animated-characters.md) ·
console commands ·
running and testing (docs/development/running-and-testing.md)