//! The mechanisms behind the four `UIOptionsFrame` camera checkboxes (`UIOptionsFrame.lua:32-35`). //! //! The reference's pitch is positive downward (`0x511f40`, `0x50de00`), benilla's //! [`super::camera::FlyCam::pitch`] positive upward, so every reference comparison is flipped. use bevy::prelude::*; use super::camera_channel::{Arm, SmoothChannel, CHANNEL_EPS}; /// `cameraPivotDXMax` default (`[0xbe0f30]`): the yaw per motion event a vertical drag stays under. pub(crate) const PIVOT_DX_MAX_DEFAULT: f32 = 0.05; /// `cameraPivotDYMin` default (`[0xbe0cec]`): the pitch per motion event a pivot drag exceeds. pub(crate) const PIVOT_DY_MIN_DEFAULT: f32 = 0.0; /// `cameraTargetSmoothSpeed` default, deg/s (`[0xbe0fc8]`): the rate the pivot bias eases home at. pub(crate) const TARGET_SMOOTH_SPEED_DEFAULT: f32 = 90.0; /// `cameraGroundSmoothSpeed` default, deg/s (`[0xbe0fc0]`). pub(crate) const GROUND_SMOOTH_SPEED_DEFAULT: f32 = 7.5; /// `cameraTerrainTiltTimeMin` default, seconds (`[0xbe1050]`). pub(crate) const TILT_TIME_MIN_DEFAULT: f32 = 3.0; /// `cameraTerrainTiltTimeMax` default, seconds (`[0xbe1054]`). pub(crate) const TILT_TIME_MAX_DEFAULT: f32 = 10.0; /// `cameraBobbingLRAmplitude`/`UDAmplitude` default, inches (`[0xbe1064]`, `[0xbe10c8]`). pub(crate) const BOB_AMPLITUDE_DEFAULT: f32 = 2.0; /// `cameraBobbingFrequency` default, Hz at a unit speed factor (`[0xbe0cf8]`). pub(crate) const BOB_FREQUENCY_DEFAULT: f32 = 0.8; /// `cameraBobbingSmoothSpeed` default (`[0xbe10bc]`): the decay rate, read only by the disarm. pub(crate) const BOB_SMOOTH_SPEED_DEFAULT: f32 = 0.8; /// Inches to yards for the amplitude CVars (`[0x7ff9d0]`). const BOB_AMPLITUDE_SCALE: f32 = 1.0 / 36.0; /// The speed the bob's rate factor divides by, yd/s (`[0x808a08]`). const BOB_SPEED_DIVISOR: f32 = 7.2; /// The rate factor's clamp, `[0x8089a0]` and `[0x80899c]`: the lower bound is the higher address. const BOB_SPEED_CLAMP: (f32, f32) = (0.5, 1.5); /// Head bob's inclusive first-person test, on the zoom distance (`[cam+0xec] <= [0x8089ac]`). pub(crate) const BOB_FIRST_PERSON_DISTANCE: f32 = 1.0 / 6.0; /// The camera option CVars; each default is the reference's registered value. #[derive(Resource, Clone, Copy, Debug)] pub(crate) struct CameraOptions { pub(crate) pivot: bool, pub(crate) pivot_dx_max: f32, pub(crate) pivot_dy_min: f32, pub(crate) target_smooth_speed: f32, pub(crate) terrain_tilt: bool, pub(crate) ground_smooth_speed: f32, /// `cameraTerrainTiltTimeMin`/`Max`, seconds, each scaled by the matrix row's `Factor`. pub(crate) tilt_time_min: f32, pub(crate) tilt_time_max: f32, pub(crate) bobbing: bool, pub(crate) bob_lr_amplitude: f32, pub(crate) bob_ud_amplitude: f32, pub(crate) bob_frequency: f32, pub(crate) bob_smooth_speed: f32, /// `cameraWaterCollision`: one read (`0x50e5ec`) adds liquid to the sweeps' trace mask and /// admits the pivot floor and cap block (`0x50e629`, [`super::camera_water`]). Neither works /// alone: that block lifts the sweep origin to `surface + 2/9`, clear of the solid waterline. pub(crate) water_collision: bool, } /// Applies a camera option CVar change. The numeric rows take any value, where the reference's /// `0x50b330` refuses an out-of-range smooth speed, tilt time or bob frequency. pub(crate) fn on_cvar(ev: On, mut opts: ResMut) { let v = ev.num(); match ev.key().as_str() { "camerapivot" => opts.pivot = v != 0.0, "camerawatercollision" => opts.water_collision = v != 0.0, "camerapivotdxmax" => opts.pivot_dx_max = v, "camerapivotdymin" => opts.pivot_dy_min = v, "cameratargetsmoothspeed" => opts.target_smooth_speed = v, "cameraterraintilt" => opts.terrain_tilt = v != 0.0, "cameragroundsmoothspeed" => opts.ground_smooth_speed = v, "cameraterraintilttimemin" => opts.tilt_time_min = v, "cameraterraintilttimemax" => opts.tilt_time_max = v, "camerabobbing" => opts.bobbing = v != 0.0, "camerabobbinglramplitude" => opts.bob_lr_amplitude = v, "camerabobbingudamplitude" => opts.bob_ud_amplitude = v, "camerabobbingfrequency" => opts.bob_frequency = v, "camerabobbingsmoothspeed" => opts.bob_smooth_speed = v, _ => {} } } impl Default for CameraOptions { fn default() -> Self { Self { pivot: true, water_collision: true, pivot_dx_max: PIVOT_DX_MAX_DEFAULT, pivot_dy_min: PIVOT_DY_MIN_DEFAULT, target_smooth_speed: TARGET_SMOOTH_SPEED_DEFAULT, terrain_tilt: false, ground_smooth_speed: GROUND_SMOOTH_SPEED_DEFAULT, tilt_time_min: TILT_TIME_MIN_DEFAULT, tilt_time_max: TILT_TIME_MAX_DEFAULT, bobbing: false, bob_lr_amplitude: BOB_AMPLITUDE_DEFAULT, bob_ud_amplitude: BOB_AMPLITUDE_DEFAULT, bob_frequency: BOB_FREQUENCY_DEFAULT, bob_smooth_speed: BOB_SMOOTH_SPEED_DEFAULT, } } } /// One frame's camera-dynamics inputs: the options, and the followed unit's facts the gates read. #[derive(Clone, Copy, Debug)] pub(super) struct DynamicsInput { pub(super) options: CameraOptions, /// Raw `cameraSmoothStyle`, which the tilt matrix indexes (`0x50dbc0`), not the follow's copy. pub(super) smooth_style: super::camera::FollowStyle, /// `cameraSmoothTrackingStyle`, read only by the pivot release's third conjunct (`0x511010`). pub(super) tracking_style: super::camera::FollowStyle, pub(super) subject: SubjectState, pub(super) nearclip: f32, /// The liquid surface over our own feet (Bevy Y), cached by the last movement tick. Unused /// under far sight: the reference reads the watched unit's liquid (`0x511ad0`), absent here. pub(super) surface_y: Option, } /// The followed unit's side of the gates. The movement word is last frame's, as the reference's /// input handler reads it (`0x50fee0`'s caller `0x514446` runs before the mover lookup). #[derive(Clone, Copy, Debug, Default)] pub(super) struct SubjectState { /// `MOVEMENTFLAGS`, whole: the mechanisms read different masks of it. pub(super) move_flags: u32, /// `UNIT_FIELD_FLAGS & 0x100000`, taxi flight. pub(super) taxi: bool, /// The camera's `Track` bit (`[cam+0x90] & 0x100`): externally driven movement. pub(super) track: bool, /// The camera's `Fear` bit (`[cam+0x90] & 0x1000`): external control. pub(super) fear: bool, /// Facing, radians, in benilla's yaw convention (`[obj+0xc98]`). pub(super) facing: f32, /// `UNIT_FIELD_MOUNTDISPLAYID > 0`. pub(super) mounted: bool, /// Current speed, yd/s (`GetCurrentSpeed`, `0x7c4c90`). pub(super) speed: f32, /// The input-command word ([`super::camera::follow_cmd`]); its edges arm head bob (`0x511170`). pub(super) command: u32, /// The aura-76 FOV lock of the spyglass scope (`[cam+0x90] & 0x8`). pub(super) scoped: bool, } impl SubjectState { /// `MOVEMENTFLAGS & 0xf`, move and strafe: turning in place is not translating (`0x5106b7`). pub(super) fn translating(&self) -> bool { self.move_flags & (mf::FORWARD | mf::BACKWARD | mf::STRAFE_LEFT | mf::STRAFE_RIGHT) != 0 } } use crate::creature_anim::move_flags as mf; /// `cameraPivot`, smart pivot: a camera the collision solver pinned tilts its view, through a bias /// channel (`[cam+0x104]`), instead of swinging its arm. The gate `0x510690` returns the solver's /// clip flags (`[cam+0x90] & 0x30000`), so only a camera clipped this frame pivots. The eye is /// seated from the unbiased pitch (`0x50ee32`-`0x50ee58`); the body's aim is the sum (`0x5103e0`). /// /// The release's `0x511010` holds the bias while the Track latch is up and tracking is Never, or /// a move-type-0 click-to-move order is in flight (`[0xc4d888]`); benilla has no click-to-move, /// so only the first is built. pub(super) struct SmartPivot { /// The bias channel `[cam+0x104]`; its armed bit is the reference's `[cam+0x90] & 0x8000000`. bias: SmoothChannel, } impl Default for SmartPivot { fn default() -> Self { Self { bias: SmoothChannel::angular(), } } } impl SmartPivot { /// The bias, radians, positive up: part of the view and the body's aim, never the seat's. pub(super) fn bias(&self) -> f32 { self.bias.live() } /// `0x50fee0`'s pitch routing: the integrator's delta, or `None` on a pure-pivot frame. /// /// | reference | here (sign flipped) | /// |---|---| /// | `[cam+0xf4] < 0` (looking up) | `pitch > 0` | /// | floor `bias ≥ −89° − f4` | ceiling `bias ≤ +89° − pitch` | /// | pure pivot: `bias ≤ 0 ∧ f4 < 0` | `bias ≥ 0 ∧ pitch > 0` | pub(super) fn route_pitch( &mut self, d_pitch: f32, d_yaw: f32, pitch: f32, subject: &SubjectState, clipped: bool, cfg: &CameraOptions, ) -> Option { // `0x510690`'s conjuncts 3-6; 1 and 2, a UNIT or PLAYER subject, always hold here. let verdict = cfg.pivot && !subject.translating() && pitch >= 0.0 && clipped; // A displaced bias with no ease armed keeps routing; a gated vertical drag starts it. let displaced = !self.bias.in_flight() && self.bias().abs() >= CHANNEL_EPS; let vertical_drag = verdict && d_pitch.abs() > cfg.pivot_dy_min && d_yaw.abs() < cfg.pivot_dx_max; if displaced || vertical_drag { let mut bias = self.bias() + d_pitch; // One-sided: only while looking up, and only toward the pitch limit. if pitch > 0.0 { bias = bias.min(super::camera::CAM_PITCH_LIMIT - pitch); } self.bias.snap(bias); if bias >= 0.0 && pitch > 0.0 { // The pure-pivot frame (`0x5100c3`): the integrator does not run. return None; } } // The ordinary path (`0x51009a`). After a routed delta it lands in both, as in the // reference, and the ease walks it off the bias. self.release(cfg); Some(d_pitch) } /// Per frame (`0x50ed77` → `0x5107f0`): a false gate eases a displaced bias home, a true one /// holds it. pub(super) fn advance( &mut self, pitch: f32, subject: &SubjectState, clipped: bool, tracking_style: super::camera::FollowStyle, cfg: &CameraOptions, dt: f32, ) { let verdict = cfg.pivot && !subject.translating() && pitch >= 0.0 && clipped; // `0x511010`'s first disjunct: tracking at Never holds the bias as the gate does. let tracking_hold = subject.track && tracking_style == super::camera::FollowStyle::Never; let holding = verdict || tracking_hold; if !holding && self.bias().abs() >= CHANNEL_EPS { self.release(cfg); } else if holding { // `0x50ed93`: a held gate snapshots the bias and disarms, cancelling an ease in flight. let live = self.bias(); self.bias.snap(live); } self.bias.advance(dt); } /// Arms the bias home at `cameraTargetSmoothSpeed` (`0x512a50(cam, 0, 0, 1.0f, now)`). fn release(&mut self, cfg: &CameraOptions) { self.bias .arm(&Arm::at(0.0, cfg.target_smooth_speed.to_radians())); } } /// `cameraTerrainTilt`, follow terrain: the camera pitches toward the slope of the ground ahead. /// The probe `0x50d900` reads its rise over run every 100 ms (a missed drop is the full downward /// tilt); the arm `0x50dbc0` eases the ground channel to the stepped angle over /// `[3, 10] × Factor` seconds, a floor that always binds (20° at 7.5°/s is 2.67 s), and the /// channel adds to the view pitch inside the ±89° clamp (`0x50f810`). pub(super) struct TerrainTilt { /// The ground channel `+0x108`, at `cameraGroundSmoothSpeed`. ground: SmoothChannel, /// The probe's output `[cam+0xa0]`, held between runs. slope_pitch: f32, /// Seconds since the probe ran, for the 100 ms throttle (`0x50d94f`). since_probe: f32, /// Mouse look holds the lean inside the pitch (the reference's refcount `[cam+0xa8]`). handed_off: bool, } impl Default for TerrainTilt { fn default() -> Self { Self { ground: SmoothChannel::angular(), slope_pitch: 0.0, // The probe runs on the first frame. since_probe: PROBE_THROTTLE, handed_off: false, } } } /// An `{Absorb, Delay, Factor}` row, from `0x84f620`. Deviation: a code table, not the reference's /// ninety `cameraTerrainTilt