diff --git a/crates/benilla-app/src/creature_anim/select/tests.rs b/crates/benilla-app/src/creature_anim/select/tests.rs index b036807f..0ae69691 100644 --- a/crates/benilla-app/src/creature_anim/select/tests.rs +++ b/crates/benilla-app/src/creature_anim/select/tests.rs @@ -939,7 +939,7 @@ fn the_granted_modes_fold_into_the_flags_word_on_every_leg_but_our_own() { speed: 7.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, relay: Default::default(), last_apply_ms: 0.0, last_apply_pos: [0.0; 3], diff --git a/crates/benilla-app/src/net/motion/remote.rs b/crates/benilla-app/src/net/motion/remote.rs index e5a6aba2..b6b1b5dc 100644 --- a/crates/benilla-app/src/net/motion/remote.rs +++ b/crates/benilla-app/src/net/motion/remote.rs @@ -30,9 +30,9 @@ pub(crate) struct RemoteMotion { pub(crate) vertical_velocity: f32, /// The launch's `(cos, sin) * xyspeed`, frozen for the arc; `[0; 2]` on the ground. pub(crate) jump_xy_vel: [f32; 2], - /// The takeoff Z of this arc, `None` if the mover entered view airborne; on landing, - /// `fall_start_z - landing_z` is the fall height that gates the grunt and dust puff. - pub(crate) fall_start_z: Option, + /// This arc's takeoff height, `None` if the mover entered view airborne; on landing, its + /// drop to the landing is the fall height that gates the grunt and dust puff. + pub(crate) fall_start: Option, /// Not-yet-due relayed moves by fire-time, the reference's `CMovement+0x150` queue. pub(crate) pending: std::collections::VecDeque, pub(crate) relay: RelayChain, @@ -42,6 +42,35 @@ pub(crate) struct RemoteMotion { pub(crate) last_apply_pos: [f32; 3], } +/// A relayed pose's height in both frames its packet carries: the world's, and the deck's while +/// aboard (`ON_TRANSPORT`'s tail). +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct FallHeight { + pub(crate) world_z: f32, + /// The transport's guid and the deck-local z. + pub(crate) aboard: Option<(u64, f32)>, +} + +impl FallHeight { + fn of(ev: &RelayMove) -> Self { + Self { + world_z: ev.position[2], + aboard: ev.transport.as_ref().map(|t| (t.guid, t.pos.z)), + } + } + + /// The drop from `self` down to `landing`. Aboard one transport at both ends it is the deck + /// frame's, where a rider's `+0x7c` lives (`0x7c6900` re-seats it from the local pose), so a + /// moving lift adds nothing. Otherwise it is the world's: a takeoff aboard reads the deck at + /// takeoff, where the reference's leave (`0x7c6790`) maps `+0x7c` by the deck at the leave. + fn drop_to(self, landing: Self) -> f32 { + match (self.aboard, landing.aboard) { + (Some((from, start)), Some((to, end))) if from == to => start - end, + _ => self.world_z - landing.world_z, + } + } +} + /// What `unit_move` did with an inbound relayed move: the `out=` field of the `rly` trace, which /// logs discarded arrivals too. #[derive(Clone, Copy)] @@ -272,8 +301,8 @@ pub(in crate::net) fn apply_move( // The `FALLING` to grounded edge is a landing; its fall height gates the grunt and dust puff. let was_falling = rm.flags & FALLING != 0; let (new_start, descent) = - fall_arc_step(was_falling, now_falling, rm.fall_start_z, ev.position[2]); - rm.fall_start_z = new_start; + fall_arc_step(was_falling, now_falling, rm.fall_start, FallHeight::of(ev)); + rm.fall_start = new_start; if let Some(descent) = descent { landings.write(crate::creature_anim::HardLanding { entity: e, descent }); } @@ -545,19 +574,19 @@ pub(crate) fn jump_seed(jump: Option, fall_time: u32, feather: bool) - } } -/// One packet's step of the landing predictor: `(fall_start_z, descent)`, the descent +/// One packet's step of the landing predictor: `(fall_start, descent)`, the descent /// `takeoff - landing` only on the `FALLING` to grounded edge with a known takeoff. pub(in crate::net) fn fall_arc_step( was_falling: bool, now_falling: bool, - fall_start_z: Option, - packet_z: f32, -) -> (Option, Option) { + fall_start: Option, + packet: FallHeight, +) -> (Option, Option) { match (was_falling, now_falling) { - (false, true) => (Some(packet_z), None), // takeoff - (true, true) => (fall_start_z, None), // still airborne - (true, false) => (None, fall_start_z.map(|start| start - packet_z)), // landing edge - (false, false) => (None, None), // grounded + (false, true) => (Some(packet), None), // takeoff + (true, true) => (fall_start, None), // still airborne + (true, false) => (None, fall_start.map(|start| start.drop_to(packet))), // landing edge + (false, false) => (None, None), // grounded } } @@ -698,7 +727,7 @@ mod under_floor { speed: 0.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, pending: std::collections::VecDeque::new(), relay: RelayChain::default(), last_apply_ms: 0.0, diff --git a/crates/benilla-app/src/net/motion/spline.rs b/crates/benilla-app/src/net/motion/spline.rs index 37aeaf23..fad00d88 100644 --- a/crates/benilla-app/src/net/motion/spline.rs +++ b/crates/benilla-app/src/net/motion/spline.rs @@ -1471,7 +1471,7 @@ mod server_moved_players { speed: 7.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, pending: std::collections::VecDeque::new(), relay: Default::default(), last_apply_ms: 0.0, diff --git a/crates/benilla-app/src/net/motion/tests.rs b/crates/benilla-app/src/net/motion/tests.rs index 9d0b2975..49065993 100644 --- a/crates/benilla-app/src/net/motion/tests.rs +++ b/crates/benilla-app/src/net/motion/tests.rs @@ -13,7 +13,7 @@ use crate::player::{GRAVITY, TERMINAL_VELOCITY}; use super::facing::resolve_facing; use super::relay::RelayChain; -use super::remote::{facing_lerp, fall_arc_step, jump_seed, reconcile_lerp}; +use super::remote::{facing_lerp, fall_arc_step, jump_seed, reconcile_lerp, FallHeight}; use super::spline::monster_move_spline; use super::{RemoteMotion, Spline}; @@ -28,20 +28,55 @@ fn speeds() -> MoveSpeeds { } } +/// A pose at world height `z`, off any transport. +fn at(z: f32) -> FallHeight { + FallHeight { + world_z: z, + aboard: None, + } +} + #[test] fn remote_fall_arc_reports_height_only_on_the_landing_edge() { - assert_eq!(fall_arc_step(false, true, None, 100.0), (Some(100.0), None)); assert_eq!( - fall_arc_step(true, true, Some(100.0), 80.0), - (Some(100.0), None) + fall_arc_step(false, true, None, at(100.0)), + (Some(at(100.0)), None) ); assert_eq!( - fall_arc_step(true, false, Some(100.0), 70.0), + fall_arc_step(true, true, Some(at(100.0)), at(80.0)), + (Some(at(100.0)), None) + ); + assert_eq!( + fall_arc_step(true, false, Some(at(100.0)), at(70.0)), (None, Some(30.0)) ); // Entered view mid-fall: no takeoff, no prediction. - assert_eq!(fall_arc_step(true, false, None, 70.0), (None, None)); - assert_eq!(fall_arc_step(false, false, None, 70.0), (None, None)); + assert_eq!(fall_arc_step(true, false, None, at(70.0)), (None, None)); + assert_eq!(fall_arc_step(false, false, None, at(70.0)), (None, None)); +} + +/// A remote's flat jump on Undercity's lift car 20652, descending 28.95 yd/s for the 0.825 s +/// arc: the deck-local heights measure it, not the world's 23.9 yd. +#[test] +fn a_remote_jump_on_a_descending_lift_lands_with_no_fall_height() { + const LIFT: u64 = 0x1fc0_0000_0000_0001; + let aboard = |world_z: f32| FallHeight { + world_z, + aboard: Some((LIFT, 0.4)), + }; + let (start, _) = fall_arc_step(false, true, None, aboard(100.0)); + let (_, descent) = fall_arc_step(true, false, start, aboard(100.0 - 28.95 * 0.825)); + let descent = descent.expect("a takeoff seen, a landing reported"); + assert!( + descent.abs() < 1e-6, + "no deck-frame drop, not the deck's {descent} yd" + ); + // Boarding at the landing, a jump from the world onto the deck measures in the world. + let (start, _) = fall_arc_step(false, true, None, at(120.0)); + assert_eq!( + fall_arc_step(true, false, start, aboard(100.0)), + (None, Some(20.0)) + ); } fn motion(flags: u32, orientation: f32) -> RemoteMotion { @@ -54,7 +89,7 @@ fn motion(flags: u32, orientation: f32) -> RemoteMotion { speed: 0.0, vertical_velocity: 0.0, jump_xy_vel: [0.0, 0.0], - fall_start_z: None, + fall_start: None, relay: Default::default(), last_apply_ms: 0.0, last_apply_pos: [0.0, 0.0, 0.0], diff --git a/crates/benilla-app/src/net/objects.rs b/crates/benilla-app/src/net/objects.rs index cc9d9589..0e7dc331 100644 --- a/crates/benilla-app/src/net/objects.rs +++ b/crates/benilla-app/src/net/objects.rs @@ -760,7 +760,7 @@ fn unit_move( speed: 0.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, pending: std::collections::VecDeque::new(), relay: Default::default(), last_apply_ms: now_ms, @@ -1036,7 +1036,7 @@ fn spline_move_mode( rm.speed = 0.0; rm.vertical_velocity = 0.0; rm.jump_xy_vel = [0.0; 2]; - rm.fall_start_z = None; + rm.fall_start = None; } } } @@ -1286,7 +1286,7 @@ mod tests { speed: 7.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, relay: Default::default(), last_apply_ms: 0.0, last_apply_pos: [0.0; 3], diff --git a/crates/benilla-app/src/player/embody.rs b/crates/benilla-app/src/player/embody.rs index 26284af0..7f452139 100644 --- a/crates/benilla-app/src/player/embody.rs +++ b/crates/benilla-app/src/player/embody.rs @@ -125,7 +125,7 @@ mod tests { speed: 0.0, vertical_velocity: 0.0, jump_xy_vel: [0.0; 2], - fall_start_z: None, + fall_start: None, pending: std::collections::VecDeque::new(), relay: Default::default(), last_apply_ms: 0.0, diff --git a/crates/benilla-app/src/player/ride.rs b/crates/benilla-app/src/player/ride.rs index 94027aaa..cc44c947 100644 --- a/crates/benilla-app/src/player/ride.rs +++ b/crates/benilla-app/src/player/ride.rs @@ -22,7 +22,7 @@ pub(super) fn carry(player: &mut Player, cam: &mut FlyCam, transports: &Transpor // `to_euler` wraps to (−π, π]; a boat crossing that seam reads as a ±2π hop. dyaw = (dyaw + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU) - std::f32::consts::PI; - player.pos = world; + player.carry_to(world); player.face_yaw += dyaw; player.model_yaw = wrap_pi(player.model_yaw + dyaw); cam.yaw += dyaw; @@ -35,6 +35,17 @@ pub(super) fn carry(player: &mut Player, cam: &mut FlyCam, transports: &Transpor } } +impl Player { + /// Moves the rider to its deck-carried `world` feet, and the arc's launch height with it: a + /// rider's fall start `+0x7c` lives in the transport's frame (`0x7c6620` maps it at board and + /// leave), where the fall height (`0x7c60c0`) and FALLINGFAR (`0x633240`) measure it. A deck + /// pose is yaw-only, so the launch height moves by the feet's own vertical carry. + pub(super) fn carry_to(&mut self, world: Vec3) { + self.fall_start_y += world.y - self.pos.y; + self.pos = world; + } +} + /// Attaches or detaches against this frame's walkable support, then re-snapshots the deck-local /// pose. `ground` is the mover's resolved support ([`super::mover::Outcome::ground`]). pub(super) fn update_attachment( @@ -158,3 +169,61 @@ pub(super) fn update_attachment( } } } + +#[cfg(test)] +mod tests { + use super::super::mover::fall_step; + use super::super::{GRAVITY, JUMP_SPEED, TERMINAL_VELOCITY}; + use super::*; + + /// Undercity's lift car 20652 descends 96.5 yd in 3.33 s (`TransportAnimation.dbc`). + const LIFT_DROP_SPEED: f32 = 28.95; + + /// A 60 fps jump on a descending deck, carried every frame as `carry` does: the launch height + /// rides the deck, so the landing reads the deck-frame drop of a flat jump. + #[test] + fn a_jump_on_a_descending_lift_lands_with_no_fall_height() { + let dt = 1.0 / 60.0; + let mut deck_y = 100.0; + let mut p = Player { + pos: Vec3::new(0.0, deck_y, 0.0), + launch_vz: JUMP_SPEED, + ..Default::default() + }; + let (mut height, mut vz) = (0.0_f32, JUMP_SPEED); + let (mut latched, mut frames) = (false, 0); + let descent = loop { + if frames > 0 { + deck_y -= LIFT_DROP_SPEED * dt; + p.carry_to(Vec3::new(0.0, deck_y + height, 0.0)); + } + let launch_y = p.pos.y; + let (v1, mean) = fall_step(vz, dt, TERMINAL_VELOCITY); + vz = v1; + height += mean * dt; + let airborne = height > 0.0; + if !airborne { + height = 0.0; + } + p.pos.y = deck_y + height; + let edges = p.advance_airborne_arc(airborne, frames == 0, frames as f32 * dt, launch_y); + latched |= p.fall_far; + frames += 1; + if edges.landed { + break p.fall_start_y - p.pos.y; + } + assert!(frames < 120, "the arc lands"); + }; + let air_time = frames as f32 * dt; + assert!( + (air_time - 2.0 * JUMP_SPEED / GRAVITY).abs() < 2.0 * dt, + "a full flat arc, {air_time} s" + ); + assert!( + descent.abs() < 0.01, + "a flat jump drops nothing in the deck's frame, not the deck's {descent} yd" + ); + assert!(descent < crate::creature_anim::HARD_LANDING_DESCENT); + assert!(!latched, "nor does it latch FALLINGFAR"); + } +} diff --git a/crates/benilla-app/src/player/state.rs b/crates/benilla-app/src/player/state.rs index 9bc7ad9e..219e0a8d 100644 --- a/crates/benilla-app/src/player/state.rs +++ b/crates/benilla-app/src/player/state.rs @@ -407,7 +407,7 @@ pub(crate) struct Player { /// live flags read them instead of the keys; re-seeded by the air nudge, stale while grounded. pub(super) airborne_dirs: u32, /// Launch height (Bevy Y), the client's `StartFalling` z snapshot (`+0x7c`): the FALLINGFAR - /// distance leg measures descent below it. + /// distance leg measures descent below it. A rider's moves with the deck ([`Self::carry_to`]). pub(super) fall_start_y: f32, /// `MOVEFLAG_FALLINGFAR` latched for this arc ([`FALL_FAR_DROP`], [`FALL_FAR_TIME`]); only /// landing clears it, as the client's `StopFalling` does.