diff --git a/crates/benilla-app/src/creature_anim/spell_visual.rs b/crates/benilla-app/src/creature_anim/spell_visual.rs index aab8b0af..880cca21 100644 --- a/crates/benilla-app/src/creature_anim/spell_visual.rs +++ b/crates/benilla-app/src/creature_anim/spell_visual.rs @@ -1229,11 +1229,15 @@ pub(crate) fn arm_aura_state_fx( armed.retain(|e, _| units.contains(*e)); } -/// Arm/reap the **lootable-corpse sparkle** (wow-re `loot-corpse-effect.md`, §5 cross-checked): -/// a DEAD unit carrying `UNIT_DYNFLAG_LOOTABLE` wears the `SpellVisualEffectName` row named +/// Arm/reap the **lootable sparkle** on both things that can wear it (wow-re +/// `loot-corpse-effect.md` + `corpse-decal-and-loot-sparkle.md` §Q2, both §5 cross-checked): +/// a DEAD unit carrying `UNIT_DYNFLAG_LOOTABLE`, **and a corpse object carrying +/// `CORPSE_FIELD_DYNAMIC_FLAGS` bit 0** (decision 1723 — a battleground body's insignia). Each +/// wears the `SpellVisualEffectName` row named /// `"HARDCODED Loot Art"` (5875: `Particles\LootFX.mdl` — a golden flare + three star-twinkle /// emitters; cadence/size/color/blend all authored in the asset, the client sets none of them) -/// attached at `0x13`, its own first sequence looping. The real client is edge-driven off the +/// attached at `0x13` — the unit's looping its own first sequence, the corpse's arming no sequence +/// at all (the divergence is named at the `stage` line below). The real client is edge-driven off the /// descriptor apply (watcher `0x600440`) with **no viewer/tap/distance/loot-window logic** — the /// server already strips the flag per viewer (vmangos's "hide lootable animation for unallowed /// players"). The falling edge (looted empty, rights lost) reaps with no fade (`0x600680`); @@ -1243,9 +1247,12 @@ pub(crate) fn arm_aura_state_fx( pub(super) fn arm_loot_fx( // Dead+lootable is a pure function of store fields — the same `Changed` gate as // `arm_level_up_fx` below and `arm_aura_state_fx` above, with the same one-shot full sweep - // when the DBC resource lands after units already streamed in. - units: Query<(Entity, &ObjectStore)>, - changed: Query<(Entity, &ObjectStore), Changed>, + // when the DBC resource lands after units already streamed in. `NetEntity` splits the two + // predicates: a **unit** answers `UNIT_DYNFLAG_LOOTABLE`, a **corpse object** answers its own + // `CORPSE_FIELD_DYNAMIC_FLAGS` bit 0 (decision 1723) — different fields at different indices, + // and a corpse descriptor has no UNIT block to ask at all. + units: Query<(Entity, &ObjectStore, &crate::net::NetEntity)>, + changed: Query<(Entity, &ObjectStore, &crate::net::NetEntity), Changed>, visuals: Option>, mut fx: MessageWriter, mut armed: Local, @@ -1259,8 +1266,16 @@ pub(super) fn arm_loot_fx( } else { changed.iter().collect::>() }; - for (entity, store) in scan { - let lootable = store.0.unit_is_dead() && store.0.unit_lootable(); + for (entity, store, net) in scan { + // The **corpse object**'s own sparkle (wow-re `corpse-decal-and-loot-sparkle.md` §Q2, §5 + // cross-checked): `0x5d6de0` watches `CORPSE_FIELD_DYNAMIC_FLAGS`, and bit 0 rising calls + // `0x5d6e30` → the same `"HARDCODED Loot Art"` row at the same attachment `0x13`. Same + // asset, same tag — so it shares this body rather than growing a second one. + let lootable = if net.kind == benilla_protocol::EntityKind::Corpse { + store.0.corpse_lootable() + } else { + store.0.unit_is_dead() && store.0.unit_lootable() + }; if lootable && armed.insert(entity) { fx.write(SpellKitFx::Begin { entity, @@ -1272,7 +1287,20 @@ pub(super) fn arm_loot_fx( // loot art is "its own first sequence, LOOPING" — which is precisely what // [`FxStage::Relive`] renders, and unlike a bare loop-flag arm it holds even if the // art model's sequence were clamp-flagged. - stage: FxStage::Relive, + // **The one place the two paths differ, and it is a real divergence.** The unit's + // loot art is an `Effect_C` node with a clip-end re-arm loop — "its own first + // sequence, LOOPING", which is what `Relive` renders. The corpse's is a **bare + // model instance**: `0x5d6e30` arms no sequence and registers no callback at all + // (`[model+0x80]` stays `-1`), so the `.m2`'s emitters are the entire look and + // nothing re-arms them. `OneShot` + `persistent` is the nearest shape this + // plumbing expresses — no watcher, no re-arm, and it lives until the flag falls. + // Named rather than smoothed over: if a lootable bone pile is ever seen sparkling + // on a loop where the reference's plays out and stops, this is the line. + stage: if net.kind == benilla_protocol::EntityKind::Corpse { + FxStage::OneShot + } else { + FxStage::Relive + }, effects: vec![(HARDCODED_FX_ATTACH, path.to_string())], }); } else if !lootable && armed.remove(&entity) { diff --git a/crates/benilla-app/src/player.rs b/crates/benilla-app/src/player.rs index f749d292..b5aa5514 100644 --- a/crates/benilla-app/src/player.rs +++ b/crates/benilla-app/src/player.rs @@ -18,6 +18,15 @@ //! the camera is ([`land`]). //! //! Movement is a thin kinematic capsule controller over avian's `MoveAndSlide` (decision 0009). +//! +//! **This file is the map, not the machine.** It holds the plugin and its ordering edges, the +//! shared types the concern modules trade in ([`Player`], [`BodyQuery`], [`TransportQuery`], +//! [`StandStateRequest`]), and the two `UNIT_FIELD_FLAGS` bits with more than one reader. The +//! per-frame system itself is [`controller::control`], and every phase it runs that is a concern +//! of its own has a file beside it: [`input`] decodes the keys, [`wire_in`] applies what the +//! server said, [`ride`] carries us on a deck, [`posture`] holds the body, [`mover`]/[`swim`] +//! move it, [`flags`] builds the move-flag word, [`gait`] + [`body_pose`] render it, [`camera`] +//! seats the eye, [`movement_net`] streams it. use bevy::input::mouse::AccumulatedMouseMotion; use bevy::prelude::*; @@ -33,7 +42,11 @@ use benilla_world::interact::{WorldClick, WorldRightClick, WorldRightPress}; use benilla_world::schedule::WorldStage; mod arc; +// Writing the frame onto the body we drive — pose, MovementState, the counter-twist gap. +mod body_pose; pub(crate) mod camera; +// The per-frame controller itself — the one system, split out so the root stays the map. +mod controller; mod world_focus; // The remembered camera pose (decision 1131) — it lives inside `player/` so it can read the rig's // own `pub(super)` fields instead of widening them for a module outside. @@ -41,14 +54,22 @@ mod camera_saved; mod drunk; // Which single unit the client embodies (decision 1277) — the `Embodied` marker's owner. mod embody; +// This frame's move-flag word — the one bitset the animation, the wire and the local gates all +// read (decision 0056). +mod flags; mod follow; mod gait; +// This frame's decoded input — the netted movement axes and the camera's command word +// (decisions 0056, 1502). Everything downstream reads the axes, never the keys. +mod input; // The land-here affordance (free-fly's other half). `pub(crate)` for its `LandHere` message, which // the debug panel's button writes. pub(crate) mod land; mod move_trace; mod movement_net; +// The stand state + the sheath toggle — how the body HOLDS itself (decisions 0080, 0881). +mod posture; // The kinematic mover step. `pub(crate)` because the grounded walk resolve is **not** the local // player's alone: a remote mover's dead-reckon (`crate::net::motion::remote`) runs its extrapolated // step through the very same code, the way the reference runs every mover through one controller @@ -56,6 +77,8 @@ mod movement_net; pub(crate) mod mover; /// The spyglass zoom — aura 76, a client-local camera override with no wire half at all (B151). mod scoped_view; +// Riding a transport — the platform-frame carry/attach (decisions 0438, 0470). +mod ride; mod server_ride; mod setup; mod state; @@ -71,11 +94,14 @@ mod wire_in; // render-alpha field, so the unit-lane material-alpha compose (`entities::apply_unit_mat_alpha`) // orders itself before it and lets that documented override stand. pub(crate) use camera::apply_self_model_fade; +// The controller system, named unqualified here so the plugin's ordering edges (and the ones +// sibling modules declare against it) read as they always have. +use controller::control; // `/follow` (decision 0890): chat asks with the message, `crate::target` resolves the subject into // the state, and this module owns the motion. use camera::{ - apply_zoom_scroll, model_pivot_height, run_look_session, seat_camera, CameraProbe, FlyCam, - LookButton, CAM_COLLISION_RADIUS, CAM_DIST_DEFAULT, + apply_zoom_scroll, model_pivot_height, run_look_session, CameraProbe, FlyCam, LookButton, + CAM_COLLISION_RADIUS, CAM_DIST_DEFAULT, }; pub(crate) use camera::{head_height, CameraControl, CameraPivot}; pub(crate) use follow::{FollowRequest, FollowState}; @@ -133,10 +159,10 @@ pub(crate) const UNIT_FLAG_IN_COMBAT: u32 = 0x0008_0000; /// Ask for a **stand state** — the client's `SetStandState(newState)` (`0x5ed430`: send /// `CMSG_STANDSTATECHANGE` + apply locally through `0x6127b0`), as a message so every path that -/// wants a posture funnels through the ONE setter in [`control`] (the [`crate::creature_anim:: +/// wants a posture funnels through the ONE setter in [`posture`] (the [`crate::creature_anim:: /// SheathRequest`] posture, decision 0080). /// -/// Two senders today: the `X` key reads the toggle inline in [`control`], and the **posture emotes** +/// Two senders today: the `X` key reads the toggle inline in [`posture`], and the **posture emotes** /// (`/sit`, `/sleep`, `/kneel`, `/stand`, `/lay`) send this — `DoEmote`'s `EmoteSpecProc == 1` /// branch calls the same `0x5ed430` the key does (wow-re `object-layer/scratch/emote-posture- /// gate.md` §1, decision 0881). Routing them here is what makes `/sit` sit at all: the *server* @@ -149,6 +175,62 @@ pub(crate) struct StandStateRequest { pub(crate) state: u8, } +/// **The body in our hands** — normally our own streamed avatar, and a possessed creature while we +/// hold its reins (decision 1277). The controller reads its server pose to take control, then +/// drives its transform (feet position + facing) and feeds its movement to the animation selector +/// via `MovementState`. Its model is attached by the entity renderer through the same path as any +/// other unit (0041), which is exactly why a creature needs nothing special: everything downstream +/// reads the body's own pivot, speeds, scale and descriptor off the entity. +/// +/// An alias because three modules take it — [`control`], [`posture`] (read-only, for the sheath +/// toggle's guard chain) and [`body_pose`] (the frame's one writer) — and one eleven-column tuple +/// written out three times is how a query silently drifts from its readers. +pub(super) type BodyQuery<'w, 's> = Query< + 'w, + 's, + ( + Entity, + &'static mut Transform, + Option<&'static mut MovementState>, + Option<&'static CameraPivot>, + Option<&'static crate::creature_anim::AnimDriver>, + Option<&'static crate::net::ObjectStore>, + Has, + Option<&'static crate::net::UnitSpeeds>, + Option<&'static mut BodyTwist>, + // What is worn in each hand and in the ranged slot — the Z toggle's cycle reads it + // (the ref's three `GetWeapon(0/1/2)` calls before the state walk). + Option<&'static crate::creature_anim::Wielded>, + // The **raw** `OBJECT_FIELD_SCALE_X`, for the camera pivot's target height — not the + // transform's, which is the 2 s-eased render scale (see [`camera::head_height`]). + Option<&'static crate::net::NetEntity>, + ), + (With, Without), +>; + +/// The armed transports — the platform-frame carry/attach ([`ride`], decision 0438 phase 2). The +/// `Without`s only disjoint the borrows against the body and the camera. +/// +/// `ColliderAabb` rides along for the **ride trace only**, and it is the column that separates +/// "the deck moved" from "the deck's collider moved": avian refreshes it in `PhysicsSchedule` +/// (i.e. `FixedPostUpdate`, *before* `Update`), while `tick_transports` writes the deck's pose *in* +/// `Update` — so the box the broad phase prunes against is always one frame behind the deck it +/// belongs to, and the down-probe's candidate set is whatever that stale box still overlaps. +pub(super) type TransportQuery<'w, 's> = Query< + 'w, + 's, + ( + &'static Transform, + &'static crate::net::Guid, + Option<&'static avian3d::prelude::ColliderAabb>, + ), + ( + With, + Without, + Without, + ), +>; + /// The player controller's **ordering handle**. Anything that must write the aim or the camera rig /// before the controller reads them orders against this — the two scripted probe drivers do /// (`capture::probe_look` / `capture::probe_cam`, decision 1174). A set rather than the `control` @@ -354,1740 +436,3 @@ fn mirror_mover_collision_height( } } } - -/// The camera pivot's **target** height for a driven body: its model-local [`CameraPivot`] × the -/// body's raw `OBJECT_FIELD_SCALE_X`, clamped — or `None` before its model has attached. -/// -/// `None` is the load-bearing half. The reference recomputes the pivot preset only on a model event -/// and *skips the camera update entirely* while the model is unresolved (`0x50e907`), so a -/// display swap reads as a brief hold and then one glide. Aiming the channel at a placeholder height -/// during those frames instead would send the camera on a round trip nobody asked for. -/// -/// The **raw** scale (not the transform's eased one) is the reference's own input — see -/// [`camera::head_height`] for the byte citation and why the distinction is visible. -fn body_pivot_target( - pivot: Option<&CameraPivot>, - net: Option<&crate::net::NetEntity>, -) -> Option { - pivot.map(|p| model_pivot_height(p, net.map_or(1.0, |n| n.scale))) -} - -/// Camera + avatar controller. Free-flies until the server reports our position; then takes -/// third-person control (WASD walks the avatar; right-drag turns it, left-drag orbits the camera, -/// wheel zooms) and streams our movement to the server as the confirmed mover. The dev chord + `F` -/// toggles free-fly (decision 1043). -#[allow(clippy::too_many_arguments, clippy::type_complexity)] -fn control( - time: Res