Files
benilla-octo/crates/benilla-app/src/player.rs
T
SamandClaude Opus 5.5 ef0fcc327c a Vanish clears your target at once, and a painted road covers the grass
- a Vanish, a stealth or a unit leaving range clears your target at once, not two seconds later, and TAB reaches a mob behind you (2345)
- a fully painted terrain layer covers fully: roads and rock no longer let about 6% of the grass underneath show through (2345)
- companion pets show their names, the saved camera pitch survives a login, and a missed resurrect offer no longer blocks later ones (2345)
- ESC on a quest giver closes it, browsing the auction house by category searches the right items, and abandon removes the quest you chose (2345)
- a profession window open at logout no longer reopens for the next character (2345)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 12:22:10 +07:00

523 lines
30 KiB
Rust

//! Avatar + camera + input. Free-flies until the server reports our position, then takes third-person
//! control of the avatar (WASD walks it, height-following the terrain) and streams our movement to the
//! server as the confirmed mover. Owns the camera entity. The cursor itself is owned by the
//! [`crate::cursor`] subsystem; this module only hides it during mouselook (`CursorOptions.visible`).
//!
//! Mouse control mirrors vanilla's two look modes (grounded in the WoW 1.12 camera CVars / mouselook
//! API): **right-drag turns the character** (movement then
//! follows the camera heading), **left-drag orbits the camera** around a stationary character; either
//! locks + hides the cursor and restores it on release. **Both buttons held together run the character
//! forward** (vanilla's "both-button move"), steering with the mouse like a right-drag. The **scroll
//! wheel** zooms the third-person
//! distance (clamped to the vanilla `cameraDistanceMax` range; the camera *glides* to the new
//! distance). A left-drag orbit offset is reeled back in by the **auto-follow** — vanilla's
//! `cameraSmoothStyle`, a real player setting since decision 1493 ([`camera::FollowStyle`]):
//! Smart (the shipped default) chases while the character moves, Always chases always, Never
//! leaves the camera exactly where the hand put it.
//! The **dev chord + `F`** toggles free-fly (1043); the **dev chord + `G`** lands the avatar where
//! 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::*;
use bevy::window::{CursorOptions, PrimaryWindow};
use crate::creature_anim::{move_flags, wrap_pi, BodyTwist, MovementState};
use crate::net::{ClientCommand, Embodied, NetCommands, TeleportMessage, WorldportMessage};
use crate::ui_script::InspectMode;
use benilla_assets::coords::wow_to_bevy;
use benilla_assets::AssetSet;
use benilla_world::interact::{WorldClick, WorldRightClick, WorldRightPress};
use benilla_world::schedule::WorldStage;
mod arc;
mod net;
// Writing the frame onto the body we drive — pose, MovementState, the counter-twist gap.
mod body_pose;
pub(crate) mod camera;
// The one smoothed-scalar channel the reference instantiates four times (wow-re
// `camera-cvar-gates.md` §8) — pitch, pitch-bias, ground tilt and the pivot height, one template.
mod camera_channel;
// The four 1.12 camera option toggles and the mechanisms behind them (decision 2149).
pub(crate) mod camera_dynamics;
// 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.
mod camera_saved;
mod camera_water;
// The five NAMED camera poses the player can jump between (decision 1745) — `camera_saved`'s
// complement: that one remembers where you left the camera, this one where you decided it should
// be able to go. Same reason for living inside `player/`: it writes the rig's `pub(super)` fields.
pub(crate) mod camera_view;
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
// (decision 0059's byte trail).
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;
/// The step-up diagnostic probe — the blocked-frame report behind the `stup` trace tag.
pub(crate) mod step_probe;
mod swim;
/// What the camera orbits, when that is not our own body — the far-sight anchor (B151, and Mind
/// Control's camera half in B211, which rides the same field).
mod view_subject;
/// The walk/run gait toggle (`TOGGLERUN`) — one latched bit and the reference's refusal chain.
mod walk;
mod wire_in;
// `apply_self_model_fade` is `pub(crate)`-visible: it is the LAST writer of a self body part's
// 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, FlyCam, LookButton, CAM_DIST_DEFAULT,
};
pub(crate) use camera::{head_height, CameraControl, CameraPivot};
pub(crate) use follow::{FollowRequest, FollowState};
// The shared avatar state + movement constants live in [`state`]; the private re-imports below are
// what lets this module and the concern modules beside it keep naming them `super::X` unchanged.
use state::{
MoveSpeed, PlayerRide, AIR_NUDGE_SPEED, FALL_FAR_DROP, FALL_FAR_TIME, FOOT_CONE_HEIGHT,
GROUND_COS, GROUND_PROBE, JUMP_SPEED, LAND_PROBE, MOUSELOOK_PITCH_CLAMP, RUN_BACK_RATIO,
SKIN_WIDTH, STATIONARY_CHASE_RATE, STEP_SLOPE_RATIO, STEP_SNAP_SLACK, STEP_UP_ADVANCE_PER_YARD,
TURN_RATE, TURN_RATE_MOVING, WALK_RATIO, WATER_WALK_PITCH_FLOOR, WEDGE_MIN_FALL,
WEDGE_STALL_RATIO, WEDGE_STILL_FRAMES,
};
// `SETTLE_TIMEOUT` is `pub(crate)`: the settle release lives in the terrain streamer (decision
// 0737 — residency releases the hold, not ground contact), which owns the deadline push while the
// resident world is still the departed map's (0710).
pub(crate) use state::{
Player, PlayerCapsule, CAPSULE_HEIGHT, CAPSULE_RADIUS, CREATURE_STEP_UP_HEIGHT,
DEFAULT_COLLISION_HEIGHT, FEATHER_TERMINAL_VELOCITY, GRAVITY, HOVER_CLIMB_RATE, HOVER_HEIGHT,
SETTLE_TIMEOUT, STEP_UP_HEIGHT, TERMINAL_VELOCITY,
};
/// The swim boundary `0.75·h` — and therefore the **wade ceiling**, since wading is the implicit
/// in-liquid-but-not-swimming state and the two cannot be different numbers. Read by the creature
/// swim marker and the footstep splash slot, which have no `Player` of their own.
pub(crate) use swim::{may_swim, swim_enter_depth};
/// The camera's followed unit when that is not our own body — read by [`crate::camera_shake`] for
/// the shake's body frame and its two suspend gates, which the reference takes off `[cam+0x88]`.
pub(crate) use view_subject::ViewSubject;
/// **`UNIT_FLAG_STUNNED`** — the `UNIT_FIELD_FLAGS` bit that freezes a character's *turning*
/// (decision 0872). Not a movement flag and not an aura: the reference reads it straight off the
/// descriptor block at `[[unit+0x110]+0xa0]` (predicate `0x5145b0` — note the inverted
/// `not/shr/and` form, which a census grepping only `test …,0x40000` misses — consumed at
/// `0x514755`, which skips both the turn and pitch emitters and force-stops either in flight).
///
/// It is the **other half** of a stun. vmangos's `HandleModStun` sets this flag *and* calls
/// `SetRooted(true)`, so `SPELL_AURA_MOD_STUN` grants both: root kills translation, this kills the
/// pivot. A pure root (Frost Nova, Entangling Roots) sets only the first — which is why a rooted
/// player can still turn and a stunned one cannot, the distinction B179 was reporting.
///
/// **It has no second consumer — the "idle/fidget" one was a misread, and this doc outlived the
/// correction.** It used to say the idle/fidget selectors bail on the bit at `0x5eb4f2`/`0x5ec219`
/// and pointed at a `MovementState::stunned` that decision **0889** had already deleted along with
/// the gate: re-read at the bytes, those two addresses are inside **`ToggleSheath 0x5eb480`** and
/// **`CanLootNow 0x5ec110`**, and **no site in the whole `0x40000` census touches animation
/// selection**. What stops the idle twitch under a stun is the animation clock being stopped
/// (0889's proc-11 freeze), not this flag. The claim came in quoted verbatim from wow-re
/// `object-layer/scratch/unit-flags-movement-gates.md` §2/§3, which is being corrected there too —
/// a stale line in one repo re-entering the other is exactly the failure mode both contracts warn
/// about, and it survived here for a fortnight because a doc comment is not a gate.
///
/// The census's other movement-relevant consumers are real and stay: the two relayed
/// `MSG_MOVE_*` wrappers that refuse outright (`0x602b20` StartTurn, `0x602b80` StartPitch — the
/// only two of twelve carrying a stun test), and the auto-face-target smoothing suppression at
/// `0x600dd7`.
pub(crate) const UNIT_FLAG_STUNNED: u32 = 0x0004_0000;
/// **`IsSelfControlled`** — the reference's `0x5fa550` (`5fa566 a9 04 00 c0 00 test eax, 0xc00004`):
/// is this unit acting under its *own* control? False while `DISABLE_MOVE` (`0x4`), `CONFUSED`
/// (`0x400000`) or `FLEEING` (`0x800000`) — and note what is **absent**: `UNIT_FLAG_STUNNED` is
/// not in the mask (it has its own, separate gate), and neither is the taxi bit. wow-re
/// `object-layer/scratch/unit-flags-movement-gates.md` §4.
///
/// Its consumers in the reference are the movement/collision layer and `DoEmote` — and the
/// polarity is the trap: it returns **1 for an ordinary player**, so a gate written on it fires
/// in the *normal* case and is suppressed while feared, not the other way round.
///
/// **One leg is deliberately unmodelled.** `POSSESSED` (`0x1000000`) does not refuse here: the
/// reference redirects at `0x5fa582` to the charmer's GUID and **recurses** into itself, so the
/// answer is the charmer's. Reproducing that needs the charmer's descriptor, and the only
/// consumer that would notice is whether a red toast appears while mind-controlled. Named rather
/// than invented (decision 1904).
pub(crate) fn self_controlled(unit_flags: u32) -> bool {
/// `DISABLE_MOVE | CONFUSED | FLEEING` — the reference's literal `0xc00004`.
const NOT_SELF_CONTROLLED: u32 = 0x0000_0004 | 0x0040_0000 | 0x0080_0000;
unit_flags & NOT_SELF_CONTROLLED == 0
}
/// `UNIT_FLAG_IN_COMBAT` — the same `UNIT_FIELD_FLAGS` word, **bit 19** (vmangos
/// `UnitDefines.h`; the client reads it as `shr reg,0x13; test rl,1`).
///
/// Its readers are deliberately unrelated to each other — the spell-usability walk's leg 8, the
/// probe preflight banner, and `UnitAffectingCombat`'s snapshot field — which is exactly why the
/// constant lives here beside its neighbour rather than being declared a fourth time: three
/// private copies of one bit is how a mask silently drifts.
///
/// **There is no player-specific combat latch to pair it with.** wow-re censused the
/// `shr reg,0x13` + `test rl,1` idiom image-wide (7 hits, 6 on this field+bit) and found the two
/// hardcoded *local-player* readers going through this same flag; `UnitAffectingCombat("player")`
/// takes a GUID fast path and lands on the identical word.
pub(crate) const UNIT_FLAG_IN_COMBAT: u32 = 0x0008_0000;
/// `UNIT_FLAG_TAXI_FLIGHT` — the same `UNIT_FIELD_FLAGS` word, **bit 20** (vmangos
/// `UnitDefines.h:511`; the client reads it as `shr reg,0x14; test rl,1` — wow-re counted 20
/// independent sites of that idiom with no shared gate, `unit-flags-movement-gates.md` §4).
///
/// Beside its neighbour for the same reason that one is here: readers that have nothing to do with
/// each other — `SetStandState`'s own guard #3, the idle handler's auto-AFK gate and `UnitOnTaxi`
/// — and a bit that is spelled out more than once is a bit that eventually drifts.
pub(crate) const UNIT_FLAG_TAXI_FLIGHT: u32 = 0x0010_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 [`posture`] (the [`crate::creature_anim::
/// SheathRequest`] posture, decision 0080).
///
/// 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*
/// does nothing for a STATE text emote (vmangos `HandleTextEmoteOpcode` breaks out of the switch
/// for SIT/SLEEP/KNEEL), so the posture is the client's own to set.
///
/// The state values are `UnitStandStateType`: 0 STAND · 1 SIT · 3 SLEEP · 8 KNEEL.
#[derive(bevy::ecs::message::Message, Clone, Copy, Debug)]
pub(crate) struct StandStateRequest {
pub(crate) state: u8,
}
/// The server's own stand state for OUR body (`SMSG_STANDSTATE_UPDATE` — the eat/drink sit, the
/// stand on damage; decision 2339). Applied by [`posture`] through the same local setter the
/// volunteered change uses, with no refusal gate and no packet back — the reference's `0x603e50`
/// → `0x6127b0`. Written by [`net`], read in `control`.
#[derive(bevy::ecs::message::Message, Clone, Copy, Debug)]
pub(crate) struct ServerStandState {
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<crate::creature_anim::Engaged>,
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<Embodied>, Without<FlyCam>),
>;
/// 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<crate::transport::Transport>,
Without<Embodied>,
Without<FlyCam>,
),
>;
/// 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`
/// symbol itself: an instrument may name the gameplay system it runs against, but exporting
/// `control` would drag its private parameter types (`MoveSpeed`, `PlayerCapsule`, `PressGesture`)
/// out with it, which is exactly the internals-publishing 1173 rejected a crate wall to avoid.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct PlayerControlSet;
/// The player/camera subsystem: spawns the camera + move/avatar resources at startup, drives the
/// third-person/free-fly controller each frame. (The cursor is the [`crate::cursor`] subsystem.)
pub(crate) struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
net::register(app);
follow::plugin(app);
camera_saved::plugin(app);
camera_view::plugin(app);
// 1160's wire (a), both directions (see `world_focus`): the game answers the world's
// "where do I stream from" before the stream stage, and reads the residency the world
// publishes after it to end its own post-snap hold.
//
// **Both ordering edges are load-bearing** (B263 round 3, decision 1336). `.before(Stream)`
// alone left the publish free to run before `control`'s teleport snap (conflicting access,
// no edge — the executor picks either order), and on the snap frame the streamer then
// streamed around the DEPARTURE position: residency read "world resident", the settle hold
// released on frame 0, and the body free-fell at the destination under the loading screen —
// every same-map `.tele` on the probe machine. `.after(Input)` pins the publish to the
// post-snap position; the schedule contract (snap → focus → stream → release → present)
// is only a contract if every arrow is an explicit edge.
app.add_systems(
Update,
(world_focus::publish_viewer, world_focus::publish_view_focus)
.after(benilla_world::schedule::WorldStage::Input)
.before(benilla_world::schedule::WorldStage::Stream),
)
.add_systems(
Update,
world_focus::release_post_snap_hold.after(benilla_world::schedule::WorldStage::Stream),
);
app.add_observer(camera::on_cvar);
app.add_observer(camera_dynamics::on_cvar);
app.init_resource::<camera::LookConfig>();
app.init_resource::<camera::ZoomLimit>();
app.init_resource::<camera::FollowConfig>();
app.init_resource::<camera_dynamics::CameraOptions>();
// Far sight: resolve `PLAYER_FARSIGHT` into a pose before `control` reads it to seat the
// camera. A separate system rather than another query on `control` for a hard reason —
// `control` already holds the self entity's `Transform` mutably, so it cannot also read an
// arbitrary unit's.
app.init_resource::<view_subject::ViewSubject>()
.init_resource::<scoped_view::ScopedView>()
.add_systems(
Update,
(
view_subject::publish_view_subject,
// The spyglass zoom (aura 76) — a different mechanism entirely, sharing only
// the family name. Also before `control`, which reads the scope to hold the rig
// in first person: the reference's camera lock.
scoped_view::apply_scoped_view,
)
.in_set(WorldStage::Input)
.before(control)
.in_set(crate::char_select::InWorldGated),
);
app.add_systems(
Startup,
// AFTER the config fold, not merely after the assets: the camera reads `gxMultisample`
// once, at spawn (1629), so a player's setting has to be in the resource by now.
setup::setup_player
.after(AssetSet::Open)
.after(crate::cvars::CvarLoad),
)
// The world camera renders only when the world can be seen (decision 0540): in world,
// or under the opaque loading screen (whose covered render is what compiles the
// world's pipelines before the first visible frame). At the glue screens the fully
// streamed world otherwise burns real GPU time behind an opaque fullscreen scene.
.add_systems(Update, setup::gate_world_camera)
// In capture mode the harness ([`crate::capture`]) pins the camera (and thus the stream
// focus), so `control` must not also drive it — gate it off when capturing. In-world
// only (decision 0193): at the character-select glue screen the controller must not
// grab the cursor, fly the camera, or queue movement sends behind the overlay.
.add_systems(
Update,
control
.in_set(PlayerControlSet)
.in_set(WorldStage::Input)
.run_if(not(resource_exists::<crate::run_mode::CaptureMode>))
.in_set(crate::char_select::InWorldGated),
)
// The posture setter's queue (the `/sit` family — decision 0881; `control` is the sole
// executor, like the sheath queue).
.add_message::<StandStateRequest>()
// The server's own stand state for our body (decision 2339), written by [`net`].
.add_message::<ServerStandState>()
// Land-here ([`land`]): the ask, and the re-attach when the server's teleport lands.
// Before `control` so the frame that applies the teleport is the frame that takes
// third-person control back — `control` reads `detached` after this has cleared it.
.add_message::<land::LandHere>()
.add_systems(
Update,
land::land_here
.in_set(WorldStage::Input)
.before(control)
.in_set(crate::char_select::InWorldGated),
)
// (The two scripted probe drivers that used to sit here — `WOW_PROBE_LOOK`'s
// mouse-turn and `WOW_PROBE_CAM`'s camera park — are the harness's now, and register
// themselves against `control` from there: decision 1174 moved every instrument out of
// this module so a player build carries none of them.)
// A server-authored spline (Charge/knockback/taxi) driving our own player is mirrored into
// `Player` here, *before* `control` reads `pos` to seat the camera and skip input. Same
// gates as `control` (not while capturing; in-world only).
.add_systems(
Update,
server_ride::drive_self_ride
.in_set(WorldStage::Input)
.before(control)
.run_if(not(resource_exists::<crate::run_mode::CaptureMode>))
.in_set(crate::char_select::InWorldGated),
)
// A session END releases the avatar — a confirmed `/logout`, or a lost session
// (decision 1262): the streamed entity is despawned by the net drain either way, and
// dropping `active` re-arms the take-control latch for the next login (possibly a
// different character). Ungated — the message lands as the state flips.
.add_systems(
Update,
wire_in::release_on_session_end.in_set(WorldStage::Input),
)
// Which body the client drives at all (decision 1277). Strictly before everything that
// reads the marker — the controller, and the collision-height mirror below.
.add_systems(
Update,
embody::maintain_embodiment
.in_set(WorldStage::Input)
.before(control)
.before(mirror_mover_collision_height),
)
// Mirror the driven body's collision height onto `Player` for the swim arm (decision
// 0645). A *continuous* sync rather than a one-shot at take-control, for the reason the
// take-control branch itself records: a cross-map worldport re-streams the entity, so
// anything latched on that edge is lost on transfer — and a possession swaps it for a
// body of an entirely different size. Before `control`, which is where the swim depth
// lines are evaluated.
.add_systems(
Update,
mirror_mover_collision_height
.in_set(WorldStage::Input)
.before(control),
)
// The camera shake (B298, decision 1540) lands on the camera AFTER `control` has
// seated it: the applier adds its offset to the pose `seat_camera` just wrote, so the
// eye it measures the distance falloff against is the un-shaken one. `control` is at
// Bevy's 16-param ceiling, so the offset cannot be threaded into it as a resource —
// and running after is the better shape anyway.
.add_systems(
Update,
crate::camera_shake::apply_camera_shake
.in_set(WorldStage::Input)
.after(control)
// Gated off in capture mode alongside `control`, and for the same reason a
// capture keeps the doodad rail static: a pinned camera that a passing kodo
// could nudge is not a regression baseline any more.
.run_if(not(resource_exists::<crate::run_mode::CaptureMode>)),
)
// **Which mouse buttons the world owns** (ledger B364) — decoded once, ahead of all
// three readers: `/follow`'s both-button cancel below, the look session, and the camera's
// input command word. `control` cannot own it, because `steer_follow` runs before
// `control` and would then read it a frame late.
.add_systems(
Update,
camera::latch_world_mouse
.in_set(WorldStage::Input)
.before(control)
.before(follow::steer_follow)
.in_set(crate::char_select::InWorldGated),
)
// `/follow` (decision 0890): steer the facing and decide this tick's synthesized forward
// input immediately BEFORE the controller, which folds the flag into its forward axis.
// The player's own turn input therefore runs after us and wins, which is exactly what
// makes the turn-away cancel reachable.
.add_systems(
Update,
follow::steer_follow
.in_set(WorldStage::Input)
.before(control)
.in_set(crate::char_select::InWorldGated),
)
// The self-avatar zoom-in fade rides the same `MeshTag`/material channel as the interior
// classifier + the appear/despawn fades, so it must run *after* both to win the frame while
// fading (and yield to them otherwise). It also writes `Visibility` (the first-person
// hide), so it must run after the model-`Visibility` authority
// (`debug_panel::apply_model_visibility`) too — otherwise whichever system Bevy's
// arbitrary sort ran last would win, and the authority could re-show the body in
// first-person. First-person correctness outranks the dev creature-toggle for these few
// submeshes. Gated off in capture mode alongside `control` (whose per-frame
// `self_fade_alpha` it consumes), so a pinned capture never hides the avatar.
.add_systems(
Update,
apply_self_model_fade
.after(benilla_world::interior::classify_entity_interior)
.after(benilla_world::model_fade::apply_render_fade)
.after(benilla_world::model_render::ModelVisSet)
.run_if(not(resource_exists::<crate::run_mode::CaptureMode>)),
);
}
}
/// Mirror the driven body's [`crate::entities::CollisionHeight`] onto [`Player`] — its swim depth
/// lines are fractions of it (decision 0645), and the swim arm runs off the resource, not the
/// entity. One entity, one copy: no work until a body streams in, and it re-syncs itself after a
/// worldport re-streams that entity under a new one — or after a possession puts a different-sized
/// body in our hands.
fn mirror_mover_collision_height(
mut player: ResMut<Player>,
mover: Query<&crate::entities::CollisionHeight, With<Embodied>>,
) {
if let Ok(&h) = mover.single() {
if player.collision_height != h {
player.collision_height = h;
}
}
}