Files
benilla-octo/crates/benilla-app/src/player/mover.rs
T
Sam SungandClaude Opus 5.5 b50efc0a53 String literals and test names in benilla-app carry no record numbers (#368)
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.

Fixes #92. Fixes #36.

- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:06:28 +07:00

2404 lines
99 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The player's kinematic mover, one [`step`] a frame: ground classify, jump and fall, the slide,
//! step-up and step-down, over avian's `MoveAndSlide` with one-sided faces (a face blocks only the
//! motion its authored winding opposes, the reference's `0x632700`).
use avian3d::character_controller::move_and_slide::MoveHitData;
use avian3d::prelude::*;
use bevy::prelude::*;
use super::{
move_trace, Player, CAPSULE_HEIGHT, FEATHER_TERMINAL_VELOCITY, FOOT_CONE_HEIGHT, GRAVITY,
GROUND_COS, GROUND_PROBE, HOVER_CLIMB_RATE, HOVER_HEIGHT, JUMP_SPEED, LAND_PROBE, SKIN_WIDTH,
STEP_SLOPE_RATIO, STEP_SNAP_SLACK, STEP_UP_ADVANCE_PER_YARD, STEP_UP_HEIGHT, TERMINAL_VELOCITY,
WATER_WALK_PITCH_FLOOR, WEDGE_MIN_FALL, WEDGE_STALL_RATIO, WEDGE_STILL_FRAMES,
};
/// The trace-mask arm `0x6315f0` as a predicate: the liquid surface [`step`] may stand on this
/// frame, or `None`. Its three gates, in the reference's order:
/// - `water_walking` (`0x631610`) ORs the two ADT liquid layers into the walk trace's class mask
/// (`0x63162e`), which is the whole of the flag: the surface is swept geometry at its raw Z.
/// - `!swimming` (`0x631617`): the swim trace takes the same layers as a bound above (`0x6320fe`),
/// so the grant does not eject a swimmer, who stands on the water once the depth compare ends
/// SWIMMING under `0.75·h − 1/36` (`0x6030c0`). Levitate still lands a swimmer on the water:
/// the hover grant jumps the body first (`0x61a620`), clearing SWIMMING.
/// - `mover_pitch` strictly above [`WATER_WALK_PITCH_FLOOR`] (`0x63161e`): aiming lower drops the
/// layers and sinks the body into the swim, the reference's way back into the water.
pub(super) fn water_floor(
water_walking: bool,
swimming: bool,
mover_pitch: f32,
surface_y: Option<f32>,
) -> Option<f32> {
(water_walking && !swimming && mover_pitch > WATER_WALK_PITCH_FLOOR)
.then_some(surface_y)
.flatten()
}
/// What [`step`] decided, for the move flags and the wire that follow it in `control`.
pub(super) struct Outcome {
/// The settle hold while the world streams in after a teleport: frozen, gravity off.
pub held: bool,
/// Supported and not rising this frame.
pub grounded: bool,
/// A jump took off this frame.
pub jumped: bool,
/// The take-off was a knockback's, which also sets [`Self::jumped`]; the wire answers it with
/// `CMSG_MOVE_KNOCK_BACK_ACK` alone. `MSG_MOVE_JUMP`'s one emission site is the jump arm
/// (`0x615ed1`), and vmangos flags a JUMP this fast as `CHEAT_TYPE_OVERSPEED_JUMP`, the jump
/// check it does not exempt during a knockback (`MovementAnticheat.cpp:650`).
pub knocked: bool,
/// The standstill-jump air nudge fired, which re-seeds the frozen airborne direction flags.
pub air_nudged: bool,
/// The walkable floor's collider: the snap's hit when it ran, else the classify's. Standing on
/// a transport's collider attaches the mover to its frame.
pub ground: Option<Entity>,
}
/// Advance the player mover one frame, writing its position and velocities.
pub(super) fn step(
player: &mut Player,
time: &Time,
world: &benilla_world::collision::WorldCollision<'_, '_>,
capsule: &Collider,
moving: bool,
dir: Vec3,
speed: f32,
want_jump: bool,
wire_jump: bool,
knockback: Option<Vec3>,
water_floor: Option<f32>,
nudge_speed: f32,
) -> Outcome {
let dt = time.delta_secs();
let input_horiz = if moving {
dir.normalize() * speed
} else {
Vec3::ZERO
};
let half_h = Vec3::Y * (CAPSULE_HEIGHT * 0.5);
let mut center = player.pos + half_h;
let cast = |from: Vec3, disp: Vec3| world.cast_mover(capsule, from, disp, SKIN_WIDTH);
let probe_down = |c: Vec3, dist: f32| cast(c, Vec3::NEG_Y * dist);
// Airborne, the ground reach tightens to [`LAND_PROBE`] so the arc ends where the slide meets
// the floor. A hovering body rests [`HOVER_HEIGHT`] up, so every ground reach grows by it.
let hover_offset = if player.modes.hover {
HOVER_HEIGHT
} else {
0.0
};
let base_reach = if player.airborne_since.is_some() {
LAND_PROBE
} else {
GROUND_PROBE
};
let ground_reach = hover_offset + base_reach;
let classify = probe_down(center, ground_reach);
let on_walkable = classify.as_ref().is_some_and(|h| h.normal1.y >= GROUND_COS);
// Water walking makes the liquid surface ground (`0x63162e`). Liquid is queried here, not
// swept, so the classify asks the plane too, or a water-walker reads airborne and coasts. The
// reach includes the hover offset: the finalize probe (`0x636e45` → `0x632ba0` → `0x631e70`)
// measures to the water, so hover rests a yard over it. Feet under the surface count too.
let on_water = water_floor.is_some_and(|s| center.y - half_h.y - s <= ground_reach);
let mut ground_entity = if on_walkable {
classify.map(|h| h.entity)
} else {
None
};
// The settle hold after a teleport or login: the world streams in over several frames, so the
// body is frozen with gravity off. The terrain streamer releases it on the destination's
// residency, never on ground contact, which a flyer or a swimmer never makes.
let held = player.settling;
// Rooted, nothing moves the body: `SetRoot` (`0x7c7340`) clears FALLING (`0x7c6290`) and the
// direction bits, so the dispatcher (`0x634040`) skips the fall integrator (`0x635b00`) and the
// walk resolver returns on a step under `2^-20`. A root caught mid-air hangs until `ClearRoot`
// (`0x7c7370`) starts the fall (`0x7c61c0`, which refuses while rooted, `0x7c61d6`).
let anchored = !held && player.modes.rooted;
// A body part-way up a foot cone stands: the straight-down probe sees only the riser it rides,
// and the ride is re-certified every frame it continues.
let on_floor =
!held && (on_walkable || on_water || player.steep_support) && player.vel_y <= 0.0;
// The wedge rest holds until real ground takes over or its support goes; its reach stays
// [`LAND_PROBE`], plus the hover offset.
if player.wedged
&& (on_floor || held || probe_down(center, LAND_PROBE + hover_offset).is_none())
{
player.wedged = false;
}
let grounded = on_floor || player.wedged;
let mut jumped = false;
let mut knocked = false;
if held || anchored {
// Frozen by the settle hold or the root: no gravity, no momentum. A knockback here is
// dropped, not deferred, and `knocked` stays false so no ack goes out; the reference drops
// one under root too (`0x615c71`), and vmangos sends none to a rooted unit.
player.vel_y = 0.0;
player.horiz_vel = Vec3::ZERO;
} else if let Some(launch) = knockback {
// The knockback: the server aims the jump and we fly it, both axes absolute in world XY
// whatever the facing or keys; `MSG_MOVE_FALL_LAND` ends it, and with it vmangos's
// knockback grace (`MovementAnticheat.cpp:698`). Hover refuses only the keyboard jump
// (`0x7c623a`), and
// vmangos's gate tests stun, root and a live spline, never FALLING (`Unit.cpp:10192`).
player.vel_y = launch.y;
player.launch_vz = launch.y;
player.horiz_vel = Vec3::new(launch.x, 0.0, launch.z);
player.wedged = false;
player.steep_support = false;
jumped = true;
knocked = true;
// The launch plants FORWARD in the direction nibble (`0x617a18`) whatever its speed, so
// even a vertical knockback never steers, and `knock_arc` keeps FORWARD on the wire.
player.arc_dirs_set = true;
player.knock_arc = true;
} else if grounded {
player.vel_y = 0.0;
// Hover refuses the keyboard jump silently, the first test in `CMovement::Jump`
// (`0x7c623a`): the key's replay passes `Jump(1)` (`0x615eba`) and drops a refusal
// (`0x615ec5`), so no `MSG_MOVE_JUMP` and no retry. `wire_jump` is `Jump(0)`, which skips
// the test (`0x7c6236`), so a hover grant can launch the hovering body. The mount flourish
// is diverted upstream in `control` (`0x60dea0`), so hover does not gate it.
if (want_jump && !player.modes.hover) || wire_jump {
player.vel_y = JUMP_SPEED;
player.launch_vz = JUMP_SPEED;
player.wedged = false;
player.steep_support = false;
jumped = true;
// A moving jump is locked to its momentum; a standing one may steer once.
player.arc_dirs_set = moving;
}
} else if !player.airborne_prev {
// A step-off: `StartFalling(0)` launches at exactly 0, which the wire tail sends and the
// FALLINGFAR legs split on; the nibble seeds from the keys.
player.launch_vz = 0.0;
player.arc_dirs_set = moving;
}
// The fall step runs on every airborne frame, the take-off's included: the reference evaluates
// its arc in closed form from `t = 0` at `StartFalling`. Feather fall swaps only the terminal
// speed (`0x7c5d20`). `vel_y` keeps the end-of-step speed that the next frame and the wire
// read; `fall_mean_vy` is the exact rate to move at ([`fall_step`]).
let mut fall_mean_vy = player.vel_y;
if !held && !anchored && (jumped || !grounded) {
let terminal = if player.modes.feather_fall {
FEATHER_TERMINAL_VELOCITY
} else {
TERMINAL_VELOCITY
};
let (end_vy, mean_vy) = fall_step(player.vel_y, dt, terminal);
player.vel_y = end_vy;
fall_mean_vy = mean_vy;
}
let mut air_nudged = false;
// The `!anchored` terms keep a body rooted mid-jump stopped dead instead of coasting.
if knocked {
// The launch owns both axes: a knockback from standing is still `grounded` this frame, and
// the walk arm would hand the horizontal back to the keys.
} else if grounded && !anchored {
player.horiz_vel = input_horiz;
} else if !held && !anchored && moving && !player.arc_dirs_set {
// Air control: one nudge for a jump taken standing, re-seeding the airborne direction
// flags. The reference opens it only while the direction nibble (`+0x40 & 0xf`) is clear
// (`0x7c6afc`, guarding the `arg = 1` openers of `0x7c5a20`/`0x7c5c20`); `nudge_speed` is
// the live `min(walk, run)` (`0x7c4c90(1)`).
player.horiz_vel = dir.normalize_or_zero() * nudge_speed;
air_nudged = true;
// The press sets the nibble, so there is no second steer.
player.arc_dirs_set = true;
}
let pre_move = center;
let (mut climb, mut snap_probe) = (None, None);
if !held && !anchored && grounded && !jumped {
let g = grounded_step(
world,
capsule,
center,
player.horiz_vel,
time.delta(),
Support {
rise: STEP_UP_HEIGHT,
offset: hover_offset,
water: water_floor,
steep: player.steep_support,
},
);
// Only this mover has a fall to elect, so it spends the no-floor drop (`pos.z -= achieved`
// at `0x636e52`) and a ledge's first fall frame starts lower. A walk frame that went
// nowhere writes the `stup` report ([`super::step_probe`]).
let resolved = g.center - Vec3::Y * g.unsupported.unwrap_or(0.0);
super::step_probe::watch(
world,
capsule,
center,
resolved,
player.horiz_vel,
dt,
time.elapsed_secs(),
);
center = resolved;
climb = g.climb;
snap_probe = g.snap;
player.steep_support = g.steep_support;
if let Some(e) = g.ground {
ground_entity = Some(e);
}
} else {
let velocity = if held || anchored {
Vec3::ZERO
} else {
player.horiz_vel + Vec3::Y * fall_mean_vy
};
center = airborne_step(world, capsule, center, velocity, time.delta());
player.steep_support = false;
}
// The wedge rest: falling, yet achieving under [`WEDGE_STALL_RATIO`] of gravity's intent for
// [`WEDGE_STILL_FRAMES`] frames, is a capsule pinched between steep faces with no way down
// (a tree trunk's funnel), so it lands there.
if !held
&& !anchored
&& !grounded
&& !jumped
&& player.vel_y < -WEDGE_MIN_FALL
&& (pre_move.y - center.y) < -player.vel_y * dt * WEDGE_STALL_RATIO
{
player.wedge_still += 1;
if player.wedge_still >= WEDGE_STILL_FRAMES {
player.wedged = true;
player.wedge_still = 0;
player.vel_y = 0.0;
let feet = center - half_h;
benilla_assets::trace::line(
"move",
&format!(
"wedge rest at ({:8.2},{:7.2},{:8.2}) -> landed standing",
feet.x, feet.y, feet.z
),
);
}
} else {
player.wedge_still = 0;
}
// The detecting frame is grounded, so the landing (`MSG_MOVE_FALL_LAND`) goes out this frame.
let mut grounded = grounded || player.wedged;
// The water floor, after the slide: a body landing on water crosses more than [`LAND_PROBE`]
// in a frame. Down is the reference's snap with its hover arm, `z − max(L − 1, 0)`
// (`0x636e52`, `0x636e74`), yielding to a walkable collider, the nearer floor; up is the
// rate-limited climb below (`0x636fa1`). The one push up is ours: the body may not end the
// frame under the surface, which the reference's swept surface never lets it reach, and only
// a body that is not rising is caught, so a hover launch under the waterline flies clear.
// `SetPitch`'s standstill dive kick (`0x7c6f70`) is not built: it serves a walk resolver that
// skips a body at rest, and our classify runs every frame, so dropping the layers is the dive.
if let Some(surface) = water_floor {
let feet = center.y - half_h.y;
let rest = surface + hover_offset;
if feet < surface && player.vel_y <= 0.0 {
center.y = surface + half_h.y;
player.vel_y = 0.0;
player.wedged = false;
grounded = true;
} else if !on_walkable && feet > rest && feet - surface <= ground_reach {
center.y = rest + half_h.y;
player.vel_y = 0.0;
player.wedged = false;
grounded = true;
}
}
// The hover climb, the walk resolver's second pass (`0x636fa1`-`0x6370f1`), which a root's
// early return skips too. The water is one more floor under it, nearest wins.
if hover_offset > 0.0 && !held && !anchored {
let solid = probe_down(center, HOVER_HEIGHT + CAPSULE_HEIGHT).map(|h| h.distance);
let water = water_floor.map(|s| (center.y - half_h.y - s).max(0.0));
if let Some(clearance) = [solid, water].into_iter().flatten().reduce(f32::min) {
if clearance < HOVER_HEIGHT {
center.y += (HOVER_HEIGHT - clearance).min(HOVER_CLIMB_RATE * dt);
player.vel_y = player.vel_y.max(0.0); // climbing, not falling
}
}
}
player.pos = center - half_h;
move_trace::frame(move_trace::Frame {
y_in: pre_move.y - half_h.y,
y_out: player.pos.y,
dx: (center - pre_move).xz().length(),
grounded,
on_walkable,
moving,
vel_y: player.vel_y,
snap: snap_probe,
climb,
anchored,
});
// The mover's own airborne record: a launch frame counts though the body still stands, or the
// next frame reads as a fresh step-off and re-seeds the arc.
player.airborne_prev = !held && !anchored && (jumped || !grounded);
Outcome {
held,
grounded,
jumped,
knocked,
air_nudged,
ground: if grounded && !held {
ground_entity
} else {
None
},
}
}
/// The body facts [`grounded_step`] cannot work out for itself, passed in by its caller.
#[derive(Clone, Copy)]
pub(crate) struct Support {
/// The rise budget, the reference's `H` (`0x617430`), which also scales the certify reach:
/// [`STEP_UP_HEIGHT`] for a player body, [`super::CREATURE_STEP_UP_HEIGHT`] for a creature
/// (`0x5fa550`'s false leg).
pub(crate) rise: f32,
/// How far above the floor the body rests: [`super::HOVER_HEIGHT`] while hovering, else 0.
pub(crate) offset: f32,
/// The liquid floor this frame (Bevy Y), [`water_floor`]'s answer. Liquid is queried, not
/// swept, so every ground question asks this plane as well as the sweep, where the reference's
/// one masked trace sees both (`0x63162e`).
pub(crate) water: Option<f32>,
/// The support entering the frame is a certified steep contact, the reference's `0x4000000`,
/// which opens the step-down's deep reach.
pub(crate) steep: bool,
}
impl Default for Support {
/// A player body on an ordinary frame.
fn default() -> Self {
Self {
rise: STEP_UP_HEIGHT,
offset: 0.0,
water: None,
steep: false,
}
}
}
/// What one [`grounded_step`] resolved.
pub(crate) struct GroundedStep {
/// The resolved capsule centre.
pub(crate) center: Vec3,
/// The walkable floor's collider the snap settled onto; `None` keeps what the caller believed.
pub(crate) ground: Option<Entity>,
/// The height (yd) this frame's pop or cone ride gained.
pub(crate) climb: Option<f32>,
/// On a certified steep contact, the reference's `0x4000000`: riding a cone up, or following a
/// surface down off a ledge. The caller keeps it grounded until a walkable floor takes over.
pub(crate) steep_support: bool,
/// For the trace: the snap's reach and the `(distance, normal.y)` it found.
pub(crate) snap: Option<(f32, Option<(f32, f32)>)>,
/// The no-floor drop, when the snap found nothing and no ride is in progress; not in
/// [`Self::center`]. The reference drops by it, then elects a fall (`0x636e52`), so only
/// [`step`], which has a fall to elect, spends it. The open-loop creature and remote callers
/// hold the server's pose instead, since spending it would ratchet them down through the world.
pub(crate) unsupported: Option<f32>,
}
/// One grounded walk step against the world: step-up, slide, then the step-down snap. The
/// reference runs every mover through one controller (`0x616620`, whose local-player compare at
/// `0x6166a9` gates only a timing budget) and reads a grounded mover's Z off the surface
/// (`0x633840`, `0x6367b0`), so our mover, the remote dead-reckon and the creature path clamp all
/// walk through here; airborne and swimming frames do not.
pub(crate) fn grounded_step(
world: &benilla_world::collision::WorldCollision<'_, '_>,
capsule: &Collider,
center: Vec3,
horiz_vel: Vec3,
dt: std::time::Duration,
support: Support,
) -> GroundedStep {
let surface_offset = support.offset;
let cast = |from: Vec3, disp: Vec3| world.cast_mover(capsule, from, disp, SKIN_WIDTH);
let speed = horiz_vel.length();
let attempt = if speed > 1.0e-6 {
let travel = speed * dt.as_secs_f32();
// The look-ahead is this frame's travel; the advance is the body's certify reach,
// `rise · tan 50°`, or the travel when a long frame covers more.
step_up(
&cast,
center,
horiz_vel / speed,
travel,
travel.max(support.rise * STEP_UP_ADVANCE_PER_YARD),
support.rise,
)
} else {
StepAttempt {
contact: None,
verdict: StepVerdict::NoFace,
}
};
if let Some((point, n)) = attempt.contact {
if benilla_assets::trace::enabled_for("step") {
let feet_y = center.y - CAPSULE_HEIGHT * 0.5;
benilla_assets::trace::line(
"step",
&format!(
"hit ({:8.2},{:7.2},{:8.2}) h={:+.2} n=({:+.2},{:+.2},{:+.2}) {}",
point.x,
point.y,
point.z,
point.y - feet_y,
n.x,
n.y,
n.z,
attempt.verdict
),
);
}
}
// The regime (`0x631be0`, `0x635c00`): the reference's solid is a cone below
// [`FOOT_CONE_HEIGHT`] and a box above it, so a certified obstacle whose blocking edge sits
// inside the cone is ridden up the 61.6° skirt, and one above it pops. The certification is
// the gate, since a wall's capsule contact also sits in the band; and the edge is measured,
// not the certified rise, which grows with the advance.
let ride_to = match (attempt.verdict, attempt.contact) {
(StepVerdict::Commit { landed, .. }, Some((p, _)))
if p.y - (center.y - CAPSULE_HEIGHT * 0.5) <= FOOT_CONE_HEIGHT =>
{
Some(landed.y)
}
_ => None,
};
// The pop is a rise in place, never a lunge: the reference's certify distance is only a sweep
// length (`ebx` at `0x636193`, fed to `0x632ba0` alone), its walk resolver writes position
// only relatively (`0x6367b0`-`0x637140`), and the certified arm rises `(0, 0, H)`
// (`0x635d5b`) before `0x63694d` resumes the heading. The snap then reaches past the rise, as
// the reference keeps `0x4000000` set until its settle has sized the probe (`0x636edf`).
let popped = match attempt.verdict {
StepVerdict::Commit { up, .. } if ride_to.is_none() => Some(up),
_ => None,
};
let start = center + Vec3::Y * popped.unwrap_or(0.0);
let mut rode = false;
let out = world.slide_mover(
capsule,
start,
horiz_vel,
dt,
&MoveAndSlideConfig::default(),
|hit| {
if let Some(ride) = walkable_ride_velocity(**hit.normal, *hit.velocity) {
*hit.velocity = ride;
return MoveAndSlideHitResponse::Accept;
}
// The skirt, on a certified low edge only, up to the certification's landing height;
// the snap takes back any overshoot.
if ride_to.is_some_and(|ceiling| hit.position.y < ceiling) {
if let Some(up) = foot_cone_ride(**hit.normal, *hit.velocity) {
*hit.velocity = up;
rode = true;
return MoveAndSlideHitResponse::Accept;
}
}
if let Some(followed) = steep_contact_shear(**hit.normal, *hit.velocity) {
*hit.velocity = followed;
}
MoveAndSlideHitResponse::Accept
},
);
let mut slid = out.position;
// The step-down snap, the reference's step-vs-fall election (`0x6367b0`): it reaches
// `travel · slope + slack` below, plus `H` on a steep support (`0x636dfc`) or a pop, plus the
// hover offset (`0x633e35`), and settles only onto a walkable floor. A deeper drop is a fall,
// and a body at rest reaches only the slack, so it falls instead of being pulled down.
let d = slid - start;
let reach = d.x.hypot(d.z) * STEP_SLOPE_RATIO
+ STEP_SNAP_SLACK
+ if support.steep || popped.is_some() {
support.rise
} else {
0.0
}
+ surface_offset;
let hit = cast(slid, Vec3::NEG_Y * reach);
// The water is one more candidate within the same reach, flat, nearest wins, as in the
// reference's one masked trace; with no entity it keeps the caller's support.
let water = support.water.and_then(|s| {
let d = slid.y - CAPSULE_HEIGHT * 0.5 - s;
(d <= reach).then_some(d.max(0.0))
});
let floor = match (hit, water) {
(Some(h), Some(w)) if w < h.distance => Some((w, 1.0, None)),
(Some(h), _) => Some((h.distance, h.normal1.y, Some(h.entity))),
(None, Some(w)) => Some((w, 1.0, None)),
(None, None) => None,
};
let snap = Some((reach, floor.map(|(d, n, _)| (d, n))));
let mut ground = None;
let mut steep_support = false;
let mut unsupported = None;
// A ride's height is earned. The reference's cone rests on the edge it climbs, but our capsule
// hangs clear of a vertical riser and the snap would find the floor it is leaving, so mid-ride
// only a floor above the ride's start may end it.
let ride_rise = (slid.y - start.y).max(0.0);
// The descent is capped at a cone's worth a frame, which the reference's finalize is not: it
// descends the whole sweep before classifying (`0x636e45`-`0x636e52`), but its foot cone keeps
// the skirt on the edge, so each sweep is short and the descent comes out at the cone's slope.
// Our capsule hangs clear and would spend the whole drop at once; the cap ports the cone's
// effect. A steep landing stays grounded with the steep bit set, as the reference's multipass
// returns (`0x636f21`), and the deep reach keeps the ground in sight.
let cone_reach = d.x.hypot(d.z) * STEP_SLOPE_RATIO + STEP_SNAP_SLACK;
if let Some((distance, normal_y, entity)) = floor {
let drop = (distance - surface_offset).max(0.0);
let walkable = normal_y >= GROUND_COS;
if !(rode && walkable && drop >= ride_rise) {
slid.y -= drop.min(cone_reach);
if drop > cone_reach {
// Further down than a cone's worth but in sight: a step-down in progress, so stay
// grounded with the deep reach open and finish it over the next frames.
steep_support = true;
} else if walkable {
ground = entity;
} else {
// Support is earned by descending, not by touching: within reach alone would perch
// a motionless body on a steep bank.
steep_support = drop > STEP_SNAP_SLACK;
}
}
} else if !rode {
// Nothing in sight: the reference still descends the whole sweep, then elects a fall.
// Measured here under the same cap, and spent only by a caller with a fall to elect; a
// ride is exempt, since mid-ride the body hangs clear by design.
unsupported = Some((reach - surface_offset).max(0.0).min(cone_reach));
}
GroundedStep {
center: slid,
climb: (rode || popped.is_some()).then_some(slid.y - center.y),
// A ride or a pop that found no walkable floor is held up by the edge it climbs.
steep_support: ((rode || popped.is_some()) && ground.is_none()) || steep_support,
ground,
snap,
unsupported,
}
}
/// One frame of the fall, exactly: the end-of-step vertical speed and the mean speed to move at,
/// whose `mean · dt` is the closed form `v₀·dt − ½g·dt²`. The reference evaluates its arc in closed
/// form from the launch every substep (`+0x7c − D(+0x78)`, `0x7c5e70`), so it cannot drift with
/// the frame rate, and nor can this. A step reaching terminal partway is split at `t_c`, since a
/// clamped mean would under-move it.
pub(crate) fn fall_step(v0: f32, dt: f32, terminal: f32) -> (f32, f32) {
let unclamped = v0 - GRAVITY * dt;
if unclamped >= -terminal {
// Wholly inside the clamp: the mean of the endpoints, exact.
return (unclamped, 0.5 * (v0 + unclamped));
}
if v0 <= -terminal {
// At terminal for the whole step: constant velocity.
return (-terminal, -terminal);
}
// The clamp engages partway: accelerate for `t_c`, then coast the remainder.
let t_c = (v0 + terminal) / GRAVITY;
let displacement = 0.5 * (v0 - terminal) * t_c - terminal * (dt - t_c);
(-terminal, displacement / dt)
}
/// One airborne step against the world: the arc's slide alone, with steep faces sheared
/// ([`steep_contact_shear`]); gravity owns the height and the next ground probe the landing.
/// Shared with the remote dead-reckon's arcs ([`crate::net::motion::remote`]).
pub(crate) fn airborne_step(
world: &benilla_world::collision::WorldCollision<'_, '_>,
capsule: &Collider,
center: Vec3,
velocity: Vec3,
dt: std::time::Duration,
) -> Vec3 {
world
.slide_mover(
capsule,
center,
velocity,
dt,
&MoveAndSlideConfig::default(),
|hit| {
if let Some(followed) = steep_contact_shear(**hit.normal, *hit.velocity) {
*hit.velocity = followed;
}
MoveAndSlideHitResponse::Accept
},
)
.position
}
/// The overhang line, just below zero: an axis-aligned wall's normal has `y = ±0` by rounding, and
/// both signs must read as a wall.
const OVERHANG_EPS: f32 = -1.0e-4;
/// A face the mover meets side-on: too steep to stand on, not a ceiling.
fn is_steep_face(ny: f32) -> bool {
(OVERHANG_EPS..GROUND_COS).contains(&ny)
}
/// The ride up a walkable slope: keep the horizontal velocity and set the vertical so the motion
/// lies in the plane. The reference's walk step is a horizontal distance (`0x6367b0`), so no
/// walkable slope slows or bends it, where a plane clip would cut it to `h·cos²θ`.
fn walkable_ride_velocity(n: Vec3, v: Vec3) -> Option<Vec3> {
if n.y < GROUND_COS || v.dot(n) >= 0.0 {
return None;
}
// `n.y ≥ cos 50°` keeps the division safe; a prior facet's vertical is replaced, not stacked.
Some(Vec3::new(v.x, -(v.x * n.x + v.z * n.z) / n.y, v.z))
}
/// The foot cone's ride: the reference's solid is a box over a cone whose bevels rise
/// `radius · 1.8494` from the foot (`0x631440`), so a low edge meets the skirt and the slide runs
/// the body up it. The lift is `0x635c00`'s `1.8494 · cosθ · len`, the closing speed times the
/// slope, and it sets the vertical rather than adding to it. Whether the body may climb at all is
/// the caller's certification, since a tall wall's contact sits in the cone band too.
fn foot_cone_ride(n: Vec3, v: Vec3) -> Option<Vec3> {
if !is_steep_face(n.y) {
return None;
}
// Steepness bounds the horizontal part below by sin 50°, so the normalize is safe.
let h = Vec3::new(n.x, 0.0, n.z).normalize();
let into = -(v.x * h.x + v.z * h.z);
if into <= 0.0 {
return None;
}
Some(Vec3::new(v.x, into * STEP_SLOPE_RATIO, v.z))
}
/// The steep-contact response: a horizontal push-out, the descent untouched. The reference's
/// `0x635090` writes only an x/y push-out along the normal's horizontal part (the vertical is
/// zeroed at `0x635166`), which `0x635600` adds to `Δx`/`Δy` alone, so
/// `v' = v − (v·n)/|n_h|² · n_h`. No contact can add lift, and a fall along a steep face keeps its
/// full rate, sliding down it faster than an orthogonal clip would. [`is_steep_face`] bounds
/// `|n_h|` below by `sin 50°`, so the reference's degeneracy guard is not needed.
fn steep_contact_shear(n: Vec3, v: Vec3) -> Option<Vec3> {
if !is_steep_face(n.y) {
return None;
}
let vn = v.dot(n);
// A contact the motion is leaving is not this response's business.
if vn >= 0.0 {
return None;
}
let h = Vec3::new(n.x, 0.0, n.z);
Some(v - (vn / h.length_squared()) * h)
}
/// What one step-up attempt decided; [`super::step_probe`] reads it back off each rung it tries.
#[derive(Clone, Copy, Debug)]
pub(crate) enum StepVerdict {
/// Nothing steep and opposing within the look-ahead: not a step-up frame.
NoFace,
/// The rise found no headroom above the capsule.
NoHeadroom,
/// The settle probe found no floor at all under the advanced point.
NoFloor { up: f32, fwd: f32 },
/// The settle found a floor, but one too steep to stand on (`ny` under [`GROUND_COS`]).
SteepFloor {
up: f32,
fwd: f32,
dist: f32,
ny: f32,
},
/// The maneuver gained no height (a graze, a too-tall wall, a pinch's gap floor): the plain
/// slide keeps the frame.
NetZero {
up: f32,
fwd: f32,
dist: f32,
ny: f32,
dy: f32,
},
/// Certified: the obstacle can be cleared, by a ride or a pop of `up` ([`grounded_step`]).
/// `landed` is the settle's floor a full advance downrange, for the trace only.
Commit {
landed: Vec3,
up: f32,
fwd: f32,
dy: f32,
},
}
impl std::fmt::Display for StepVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
StepVerdict::NoFace => write!(f, "no opposing face"),
StepVerdict::NoHeadroom => write!(f, "up=0.00 NO-HEADROOM -> slide"),
StepVerdict::NoFloor { up, fwd } => {
write!(f, "up={up:.2} fwd={fwd:.2} down=miss NO-FLOOR -> slide")
}
StepVerdict::SteepFloor { up, fwd, dist, ny } => write!(
f,
"up={up:.2} fwd={fwd:.2} down=(d={dist:.2} ny={ny:+.2}) STEEP-FLOOR -> slide"
),
StepVerdict::NetZero {
up,
fwd,
dist,
ny,
dy,
} => write!(
f,
"up={up:.2} fwd={fwd:.2} down=(d={dist:.2} ny={ny:+.2}) dy={dy:+.3} NET-ZERO -> slide"
),
StepVerdict::Commit { up, fwd, dy, .. } => {
write!(f, "up={up:.2} fwd={fwd:.2} dy={dy:+.3} -> COMMIT")
}
}
}
}
/// One step-up attempt: the face that triggered it and the verdict.
pub(crate) struct StepAttempt {
/// The face's contact point and authored normal; `None` when there was nothing to step.
pub(crate) contact: Option<(Vec3, Vec3)>,
pub(crate) verdict: StepVerdict,
}
/// The step-up certification, a standard kinematic-controller probe rather than the reference
/// resolver's: against a steep opposing face within `look`, rise by the free headroom (at most
/// `rise`), advance by `advance` along the input, and settle by the snap's reach onto a walkable
/// floor that is higher. A graze or a wall taller than `rise` lands no higher, and a pinch between
/// trunks only on steep faces, so none commits and the plain slide keeps the frame.
/// [`super::step_probe`] sweeps `advance` to find where a step would clear.
pub(crate) fn step_up(
cast: &impl Fn(Vec3, Vec3) -> Option<MoveHitData>,
center: Vec3,
dir_h: Vec3,
look: f32,
advance: f32,
rise: f32,
) -> StepAttempt {
let none = |verdict| StepAttempt {
contact: None,
verdict,
};
// A steep, non-overhanging face opposing the motion within `look`. The reference has no
// incidence gate; a graze nets zero in the settle.
let Some(ahead) = cast(center, dir_h * look) else {
return none(StepVerdict::NoFace);
};
let n = ahead.normal1;
if n.y >= GROUND_COS || n.y < OVERHANG_EPS || n.dot(dir_h) >= 0.0 {
return none(StepVerdict::NoFace);
}
let at = |verdict| StepAttempt {
contact: Some((ahead.point1, n)),
verdict,
};
// Rise: the free headroom, at most `rise`.
let up = cast(center, Vec3::Y * rise).map_or(rise, |h| h.distance);
if up < 1e-3 {
return at(StepVerdict::NoHeadroom);
}
// Advance: along the input dir, swept at the raised height.
let raised = center + Vec3::Y * up;
let fwd = cast(raised, dir_h * advance).map_or(advance, |h| h.distance);
let over = raised + dir_h * fwd;
// Settle: the rise undone plus the snap's reach, onto a walkable floor only.
let reach = up + advance * STEP_SLOPE_RATIO + STEP_SNAP_SLACK;
let Some(down) = cast(over, Vec3::NEG_Y * reach) else {
return at(StepVerdict::NoFloor { up, fwd });
};
let (dist, ny) = (down.distance, down.normal1.y);
if ny < GROUND_COS {
return at(StepVerdict::SteepFloor { up, fwd, dist, ny });
}
let landed = over + Vec3::NEG_Y * dist;
let dy = landed.y - center.y;
// Commit only a landing that gained height: a net-zero one belongs to the slide, whose
// deflection is what sliding along a fence is.
if dy <= 0.05 {
return at(StepVerdict::NetZero {
up,
fwd,
dist,
ny,
dy,
});
}
at(StepVerdict::Commit {
landed,
up,
fwd,
dy,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::player::state::STEP_UP_ADVANCE;
use crate::player::AIR_NUDGE_SPEED;
use crate::player::CAPSULE_RADIUS;
use bevy::ecs::system::RunSystemOnce;
/// A Stormwind Trade District kerb: a 0.28 yd sidewalk behind a ~61° bevel, profiled from a
/// `stup` scan at the spot.
fn world_with_kerb() -> App {
const PROFILE: [(f32, f32); 4] = [(-2.0, 0.0), (0.29, 0.0), (0.446, 0.28), (3.0, 0.28)];
world_from_profile(&PROFILE)
}
/// A headless world from a `(x, y)` polyline extruded across `z`, wound so every face's
/// authored normal points up and back at the approaching body: faces are one-sided.
fn world_from_profile(profile: &[(f32, f32)]) -> App {
const W: f32 = 3.0;
let mut app = App::new();
// `WorldCollision` needs the mover's trace exclusions, which the world plugins insert.
app.init_resource::<benilla_world::collision::MoverTraceExclusions>();
// avian's collider backend reads `Assets<Mesh>` and `SceneSpawner` even without meshes.
app.add_plugins((
MinimalPlugins,
bevy::transform::TransformPlugin,
bevy::asset::AssetPlugin::default(),
bevy::scene::ScenePlugin,
PhysicsPlugins::new(bevy::app::PostUpdate),
));
app.init_asset::<Mesh>();
let (mut verts, mut tris) = (Vec::new(), Vec::new());
for w in profile.windows(2) {
let (&(x0, y0), &(x1, y1)) = (&w[0], &w[1]);
let b = verts.len() as u32;
verts.extend([
Vec3::new(x0, y0, -W),
Vec3::new(x1, y1, -W),
Vec3::new(x1, y1, W),
Vec3::new(x0, y0, W),
]);
// (a, c, b) / (a, d, c): normal = (-dy, dx, 0), up for the flats and up-and-back for
// the riser; the reverse winding would block nothing.
tris.extend([[b, b + 2, b + 1], [b, b + 3, b + 2]]);
}
app.world_mut().spawn((
RigidBody::Static,
Collider::trimesh(verts, tris),
Transform::default(),
));
app.update(); // one frame builds Position/Rotation and the spatial-query trees
app
}
fn player_capsule() -> Collider {
Collider::capsule(CAPSULE_RADIUS, CAPSULE_HEIGHT - 2.0 * CAPSULE_RADIUS)
}
/// Walk into the kerb until it stops the body, then run one step-up attempt at `advance`.
fn step_at(advance: f32) -> StepVerdict {
world_with_kerb()
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let cast =
|from: Vec3, disp: Vec3| world.cast_mover(&capsule, from, disp, SKIN_WIDTH);
let start = Vec3::new(-1.0, CAPSULE_HEIGHT * 0.5, 0.0);
let run = cast(start, Vec3::X).map_or(1.0, |h| h.distance);
let center = start + Vec3::X * run;
step_up(
&cast,
center,
Vec3::X,
TRAVEL_60FPS,
advance,
STEP_UP_HEIGHT,
)
.verdict
})
.unwrap()
}
/// One frame's travel at a run (7.0 yd/s) and 60 fps.
const TRAVEL_60FPS: f32 = 7.0 / 60.0;
/// `walk_profile` on the kerb.
fn walk_kerb(start_y: f32, frames: usize) -> Vec<Row> {
walk_profile(world_with_kerb(), start_y, frames)
}
/// One frame of a walked fixture: `(feet height, climb, steep support, walkable ground)`.
type Row = (f32, Option<f32>, bool, bool);
/// Walk into `world` from `(-1, start_y)` along +X, then run `frames` whole grounded steps.
fn walk_profile(world: App, start_y: f32, frames: usize) -> Vec<Row> {
walk_from(world, Vec3::new(-1.0, start_y, 0.0), Vec3::X, frames)
}
/// Feet at `start`, walk in `dir` until blocked (a yard at most), then `frames` grounded steps.
fn walk_from(mut world: App, start: Vec3, dir: Vec3, frames: usize) -> Vec<Row> {
world
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let cast =
|from: Vec3, disp: Vec3| world.cast_mover(&capsule, from, disp, SKIN_WIDTH);
let start = start + Vec3::Y * (CAPSULE_HEIGHT * 0.5);
let run = cast(start, dir).map_or(1.0, |h| h.distance);
let mut center = start + dir * run;
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
// The steep bit carries between frames, as in the mover, gating the deep reach.
let mut support = Support::default();
(0..frames)
.map(|_| {
let g = grounded_step(&world, &capsule, center, dir * 7.0, dt, support);
center = g.center;
support.steep = g.steep_support;
(
center.y - CAPSULE_HEIGHT * 0.5,
g.climb,
g.steep_support,
g.ground.is_some(),
)
})
.collect::<Vec<_>>()
})
.unwrap()
}
/// Booty Bay's 0.40 yd vertical riser onto a ~22° ramp, at WoW `(-14314.3, 466.2, 18.5)`,
/// leaning 0.002 yd so the sign of a `±0` normal cannot decide the test.
fn world_with_vertical_step() -> App {
const PROFILE: [(f32, f32); 4] = [(-2.0, 0.0), (0.30, 0.0), (0.302, 0.40), (3.0, 1.48)];
world_from_profile(&PROFILE)
}
#[test]
fn a_vertical_step_is_climbed_not_stalled() {
// The 0.40 yd rise rides, and a capsule on a vertical riser hangs clear of the street, so
// the snap must not pull the ride back down.
let frames = walk_profile(world_with_vertical_step(), 0.0, 6);
let top = frames.last().unwrap().0;
assert!(
top > 0.40,
"the body must finish on the step, not stalled below it: {frames:?}"
);
for w in frames.windows(2) {
assert!(
w[1].0 >= w[0].0 - 1.0e-3,
"the climb must never go backwards: {frames:?}"
);
}
assert!(
frames[0].0 < 0.40,
"…and it is still a ride, not a one-frame pop: {frames:?}"
);
}
#[test]
fn a_tall_step_behind_an_unwalkable_face_is_climbed() {
// A Stormwind step at WoW `(-8427.4, 607.4, 95.1)`: a 0.91 yd top behind a 66° bevel most
// of a yard deep, so the settle must reach past the bevel to find it.
const P: [(f32, f32); 4] = [(-2.0, -0.02), (0.28, -0.02), (0.694, 0.91), (3.0, 0.91)];
let frames = walk_profile(world_from_profile(&P), -0.02, 8);
assert!(
frames.last().unwrap().0 > 0.88,
"the body must finish on the 0.91 yd top: {frames:?}"
);
for w in frames.windows(2) {
assert!(
w[1].0 >= w[0].0 - 1.0e-3,
"the climb must never go backwards: {frames:?}"
);
}
// The blocking edge is low on the bevel, inside the cone, so this rides rather than pops.
assert!(
frames[0].0 < 0.3,
"a 0.91 yd gain in one frame is the teleport the ride avoids: {frames:?}"
);
}
#[test]
fn stepping_off_a_kerb_follows_the_surface_down() {
// Walking off the kerb the probe finds only its ~61° riser. Every frame must stay
// supported, since the caller turns an unsupported frame into a fall.
let frames = walk_from(world_with_kerb(), Vec3::new(1.6, 0.28, 0.0), Vec3::NEG_X, 8);
for (i, f) in frames.iter().enumerate() {
assert!(
f.2 || f.3,
"frame {i} left the surface entirely — that is the dive: {frames:?}"
);
}
assert!(
frames.last().unwrap().0 < 0.05,
"the walk-off must reach the street: {frames:?}"
);
}
#[test]
fn a_face_steeper_than_the_cone_still_descends_what_it_can() {
// A 66° face (`ny ≈ 0.41`) walked off at a run: past the lip the mover descends what it
// can follow, rather than holding its height until it falls.
const P: [(f32, f32); 4] = [(-2.0, 0.0), (0.60, 0.0), (1.0, -0.91), (3.0, -0.91)];
// Nothing blocks the approach, which then moves a full yard; the start allows for it.
let frames = walk_from(
world_from_profile(&P),
Vec3::new(-0.6, 0.0, 0.0),
Vec3::X,
5,
);
let cone = TRAVEL_60FPS * STEP_SLOPE_RATIO;
let biggest = frames
.windows(2)
.map(|w| w[0].0 - w[1].0)
.fold(frames[0].0.abs(), f32::max);
assert!(
biggest > cone * 0.5,
"some frame past the lip must follow the face down rather than hold height: \
{frames:?} (best drop {biggest:.3}, a cone's worth is {cone:.3})"
);
}
#[test]
fn a_ledge_deeper_than_the_probe_is_a_fall_not_an_absorbed_step() {
// A 1.2 yd sheer drop, deeper than the snap reaches: the body leaves the ground before the
// bottom, as the reference's does, instead of absorbing the drop in one grounded frame.
const P: [(f32, f32); 4] = [(-2.0, 0.0), (0.60, 0.0), (0.62, -1.20), (3.0, -1.20)];
let frames = walk_from(
world_from_profile(&P),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::X,
12,
);
// A frame's descent is the slide's plus the snap's, each at most a cone's reach.
let bound = 2.0 * (TRAVEL_60FPS * STEP_SLOPE_RATIO + STEP_SNAP_SLACK);
let worst = frames
.windows(2)
.map(|w| w[0].0 - w[1].0)
.fold(0.0_f32, f32::max);
assert!(
worst <= bound,
"no frame may drop more than {bound:.3} yd; worst was {worst:.3}\n{frames:#?}"
);
}
#[test]
fn an_idle_body_is_not_pulled_down_through_open_air() {
// Standing still the snap reaches only the slack, so a body a yard up keeps its height.
const P: [(f32, f32); 2] = [(-2.0, 0.0), (3.0, 0.0)];
let (dy, ground) = world_from_profile(&P)
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
let center = Vec3::new(0.0, CAPSULE_HEIGHT * 0.5 + 1.0, 0.0);
let g = grounded_step(&world, &capsule, center, Vec3::ZERO, dt, Support::default());
(g.center.y - center.y, g.ground.is_some())
})
.unwrap();
assert!(!ground, "a body a yard up is standing on nothing");
assert!(
dy.abs() <= STEP_SNAP_SLACK * 2.0,
"it must keep its height and fall, not be snapped a yard down: dy={dy:+.3}"
);
}
#[test]
fn a_step_up_never_outruns_the_frame_it_happens_in() {
// The reference's walk resolver holds `L / t_remaining` across every hit (`0x6367b0`), so a
// pop never lunges. A Goldshire table: a slab from 0.62 to 0.737 over legs recessed past a
// body radius, so the contact sits above the cone and the step pops rather than rides.
const P: [(f32, f32); 6] = [
(-2.0, 0.0),
(0.95, 0.0),
(0.95, 0.62),
(0.60, 0.62),
(0.60, 0.737),
(3.0, 0.737),
];
let track = world_from_profile(&P)
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
let mut center = Vec3::new(-1.0, CAPSULE_HEIGHT * 0.5, 0.0);
let mut support = Support::default();
let mut rows = Vec::new();
for _ in 0..24 {
let g = grounded_step(&world, &capsule, center, Vec3::X * 7.0, dt, support);
rows.push((g.center.x - center.x, g.center.y - CAPSULE_HEIGHT * 0.5));
center = g.center;
support.steep = g.steep_support;
}
rows
})
.unwrap();
let worst = track.iter().map(|r| r.0).fold(0.0_f32, f32::max);
assert!(
worst <= TRAVEL_60FPS + 1.0e-3,
"no frame may travel further than the {TRAVEL_60FPS:.3} yd it asked to walk; worst \
was {worst:.3}\n{track:#?}"
);
let top = track.last().expect("frames").1;
assert!(
top > 0.70,
"and it still has to get onto the 0.737 yd table: ended at {top:.3}\n{track:#?}"
);
}
#[test]
fn stepping_off_a_fence_hugs_the_edge_down_instead_of_dropping_at_once() {
// A 0.72 yd Goldshire fence with a ~53° chamfered lip. No frame may descend more than a
// cone's worth, and none may lose support: a cap alone would fall one frame later.
const P: [(f32, f32); 5] = [
(-2.0, 0.72),
(0.60, 0.72),
(0.69, 0.60),
(0.692, 0.0),
(3.0, 0.0),
];
let frames = walk_from(
world_from_profile(&P),
Vec3::new(-1.0, 0.72, 0.0),
Vec3::X,
14,
);
let cone = TRAVEL_60FPS * STEP_SLOPE_RATIO + STEP_SNAP_SLACK;
let worst = frames
.windows(2)
.map(|w| w[0].0 - w[1].0)
.fold(0.0_f32, f32::max);
assert!(
worst <= cone + 1.0e-3,
"the step-down must take at most a cone's worth ({cone:.3}) a frame; worst was \
{worst:.3}\n{frames:#?}"
);
let air = frames
.iter()
.position(|&(_, _, steep, walk)| !steep && !walk);
assert!(
air.is_none(),
"and it must stay on the surface the whole way down, not fall the rest: first \
unsupported frame {air:?}\n{frames:#?}"
);
let bottom = frames.last().expect("frames").0;
assert!(
bottom < 0.05,
"and it does have to get all the way down: ended at {bottom:.3}\n{frames:#?}"
);
}
#[test]
fn a_frame_that_finds_nothing_still_spends_only_one_cone() {
// The no-hit leg with the steep bit set and the floor beyond even the deep reach, which a
// profiled fence cannot reach: its riser stays under the capsule.
const P: [(f32, f32); 2] = [(-2.0, 0.0), (3.0, 0.0)];
let dy = world_from_profile(&P)
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
let center = Vec3::new(0.0, CAPSULE_HEIGHT * 0.5 + 1.6, 0.0);
let g = grounded_step(
&world,
&capsule,
center,
Vec3::X * 7.0,
dt,
Support {
steep: true,
..Support::default()
},
);
g.center.y - center.y
})
.unwrap();
let cone = TRAVEL_60FPS * STEP_SLOPE_RATIO + STEP_SNAP_SLACK;
assert!(
-dy <= cone + 1.0e-3,
"a frame that found nothing may still spend only a cone's worth ({cone:.3}); it spent \
{:.3}",
-dy
);
}
#[test]
fn a_motionless_body_is_never_perched_on_a_steep_slope() {
// A 60° bank, where a standing body's cone bound is the slack alone.
const P: [(f32, f32); 3] = [(-2.0, 0.0), (0.0, 0.0), (3.0, -5.196)];
let out = world_from_profile(&P)
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
let mut center = Vec3::new(0.6, CAPSULE_HEIGHT * 0.5 - 1.039, 0.0);
let mut rows = Vec::new();
let mut support = Support::default();
for i in 0..4 {
let v = if i < 2 { Vec3::X * 7.0 } else { Vec3::ZERO };
let g = grounded_step(&world, &capsule, center, v, dt, support);
center = g.center;
support.steep = g.steep_support;
rows.push((v.length() > 0.0, g.steep_support));
}
rows
})
.unwrap();
for (moving, supported) in out {
assert!(
moving || !supported,
"a motionless body must never hold a steep face — it slides off"
);
}
}
#[test]
fn an_exactly_vertical_riser_still_certifies() {
// An axis-aligned riser's normal has `y = ±0` by rounding; either sign must climb.
const PROFILE: [(f32, f32); 4] = [(-2.0, 0.0), (0.30, 0.0), (0.30, 0.40), (3.0, 1.48)];
let frames = walk_profile(world_from_profile(&PROFILE), 0.0, 6);
assert!(
frames.last().unwrap().0 > 0.40,
"an exactly-vertical riser must climb exactly like a tilted one: {frames:?}"
);
}
#[test]
fn the_kerb_is_ridden_up_its_skirt_never_popped() {
// A 0.28 yd kerb is inside the foot cone: it must reach the tread, and not in one frame.
let frames = walk_kerb(0.0, 4);
let arrived = frames
.iter()
.position(|&(y, _, _, _)| (y - 0.28).abs() < 0.03)
.expect("the ride should put the feet on the 0.28 yd tread");
assert!(
arrived > 0,
"arriving on the very first frame is the pop, not a ride: {frames:?}"
);
assert!(
frames[0].2,
"the first frame is mid-ride and must report itself grounded: {frames:?}"
);
assert!(
frames[0].0 > 0.0 && frames[0].0 < 0.28,
"the first frame should be part-way up the skirt, got {:+.3}",
frames[0].0
);
}
#[test]
fn a_wall_is_never_ridden() {
// The kerb read from 2 yd below is a 2.3 yd wall, whose contact also sits in the cone band.
for (y, climb, riding, _) in walk_kerb(-2.0, 4) {
assert!(!riding, "a wall must never start a cone ride");
assert!(climb.is_none(), "a wall must never register a climb");
assert!(
y < -2.0 + 0.05,
"the body must not rise against a wall, feet at {y:+.3}"
);
}
}
#[test]
fn the_cone_ride_gains_the_reference_slope() {
// `0x635c00`'s `T = 1.8494 · cosθ · len`: head-on, the slope times the speed…
let head_on = foot_cone_ride(Vec3::new(-1.0, 0.0, 0.0), Vec3::X * 7.0).unwrap();
assert!(
(head_on.y - 7.0 * STEP_SLOPE_RATIO).abs() < 1e-4,
"{head_on:?}"
);
assert_eq!(
head_on.x, 7.0,
"horizontal speed is never touched by a ride"
);
// …and at 60° off the face, exactly cos 60° of it.
let oblique = foot_cone_ride(
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(7.0 * 0.5, 0.0, 7.0 * 0.866_025_4),
)
.unwrap();
assert!(
(oblique.y - 7.0 * 0.5 * STEP_SLOPE_RATIO).abs() < 1e-3,
"{oblique:?}"
);
}
#[test]
fn the_cone_ride_declines_what_is_not_its_business() {
let v = Vec3::X * 7.0;
// A walkable face already had its ride ([`walkable_ride_velocity`])…
assert!(foot_cone_ride(Vec3::new(-0.5, 0.866, 0.0).normalize(), v).is_none());
// …an overhang is a ceiling, not a skirt…
assert!(foot_cone_ride(Vec3::new(-0.5, -0.866, 0.0).normalize(), v).is_none());
// …and a face we are moving away from is not in the way at all.
assert!(foot_cone_ride(Vec3::new(1.0, 0.0, 0.0), v).is_none());
}
#[test]
fn the_kerb_is_out_of_reach_of_one_frames_travel() {
// At one frame's travel the settle lands on the bevel, which the walkable gate refuses.
let v = step_at(TRAVEL_60FPS);
assert!(
matches!(v, StepVerdict::SteepFloor { .. }),
"one frame's travel should still be over the bevel, got {v}"
);
}
#[test]
fn a_body_scaled_advance_climbs_the_kerb() {
// …and at the certify advance it is over the tread, with `dy` the kerb's height.
let v = step_at(STEP_UP_ADVANCE);
let StepVerdict::Commit { dy, .. } = v else {
panic!("a body-radius advance should reach the tread, got {v}");
};
assert!(
(dy - 0.28).abs() < 0.03,
"should land on the 0.28 yd tread, gained {dy:+.3}"
);
}
#[test]
fn the_advance_never_climbs_past_the_rise_ceiling() {
// The kerb read from 2 yd below is a wall taller than the rise budget: it clips the raised
// sweep, so the settle lands back on the origin floor.
let v = world_with_kerb()
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let cast =
|from: Vec3, disp: Vec3| world.cast_mover(&capsule, from, disp, SKIN_WIDTH);
let start = Vec3::new(-1.0, CAPSULE_HEIGHT * 0.5 - 2.0, 0.0);
let run = cast(start, Vec3::X).map_or(1.0, |h| h.distance);
step_up(
&cast,
start + Vec3::X * run,
Vec3::X,
TRAVEL_60FPS,
STEP_UP_ADVANCE,
STEP_UP_HEIGHT,
)
.verdict
})
.unwrap();
assert!(
!matches!(v, StepVerdict::Commit { .. }),
"a face above the rise ceiling must never certify, got {v}"
);
}
/// Outward normal of a face rising toward +x, tilted `deg` from horizontal.
fn face(deg: f32) -> Vec3 {
let r = deg.to_radians();
Vec3::new(-r.sin(), r.cos(), 0.0)
}
#[test]
fn a_walkable_ramp_rides_at_full_horizontal_speed() {
// A plane clip would cut 45° uphill to `h·cos²45°` = 3.5.
let n = face(45.0);
let v = Vec3::new(7.0, 0.0, 0.0);
let ride = walkable_ride_velocity(n, v).expect("must ride");
assert_eq!((ride.x, ride.z), (7.0, 0.0));
assert!(ride.y > 0.0);
assert!(ride.dot(n).abs() < 1e-6);
}
#[test]
fn a_diagonal_approach_is_not_deflected() {
let v = Vec3::new(5.0, 0.0, 5.0);
let ride = walkable_ride_velocity(face(40.0), v).expect("must ride");
assert_eq!((ride.x, ride.z), (5.0, 5.0));
}
#[test]
fn a_prior_facet_ride_is_recomputed_not_stacked() {
// The incoming 3.0 is a prior facet's ride vertical.
let n = face(45.0);
let ride = walkable_ride_velocity(n, Vec3::new(7.0, 3.0, 0.0)).expect("must ride");
assert_eq!((ride.x, ride.z), (7.0, 0.0));
assert!(ride.dot(n).abs() < 1e-6);
}
#[test]
fn steep_flat_and_receding_planes_never_ride() {
let push = Vec3::new(7.0, 0.0, 0.0);
assert!(walkable_ride_velocity(face(60.0), push).is_none());
assert!(walkable_ride_velocity(Vec3::Y, push).is_none());
assert!(walkable_ride_velocity(face(40.0), -push).is_none());
}
#[test]
fn the_ride_covers_the_walkable_range_up_to_the_gate() {
let v = Vec3::new(7.0, 0.0, 0.0);
let ride = walkable_ride_velocity(face(49.9), v).expect("must ride");
assert_eq!((ride.x, ride.z), (7.0, 0.0));
assert!(ride.y <= 7.0 * 50.0_f32.to_radians().tan() + 1e-3);
assert!(walkable_ride_velocity(face(50.1), v).is_none());
assert!(steep_contact_shear(face(50.1), v).is_some());
}
#[test]
fn walking_into_a_steep_face_spends_the_push_along_it() {
let v = Vec3::new(7.0, 0.0, 0.0);
let out = steep_contact_shear(face(60.0), v).expect("must strip");
assert!(out.x.abs() < 1e-6 && out.y == 0.0);
}
#[test]
fn a_fall_against_a_face_keeps_the_whole_of_its_descent() {
// Falling at 4.9 yd/s into a 55° face with a run held into it, an orthogonal clip keeps ~1%
// of the descent, which the wedge rest would read as a landing.
let (n, v) = (face(55.0), Vec3::new(7.0, -4.9, 0.0));
let orthogonal = v - v.dot(n) * n;
assert!(
orthogonal.y > -0.2,
"the orthogonal clip must be the near-cancel it was: {:.3}",
orthogonal.y
);
let out = steep_contact_shear(n, v).expect("a steep contact responds");
assert!(
(out.y - v.y).abs() < 1.0e-4,
"the descent passes through untouched: {:.3} vs {:.3}",
out.y,
v.y
);
// The horizontal is `cot θ` per unit of drop, downhill; the push into the face cancels.
let cot = 1.0 / 55.0_f32.to_radians().tan();
assert!(
(out.x - v.y * cot).abs() < 1.0e-3,
"downhill drift must be cot55° = {cot:.3} per unit of drop: {:.3}",
out.x
);
// The residual lies in the plane, so nothing further is clipped off it.
assert!(out.dot(n).abs() < 1.0e-4, "Δ'·A = 0: {:.5}", out.dot(n));
}
#[test]
fn a_plumb_fall_is_carried_down_the_face_at_full_speed() {
// A plumb fall has no push to remove, yet following the surface is horizontal motion.
let out = steep_contact_shear(face(60.0), Vec3::new(0.0, -10.0, 0.0))
.expect("a plumb fall still follows the surface");
assert!(
(out.y + 10.0).abs() < 1.0e-4,
"full free-fall rate: {:.3}",
out.y
);
let cot = 1.0 / 60.0_f32.to_radians().tan();
assert!(
(out.x + 10.0 * cot).abs() < 1.0e-3,
"downhill: {:.3}",
out.x
);
// A contact the motion is leaving is not this response's: no push-out, no drift.
assert!(steep_contact_shear(face(60.0), Vec3::new(7.0, 20.0, 0.0)).is_none());
}
#[test]
fn a_rising_jump_keeps_its_own_lift() {
let v = Vec3::new(7.0, 8.0, 0.0);
let out = steep_contact_shear(face(60.0), v).expect("boost must strip");
assert!((out.y - v.y).abs() < 1e-6);
let n = face(60.0);
// …and the true plane no longer opposes what is left, so nothing further is clipped.
assert!(out.dot(n) >= -1e-6);
}
/// The three gates of `0x6315f0`; the pitch gate is strict, so −37.0° exactly sinks.
#[test]
fn the_water_is_geometry_only_while_all_three_gates_hold() {
const SURFACE: f32 = 12.5;
let eps = 1.0e-4;
assert_eq!(water_floor(true, false, 0.0, Some(SURFACE)), Some(SURFACE));
assert_eq!(
water_floor(true, false, WATER_WALK_PITCH_FLOOR + eps, Some(SURFACE)),
Some(SURFACE),
"just shallower than -37 deg still walks on water"
);
assert_eq!(
water_floor(true, false, WATER_WALK_PITCH_FLOOR, Some(SURFACE)),
None,
"-37.0 deg exactly is EXCLUDED — `0x63162c`'s jne reads ZF"
);
assert_eq!(
water_floor(true, false, WATER_WALK_PITCH_FLOOR - eps, Some(SURFACE)),
None,
"aim past -37 deg and the liquid layers leave the mask: the dive"
);
// The arm has no upper bound.
assert_eq!(
water_floor(true, false, 1.5, Some(SURFACE)),
Some(SURFACE),
"the arm gates the nose-down half only"
);
// Swimming excludes it (`0x631617`), and so does not having the aura.
assert_eq!(water_floor(true, true, 0.0, Some(SURFACE)), None);
assert_eq!(water_floor(false, false, 0.0, Some(SURFACE)), None);
assert_eq!(water_floor(true, false, 0.0, None), None);
}
/// Levitate's hover and water walking compose: the finalize probe's trace (`0x632d29`) is
/// headed by the mask arm (`0x6320a0`), so it measures to the water and the body rests a yard
/// over it, reached at the climb rate (`0x636fa1`) from below and stopped there from above.
#[test]
fn a_hovering_water_walker_floats_a_yard_over_the_waterline() {
const SURFACE: f32 = 0.0;
// Open water: nothing but the liquid plane can hold the body up.
const OPEN: [(f32, f32); 2] = [(-3.0, -40.0), (3.0, -40.0)];
let (first, rest, dropped) = world_from_profile(&OPEN)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let levitate = super::super::state::MoveModes {
water_walking: true,
hover: true,
..Default::default()
};
let idle = |player: &mut Player, frames: usize| {
for _ in 0..frames {
step(
player,
&time,
&world,
&capsule,
false,
Vec3::ZERO,
7.0,
false,
false,
None,
Some(SURFACE),
AIR_NUDGE_SPEED,
);
}
player.pos.y
};
let mut player = Player {
modes: levitate,
pos: Vec3::new(0.0, SURFACE, 0.0),
..Default::default()
};
let first = idle(&mut player, 1);
let rest = idle(&mut player, 60);
let mut faller = Player {
modes: levitate,
pos: Vec3::new(0.0, SURFACE + 8.0, 0.0),
..Default::default()
};
let dropped = idle(&mut faller, 240);
(first, rest, dropped)
})
.unwrap();
assert!(
first > SURFACE && first - SURFACE < HOVER_HEIGHT * 0.5,
"the rise off the waterline is rate-limited, not a pop: {first} after one frame"
);
assert!(
(rest - (SURFACE + HOVER_HEIGHT)).abs() < 1.0e-3,
"a hovering water-walker rests exactly {HOVER_HEIGHT} yd over the surface: {rest}"
);
assert!(
(dropped - (SURFACE + HOVER_HEIGHT)).abs() < 1.0e-3,
"a hovering water-walker dropped in from above stops on the hover line: {dropped}"
);
}
/// A wader in liquid too shallow to swim is lifted onto the surface, which the reference's
/// swept surface keeps a body on top of.
#[test]
fn water_walking_lifts_a_wader_out_of_the_water() {
const SURFACE: f32 = 0.0;
// A lakebed 1.4 yd under the surface: wading depth for a human (swim starts at ~1.52).
const BED: [(f32, f32); 2] = [(-4.0, -1.4), (4.0, -1.4)];
let (walker, levitator) = world_from_profile(&BED)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let run = |modes: super::super::state::MoveModes, frames: usize| {
let mut player = Player {
modes,
pos: Vec3::new(0.0, -1.4, 0.0),
..Default::default()
};
for _ in 0..frames {
step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::ZERO,
7.0,
false,
false,
None,
Some(SURFACE),
AIR_NUDGE_SPEED,
);
}
player.pos.y
};
let walker = run(
super::super::state::MoveModes {
water_walking: true,
..Default::default()
},
30,
);
let levitator = run(
super::super::state::MoveModes {
water_walking: true,
hover: true,
..Default::default()
},
60,
);
(walker, levitator)
})
.unwrap();
assert!(
(walker - SURFACE).abs() < 1.0e-3,
"water walking must lift a wader onto the surface: {walker}"
);
assert!(
(levitator - (SURFACE + HOVER_HEIGHT)).abs() < 1.0e-3,
"…and Levitate's hover carries it the last yard: {levitator}"
);
}
/// A water-walker walking off land keeps steering and stops when the keys go: over water the
/// classify must see the plane, or every frame reads airborne and coasts on frozen momentum.
#[test]
fn a_water_walker_steers_and_stops_instead_of_coasting() {
const SURFACE: f32 = 0.0;
const SPEED: f32 = 7.0;
// Land out to x = 0, flush with the water; open water beyond it, with no collider at all.
const QUAY: [(f32, f32); 2] = [(-3.0, SURFACE), (0.0, SURFACE)];
let (east, north, idle) = world_from_profile(&QUAY)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let mut player = Player {
modes: super::super::state::MoveModes {
water_walking: true,
..Default::default()
},
pos: Vec3::new(-1.0, SURFACE, 0.0),
..Default::default()
};
let drive = |player: &mut Player, dir: Vec3, frames: usize| {
for _ in 0..frames {
step(
player,
&time,
&world,
&capsule,
dir != Vec3::ZERO,
dir,
SPEED,
false,
false,
None,
Some(SURFACE),
AIR_NUDGE_SPEED,
);
}
player.pos
};
let east = drive(&mut player, Vec3::X, 60);
let north = drive(&mut player, Vec3::Z, 30);
let idle = drive(&mut player, Vec3::ZERO, 30);
(east, north, idle)
})
.unwrap();
assert!(
east.x > 1.0,
"the walk onto the water must actually travel: {east:?}"
);
for (label, p) in [("east", east), ("north", north), ("idle", idle)] {
assert!(
(p.y - SURFACE).abs() < 1.0e-3,
"the feet must stay on the liquid surface ({label}): {p:?}"
);
}
assert!(
north.z - east.z > 1.0,
"a water-walker must steer: z {} -> {}",
east.z,
north.z
);
assert!(
(north.x - east.x).abs() < 0.1,
"the abandoned direction must stop advancing: x {} -> {} (the frozen air-nudge drift)",
east.x,
north.x
);
assert!(
idle.distance(north) < 1.0e-3,
"released keys must stop a water-walker dead: {north:?} -> {idle:?}"
);
}
#[test]
fn a_hovering_mover_refuses_the_jump_and_leaves_no_trace() {
// Hover is the first test in `CMovement::Jump` (`0x7c6230`, at `0x7c623a`); the press with
// hover off is the control.
let flat: [(f32, f32); 2] = [(-3.0, 0.0), (3.0, 0.0)];
let rows = world_from_profile(&flat)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
[true, false].map(|hover| {
let mut player = Player {
modes: super::super::state::MoveModes {
hover,
..Default::default()
},
..Default::default()
};
let out = step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::X,
0.0,
true,
false,
None,
None,
AIR_NUDGE_SPEED,
);
(out.jumped, player.launch_vz)
})
})
.unwrap();
let [(hover_jumped, hover_vy), (plain_jumped, plain_vy)] = rows;
assert!(
!hover_jumped && hover_vy == 0.0,
"hover must refuse the jump silently: jumped {hover_jumped}, vel_y {hover_vy}"
);
assert!(
plain_jumped && plain_vy > 0.0,
"the hover-free control must take off: jumped {plain_jumped}, vel_y {plain_vy}"
);
}
/// `Jump(0)` skips the hover test (`0x7c6236`), and its one caller is the hover grant
/// (`0x61a62e`), so it launches the very body the keyboard cannot.
#[test]
fn the_wire_jump_launches_the_hovering_body_the_keyboard_cannot() {
let flat: [(f32, f32); 2] = [(-3.0, 0.0), (3.0, 0.0)];
let (keyboard, wire) = world_from_profile(&flat)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let press = |want_jump: bool, wire_jump: bool| {
let mut player = Player {
modes: super::super::state::MoveModes {
hover: true,
..Default::default()
},
..Default::default()
};
let out = step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::X,
0.0,
want_jump,
wire_jump,
None,
None,
AIR_NUDGE_SPEED,
);
(out.jumped, player.launch_vz)
};
(press(true, false), press(false, true))
})
.unwrap();
assert_eq!(
keyboard,
(false, 0.0),
"`0x7c623a` still refuses Space while hovering"
);
assert_eq!(
wire,
(true, JUMP_SPEED),
"`force = 0` skips that test and takes off on the land seed `0xc0fe93d8` (the launch \
speed, not the post-gravity `vel_y`)"
);
}
/// One step of `dt` and `n` steps of `dt/n` land on the same height, as the reference's closed
/// form does.
#[test]
fn the_fall_step_is_frame_rate_independent() {
let total = 0.5_f32;
let drop = |steps: u32| {
let dt = total / steps as f32;
let (mut v, mut y) = (JUMP_SPEED, 0.0_f32);
for _ in 0..steps {
let (end, mean) = fall_step(v, dt, TERMINAL_VELOCITY);
y += mean * dt;
v = end;
}
y
};
let analytic = JUMP_SPEED * total - 0.5 * GRAVITY * total * total;
for steps in [1_u32, 2, 8, 30, 120] {
let got = drop(steps);
assert!(
(got - analytic).abs() < 1.0e-4,
"{steps} steps must reproduce the closed form {analytic}, got {got}"
);
}
}
/// The jump apex is `v₀²/2g` = 1.640 yd at any frame rate.
#[test]
fn the_jump_apex_is_the_analytic_one_at_any_frame_rate() {
let apex = JUMP_SPEED * JUMP_SPEED / (2.0 * GRAVITY);
for fps in [30.0_f32, 60.0, 144.0] {
let dt = 1.0 / fps;
let (mut v, mut y, mut high) = (JUMP_SPEED, 0.0_f32, 0.0_f32);
while v > -1.0 {
let (end, mean) = fall_step(v, dt, TERMINAL_VELOCITY);
y += mean * dt;
high = high.max(y);
v = end;
}
// The apex falls between frames: a step's worth of sampling, not drift.
assert!(
(high - apex).abs() < 0.5 * GRAVITY * dt * dt + 1.0e-4,
"at {fps} fps the apex should be ~{apex}, got {high}"
);
}
}
/// A step reaching terminal partway is two regimes; below terminal the clamp changes nothing.
#[test]
fn the_terminal_clamp_is_exact_on_the_step_that_reaches_it() {
let dt = 1.0 / 60.0;
let (end, mean) = fall_step(0.0, dt, TERMINAL_VELOCITY);
assert!((end - -GRAVITY * dt).abs() < 1.0e-6);
assert!((mean - -0.5 * GRAVITY * dt).abs() < 1.0e-6);
// Straddling terminal: reach it at `t_c`, coast the rest.
let v0 = -TERMINAL_VELOCITY + 0.5 * GRAVITY * dt;
let (end, mean) = fall_step(v0, dt, TERMINAL_VELOCITY);
assert_eq!(end, -TERMINAL_VELOCITY, "the step ends at terminal");
let t_c = (v0 + TERMINAL_VELOCITY) / GRAVITY;
let want = (0.5 * (v0 - TERMINAL_VELOCITY) * t_c - TERMINAL_VELOCITY * (dt - t_c)) / dt;
assert!(
(mean - want).abs() < 1.0e-6,
"piecewise, not a clamped mean"
);
let (end, mean) = fall_step(-TERMINAL_VELOCITY, dt, TERMINAL_VELOCITY);
assert_eq!((end, mean), (-TERMINAL_VELOCITY, -TERMINAL_VELOCITY));
}
/// Every knockback apply plants FORWARD (`0x6179c0`, at `0x617a18`) whatever its speed, and air
/// control opens only while the nibble is clear (`0x7c6afc`), so a vertical toss never steers.
#[test]
fn a_vertical_knockback_is_no_more_steerable_than_a_fast_one() {
let flat: [(f32, f32); 2] = [(-3.0, 0.0), (3.0, 0.0)];
const STRAIGHT_UP: Vec3 = Vec3::new(0.0, 12.0, 0.0);
let horiz = world_from_profile(&flat)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let mut player = Player::default();
// Frame 1: the launch.
let out = step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::ZERO,
0.0,
false,
false,
Some(STRAIGHT_UP),
None,
AIR_NUDGE_SPEED,
);
assert!(out.knocked && out.jumped, "the toss opened an arc");
assert_eq!(player.horiz_vel, Vec3::ZERO, "and it is purely vertical");
// Frames 2..6: airborne, with a movement key held.
let mut nudged = false;
for _ in 0..5 {
let out = step(
&mut player,
&time,
&world,
&capsule,
true,
Vec3::NEG_X,
7.0,
false,
false,
None,
None,
AIR_NUDGE_SPEED,
);
nudged |= out.air_nudged;
}
(player.horiz_vel, nudged)
})
.unwrap();
assert_eq!(
horiz,
(Vec3::ZERO, false),
"the planted nibble locks the arc: no nudge fires and the body does not drift, however \
hard the key is held"
);
}
/// The nudge is the live `min(walk, run)`: `0x7c5c20(1)` calls `0x7c4c90(1)`, whose walk
/// override returns `min(+0x88, +0x8c)`.
#[test]
fn the_standstill_nudge_takes_the_speed_it_is_given() {
let flat: [(f32, f32); 2] = [(-3.0, 0.0), (3.0, 0.0)];
let steer = |nudge: f32| {
world_from_profile(&flat)
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let mut player = Player::default();
// A standing jump: the nibble is clear, so one steer is in hand.
step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::ZERO,
0.0,
true,
false,
None,
None,
nudge,
);
let out = step(
&mut player,
&time,
&world,
&capsule,
true,
Vec3::NEG_X,
7.0,
false,
false,
None,
None,
nudge,
);
// A second press finds the nibble set and changes nothing.
step(
&mut player,
&time,
&world,
&capsule,
true,
Vec3::NEG_Z,
7.0,
false,
false,
None,
None,
nudge,
);
(out.air_nudged, player.horiz_vel)
})
.unwrap()
};
let (fired, v) = steer(2.5);
assert!(fired, "a standstill jump has one steer in hand");
assert!(
(v.length() - 2.5).abs() < 1.0e-4,
"at the speed given: {v:?}"
);
assert!(v.x < 0.0, "and in the pressed direction: {v:?}");
// A walk aura lowers the live speed.
let (fired, v) = steer(1.0);
assert!(fired);
assert!(
(v.length() - 1.0).abs() < 1.0e-4,
"a slowed body steers slower — the constant is not the law: {v:?}"
);
assert!(
v.z.abs() < 1.0e-4,
"the second press must not steer again: {v:?}"
);
}
/// The launch seeds both axes from the server, not the keys, and only ROOT refuses it: the
/// reference's drain tests nothing else before applying one, and discards it under root with
/// no apply and no ack (`0x615c71`).
#[test]
fn a_knockback_launch_overrides_the_keys_and_only_a_root_refuses_it() {
let flat: [(f32, f32); 2] = [(-3.0, 0.0), (3.0, 0.0)];
// Due east at 25 yd/s with 12 yd/s of lift, while the player holds "run west" at 7 yd/s.
const PUSH: Vec3 = Vec3::new(25.0, 12.0, 0.0);
let (free, rooted) = world_from_profile(&flat)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let knock = |rooted: bool| {
let mut player = Player {
modes: super::super::state::MoveModes {
rooted,
..Default::default()
},
..Default::default()
};
let out = step(
&mut player,
&time,
&world,
&capsule,
true, // moving …
Vec3::NEG_X, // … the other way, at a run
7.0,
false,
false,
Some(PUSH),
None,
AIR_NUDGE_SPEED,
);
(out.jumped, out.knocked, player.launch_vz, player.horiz_vel)
};
(knock(false), knock(true))
})
.unwrap();
let (jumped, knocked, vel_y, horiz) = free;
assert!(
jumped && knocked,
"the launch opens an arc, and says which kind it was: {free:?}"
);
assert_eq!(
vel_y, PUSH.y,
"the vertical is the server's take-off speed, not JUMP_SPEED ({JUMP_SPEED}) — read off \
`launch_vz`, because `vel_y` has already taken this frame's gravity step"
);
assert_eq!(
horiz,
Vec3::new(PUSH.x, 0.0, PUSH.z),
"the horizontal is the server's too — the held key must not claw it back"
);
assert_eq!(
rooted,
(false, false, 0.0, Vec3::ZERO),
"a rooted body is anchored: no launch, and `knocked` false so no ack goes out either"
);
}
/// The wire's `zspeed` is down-positive: a rising knockback's is negative (`0x7c61f0` stores it
/// in `CMovement+0xa0`, where a jump seeds `0xc0fe93d8` = −7.955547), and the ack echoes it.
#[test]
fn a_rising_knockback_carries_negative_wire_zspeed_and_positive_lift() {
let launch = benilla_protocol::JumpInfo {
zspeed: -12.0, // down-positive: negative is upward
cos_angle: 1.0,
sin_angle: 0.0,
xy_speed: 25.0,
};
// The controller's resolve, verbatim (`player::control`).
let bevy_launch = benilla_assets::coords::wow_to_bevy([
launch.cos_angle * launch.xy_speed,
launch.sin_angle * launch.xy_speed,
-launch.zspeed,
]);
assert_eq!(bevy_launch.y, 12.0, "a negative wire zspeed lifts the body");
// Back out as `stream_self_movement` builds the tail, negating the +up `jump_zspeed`.
let jump_zspeed = bevy_launch.y;
assert_eq!(
-jump_zspeed, launch.zspeed,
"the echo must be bit-identical — the server matches it within 0.01 before relaying"
);
let back = benilla_assets::coords::bevy_to_wow(bevy_launch.with_y(0.0));
assert_eq!(
[back[0], back[1]],
[
launch.cos_angle * launch.xy_speed,
launch.sin_angle * launch.xy_speed
],
"world XY is absolute and survives WoW → Bevy → WoW"
);
}
/// The waterline push-out stands in for the reference's swept surface, so like a sweep it
/// catches a sinking body and lets a rising one through.
#[test]
fn the_waterline_catches_a_sinking_body_and_lets_a_launched_one_through() {
const SURFACE: f32 = 0.0;
const OPEN: [(f32, f32); 2] = [(-3.0, -40.0), (3.0, -40.0)];
const UNDER: f32 = SURFACE - 0.5;
let (launched, at_rest) = world_from_profile(&OPEN)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let one_step = |vel_y: f32| {
let mut player = Player {
modes: super::super::state::MoveModes {
water_walking: true,
..Default::default()
},
pos: Vec3::new(0.0, UNDER, 0.0),
vel_y,
..Default::default()
};
step(
&mut player,
&time,
&world,
&capsule,
false,
Vec3::ZERO,
0.0,
false,
false,
None,
Some(SURFACE),
AIR_NUDGE_SPEED,
);
(player.pos.y, player.vel_y)
};
// The seed a `SetHover(true)` picks while SWIMMING (`0x7c6261` → `0xc1118c48`).
(one_step(9.096_748), one_step(0.0))
})
.unwrap();
let (up_y, up_vy) = launched;
assert!(
up_y > UNDER && up_y < SURFACE,
"the launch keeps rising under its own seed, un-snapped: {up_y}"
);
assert!(
up_vy > 0.0,
"and keeps its velocity — a clamp to the surface would zero it: {up_vy}"
);
let (rest_y, rest_vy) = at_rest;
assert!(
(rest_y - SURFACE).abs() < 1.0e-3,
"a body at rest under the surface is still lifted onto it: {rest_y}"
);
assert_eq!(rest_vy, 0.0, "and the floor takes its velocity");
}
/// A hovering water-walker holds the hover line walking and standing: the snap must see the
/// water plane, or it takes the no-floor leg every frame and the climb fights it.
#[test]
fn a_levitating_walker_holds_its_line_over_water_moving_or_still() {
const SURFACE: f32 = 0.0;
const OPEN: [(f32, f32); 2] = [(-400.0, -40.0), (400.0, -40.0)];
let (first_rise, settled, walk_low, walk_end, stopped) = world_from_profile(&OPEN)
.world_mut()
.run_system_once(|world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let mut time = Time::default();
time.advance_by(std::time::Duration::from_secs_f32(1.0 / 60.0));
let mut player = Player {
modes: super::super::state::MoveModes {
water_walking: true,
hover: true,
..Default::default()
},
pos: Vec3::new(0.0, SURFACE, 0.0),
..Default::default()
};
let run = |player: &mut Player, moving: bool, n: usize| {
let mut low = f32::INFINITY;
for _ in 0..n {
step(
player,
&time,
&world,
&capsule,
moving,
if moving { Vec3::X } else { Vec3::ZERO },
7.0,
false,
false,
None,
Some(SURFACE),
AIR_NUDGE_SPEED,
);
low = low.min(player.pos.y);
}
low
};
run(&mut player, false, 2);
let first_rise = player.pos.y;
run(&mut player, false, 88);
let settled = player.pos.y;
let walk_low = run(&mut player, true, 90);
let walk_end = player.pos.y;
run(&mut player, false, 60);
(first_rise, settled, walk_low, walk_end, player.pos.y)
})
.unwrap();
let line = SURFACE + HOVER_HEIGHT;
assert!(
(first_rise - 2.0 * HOVER_CLIMB_RATE / 60.0).abs() < 1.0e-5,
"two frames of climb are exactly two frames of the rate, with nothing pulling back \
against them: {first_rise}"
);
assert!(
(settled - line).abs() < 1.0e-4,
"an idle Levitator rests on the hover line: {settled}"
);
assert!(
(walk_low - line).abs() < 1.0e-4,
"and does not sag a millimetre while walking — lowest was {walk_low}, want {line}"
);
assert!(
(walk_end - line).abs() < 1.0e-4,
"still on the line after 90 walking frames: {walk_end}"
);
assert!(
(stopped - line).abs() < 1.0e-4,
"and stopping is not a rise, because nothing was lost: {stopped}"
);
}
/// An Elwynn hillside out of flat ground, a 55° face (`ny = +0.574`, read at WoW
/// `(-9236.8, -341.9, 101.6)`), too steep to stand on anywhere.
fn world_with_steep_hillside() -> App {
const P: [(f32, f32); 3] = [(-3.0, 0.0), (0.0, 0.0), (2.5, 3.570)];
world_from_profile(&P)
}
/// One airborne frame's readings: `(feet height, vertical velocity, fraction of the descent
/// gravity intended that the frame actually achieved)`.
type AirRow = (f32, f32, f32);
/// Jump from `start_feet`, flying the bare arc through [`airborne_step`] at a run along `dir`.
fn jump_into(mut world: App, start_feet: Vec3, dir: Vec3, frames: usize) -> Vec<AirRow> {
world
.world_mut()
.run_system_once(move |world: benilla_world::collision::WorldCollision| {
let capsule = player_capsule();
let dt = std::time::Duration::from_secs_f32(TRAVEL_60FPS / 7.0);
let secs = dt.as_secs_f32();
let mut center = start_feet + Vec3::Y * (CAPSULE_HEIGHT * 0.5);
let mut vel_y = JUMP_SPEED;
(0..frames)
.map(|_| {
vel_y = (vel_y - GRAVITY * secs).max(-TERMINAL_VELOCITY);
let before = center.y;
center = airborne_step(
&world,
&capsule,
center,
dir * 7.0 + Vec3::Y * vel_y,
dt,
);
let intent = vel_y * secs;
let achieved = center.y - before;
// Only a descent has a stall to measure; a rise reports 1.0.
let frac = if intent < 0.0 { achieved / intent } else { 1.0 };
(center.y - CAPSULE_HEIGHT * 0.5, vel_y, frac)
})
.collect::<Vec<_>>()
})
.unwrap()
}
#[test]
fn a_jump_into_a_steep_hillside_banks_no_height() {
let arc = jump_into(
world_with_steep_hillside(),
Vec3::new(-0.6, 0.0, 0.0),
Vec3::X,
150,
);
let peak = arc.iter().map(|r| r.0).fold(f32::MIN, f32::max);
let end = arc.last().unwrap().0;
assert!(
peak > 1.0,
"the jump must actually leave the ground: {peak:.3}"
);
assert!(
end < 0.05,
"the arc must end back at the foot of the slope, not banked part-way up it: \
ended at {end:.3} (peak {peak:.3})"
);
// Nor may the descent stall into a wedge rest, which would stand the body up part-way.
let (mut run, mut worst_run) = (0u8, 0u8);
for r in arc.iter().filter(|r| r.0 > 0.2) {
run = if r.1 < -WEDGE_MIN_FALL && r.2 < WEDGE_STALL_RATIO {
run + 1
} else {
0
};
worst_run = worst_run.max(run);
}
assert!(
worst_run < WEDGE_STILL_FRAMES,
"the descent must never stall long enough to read as a wedge rest: \
{worst_run} consecutive stalled frames"
);
}
#[test]
fn a_steep_face_never_cancels_a_fall() {
// A plane clip turns a run into the hill into lift (`v'.y - v.y = -(v·n)·n.y`), which all
// but cancels a fall against a 55° face.
let arc = jump_into(
world_with_steep_hillside(),
Vec3::new(-0.6, 0.0, 0.0),
Vec3::X,
150,
);
// Only frames clear of the flat ground, which the bare arc has no ground probe to stand on.
let worst = arc
.iter()
.filter(|r| r.1 < -WEDGE_MIN_FALL && r.0 > 0.2)
.map(|r| r.2)
.fold(f32::MAX, f32::min);
assert!(
worst > 0.98,
"a falling frame against the face keeps ALL of its descent — the reference's response \
is horizontal only: worst was {worst:.2} of intent"
);
}
#[test]
fn walkable_and_overhanging_faces_are_untouched() {
let push = Vec3::new(7.0, 0.0, 0.0);
assert!(steep_contact_shear(face(40.0), push).is_none());
// An overhang keeps the plain ceiling clip.
assert!(steep_contact_shear(Vec3::new(-0.5, -0.7, 0.0).normalize(), push).is_none());
assert!(steep_contact_shear(face(60.0), -push).is_none());
}
#[test]
fn a_vertical_wall_takes_the_whole_push_and_no_more() {
let v = Vec3::new(7.0, -4.0, 0.0);
let out = steep_contact_shear(face(90.0), v).expect("must strip");
assert!(out.x.abs() < 1e-6 && (out.y - v.y).abs() < 1e-6);
}
}