diff --git a/crates/benilla-app/src/aura_visual.rs b/crates/benilla-app/src/aura_visual.rs index 7f4c01f2..177ace8b 100644 --- a/crates/benilla-app/src/aura_visual.rs +++ b/crates/benilla-app/src/aura_visual.rs @@ -1,101 +1,21 @@ -//! **The aura-state CharProc layer** — what an aura does to the *body itself*: its translucency, its -//! tint and its animation clock, for exactly as long as the aura lives. This is the half of a stage-2 -//! "state kit" that -//! isn't an attach-point emitter (those are `creature_anim::spell_visual`'s -//! [`arm_aura_state_fx`](crate::creature_anim::arm_aura_state_fx), decision 0393): where that -//! watcher hangs *models* on the unit, this one changes how the unit's own model **renders**. +//! The aura-state CharProc layer: what an aura does to its unit's own body (translucency, tint, +//! animation clock) while it lives; the kit's attach-point models are +//! [`arm_aura_state_fx`](crate::creature_anim::arm_aura_state_fx)'s. //! -//! B114 ("Stealth shows nothing on the character") was this whole layer missing. Stealth's state -//! kit carries no effect models and no anim at all — its entire visual is one CharProc — so the -//! effects-only watcher resolved the kit, found an empty slot list, and dropped it on the floor. +//! In the reference an aura slot change (`0x604d00` → `0x6123f0`) reaches `0x5ff350`, which plays +//! the spell's state kit (`SpellVisual` field 4) at stage 2, and the kit's tail (`0x60f35c`) runs +//! the CharProc dispatcher `0x60d7c0` over its four proc slots (`SpellVisualKit` fields 15-34). +//! Three procs act on the body: 14 installs an alpha node (`0x60d972`), 1 a tint node +//! (`0x60d840`), 11 an animation rate (`0x60db7e`). A node is keyed by spell id and linked at the +//! head of its list, only the head counts, and the aura's removal drops its spell's nodes +//! (`0x5ff290`). //! -//! ## The mechanism +//! `UNIT_FIELD_BYTES_1` byte 3's stealth and ghost bits drive no body render in the reference: its +//! readers (`0x5ff80d`, `0x607101`, `0x60f62e`) only suppress nameplates and markers. The body +//! keys on the aura, never on the flag. //! -//! `UNIT_FIELD_AURA` slot change → the aura watcher `0x604d00` → `0x6123f0` → **`0x5ff350` -//! PlayAuraStateVisual**: the slot's spell → `Spell.dbc` → `SpellVisual` → **field 4 = the state -//! kit** → `PlaySpellVisualKit(kit, stage 2)`, whose tail (`0x60f358: push esi(kit); -//! 0x60f35c: call 0x60d7c0`) runs the **CharProc dispatcher** over the kit's four proc slots -//! (`SpellVisualKit` fields 15–34 — `benilla_formats::CharProc`). Two of the dispatcher's nine cases -//! act on the body: -//! -//! - **proc 14 = translucency** (`0x60d972`): the param becomes a node keyed by spell id -//! (`node+0x18`, value at `node+0x78`), linked at the HEAD of the unit's list `unit+0xb50`. The -//! unit's effective alpha is then recomputed by `0x60d180` as **`baseAlpha × the head node's -//! factor`** — at most one node term, never a product over the chain — `baseAlpha` from the -//! vtbl+0x6c getter `0x60d2d0` (`CreatureDisplayInfo+0x14 × 1/255`, the SAME slot in the unit -//! and player vtables: a player's 1.0 is the data talking, not a type fork) — and handed -//! to **`0x614f80` StartAlphaFade(target, 1000 ms)**, the same ramp block (`+0xec..0x100`) and -//! the same `clamp01(t)³` ease (`0x614a90`) our appear-fade already rides -//! ([`benilla_world::model_fade::fade_alpha`]). Per frame `model+0x180 = master × fade` -//! (`0x614baa → 0x710cb0`), which multiplies into the per-batch alpha (`0x707680`). -//! The recompute has a second, aura-free caller: the DISPLAYID watcher's refresh (`0x60abe0 → -//! 0x60ad9f`), which is how a display whose row says `CreatureModelAlpha < 255` — Ghost Wolf's -//! 4613 = 102 — renders translucent by itself; [`refresh_base_alpha`] is that leg. -//! - **proc 1 = tint** (`0x60d840`): `round(param)` is a packed `0x00RRGGBB` OR'd with `0xff000000` -//! into a tint node (list `unit+0xce0`); per frame the head node goes `×1/255` into -//! `model+0x184/188/18c`. -//! -//! Removal is the mirror image: the aura leaves the slot → `0x5ff290` drops that spell's nodes and -//! recomputes, so the alpha **ramps back** to the new target over the same 1000 ms. -//! -//! The vis-flag is a decoy and must not be used here: `UNIT_FIELD_BYTES_1` byte 3's CREEP (stealth) -//! and GHOST bits drive **no** body render at all — an exhaustive census of every `+0x213` reference -//! found three sites, all nameplate/marker suppression (`0x5ff80d`, `0x607101`, `0x60f62e`). The -//! two co-travel only because the server sets the flag and the aura from the same spell. Keying -//! the body off the flag would be the wrong mechanism that happens to look right on stealth and -//! then fails on every other member of the family. -//! -//! ## The shipped data (read from the real 5875 `SpellVisualKit.dbc` this session — -//! `benilla-extract charprocs` censuses it) -//! -//! 185 of 1772 kits carry a CharProc, and the **state** stage is far and away their home: 103 state -//! kits, ~1200 (spell, proc) pairs. Proc 14 covers **11 kits / 135 spells** — kit 312 `0.3` is the -//! whole sneak family (Stealth 1784-1787, Prowl, Vanish, Shadowmeld, Hide, Sneak, Disguise), kit -//! 3450 `0.5` is Invisibility/Fade, kit 989 `0.5` the ghost aura, 3129 `0.65` Shadowform, 1286 `0.3` -//! Banish, 3989 `1.0` Possess, 5129 `0.0` a quest fade-out. Proc 1 covers 73 state kits (~800 -//! spells): Frostbolt's chill blue, Immolate's ember orange, the poison green. -//! -//! ## What is built here, and what is not -//! -//! **Proc 14 ships whole** — nodes, the `baseAlpha × head node` target, the 1000 ms cubic ramp both ways, -//! and the render composition below. -//! -//! **Proc 1 ships too** (decision 0812): its nodes live here ([`AuraNodes::tint`], head-node-wins as -//! the reference's `unit+0xce0` list is) and [`apply_aura_tint`] publishes the head to -//! [`benilla_world::instance_tint`], the per-instance modulate channel keyed on the part's rig slot. Written -//! on change, never eased — the reference's own asymmetry (`unit+0xd04` change-detection straight to -//! `0x710cf0`, with none of the alpha's 1000 ms ramp). -//! -//! 0806 recorded this as blocked on a render-architecture call between per-unit material clones and a -//! tag-indexed palette. That framing was wrong on its own terms: the `MeshTag` rig field already IS a -//! per-instance slot into the shared light buffer, so the tint needed **no** new tag bits (which was -//! the whole stated blocker — the payload has 5 spare bits outdoors and zero indoors) and no material -//! churn. What it did need was reading our own renderer properly. -//! -//! The tint reaches a unit **whole** — the reference's `0x714000` recursion, where an attached model -//! composes onto its parent CM2's computed colours — by two routes, because benilla has two kinds of -//! attached model: -//! -//! - the ordinary one **borrows its wearer's slot**: an item's parts, cards and boneless geosets all -//! spawn carrying the unit's instance bits (decision 0820), so they read the unit's table entry; -//! - a **rigged** one carries its OWN slot, because the vertex stage indexes the skin palette with -//! that same field and cannot borrow it (a spell-effect instance; since 0841, the seven shoulder -//! models that weld geometry to a billboard bone). [`chained_tint`] walks such an instance's -//! [`benilla_world::model_fade::ParentModel`] link up to the unit — the recursion itself, and the same walk -//! `ModelAlphas` already does for alpha. -//! -//! **Proc 11 ships too** (decisions 0889 + 0891): the **freeze**. Its param is a playback *rate* -//! written onto the unit's own animation clocks (`0x60db7e` → `SetBoneAnimSpeed 0x712910` on the -//! mount's bone 0, the body's key-bone 4 — the upper-body split — and the body's bone 0), old rates -//! saved in the node and written back when it expires (`0x6203e0`). Rate 0 is Ice Block: the pose -//! holds exactly where it was, *including* a cast one-shot that had just been armed, which is why the -//! caster never gets to raise their hand. [`apply_aura_anim_rate`] is that leg — and it expresses -//! rate 0 as a **pause**, not as a speed, because the reference's rate belongs to a bone while a speed -//! here belongs to a clip that other code copies from ([`AnimRateFreeze`] carries the story). -//! -//! Types 2, 7 and 13 also appear on state kits (Berserk/Bloodlust's 2, the Sap/Feign-Death 7, the -//! "Glowy (Red)" 13) and have **no verified mechanism** — they fall through [`node_for`]'s single `_` -//! arm, which is where each lands as one match arm the day its mechanism is known. +//! Proc types 2, 7 and 13 also appear on state kits; their mechanism is untraced, so [`node_for`] +//! ignores them. use benilla_protocol::EntityKind; use bevy::mesh::MeshTag; @@ -105,75 +25,51 @@ use benilla_assets::materials::WowModelMaterial; use benilla_world::interior::InteriorLit; use benilla_world::model_fade::{fade_alpha, FadeMaterials, PendingAppearFade, RenderFade}; -/// The reference's aura-alpha ramp: `StartAlphaFade(target, **1000 ms**)` (`0x614f80` from -/// `0x60d180`'s recompute), eased `clamp01(t)³` by `0x614a90` — [`fade_alpha`] is that curve. -/// -/// It is a **code constant, not a kit column**: proc 14's `params[1..]` are `[2.0, 1000.0, 0.0]` on -/// the sneak kit but `[0.0, 0.0, 0.0]` on the ghost's, and the ghost's ramp is byte-cited at the same -/// 1000 ms — so the sneak kit's literal `1000.0` cannot be the source (a data-sourced duration would -/// snap the ghost instantly). Across the whole shipped table proc 14's `params[2]` is only ever -/// `0.0`, `2.0` or `1000.0`, which is no duration scale in any unit. Read from `params[0]` only. +/// The aura-alpha ramp, `StartAlphaFade(target, 1000 ms)` (`0x614f80`) eased `clamp01(t)³` by +/// [`fade_alpha`] (`0x614a90`). A code constant, not a kit column: the ghost kit's proc 14 carries +/// no `1000.0` yet ramps over the same second. pub(crate) const AURA_ALPHA_FADE_SECS: f32 = 1.0; -/// Settled-ramp epsilon — under half a step of the tag's 6-bit alpha field (1/64), so a ramp that -/// has visually arrived is treated as arrived. +/// Under half a step of the tag's 6-bit alpha field (1/64): a ramp this close has arrived. const ALPHA_EPS: f32 = 1.0 / 128.0; -/// One CharProc node the dispatcher installed on a unit, keyed by the aura's spell id exactly as the -/// reference keys `node+0x18`. Held in [`AuraNodes`]; a `Reap` for that spell id removes it. +/// One CharProc node a kit installs on a unit, keyed by spell id as the reference keys `node+0x18`. #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum AuraNode { - /// Proc 14: this aura's alpha factor (`node+0x78`) — the target's node term while it is the head. + /// Proc 14: the alpha factor (`node+0x78`). Alpha(f32), - /// Proc 1: this aura's body tint, unpacked from the param's `0x00RRGGBB`. Modelled, not yet - /// rendered (module docs). + /// Proc 1: the body tint, unpacked from the param's `0x00RRGGBB`. Tint([u8; 3]), - /// Proc 11: this aura's animation playback **rate** for the unit's own model — `0.0` for every - /// member of the freeze family (Ice Block, Freeze, Petrify, Entangle, Stilled). + /// Proc 11: the playback rate of the unit's own clocks, `0.0` for the whole freeze family. AnimRate(f32), } -/// The CharProc node lists of one unit — the ECS twin of the reference's per-unit `unit+0xb50` -/// (alpha) and `unit+0xce0` (tint) lists, plus the ramp `0x614f80` drives from their heads. -/// -/// Lives on the **net entity root**, like [`benilla_world::entity_shade::GroundShade`]: the reference has one -/// of these per CGUnit and every attached model (held item, helm, shoulder) renders off the owner's, -/// which is also why [`apply_aura_alpha`] walks the root's whole descendant tree rather than its -/// direct children. +/// One unit's CharProc node lists (the reference's `unit+0xb50` alpha and `unit+0xce0` tint lists) +/// and its alpha ramp. On the net entity root: the reference keeps one per CGUnit, and every +/// attached model renders off its owner's. #[derive(Component, Debug, Default)] pub(crate) struct AuraNodes { - /// `(spell id, factor)` — proc-14 nodes, newest at the front (the reference links a fresh node - /// at the list head). The HEAD times [`Self::base`] is the target — never a product. + /// Proc-14 `(spell id, factor)` nodes, newest first, as the reference links at the head. alpha: Vec<(u32, f32)>, - /// `(spell id, rgb)` — proc-1 nodes, newest at the front like the alpha list; the head is what - /// the per-frame apply reads. + /// Proc-1 `(spell id, rgb)` nodes, newest first. tint: Vec<(u32, [u8; 3])>, - /// `(spell id, rate)` — proc-11 nodes, head-first like the other two. The head is the rate the - /// unit's animation clocks run at ([`apply_aura_anim_rate`]); empty = the clocks are the - /// driver's own again. + /// Proc-11 `(spell id, rate)` nodes, newest first; the head drives the unit's clocks. rate: Vec<(u32, f32)>, - /// `baseAlpha`: the display row's `CreatureModelAlpha / 255` — for EVERY unit, players - /// included; the getter `0x60d2d0` has no type fork, a normal character's row just says 255. - /// Owned by [`refresh_base_alpha`], which re-resolves it on every display-id change (a - /// shapeshift swaps it live) and ramps to the new value. + /// `baseAlpha`, the display row's `CreatureModelAlpha / 255` for every unit, players included + /// (`0x60d2d0`); kept current by [`refresh_base_alpha`]. base: f32, - /// Where the ramp is now — what the parts actually render at. + /// The ramp's live value, which the parts render at. current: f32, - /// The ramp: `from → to` over [`AURA_ALPHA_FADE_SECS`] from `started` (`Time::elapsed_secs`). + /// The ramp, `from` to `to` over [`AURA_ALPHA_FADE_SECS`] from `started` (elapsed seconds). from: f32, to: f32, started: f32, - /// Whether [`apply_aura_alpha`] still owns this unit's parts. Set while the unit is translucent - /// and for the one frame the ramp latches back at opaque — that frame performs the **release** - /// (writes the settled alpha and hands the material back to the part's light law), the same - /// hand-back protocol the self-avatar feather uses (decision 0213). Without it, a unit whose - /// stealth drops stays latched on the blend twin at its last low alpha until something else - /// happens to rewrite the channel. + /// Whether [`apply_aura_alpha`] owns this unit's parts: while translucent, plus the one frame + /// at opaque that writes the settled alpha and hands the material back to its light law. authoring: bool, } impl AuraNodes { - /// A fresh node set for a unit whose `baseAlpha` is `base`, ramp settled at `base`. fn new(base: f32) -> Self { Self { base, @@ -184,18 +80,15 @@ impl AuraNodes { } } - /// The reference's `0x60d180`: `baseAlpha × the HEAD alpha node's factor` — at most ONE node - /// term, skipped entirely when the list is empty (`0x60d195 je`), never a product over the - /// chain. Nodes are linked at the head as they install, so the newest one is the term that - /// counts. + /// `0x60d180`: `baseAlpha` times the head alpha node's factor alone, never a product over the + /// list; an empty list leaves `baseAlpha` (`0x60d195`). fn target(&self) -> f32 { let head = self.alpha.first().map_or(1.0, |(_, f)| *f); (self.base * head).clamp(0.0, 1.0) } - /// Point the ramp at the current target, from wherever it is now. A no-op when the target - /// hasn't moved, so an aura refresh (or a node whose factor equals the live one) doesn't restart - /// the ease. + /// Aim the ramp at the target from where it is now; an unmoved target leaves it alone, so an + /// aura refresh does not restart the ease. fn retarget(&mut self, now: f32) { let target = self.target(); if (target - self.to).abs() <= f32::EPSILON { @@ -206,34 +99,27 @@ impl AuraNodes { self.started = now; } - /// Advance the ramp to `now` and return the live alpha. fn tick(&mut self, now: f32) -> f32 { let t = (now - self.started) / AURA_ALPHA_FADE_SECS; self.current = fade_alpha(self.from, self.to, t); self.current } - /// Whether the unit renders translucent right now (the ramp has arrived at, or is heading - /// anywhere below, opaque). fn translucent(&self) -> bool { self.current < 1.0 - ALPHA_EPS } - /// The head tint node's RGB — the reference's `unit+0xce0` head, which its per-frame apply reads - /// (`0x60cbd9`). `None` when the unit carries no tint aura. Published to the render channel by - /// [`apply_aura_tint`]. + /// The head tint node's RGB, which the reference's per-frame apply reads (`0x60cbd9`). pub(crate) fn head_tint(&self) -> Option<[u8; 3]> { self.tint.first().map(|(_, rgb)| *rgb) } - /// The head proc-11 node's rate — what this unit's animation clocks are being held at, or `None` - /// when no freeze aura is on it. Head-node-wins like the other two lists. + /// The head proc-11 node's rate, the one the unit's clocks run at. pub(crate) fn head_anim_rate(&self) -> Option { self.rate.first().map(|(_, r)| *r) } - /// A node list holding one proc-11 rate node — the state a freeze aura leaves on a unit, for - /// the driver's wound-refusal tenant (decision 2063) without running the aura-slot watcher. + /// A node set holding one proc-11 node, as a freeze aura leaves it, without the slot watcher. #[cfg(test)] pub(crate) fn with_rate_node_for_tests(spell_id: u32, rate: f32) -> Self { Self { @@ -243,20 +129,9 @@ impl AuraNodes { } } -/// Publish every rig's body tint to the per-instance channel ([`benilla_world::instance_tint`], decision -/// 0812): the head node's RGB packed the reference's way, keyed on the unit's rig slot — the slot -/// each of its skinned parts already carries in its `MeshTag`, and which the fragment stage reads to -/// index the table. -/// -/// **On change, not eased** — the reference's own asymmetry between the two node lists: the alpha -/// gets a 1000 ms cubic ramp (`0x614f80`), the tint goes straight in on change-detection -/// (`unit+0xd04` → `0x710cf0`). `InstanceTints::set` is idempotent, so the steady state costs one -/// compare per rig and never touches the upload. -/// -/// Iterates every **`RigSkin`**, not every `AuraNodes`, deliberately: reasserting the whole table -/// each frame means no edge can leak a stale colour into a recycled slot — not an `AuraNodes` that -/// went away, not a unit whose last tint aura dropped. (The despawn/rebuild edge is covered -/// structurally too, in `RigSkin`'s free hook.) +/// Publish every rig's head tint to [`benilla_world::instance_tint`], keyed on its rig slot. The +/// reference writes the tint as it changes, with none of the alpha's ramp (`unit+0xd04` → +/// `0x710cf0`). Every `RigSkin` is rewritten each frame so a recycled slot never keeps a colour. pub(crate) fn apply_aura_tint( rigs: Query<(Entity, &benilla_world::rig_palette::RigSkin)>, chain: Query<( @@ -282,15 +157,9 @@ pub(crate) fn apply_aura_tint( } } -/// The tint a rig instance renders with: its own head node, else the nearest one **up its -/// [`benilla_world::model_fade::ParentModel`] chain** — the reference's `0x714000` recursion, which composes -/// an attached model's colours onto its parent's computed ones (the same walk [`ModelAlphas`] does -/// for alpha, and the same law `mesh_tag` cites for why an item's parts carry their wearer's slot). -/// -/// It exists because a rigged **item** carries its own instance slot rather than its wearer's -/// (decision 0841 — the vertex stage indexes the palette with that field, so it cannot be borrowed): -/// without the walk, a dwarf's Stoneform tint would stop at exactly the pauldrons the rig was added -/// for. A despawned link ends the walk. +/// The tint a rig renders with: its own head node, else the nearest up its +/// [`benilla_world::model_fade::ParentModel`] chain, as the reference composes an attached model's +/// colours onto its parent's (`0x714000`); only this walk reaches a rigged item's own slot. fn chained_tint( chain: &Query<( Option<&AuraNodes>, @@ -311,16 +180,9 @@ fn chained_tint( None } -/// `WOW_TINT_PROBE=RRGGBB` — paint **every live rig slot** that colour, auras ignored. -/// -/// The one machine check for the GPU half of the channel: the region upload, the fragment stage's -/// slot lookup, the unpack, and the placement of the multiply. No headless test can reach any of -/// that, and the capture harness cannot stage a real aura to do it with — it sees no networked -/// entities at all (0799), so a tinted *unit* is unstageable there. Pointed at a scene with animated -/// (rigged) doodads it turns each of them the probe colour, which the visual harness then diffs. -/// -/// It logs the slot count once, so an unchanged capture reads as "nothing in this scene is rigged" -/// rather than as "the channel is broken" — the two are indistinguishable from the image alone. +/// `WOW_TINT_PROBE=RRGGBB` paints every live rig slot that colour, auras ignored: the machine check +/// for the GPU half of the tint channel. It logs the slot count once, since an unchanged capture +/// cannot tell "nothing is rigged" from "the channel is broken". fn tint_probe() -> Option<[u8; 3]> { static PROBE: std::sync::OnceLock> = std::sync::OnceLock::new(); *PROBE.get_or_init(|| { @@ -335,54 +197,21 @@ fn tint_probe() -> Option<[u8; 3]> { }) } -/// Marker: this rig's clocks are being held by a proc-11 aura, so the release knows to let them go. -/// -/// It carries **nothing**, and that is the fix for the first shipped version's bug. That one stored -/// the speed of every clip it took over and wrote the values back on release — the reference's own -/// `+0x60`/`+0x64`/`+0x68` save/restore, transliterated one level too low. It cannot work here, -/// because in the reference a rate belongs to a *bone* (one value, saved once, and clips armed later -/// never carry one) while ours would have to belong to a *clip*, and clips get armed under the freeze -/// carrying values copied from other, already-frozen clips. `transplant_up` is exactly that: moving a -/// one-shot to the torso overlay copies the source clip's speed, which under the freeze was `0`, so -/// the overlay was saved at `0`, restored to `0`, and never advanced again — a cast pose welded to the -/// upper body for the rest of the session, surviving every later cast (it never *finished*, so the -/// overlay never released). Pausing owns no value and so cannot poison one. +/// Marker: a proc-11 aura holds this rig's clocks. It saves no speeds, unlike the reference's +/// per-bone save and restore (`0x6203e0`): here a rate belongs to a clip, and `transplant_up` arms +/// clips under the freeze with speeds copied from frozen ones, so a saved speed restores as 0. #[derive(Component, Default, Debug)] pub(crate) struct AnimRateFreeze; -/// Hold the clocks of every unit whose head proc-11 node says so — its own rig and its mount's -/// (`[unit+0xd8]` and `[unit+0xdc]`, the two models `0x6201d0` writes) — and let them go when the aura -/// does. +/// Pause the clocks of every unit whose head proc-11 rate is 0, its own rig and its mount's (the +/// two models `0x6201d0` writes), and resume them when the aura goes; effect models and global +/// sequences keep running. The pause is reasserted every frame, since the reference's rate lives +/// on the bone (`+0xb0`, read by the clock at `0x71458e`) and arming leaves it alone +/// (`0x7121a0`), and after the animation driver, so a cast armed this frame freezes too. /// -/// **A rate of 0 is expressed as a PAUSE, not as `set_speed(0.0)`.** They are the same picture on -/// screen (Bevy evaluates a paused animation at its frozen seek, exactly as `0x712910`'s `bias` rebase -/// holds the reference's current frame) but not the same object: a pause is a bit the clock reads, -/// where a speed is a *value* other code copies. `transplant_up` copies a clip's speed when it moves a -/// one-shot to the torso overlay, and that is how the first version welded a cast pose to the upper -/// body permanently — see [`AnimRateFreeze`] and decision 0891. Nothing here reads or writes a speed, so nothing can -/// inherit the freeze and outlive it. -/// -/// **Reasserted every frame, and that is the faithful shape, not a belt-and-braces clamp.** The -/// reference's rate lives on the bone (`+0xb0`, the factor the clock at `0x71458e` multiplies its -/// window by) and **arming an animation does not touch it**: op4 `0x7121a0`'s success leg writes the -/// track index and the cursors and leaves `+0xb0` alone (only its disarm leg and `SetBoneAnimSpeed -/// 0x712910` write it). So "rate 0 until the aura is reaped" is literally the reference's state -/// across every re-arm in between, and re-pausing each frame is how a per-clip flag says the same -/// thing — including for clips armed *under* the freeze, which the reference freezes by construction. -/// -/// It runs **after the driver**, so this frame's arms are already in — which is the whole Ice Block -/// observable: the cast one-shot is armed and then frozen at the frame it was on, so the caster never -/// gets to raise their hand. -/// -/// **A non-zero rate does nothing** (and only kit 1744's `8947848.0` is one — a packed grey that -/// landed in the rate column, on Stoned / Petrification / Thadius Spawn). Our clock is f32 seconds -/// where the reference's is integer milliseconds with a modulo, so a rate that large is noise there -/// and different noise here; there is no honest transliteration, and inventing one would be worse than -/// saying so. Recorded, not special-cased. -/// -/// What it deliberately does **not** touch: attached effect models (the ice block's own -/// `icebarrier_state.mdx` keeps shimmering — the reference writes the unit's two models only) and -/// global sequences (a different clock there, and a different writer here). +/// Deviation: a non-zero rate is ignored, because the only one shipped (`8947848.0` on kit 1744, a +/// packed grey in the rate column) is noise on the reference's integer-ms clock and has no faithful +/// form on our f32-second one. pub(crate) fn apply_aura_anim_rate( units: Query<( Entity, @@ -409,10 +238,7 @@ pub(crate) fn apply_aura_anim_rate( } } (false, true) => { - // Resume everything: the set we paused is "whatever was playing", and it grew - // every frame the freeze held. Nothing else on a unit rig pauses an animation - // (the portrait booth does, on its own rigs), so there is no third party's pause - // to trample. + // Resume every clip: nothing else pauses an animation on a unit rig. for (_, anim) in player.playing_animations_mut() { anim.resume(); } @@ -425,12 +251,8 @@ pub(crate) fn apply_aura_anim_rate( } } -/// The freeze's own instrument (`WOW_MOVE_TRACE`, tag `aur`): one line per rig at each edge, naming -/// the clips it took over and the speed each is *left* running at — the difference between "the clocks -/// stopped" and "nothing was playing anyway", which no count of *absent* anim events can tell apart. -/// -/// It prints the speeds precisely because the first version's bug was invisible without them: a `@0` -/// on the thaw line is a clip that will never advance again. +/// Trace (`WOW_MOVE_TRACE`, tag `aur`) one line per rig at each freeze edge, with each clip's +/// speed: a `@0` on a thaw line is a clip that will never advance again. fn trace_clock_edge(what: &str, rig: Entity, player: &AnimationPlayer) { if !benilla_assets::trace::enabled_for("aur") { return; @@ -445,9 +267,7 @@ fn trace_clock_edge(what: &str, rig: Entity, player: &AnimationPlayer) { ); } -/// One aura's CharProc edge, written by the aura-slot watcher -/// ([`crate::creature_anim::arm_aura_state_fx`], which owns the slot diff and the state-kit resolve -/// for both halves of a kit) and drained by [`drain_aura_procs`]. +/// One aura's CharProc edge, from [`crate::creature_anim::arm_aura_state_fx`]'s slot watch. #[derive(Message, Clone, Debug)] pub(crate) enum AuraProc { /// The aura landed in a slot: install this kit's body procs, keyed by its spell id. @@ -461,16 +281,14 @@ pub(crate) enum AuraProc { Reap { entity: Entity, spell_id: u32 }, } -/// Turn one `SpellVisualKit` CharProc into the node it installs, or `None` for a proc type we have -/// no verified mechanism for (module docs). **The single dispatch point** — the ECS twin of the -/// client's jump table `0x60dbfc`, and the one place a newly-REd proc type gets an arm. +/// The node one `SpellVisualKit` CharProc installs (the dispatcher's jump table `0x60dbfc`), or +/// `None` for a proc type whose mechanism is untraced. pub(crate) fn node_for(proc: benilla_formats::CharProc) -> Option { use benilla_formats::char_proc_type as ty; match proc.ty { ty::ALPHA => Some(AuraNode::Alpha(proc.params[0])), - // `round(param)` → `0x00RRGGBB` (`0x60d8cc` then ORs the opaque byte on). The param is a - // float column holding an integral colour: the ghost's 9222653.0 is 0x8CB9FD, pale - // blue-white; the poison kit's 65280.0 is 0x00FF00. + // A float holding an integral `0x00RRGGBB`, rounded (`0x60d8cc`): the ghost's 9222653.0 + // is 0x8CB9FD. ty::TINT => { let packed = proc.params[0].round().clamp(0.0, 0xff_ffff as f32) as u32; Some(AuraNode::Tint([ @@ -479,22 +297,14 @@ pub(crate) fn node_for(proc: benilla_formats::CharProc) -> Option { packed as u8, ])) } - // `params[0]` is a playback rate, straight through — `0x60db7e` hands it to - // `SetBoneAnimSpeed` unexamined, so the one outlier (kit 1744's `8947848.0`, a packed grey - // that landed in the rate column) is passed through rather than special-cased. + // A playback rate, passed through: `0x60db7e` hands it to `SetBoneAnimSpeed` (`0x712910`) + // unexamined. ty::ANIM_RATE => Some(AuraNode::AnimRate(proc.params[0])), _ => None, } } -/// Apply this frame's [`AuraProc`] edges to the units' node lists, then re-aim every live ramp. -/// -/// `base_alpha` here only SEEDS a node set created by an aura edge — its steady-state owner is -/// [`refresh_base_alpha`], which re-resolves it on every display change. The getter is the same -/// for players and creatures (the display row's `CreatureModelAlpha`, read by `0x60d2d0`). -/// A unit whose display is unknown falls back to `1.0` — opaque, i.e. the aura's factor alone, -/// which is the safe direction (the alternative would hide a unit whose row simply failed to -/// load). +/// Apply this frame's [`AuraProc`] edges to the units' node lists. pub(crate) fn drain_aura_procs( mut edges: MessageReader, time: Res