SlagSaint
The mind of the thing in the basin. Attach it to `boss_root`; the scene's `BattleBoot` does that automatically the moment this component exists.
SlagSaint
The mind of the thing in the basin. Attach it to boss_root; the scene's
BattleBoot does that automatically the moment this component exists.
The saint takes its turn. It announces every blow at least eight tenths of a second before it throws it, it tracks the Emberward, its poise breaks when it is punished hard enough, and it thanks her when it dies.
The cycle
idle ──▶ tell ──▶ strike ──▶ recover ──▶ idle
▲ │
└──────── stagger ◀── poise 0 ───────────┘
│
shift ◀── health crosses 62% / 24%
│
dead ◀── health 0
- idle — waits out this phase's
attackInterval, turns toward the player, and from phase 2 walks in when she is further away than 7.5 metres. It does not walk in phase 1: it is still bolted to the basin. - tell — the wind-up. Never shorter than 0.8s, whatever the phase. The
threatened area is drawn on the floor (see
boss/telegraph), the saint coils back and leans away, and the core in its chest swells. - strike — the release. Raises
boss:strike, lunges, flashes the tell.double_stomplands twice. - recover — the punish window, 0.7 to 1.45 seconds depending on how big the blow was.
- stagger — 5.5 seconds with the core exposed. The Quench lands here.
- shift — 2.5 seconds of phase transition; interrupts anything.
Interrupts run in priority order: death beats a phase change beats a stagger. A parry interrupts a wind-up outright.
What it raises
boss:telegraph boss:strike boss:poise boss:stagger:begin
boss:stagger:end boss:defeated battle:victory battle:defeat
camera:beat hit shake hitstop ui:prompt vo sfx, and
phase:changed through Rules.setPhase.
boss:strike carries more than the contract's { move, origin } — it also
carries damage, range, arc, phase and inside (whether the player was
in the threatened area on the frame it landed), so the combat slice can
resolve the hit without re-deriving the geometry.
What it consumes
boss:damaged is the damage channel. If the payload carries health, that
number is adopted; otherwise amount is subtracted. Either way the hit wears
down Rules.state.boss.damage — the named part takes the brunt and every
part takes a little, so by the end the saint is visibly a wreck. Send poise
explicitly to control the meter; without it the poise cost is inferred from
how big the hit was.
player:parry rings the bell: 30 poise, damage on headBell, and it
interrupts whatever the saint was winding up.
player:died and mode:changed do the obvious things. Returning the mode to
title or intro resets the fight to full health in phase 1, which is what a
retry needs.
State it writes
Rules.state.boss.health .poise .staggered .coreExposed .telegraphing
.move .damage, plus one key this slice adds:
Rules.state.boss.stance—"kneeling","standing","unshuttered"or"dead". The art slice can read it instead of deriving the pose from the phase index.
Fields
| field | default | what it does |
|---|---|---|
showTells | true | draw the threatened area during a wind-up |
aggression | 1.0 | multiplies the gap between attacks; above 1 is slower |
autoStartSeconds | 16 | if no staging slice exists, start the fight anyway after this long |
verbose | false | log each decision |
Methods
local saint = entity.find("boss_root").component.get("SlagSaint")
saint:status() -- everything about the fight, including `read`
saint:moves() -- every move available now, with its tell sentence
saint:hurt(24, "legs") -- deal damage the way combat would, events and all
saint:ring() -- a perfect parry, without hands on a keyboard
saint:quench() -- the critical; only lands while the core is exposed
saint:cast("bell_toll") -- force the next wind-up
saint:restart() -- full health, phase 1, damage cleared
status().read is the sentence a player should be able to say out loud during
the current wind-up. If it does not match what you can see happening, the
telegraph is broken.
Standing alone
The saint is playable without its neighbours. If the Emberward component is
not in the world it resolves its own blows against the player, and if the
Director is not in the world it starts the fight itself after
autoStartSeconds. Both checks are by existence, so neither fires once the
real slice lands.
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
conforms to
zero/source-extract/v2SlagSaint — the mind of the thing in the basin. It is fifteen and a half metres of scorched bronze that has knelt in this room for a hundred years waiting for someone to come and put it out. It is not angry. It takes its turn, it announces every blow before it throws it, and when the Emberward finally quenches its core it thanks her. This component is that behaviour and nothing else. The saint's BODY is the `hero-boss` slice (`SaintBody`, on the same entity); the arena, the screens, the score and the camera are other slices again. Everything crosses the `game/rules` event bus, so this file reaches into none of them. What it owns * the nine-move set from `Rules.MOVES`, chosen by phase and by range * the telegraph -> strike -> recovery cycle, wind-up never under 0.8s * target tracking, and the walk it earns when the chains come off * poise, its regeneration, and the 5.5s stagger the Quench opens * the two phase transitions, at 62% and 24% health * damage accumulating into `Rules.state.boss.damage`, so the wreck shows * death What it raises: `boss:telegraph`, `boss:strike`, `boss:poise`, `boss:stagger:begin` / `:end`, `boss:defeated`, `battle:victory`, `battle:defeat`, `phase:changed` (through `Rules.setPhase`), `camera:beat`, `hit`, `shake`, `hitstop`, `ui:prompt`, `vo` and `sfx`. What it consumes: `boss:damaged` (combat's damage channel), `player:parry`, `player:died` and `mode:changed`.
freshState( ) → any
say(msg: string) → void
| arg | type | description |
|---|---|---|
| msg | string |
componentPresent(name: string) → boolean
Geometry helpers Is a component type loaded in this engine? This has to ask the REGISTRY. `asset.exists` reads the authored store on disk, and a sibling slice being written during a live session has its files in the session's play shadow rather than on disk — so `asset.exists` says "no" about a component that is loaded, attached and running right now. Answering the question that way made this component resolve its own blows against the player while the combat slice was already resolving them, and the player died on every exchange. `asset.tryResolve` would answer correctly but takes a handle, which pins a content dependency on a component another slice owns. Scanning the list asks the same question and pins nothing. Asked once per strike and, before the fight starts, once a second — never per frame — so the scan does not need a cache.
| arg | type | description |
|---|---|---|
| name | string |
wrapAngle(a: number) → number
| arg | type | description |
|---|---|---|
| a | number |
yawTo(fx: number, fz: number, tx: number, tz: number) → number
Yaw that points the saint's visual forward (+Z, the convention the scene was measured against) from `(fx,fz)` at `(tx,tz)`.
| arg | type | description |
|---|---|---|
| fx | number | |
| fz | number | |
| tx | number | |
| tz | number |
facing(yaw: number) → void
| arg | type | description |
|---|---|---|
| yaw | number |
poseQuat(yaw: number, pitchDeg: number) → void
yaw * pitch, as a quaternion. Yaw turns the saint at the player; the pitch is the coil and the punch, which is what makes a blow read as thrown rather than teleported.
| arg | type | description |
|---|---|---|
| yaw | number | |
| pitchDeg | number |
playerPos( ) → any
inferPoise(amount: number) → number
Damage Poise damage for a hit whose payload did not name one. Combat is free to send `poise` explicitly; this keeps the meter honest when it does not.
| arg | type | description |
|---|---|---|
| amount | number |
accumulate(part: string?, amount: number) → void
Wear the body down. The named part takes the brunt and everything takes a little, so by the time the saint dies every piece of it is a wreck — which is the promise `Rules.state.boss.damage` makes to the art slice.
| arg | type | description |
|---|---|---|
| part | string? | |
| amount | number |
floorDamageTo(minimum: number) → void
| arg | type | description |
|---|---|---|
| minimum | number |
clearTell( ) → void
enterIdle(delay: number?) → void
| arg | type | description |
|---|---|---|
| delay | number? |
beginTell(move: string) → boolean
| arg | type | description |
|---|---|---|
| move | string |
enterTell( ) → void
Pick the next attack and start winding it up.
landStrike( ) → void
enterStrike( ) → void
enterRecover( ) → void
enterStagger( ) → void
leaveStagger( ) → void
enterShift(target: number) → void
| arg | type | description |
|---|---|---|
| target | number |
enterDeath( ) → void
onBossDamaged(p: any) → void
Damage intake
| arg | type | description |
|---|---|---|
| p | any |
onParry(p: any) → void
| arg | type | description |
|---|---|---|
| p | any |
onPlayerDied( ) → void
resetFight( ) → void
onModeChanged(p: any) → void
| arg | type | description |
|---|---|---|
| p | any |
unsubscribeAll( ) → void
Lifecycle Drop every handler a previous generation of this component registered. The tokens are held in `Rules.state`, not in a module local. A module local is reset every time this file is re-executed, and anything that re-executes the chunk without first running the previous life's `onDestroy` would leave that life's closures subscribed to a bus this one can no longer reach — handlers stacking silently, one extra copy of every hit. `Rules.state` belongs to a module this slice never edits, so the list outlives any number of reloads and `awake` can always find what to unsubscribe.
awake( ) → void
onDestroy( ) → void
ensureRig(dt: number) → void
The frame
| arg | type | description |
|---|---|---|
| dt | number |
applyPose( ) → void
trackTarget(dt: number) → void
| arg | type | description |
|---|---|---|
| dt | number |
walk(dt: number, distance: number) → void
The saint only walks once the chains are off. In phase 1 it is bolted to the basin, which is why phase 2 feels like the floor moved.
| arg | type | description |
|---|---|---|
| dt | number | |
| distance | number |
regenPoise(dt: number) → void
| arg | type | description |
|---|---|---|
| dt | number |
checkInterrupts( ) → boolean
Interrupts, in priority order: death beats a phase change beats a stagger.
runState(dt: number) → void
| arg | type | description |
|---|---|---|
| dt | number |
maybeSelfStart(dt: number) → void
| arg | type | description |
|---|---|---|
| dt | number |
update(dt: number) → void
| arg | type | description |
|---|---|---|
| dt | number |
status( ) → void
Methods — how the rest of the run drives and reads the saint Everything worth knowing about the fight in one table. `read` is the sentence a player should be able to say during the current wind-up.
hurt(amount: number, part: string?, poise: number?) → void
Apply damage to the saint the way combat would, and raise `boss:damaged` so every other slice sees it. This is the damage channel the fight uses when it is being driven from a test rather than from a player's hands.
| arg | type | description |
|---|---|---|
| amount | number | |
| part | string? | |
| poise | number? |
ring( ) → void
Break the poise outright — the parry the fight is built around, without needing hands on a keyboard.
quench( ) → boolean
The Quench. Only lands while the core is exposed; that is the whole point of the stagger window.
cast(move: string) → boolean
Force the next move, skipping the wait. Names come from `Rules.MOVES`.
| arg | type | description |
|---|---|---|
| move | string |
restart( ) → void
Put the saint back at full health in phase 1, for a retry that does not reload the world.
moves( ) → void
Every move available right now, with the sentence its wind-up should say.
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Showing up to 3 levels deep. Open a part to see what it holds further down.
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
agent_score is exposed.+ quality × 0.35
+ performance × 0.25
± compat factor
Usability ratings
Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".
Scoped to this part · feeds back into the world's score.