How to add a new playable body
Audience Engine developer.
Prerequisites ../explanation/ecs-in-nihilurk.md, ../how-to/work-with-the-ecs.md.
../tutorial/add-your-first-body.md is the long version,
as a lesson, if you want to feel the save-marker bug first.
Result A third hand-written `Body::Variant`, selectable with
`-b <name>`, that survives a save/load round trip.
Not a content-table recipe. -am <species> already lets anyone wear a
bestiary row (add-a-monster.md is that whole story) — this page is for the
other kind of body, one written by hand the way the lurk was: its own dice,
its own kit, its own rules about gear. Reach for this only when a species
from -am genuinely won’t do.
The table
models/src/body.rs -> enum Body
Three variants today: Nihil (the default), Lurk, Monster(&'static MonsterDef). One enum, not a flag per body, because a player is exactly one
of them and the argument parser has nothing to cross-check — see the doc
comment at the top of body.rs before you touch it.
The recipe
Follow wear_lurk line for line; a fourth body is a sibling of it, not a
new shape.
-
Add the variant.
pub enum Body { Nihil, Lurk, Wraithkin, // new Monster(&'static MonsterDef), }Body::name()needs an arm too — it is what-bmatches against and what an error about the body prints. -
Write
wear_wraithkin, next towear_lurk. It inserts the startingRenderable,Fighter,MagicandSpeedthe same way, and grants whatever innate effects the body is born with throughgrant_all— aconst &[Grant]slice, the same shapeLURK_GRANTSuses, so it can be handed togrant_allunchanged. Pull every number out to its ownconstants::wraithkinmodule (see../reference/constants.mdbelow) rather than writing them inline — that is the one linewear_lurkitself would fail review on if it didn’t. -
Wire it into
wear:Body::Wraithkin => wear_wraithkin(world, player), -
Give it a marker that survives a save. A monster body is the one body the save writes down (
SaveGame::monster_body, the species id); the lurk is read back from its ownLurkeffect marker, which rides the effect ledger like anything else a creature was born with. Your body needs the same: an effect (or existing marker) that (a) is already saved, and (b) nothing else in the game would attach to the player by accident. A new saved field changes the save format: see theEntitySavenote inadd-an-item-category.md. -
Teach
innate_tempoif the body doesn’t return toSpeedKind::Normalafter a staircase lifts whatever the floor lent it (the lurk always returns toQuick). -
Teach
equip_refusalif the body can’t wear everything nihil can. The lurk refuses every slot butSlot::Finger; a monster body refuses everything unless its bestiary row carriesItemUser. Say why in the refusal string — it’s what the player reads when they try. -
Wire the flag.
engine/src/main.rsmatches("-b", "nihil")/("-b", "lurk")literally; add("-b", "wraithkin") => body = Some((models::Body::Wraithkin, "-b")).-band-amalready refuse being passed together and refuse an unrecognised name — nothing else to touch there. -
brings_a_pack. OnlyNihilreturnstrue. Leave it that way unless the new body is meant to start holding a mace, which none of the hand-written ones are — the whole point is a body with nothing to buy.
Checks that will catch you
models/tests/it/body.rs and lurk.rs are the pattern to copy for a new
wraithkin.rs, declared as mod wraithkin; in models/tests/it/main.rs
and run with cargo test --test it wraithkin::. Build a World with
StartingBody(Body::Wraithkin),
call initialize_world, then assert the glyph, the dice, the grants, and
that a save/load round trip comes back as the same body. Nothing else in the
suite exercises a body that doesn’t exist yet.
cargo test --test it content:: won’t see this at all — a hand-written body is
not a table row, so none of its checks apply. That absence is the tell that
you’re outside the tables and should be moving carefully, not the sign that
you can skip testing.
Ship it with the docs that describe it
Every one of these said something specific about the lurk when it landed, and will say something wrong about your body if you don’t touch it:
../reference/cli-and-env.md— the-b <body>section spells out each body’s dice, kit and quirks for the player.../reference/constants.md— a row in the modules table for the newconstants::<name>module.../reference/components.md— the “what the player is” section, if the new marker or resource is worth a reader reaching for.MANUAL.md/doc/nihilurk.6— the shipped manual page.
Appendix: quick check
- Add the
Bodyvariant inmodels/src/body.rs, and its arm inBody::name(). - Write
wear_<name>next towear_lurk, with every number in a newconstants::<name>module. - Add the
Body::<Name> => wear_<name>(world, player)arm inwear. - Give it a save marker: an effect already in
EFFECTSthat nothing else puts on the player. - Teach
innate_tempoif the body does not return toSpeedKind::Normal. - Teach
equip_refusalif the body cannot wear everything, and say why in the string. - Add the
("-b", "<name>")arm inengine/src/main.rs. - Leave
brings_a_packreturningfalse. - Copy
models/tests/it/body.rsandlurk.rsinto a new test file named for the body: glyph, dice, grants, save round trip. - Run that test with
cargo test --test <file>, thencargo test. - Fix the docs:
update-the-docs.md, “A playable body”.
See also
../tutorial/add-your-first-body.md the long version, as a lesson
../reference/cli-and-env.md the player-facing story, -b and -am both
add-a-monster.md the other way to play as something else
add-an-effect.md writing the Grants a body is born with
../reference/components.md Body, MonsterBody, StartingBody
../explanation/combat-and-balance.md picking dice that don’t need gear to matter