diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml index 2e366710..a1a330dd 100644 --- a/crates/benilla-app/assets/ui/UiPanels.xml +++ b/crates/benilla-app/assets/ui/UiPanels.xml @@ -2437,16 +2437,36 @@ end function BenillaTabButton_OnUpdate(tab) - local text = getglobal(tab:GetName() .. "Text") + local name = tab:GetName() + local text = getglobal(name .. "Text") local w = (text and text:GetStringWidth()) or 0 if w <= 0 then return end local pad = tab.benillaTabPadding or 0 local room = BenillaTab_Room(tab) if tab.benillaFitWidth == w and tab.benillaFitRoom == room then return end tab.benillaFitWidth, tab.benillaFitRoom = w, room - local fitted = PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth) - if room and fitted > room then - PanelTemplates_TabResize(pad, tab, room) + PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth) + -- THE CLAMP READS THE FIT BACK OFF THE ART, not off a return value and not off the + -- button. `PanelTemplates_TabResize` is the REFERENCE's now (1837 put + -- `UIPanelTemplates.lua` on the chain below this file, so its definition wins) and it + -- returns nothing; our own copy's benilla-only `return tabWidth` had already stopped + -- running, which is what took 1002's clamp off the air silently. `tab:GetWidth()` + -- cannot serve either — it reads the last RESOLVED rect, this frame's stale input + -- inside a settle, which is the whole reason 1002 added that return. + -- + -- `$parentMiddle` can. `PanelTemplates_TabResize` sets it on its way out, a Texture's + -- width is its AUTHORED size, and a region's GetWidth reports the authored value + -- without consulting a resolved rect (region::virtual_span) — so it is the number the + -- verb just computed, same tick. The tab is that plus its two end caps, which is the + -- reference's own `tabWidth = width + sideWidths`. + if room then + local middle = getglobal(name .. "Middle") + local left = getglobal(name .. "Left") + local fitted = (middle and middle:GetWidth() or 0) + + 2 * (left and left:GetWidth() or 0) + if fitted > room then + PanelTemplates_TabResize(pad, tab, room) + end end -- No highlight sizing here on purpose — BOTH tab templates anchor their highlight to -- their own two side edges, so it IS the tab by construction (decision 1004). diff --git a/crates/benilla-app/src/camera_shake.rs b/crates/benilla-app/src/camera_shake.rs index 2ef7fc15..284a28ba 100644 --- a/crates/benilla-app/src/camera_shake.rs +++ b/crates/benilla-app/src/camera_shake.rs @@ -90,11 +90,12 @@ //! //! **Lifetime.** A record holds a *snapshot* position and no reference to its source, so a shake //! fully outlives the creature that spawned it. The whole evaluation is skipped — offset zero, -//! nothing expiring — while the followed unit is **swimming**. +//! nothing expiring — while the **followed unit** (the far-sight subject when there is one, our own +//! body otherwise) is **swimming** or riding a **fly-or-swim spline**: the reference's two suspend +//! gates, both of which jump the same block. //! //! Not modelled, deliberately: the reference's second wholesale free on `SetTarget(nullptr)`, and -//! its destructor's unlink-without-free (a shipped leak — our `Vec` cannot reproduce it). The -//! flying-spline (taxi) arm of the skip is noted in [`CameraShakes::evaluate`]. +//! its destructor's unlink-without-free (a shipped leak — our `Vec` cannot reproduce it). use std::f32::consts::TAU; @@ -103,10 +104,11 @@ use bevy::prelude::*; use benilla_formats::{CameraShake, CameraShakeCatalog, SpellShakeGroup}; use crate::creature_anim::{ - footfall_side, move_flags, AnimSoundEvent, MovementState, SpellKitShake, + footfall_culls, footfall_side, move_flags, AnimSoundEvent, MovementState, SpellKitShake, }; use crate::entities::{BoneAttach, Creatures}; -use crate::net::{Embodied, NetEntity, ObjectStore}; +use crate::net::{Embodied, NetEntity, ObjectStore, Spline}; +use crate::player::ViewSubject; use benilla_assets::{AssetSet, LockRecover, WorldAssets}; use benilla_protocol::EntityKind; use benilla_world::schedule::WorldStage; @@ -119,9 +121,6 @@ const FULL_DISTANCE_SQ: f32 = 81.0; /// The falloff's half-life base and its per-9-yd exponent divisor. const FALLOFF_BASE: f32 = 0.7; const FALLOFF_SPAN: f32 = 9.0; -/// The footstep emitter's own radius, camera→footplant (`0x5fc00a`, `2500 yd²` = 50 yd). **The -/// camera, not the player** — the same correction wow-re applied to `dist2-gate.md` this round. -const EMIT_DISTANCE_SQ: f32 = 2500.0; /// `CameraShakes.Amplitude` is authored in inches; the client scales it at spawn (`0x511d78`). const INCHES_TO_YARDS: f32 = 1.0 / 36.0; @@ -171,13 +170,15 @@ impl CameraShakes { /// Retire what has expired and compose this frame's offset. /// - /// `swimming` is the reference's skip (`0x50ea87`/`0x50ea8b`): while the followed unit swims, - /// the whole block is bypassed — the offset is zero **and nothing expires**, so a shake - /// resumes mid-flight on surfacing. The sibling arm of that skip, a flying (taxi) spline, is - /// not modelled here: we have no flying-spline predicate at the camera seat, and the case is a - /// heavy creature's footfall within 50 yd of a taxi path. Recorded rather than guessed. - fn evaluate(&mut self, eye: Vec3, facing_yaw: f32, now: f32, swimming: bool) -> Vec3 { - if swimming { + /// `suspended` is the reference's skip — **two** gates that both jump the same block + /// (`0x50ea87` / `0x50ea8b` → `0x50eb01`, wow-re `camera-shake-law.md` §6): the followed unit + /// is **swimming** (`[[unit+0x118]+0x40] & 0x200000`), or it is riding a **flying server + /// spline** (`MI = [[unit+0x118]+0xa4]` non-null, not done, Flying bit set) — a taxi flight. + /// The accumulator is zeroed *before* both gates, so a suspended frame yields the offset zero + /// **and expires nothing**: a shake resumes mid-flight on surfacing or landing, rather than + /// having quietly aged out while the camera was not reading it. + fn evaluate(&mut self, eye: Vec3, facing_yaw: f32, now: f32, suspended: bool) -> Vec3 { + if suspended { return Vec3::ZERO; } // Retire on the *unattenuated* clock: distance decides contribution, never lifetime. @@ -386,7 +387,7 @@ fn fire_shakes( // derives it. No marker/joint = the unit origin. let foot = event_point(attach, pose, &joints, &ev.ident) .unwrap_or_else(|| transform.translation()); - if eye.distance_squared(foot) > EMIT_DISTANCE_SQ { + if footfall_culls(eye, foot) { continue; } live.add(*row, foot, now); @@ -461,9 +462,35 @@ fn fire_kit_shakes( } } -/// The applier's read of the followed unit: its facing (the shake's body frame) and whether it is -/// swimming (the reference's skip). -type FollowedUnit = (&'static Transform, Option<&'static MovementState>); +/// The applier's read of the followed unit: its facing (the shake's body frame), its move flags +/// (the swim half of the reference's skip) and its live path (the spline half). +type FollowedUnit = ( + &'static Transform, + Option<&'static MovementState>, + Option<&'static Spline>, +); + +/// Is the whole shake block skipped this frame? The reference's two suspend gates, OR'd — both +/// `jmp` past the same block (`0x50ea87` / `0x50ea8b` → `0x50eb01`), on the **followed unit**. +/// +/// **Swimming** is its move flags (`[[unit+0x118]+0x40] & 0x200000`). +/// +/// **The spline gate** is read through to the LIVE path, exactly as the anim selector's `unify` +/// does ([`crate::creature_anim`], RF-0057 `0x5fd19c`) — the reference re-reads +/// `[[unit+0x118]+0xa4]` every frame rather than trusting a stamped flag, and our stored +/// [`MovementState::flying`] is the selector's own derived value, left `false` on the controller's +/// component. The [`Spline`]'s *presence* is the reference's "descriptor non-null **and** not done" +/// (`sample_splines` removes the component the frame the path completes — the `& 0x4` DONE term), +/// and `!grounded` is spline flag `0x200`. +/// +/// **`0x200` is the shared fly-*or*-swim spline bit, not "taxi"** (wow-re `swim-transition.md`) — +/// so this arm covers a swimming server path as well as a flight, which is the same reason it +/// sits beside a swim gate at all. Naming it "the taxi arm" (as 1540 did, and as this was first +/// built) over-specifies what the bit means. A **ground** spline — Charge, knockback, a fleeing +/// walk, all of which install the same component — does not suspend anything. +fn suspended(mv: Option<&MovementState>, spline: Option<&Spline>) -> bool { + mv.is_some_and(|m| m.flags & move_flags::SWIMMING != 0) || spline.is_some_and(|s| !s.grounded) +} /// Add this frame's shake to the camera's seated eye. /// @@ -476,22 +503,40 @@ type FollowedUnit = (&'static Transform, Option<&'static MovementState>); /// the base pose every frame, so the transform this reads is the un-shaken eye rather than last /// frame's shaken one. A zero offset writes nothing at all, preserving the camera's bit-equality /// no-op gate (decision 1362) — a still camera stays bit-stable and its propagation stays quiet. +/// +/// **The body frame and both suspend gates come from the FOLLOWED unit** — `[cam+0x88/0x8c]` in +/// the reference, never `0x468550` (the active player). Ordinarily they are the same object; under +/// far sight (Mind Vision, Eye of Kilrogg, and possession, which rides the same field) they are +/// not, and the camera orbits the subject while our body walks on somewhere else. Reading our own +/// body there would frame the shake on the wrong facing and suspend it on the wrong unit's swim +/// state. [`ViewSubject`] already resolves exactly this object for the rig. +/// +/// The reference additionally requires the followed object to be **TYPEMASK_UNIT** +/// (`0x50e90d`–`0x50e92c`) before reading its `CMovement` at all. That falls out here: the +/// fallback is our own body, and a resolved far-sight subject with no [`MovementState`] and no +/// [`Spline`] — a GameObject or a DynamicObject — reads as "not suspended", which is what a +/// unit-only gate on a non-unit yields. pub(crate) fn apply_camera_shake( mut live: ResMut, time: Res