UnitBodies
Every body standing on the board — the four gardener-monks and the Blight's crystalline forms. It builds them, moves them, and gives each one a life of its own: a distinct idle, an attack with anticipation and follow-through, cloth that lags the motion, and a death that topples.
UnitBodies
Every body standing on the board — the four gardener-monks and the Blight's crystalline forms. It builds them, moves them, and gives each one a life of its own: a distinct idle, an attack with anticipation and follow-through, cloth that lags the motion, and a death that topples.
It changes no game state. Every clock in it drives a transform.
Where it lives
On MonkHost, a child of the Units anchor. If it is not there — a fresh
engine, a scene reload — put it back with one line:
local host = entity.find("MonkHost")
or entity.spawn("MonkHost", { parent = entity.find("Units") })
if not host.component.get("UnitBodies") then
host.component.add("UnitBodies", {})
end
Each unit's body is a child of MonkHost named unit_<unitId>, and every part
of it is named unit_<unitId>_<part> (_hips, _torso, _head, _armL,
_armR, _tool, _chain, _ember, _pack, _core, _halo).
Fields
| Field | Meaning |
|---|---|
demo | stand a demo squad when no Director has published units (default true) |
stepTime | seconds a body takes to walk one tile (default 0.30) |
attackTime | seconds an attack takes, anticipation to recovery (default 0.95) |
Calling it
Get it, then call with a colon:
local host = entity.find("MonkHost").component.get("UnitBodies")
| Call | Does |
|---|---|
host:spawnUnit(unitId, kind, x, y, facing) | build a body — kind is a monk id or a blight form id |
host:despawnUnit(unitId) | take it off the board |
host:placeUnit(unitId, x, y, facing?) | put it on a tile with no animation |
host:moveUnit(unitId, path) | walk a committed path; returns the seconds it will take |
host:attack(unitId, tx?, ty?) | play the attack, turning to face the target first |
host:hurt(unitId) | flinch |
host:kill(unitId) | topple, and stay toppled |
host:setFacing(unitId, facing) | turn on the spot; the hood arrives after |
host:bodyOf(unitId) | the root entity — what a camera follows |
host:busy() | is anything mid-move or mid-attack |
host:list() | every unit id that has a body |
host:rebuild() | flush the cached geometry and rebuild every body |
path is an array of { x, y } tiles. facing is "n", "e", "s" or
"w"; "n" is up the board, away from the camera, toward the blight.
It follows the referee on its own
Nothing has to tell it what is on the board. Three times a second it reads the
Director component's snapshot() and mirrors snapshot.units: it builds a
body for a unit that has none, drops a body whose unit has gone, moves one whose
tile has changed, and topples one whose alive is false. The unit's kind
picks the model; a kind it does not recognise falls back to a monk or a blight
shard by side, so an unfamiliar unit is still visible rather than invisible.
It also listens on the shared bus, so these arrive for free:
| Signal | What it plays |
|---|---|
unitMoved | walks the path (or straight to to) |
unitActed with action = "attack" / "overwatchFired" / "signature" | the attack |
unitDamaged | the flinch |
unitDied | the topple |
Until a Director publishes any units it stands a demo squad — the four monks and one of each blight form — so the slice is provable on its own. The moment a snapshot carries units, the demo squad is removed.
What the animation is made of
- A follower, not a keyframe. Each body keeps a smoothed copy of its own position and yaw. Everything that lags — hood, sleeves, censer chain, the roll through the hips — is driven by the GAP between where the body is and where the follower has got to. One piece of maths covers a turn, a walk, and the recoil off an attack, and cloth settles instead of stopping dead.
- Idles that are not the same sine wave. Anselm breathes slowly and scans; Meridia is quick and always working her apron; Cassian sways under the pack and rides a pressure surge through it; Vespera barely moves while the censer swings a cone on its chain and the ember breathes. The blight does not breathe at all — the shard ticks and snaps, the spire's halo turns, the hulk heaves.
- An attack in four beats — anticipation away from the target, a strike that passes neutral, a follow-through that decays, then recovery. Each monk's own weight is in it: Anselm swings the whole body through the arc, Meridia's sling loops back and flicks, Cassian leans his mass in, Vespera's censer overshoots her on the chain.
editorUpdateas well asupdate, because a still board of monks reads as a screenshot of a broken game.
Two things to know
executegets its own require registry, soCore.emitfrom anexecutecall does not reach this component's signal bus. Drive it with the methods above when you are testing fromexecute; the signals work between components.- The geometry is cached per model, so editing
monkbodyorblightformneedshost:rebuild()to be seen.
Scoped to this part · feeds back into the world's score.