A jump on a moving lift measures its fall in the lift's frame (#1818)

A jump on a descending lift (Undercity, Gnomeregan) played the hard-landing grunt and dust. While airborne, benilla carried the rider with the deck but kept the jump's start height in world coordinates. So the lift's drop during the arc, about 24 yd on Undercity's 29 yd/s cars, counted as a fall, and FALLINGFAR latched early.

1.12.1 measures the fall height (`0x7c60c0`) in the mover's own frame: transport-local while aboard, re-mapped when the mover boards or leaves (`0x7c6620`). `Player::carry_to` now moves the start height with the deck. A remote mover's arc is measured in the deck's frame when it takes off and lands on the same transport.

Tests: a 60 fps jump on a deck dropping 28.95 yd/s lands with no fall height and no FALLINGFAR, for us (23.64 yd before) and through packets for a remote (23.88 yd before).

Fixes #1250

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 560d7ff63e8394ca08c97a0867c852155f3e6b52)

# Conflicts:
#	crates/benilla-app/src/spell/range_units.rs
#	docs/DEVIATIONS.md

Absorbed into the octo fork. spell/range_units.rs is upstream-only (added after our base) and its change here was a test fixture's fall_start_z -> fall_start rename, so the file is dropped rather than orphaned; the rename's real call sites applied unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
Sam Sung
2026-10-25 23:20:10 -06:00
committed by grom
co-authored by Claude Opus 5
parent b69ea5cd04
commit 2c4458ee1d
8 changed files with 163 additions and 30 deletions
@@ -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],
+43 -14
View File
@@ -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<f32>,
/// 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<FallHeight>,
/// Not-yet-due relayed moves by fire-time, the reference's `CMovement+0x150` queue.
pub(crate) pending: std::collections::VecDeque<PendingMove>,
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<JumpInfo>, 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<f32>,
packet_z: f32,
) -> (Option<f32>, Option<f32>) {
fall_start: Option<FallHeight>,
packet: FallHeight,
) -> (Option<FallHeight>, Option<f32>) {
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,
+1 -1
View File
@@ -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,
+43 -8
View File
@@ -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],
+3 -3
View File
@@ -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],
+1 -1
View File
@@ -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,
+70 -1
View File
@@ -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");
}
}
+1 -1
View File
@@ -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.