mod common;
use common::math::*;
use common::walker::*;
use common::*;
use oxijolt::*;
const SPEED: f64 = 2.0;
fn dt() -> f64 {
f64::from(DT)
}
fn flat_ground(x: f64, z: f64) -> f64 {
(R * R - x * x - z * z).sqrt() - R
}
fn resting_at(x: f64, z: f64) -> V3 {
let ground = [x, flat_ground(x, z), z];
add(ground, scale(up_at(ground), f64::from(REST_HEIGHT)))
}
fn flat_scene(x: f64, z: f64) -> (PhysicsWorld, Layers, Walker) {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
let walker = add_walker(&mut world, &layers, resting_at(x, z));
(world, layers, walker)
}
fn run(
world: &mut PhysicsWorld,
walker: &Walker,
ticks: usize,
mut desired: impl FnMut(usize, V3) -> V3,
) -> Vec<NearOutput> {
let mut carry = Carry::RESTING;
(0..ticks)
.map(|tick| {
let wanted = desired(tick, origin(world, walker));
near_tick(world, walker, &mut carry, wanted)
})
.collect()
}
fn walking(direction: V3, speed: f64) -> impl FnMut(usize, V3) -> V3 {
move |_, origin| tangent(origin, direction, speed * dt())
}
fn still(_: usize, _: V3) -> V3 {
[0.0; 3]
}
fn height_above_flat(p: V3) -> f64 {
let ground = [p[0], flat_ground(p[0], p[2]), p[2]];
dot(sub(p, ground), up_at(ground))
}
fn path_length(points: impl IntoIterator<Item = V3>) -> f64 {
let points: Vec<V3> = points.into_iter().collect();
points
.windows(2)
.map(|pair| norm(sub(pair[1], pair[0])))
.sum()
}
fn conversion_error(p: V3) -> f64 {
let largest = p.iter().fold(0.0_f64, |m, c| m.max(c.abs()));
largest * f64::from(f32::EPSILON)
}
#[test]
fn walker_rest_height_is_foot_plus_radius_plus_padding() {
let (mut world, _, walker) = flat_scene(0.0, 0.0);
let outputs = run(&mut world, &walker, 30, still);
let last = outputs.last().unwrap();
assert!(last.grounded);
let height = height_above_flat(last.pos);
assert!((height - f64::from(REST_HEIGHT)).abs() < 1e-3, "{height}");
}
#[test]
fn a_step_moves_exactly_the_desired_displacement() {
for angle in [0.0, 0.6] {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, angle);
let (centre, rotation) = chunk_pose(angle);
let start = add(v3(centre), scale(up_at(v3(centre)), f64::from(REST_HEIGHT)));
let walker = add_walker(&mut world, &layers, start);
let mut carry = Carry::RESTING;
near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
let before = origin(&world, &walker);
let chart_x = rotate(rotation, [1.0, 0.0, 0.0]);
let desired = tangent(before, chart_x, SPEED * dt());
let out = near_tick(&mut world, &walker, &mut carry, desired);
let moved = sub(out.pos, before);
let along = normalize(desired);
let tolerance = 2.0 * conversion_error(before) + 1e-4;
let error = norm(sub(moved, desired));
assert!(
error < tolerance,
"angle {angle}: moved {moved:?}, wanted {desired:?} ({error} > {tolerance})"
);
assert!((dot(moved, along) - SPEED * dt()).abs() < tolerance);
assert!(out.grounded, "angle {angle}");
}
}
#[test]
fn an_airborne_step_falls_radially() {
let (mut world, layers) = fixture_world(1);
let angle: f64 = 1.2;
let surface = add(CENTRE, [R * angle.sin(), R * angle.cos(), 0.0]);
let start = add(surface, scale(up_at(surface), 5.0));
let walker = add_walker(&mut world, &layers, start);
let mut carry = Carry {
vel_up: 0.0,
grounded: false,
};
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
let expected = scale(up_at(start), -f64::from(G) * dt() * dt());
let error = norm(sub(sub(out.pos, start), expected));
assert!(error < 2.0 * conversion_error(start) + 1e-6, "{error}");
assert!(!out.grounded);
}
#[test]
fn a_walker_lands_on_a_floor_box() {
let (mut world, layers, _) = flat_scene(5.0, 5.0);
structure_box(&mut world, &layers, [0.0, 0.4, 0.0], [2.0, 0.4, 2.0], 0.0);
let walker = add_walker(&mut world, &layers, [0.0, 0.9 + f64::from(RADIUS), 0.0]);
let mut carry = Carry {
vel_up: 0.0,
grounded: false,
};
let mut last = None;
for _ in 0..30 {
last = Some(near_tick(&mut world, &walker, &mut carry, [0.0; 3]));
}
let out = last.unwrap();
let feet = out.pos[1] - f64::from(RADIUS);
assert!((feet - 0.8).abs() < 0.05, "feet at {feet}");
assert!(out.grounded);
}
fn walk_into_block(height: f64, step_height: f32) -> (NearOutput, f64) {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
let face = 1.0 + f64::from(RADIUS);
let top = flat_ground(face, 0.0) + height;
structure_box(
&mut world,
&layers,
[face + 2.0, top - 1.5, 0.0],
[2.0, 1.5, 2.0],
0.0,
);
let mut walker = add_walker(&mut world, &layers, resting_at(0.0, 0.0));
walker.step_height = step_height;
let outputs = run(&mut world, &walker, 120, walking([1.0, 0.0, 0.0], SPEED));
(*outputs.last().unwrap(), face)
}
fn climbed(out: &NearOutput, height: f64, face: f64) -> bool {
out.pos[1] - f64::from(RADIUS) > flat_ground(face, 0.0) + height - 0.05
}
fn max_climbable_block(step_height: f32) -> f64 {
let (mut low, mut high) = (0.0, 1.5);
while high - low > 1e-3 {
let mid = 0.5 * (low + high);
let (out, face) = walk_into_block(mid, step_height);
if climbed(&out, mid, face) {
low = mid;
} else {
high = mid;
}
}
low
}
#[test]
fn step_law_climbs_up_to_045_but_not_05() {
for height in [0.40, 0.45] {
let (out, face) = walk_into_block(height, STEP_HEIGHT);
assert!(climbed(&out, height, face), "{height}: {out:?}");
assert!(out.grounded, "{height}");
}
let (out, face) = walk_into_block(0.5, STEP_HEIGHT);
assert!(!climbed(&out, 0.5, face), "{out:?}");
assert!(
face - out.pos[0] > 0.3,
"stopped at {} before the face at {face}",
out.pos[0]
);
assert_eq!(out.blocker.and_then(|b| b.group), Some(Groups::STRUCTURE));
let highest = max_climbable_block(STEP_HEIGHT);
assert!((0.45..0.5).contains(&highest), "climbs up to {highest}");
}
#[derive(Clone, Copy)]
struct SharpBox {
near_x: f64,
depth: f64,
bottom: f64,
top: f64,
dynamic: bool,
}
fn walk_into_boxes(boxes: &[SharpBox], in_filter: bool) -> (NearOutput, f64) {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
let face = 1.0 + f64::from(RADIUS);
let ground = flat_ground(face, 0.0);
for b in boxes {
let centre = [
face + b.near_x + 0.5 * b.depth,
ground + 0.5 * (b.bottom + b.top),
0.0,
];
let half = [0.5 * b.depth, 0.5 * (b.top - b.bottom), 2.0];
if b.dynamic {
let block = Shape::new_box_with_convex_radius(vec3(half), 0.0).unwrap();
world
.create_body(
&block,
&BodySettings::new_dynamic()
.position(rvec3(centre))
.object_layer(layers.item),
)
.unwrap();
} else {
structure_box(&mut world, &layers, centre, half, 0.0);
}
}
let mut walker = add_walker(&mut world, &layers, resting_at(0.0, 0.0));
walker.items_in_filter = in_filter;
let outputs = run(&mut world, &walker, 120, walking([1.0, 0.0, 0.0], SPEED));
(*outputs.last().unwrap(), face)
}
fn walk_into_item(height: f64, in_filter: bool) -> (NearOutput, f64) {
let item = SharpBox {
near_x: 0.0,
depth: 4.0,
bottom: height - 3.0,
top: height,
dynamic: true,
};
walk_into_boxes(&[item], in_filter)
}
fn max_climbable_item() -> f64 {
let (mut low, mut high) = (0.0, 1.5);
while high - low > 1e-3 {
let mid = 0.5 * (low + high);
let (out, face) = walk_into_item(mid, true);
if climbed(&out, mid, face) {
low = mid;
} else {
high = mid;
}
}
low
}
#[test]
fn items_outside_the_controller_filter_are_no_obstacle() {
let (out, _) = walk_into_item(0.4, false);
assert!(out.pos[0] > 3.5, "{out:?}");
assert!(out.grounded, "{out:?}");
let feet = height_above_flat(out.pos) - f64::from(REST_HEIGHT);
assert!(feet.abs() < 0.05, "feet {feet} off the ground");
}
#[test]
fn the_autostep_never_climbs_a_dynamic_body_in_the_filter() {
let (out, face) = walk_into_item(0.4, true);
assert!(!climbed(&out, 0.4, face), "{out:?}");
assert_eq!(out.blocker.and_then(|b| b.group), Some(Groups::ITEM));
let (out, face) = walk_into_item(0.3, true);
assert!(climbed(&out, 0.3, face), "native creep: {out:?}");
let highest = max_climbable_item();
assert!(
(0.3..0.4).contains(&highest),
"climbs dynamic boxes up to {highest}"
);
}
#[test]
fn a_dynamic_body_in_front_of_a_step_does_not_start_the_autostep() {
let scene = [
SharpBox {
near_x: 0.0,
depth: 0.05,
bottom: -1.0,
top: 0.40,
dynamic: true,
},
SharpBox {
near_x: 0.05,
depth: 4.0,
bottom: -1.0,
top: 0.45,
dynamic: false,
},
];
let (out, face) = walk_into_boxes(&scene, true);
assert!(!climbed(&out, 0.45, face), "{out:?}");
assert_eq!(out.blocker.and_then(|b| b.group), Some(Groups::ITEM));
let (out, face) = walk_into_boxes(&scene, false);
assert!(climbed(&out, 0.45, face), "plate not seen: {out:?}");
}
#[test]
fn the_autostep_does_not_land_on_a_dynamic_body() {
let scene = |dynamic| {
[
SharpBox {
near_x: 0.0,
depth: 0.1,
bottom: -1.0,
top: 0.40,
dynamic: false,
},
SharpBox {
near_x: 0.1,
depth: 4.0,
bottom: -1.0,
top: 0.45,
dynamic,
},
]
};
let (out, face) = walk_into_boxes(&scene(true), true);
assert!(!climbed(&out, 0.45, face), "{out:?}");
assert_eq!(out.blocker.and_then(|b| b.group), Some(Groups::STRUCTURE));
let (out, face) = walk_into_boxes(&scene(false), true);
assert!(climbed(&out, 0.45, face), "static top: {out:?}");
}
fn walk_under_slab(slab_bottom_above_ground: f64) -> (NearOutput, f64) {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
let face = 1.0 + f64::from(RADIUS);
let top = flat_ground(face, 0.0) + 0.4;
structure_box(
&mut world,
&layers,
[face + 2.0, top - 1.5, 0.0],
[2.0, 1.5, 2.0],
0.0,
);
let slab_centre = flat_ground(face, 0.0) + slab_bottom_above_ground + 0.25;
structure_box(
&mut world,
&layers,
[face, slab_centre, 0.0],
[3.0, 0.25, 2.0],
0.0,
);
let walker = add_walker(&mut world, &layers, resting_at(0.0, 0.0));
let outputs = run(&mut world, &walker, 120, walking([1.0, 0.0, 0.0], SPEED));
(*outputs.last().unwrap(), face)
}
#[test]
fn a_step_under_a_low_ceiling_is_not_taken() {
let head = f64::from(CENTRE_UP + HALF_HEIGHT + RADIUS);
let (out, face) = walk_under_slab(2.40);
assert!(!climbed(&out, 0.4, face), "{out:?}");
assert!(out.grounded, "{out:?}");
let feet = height_above_flat(out.pos) - f64::from(REST_HEIGHT);
assert!(feet.abs() < 0.05, "feet {feet} off the ground");
assert!(
out.pos[1] + head < flat_ground(face, 0.0) + 2.40,
"head at {}",
out.pos[1] + head
);
assert_eq!(out.blocker.and_then(|b| b.group), Some(Groups::STRUCTURE));
let (out, face) = walk_under_slab(3.0);
assert!(climbed(&out, 0.4, face), "{out:?}");
}
fn slope_scene(tan: f64) -> (PhysicsWorld, Layers) {
let (mut world, layers) = fixture_world(1);
add_terrain(
&mut world,
&layers,
&sloped(0.0, tan),
(RVec3::ZERO, Quat::IDENTITY),
);
(world, layers)
}
fn slope_normal(tan: f64) -> V3 {
normalize([-tan, 1.0, 0.0])
}
fn resting_on_slope(x: f64, tan: f64) -> V3 {
add(
[x, tan * x, 0.0],
scale(slope_normal(tan), f64::from(REST_HEIGHT)),
)
}
fn off_slope(p: V3, tan: f64) -> f64 {
dot(p, slope_normal(tan)) - f64::from(REST_HEIGHT)
}
#[test]
fn slope_law_climbs_30_degrees_and_slides_back_from_60() {
let tan30 = 30.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan30);
let start = resting_on_slope(1.0, tan30);
let walker = add_walker(&mut world, &layers, start);
let outputs = run(&mut world, &walker, 30, walking([1.0, 0.0, 0.0], SPEED));
for (tick, out) in outputs.iter().enumerate() {
assert!(out.grounded, "tick {tick}");
let off = off_slope(out.pos, tan30);
assert!(off.abs() < 0.03, "tick {tick}: {off} off the slope");
}
let run_x = outputs.last().unwrap().pos[0] - start[0];
let cos2 = 30.0_f64.to_radians().cos().powi(2);
assert!(run_x >= SPEED * 0.5 * cos2 - 0.05, "run {run_x}");
let tan60 = 60.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan60);
let start = resting_on_slope(0.3, tan60);
let walker = add_walker(&mut world, &layers, start);
let outputs = run(&mut world, &walker, 60, walking([1.0, 0.0, 0.0], SPEED));
let highest = outputs
.iter()
.map(|out| out.pos[1])
.fold(f64::MIN, f64::max);
assert!(
highest <= start[1] + 1e-3,
"rose to {highest} from {}",
start[1]
);
let last = outputs.last().unwrap();
assert!(last.pos[0] < start[0], "slid back: {:?}", last.pos);
}
#[test]
fn walking_down_a_30_degree_slope_has_no_hops() {
let tan30 = 30.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan30);
let walker = add_walker(&mut world, &layers, resting_on_slope(4.0, tan30));
let outputs = run(&mut world, &walker, 40, walking([-1.0, 0.0, 0.0], SPEED));
for (tick, out) in outputs.iter().enumerate() {
assert!(out.grounded, "tick {tick}: {out:?}");
let off = off_slope(out.pos, tan30);
assert!(off.abs() < 0.03, "tick {tick}: {off} off the slope");
}
}
#[test]
fn a_chunk_seam_is_crossed() {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
flat_chunk(&mut world, &layers, 2.0 * (16.0 / R).asin());
let start = resting_at(14.0, 0.3);
let walker = add_walker(&mut world, &layers, start);
let outputs = run(&mut world, &walker, 120, walking([1.0, 0.0, 0.0], SPEED));
let crossed = outputs.last().unwrap().pos;
assert!(crossed[0] > 17.0, "{crossed:?}");
for (tick, out) in outputs.iter().enumerate() {
let height = dot(sub(out.pos, CENTRE), up_at(out.pos)) - R;
assert!(
(height - f64::from(REST_HEIGHT)).abs() < 0.05,
"tick {tick}: height {height}"
);
assert!(out.grounded, "tick {tick}");
}
let path = path_length(std::iter::once(start).chain(outputs.iter().map(|out| out.pos)));
assert!((path - 4.0).abs() < 0.08, "path {path}");
}
#[test]
fn a_buried_walker_is_put_back_on_the_terrain() {
for depth in [1.0, 3.0, 10.0] {
let (mut world, layers) = fixture_world(1);
flat_chunk(&mut world, &layers, 0.0);
let buried = add(
resting_at(0.5, 0.5),
[0.0, -depth - f64::from(REST_HEIGHT), 0.0],
);
let walker = add_walker(&mut world, &layers, buried);
let mut carry = Carry {
vel_up: -3.0,
grounded: false,
};
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
assert!(out.recovered, "depth {depth}");
let height = height_above_flat(out.pos);
assert!(
(height - f64::from(REST_HEIGHT)).abs() < 0.05,
"depth {depth}: height {height}"
);
assert!(out.grounded, "depth {depth}");
}
let (mut world, _, walker) = flat_scene(0.5, 0.5);
let start = origin(&world, &walker);
let mut carry = Carry::RESTING;
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
assert!(!out.recovered);
assert!(norm(sub(out.pos, start)) < 0.01);
}
fn pushed_from_wall(desired: V3, ticks: usize) -> Vec<f64> {
let (mut world, layers, walker) = flat_scene(0.0, 0.0);
structure_box(&mut world, &layers, [0.45, 1.0, 0.0], [0.1, 1.0, 2.0], 0.0);
let start = origin(&world, &walker)[0];
run(&mut world, &walker, ticks, move |_, _| desired)
.iter()
.map(|out| out.pos[0] - start)
.collect()
}
#[test]
fn an_overlapping_wall_pushes_the_walker_out() {
for desired in [[0.0, 0.0, SPEED * dt()], [0.0; 3]] {
let xs = pushed_from_wall(desired, 10);
assert!(xs[0] < -0.04, "{desired:?}: {xs:?}");
assert!(xs.iter().all(|&x| x < -0.04), "{desired:?}: {xs:?}");
}
}
#[test]
fn a_rotated_floor_far_from_the_anchor_keeps_the_path() {
let (mut world, layers) = fixture_world(1);
let angle = 1.0;
let (centre, rotation) = chunk_pose(angle);
let floor = Shape::new_box_with_convex_radius(Vec3::new(8.0, 0.5, 8.0), 0.0).unwrap();
let floor_centre = sub(v3(centre), scale(up_at(v3(centre)), 0.5));
world
.create_body(
&floor,
&BodySettings::new_static()
.position(rvec3(floor_centre))
.rotation(rotation)
.object_layer(layers.chunk),
)
.unwrap();
let start = add(v3(centre), scale(up_at(v3(centre)), f64::from(REST_HEIGHT)));
let walker = add_walker(&mut world, &layers, start);
let chart_x = rotate(rotation, [1.0, 0.0, 0.0]);
let outputs = run(&mut world, &walker, 120, walking(chart_x, SPEED));
assert!(outputs.iter().all(|out| out.grounded));
let path = path_length(std::iter::once(start).chain(outputs.iter().map(|out| out.pos)));
assert!((path - 4.0).abs() < 0.08, "path {path}");
}
#[test]
fn touching_a_floor_is_not_overlapping() {
let (mut world, layers, _) = flat_scene(5.0, 5.0);
structure_box(&mut world, &layers, [0.0, 0.4, 0.0], [2.0, 0.4, 2.0], 0.0);
let start = [0.0, 0.8 + f64::from(REST_HEIGHT), 0.0];
let walker = add_walker(&mut world, &layers, start);
let mut carry = Carry::RESTING;
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
let push = norm(sub(out.pos, start));
assert!(push < 0.05, "pushed {push}");
}
#[test]
fn steep_terrain_slides_and_gentle_terrain_holds() {
let tan60 = 60.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan60);
let walker = add_walker(&mut world, &layers, resting_on_slope(6.0, tan60));
let mut positions = vec![origin(&world, &walker)];
let outputs = run(&mut world, &walker, 40, still);
positions.extend(outputs.iter().map(|out| out.pos));
let downhill: Vec<f64> = positions
.windows(2)
.map(|pair| pair[0][0] - pair[1][0])
.collect();
for (tick, out) in outputs.iter().enumerate() {
assert!(!out.grounded && out.sliding, "tick {tick}: {out:?}");
}
for (tick, pair) in downhill.windows(2).enumerate() {
assert!(
pair[1] >= pair[0],
"tick {}: {} after {}",
tick + 1,
pair[1],
pair[0]
);
}
assert!(downhill[30] > 0.02, "tick 30: {}", downhill[30]);
assert!(outputs[30].vel_up < -1.0, "tick 30: {}", outputs[30].vel_up);
let tan30 = 30.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan30);
let start = resting_on_slope(3.0, tan30);
let walker = add_walker(&mut world, &layers, start);
let outputs = run(&mut world, &walker, 60, still);
for (tick, out) in outputs.iter().enumerate() {
assert!(out.grounded && !out.sliding, "tick {tick}");
let moved = norm(sub(out.pos, start));
assert!(moved < 1e-3, "tick {tick}: moved {moved}");
}
}
#[test]
fn steep_slopes_always_bring_the_walker_down() {
for degrees in [50.0_f64, 60.0, 75.0] {
let tan = degrees.to_radians().tan();
for (x, z) in [(1.0, -3.0), (2.0, -1.0), (3.0, 1.0), (4.0, 3.0)] {
let (mut world, layers) = slope_scene(tan);
let start = add(resting_on_slope(x, tan), [0.0, 0.0, z]);
let walker = add_walker(&mut world, &layers, start);
let mut previous = start;
let outputs = run(&mut world, &walker, 600, still);
for (tick, out) in outputs.iter().enumerate() {
let rise = dot(sub(out.pos, previous), up_at(previous));
assert!(
rise <= 0.02,
"{degrees}° from x {x}, tick {tick}: rose {rise}"
);
previous = out.pos;
}
let last = outputs.last().unwrap();
assert!(
last.pos[0] < 0.5,
"{degrees}° from x {x}: stuck at {:?}",
last.pos
);
let height = height_above_flat(last.pos);
assert!(
height < f64::from(REST_HEIGHT) + 0.05,
"{degrees}° from x {x}: {height}"
);
assert!(last.grounded, "{degrees}° from x {x}");
}
}
}
#[test]
fn jogging_covers_the_chord() {
let (mut world, _, walker) = flat_scene(0.0, 0.0);
let start = origin(&world, &walker);
let outputs = run(&mut world, &walker, 60, walking([1.0, 0.0, 0.0], 3.5));
assert!(outputs.iter().all(|out| out.grounded));
let end = outputs.last().unwrap().pos;
let chord = norm(sub(end, start));
let tolerance = 2.0 * conversion_error(end) + 1e-3;
assert!(
(chord - 3.5).abs() < tolerance,
"chord {chord} (tolerance {tolerance})"
);
}
#[test]
fn a_jump_reaches_the_ballistic_apex_and_lands() {
let (mut world, _, walker) = flat_scene(0.0, 0.0);
let start = origin(&world, &walker);
let mut carry = Carry {
vel_up: 4.5,
grounded: true,
};
let mut apex = 0.0_f64;
let mut landed = false;
for _ in 0..90 {
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
apex = apex.max(dot(sub(out.pos, start), up_at(start)));
landed = out.grounded;
}
let g_dt = f64::from(G) * dt();
let ballistic: f64 = (1..)
.map(|k| (4.5 - k as f64 * g_dt) * dt())
.take_while(|&rise| rise > 0.0)
.sum();
assert!(
(apex - ballistic).abs() < 0.005,
"apex {apex}, ballistic {ballistic}"
);
assert!(landed);
}
#[test]
fn released_input_decays_by_air_friction() {
let (mut world, _, walker) = flat_scene(0.0, 0.0);
let mut player = Player::new(&mut world, &walker);
let ahead = [1.0, 0.0, 0.0];
for _ in 0..20 {
player.tick(&mut world, &walker, ahead, 3.5, false);
}
player.tick(&mut world, &walker, ahead, 3.5, true);
let mut horizontal = Vec::new();
for _ in 0..40 {
let out = player.tick(&mut world, &walker, [0.0; 3], 3.5, false);
if out.grounded {
break;
}
horizontal.push(norm(player.horizontal_velocity()));
}
assert!(horizontal.len() > 20, "{horizontal:?}");
for pair in horizontal.windows(2) {
let ratio = pair[1] / pair[0];
assert!((ratio - 0.98).abs() < 1e-3, "{ratio} in {horizontal:?}");
}
}
#[test]
fn a_running_jump_at_a_steep_rise_gets_past_its_start() {
let tan60 = 60.0_f64.to_radians().tan();
let (mut world, layers) = slope_scene(tan60);
let walker = add_walker(&mut world, &layers, [-4.0, f64::from(REST_HEIGHT), 0.0]);
let mut player = Player::new(&mut world, &walker);
let mut furthest = f64::MIN;
let mut jumped = false;
for _ in 0..120 {
let jump = !jumped && player.last.pos[0] > -1.2;
let out = player.tick(&mut world, &walker, [1.0, 0.0, 0.0], 3.5, jump);
jumped |= jump;
furthest = furthest.max(out.pos[0]);
let feet = sub(out.pos, scale(up_at(out.pos), f64::from(RADIUS)));
let terrain = tan60 * feet[0].clamp(0.0, 12.0);
assert!(
feet[1] >= terrain - 1e-3,
"feet {feet:?} below terrain {terrain}"
);
}
assert!(jumped);
assert!(furthest > 0.0, "reached x {furthest}");
}
#[test]
fn rising_into_a_ceiling_resets_vel_up() {
let (mut world, layers, walker) = flat_scene(0.0, 0.0);
let start = origin(&world, &walker);
let head = start[1] + f64::from(CENTRE_UP + HALF_HEIGHT + RADIUS);
structure_box(
&mut world,
&layers,
[0.0, head + 0.3 + 0.25, 0.0],
[2.0, 0.25, 2.0],
0.0,
);
let mut carry = Carry {
vel_up: 4.5,
grounded: true,
};
let mut hit = None;
let mut landed = false;
for tick in 0..90 {
let out = near_tick(&mut world, &walker, &mut carry, [0.0; 3]);
if out.ceiling && hit.is_none() {
assert_eq!(out.vel_up, 0.0, "tick {tick}");
hit = Some(tick);
}
if hit.is_some() && out.grounded {
landed = true;
break;
}
}
assert!(hit.is_some(), "no ceiling contact");
assert!(landed);
}
fn scripted_run(ticks: usize, block: usize) -> Vec<Vec<u8>> {
let (mut world, layers) = script_scene(1);
let walker = add_walker(&mut world, &layers, script_start());
let mut player = Player::new(&mut world, &walker);
let mut state = world.character(walker.id).unwrap().save_state();
drop(world);
let mut records = Vec::with_capacity(ticks);
for first in (0..ticks).step_by(block) {
let (mut world, layers) = script_scene(1);
let walker = add_walker(&mut world, &layers, player.last.pos);
world
.character_mut(walker.id)
.unwrap()
.restore_state(&state)
.unwrap();
for tick in first..(first + block).min(ticks) {
let out = script_tick(&mut world, &walker, &mut player, tick);
let mut record = Vec::new();
record_walker(&world, &walker, &out, &mut record);
records.push(record);
}
state = world.character(walker.id).unwrap().save_state();
}
records
}
fn single_pass_run(ticks: usize) -> Vec<Vec<u8>> {
let (mut world, layers) = script_scene(1);
let walker = add_walker(&mut world, &layers, script_start());
let mut player = Player::new(&mut world, &walker);
(0..ticks)
.map(|tick| {
let out = script_tick(&mut world, &walker, &mut player, tick);
let mut record = Vec::new();
record_walker(&world, &walker, &out, &mut record);
record
})
.collect()
}
#[test]
fn chained_near_step_replay_is_bit_exact() {
const TICKS: usize = 300;
let reference = single_pass_run(TICKS);
assert_eq!(reference.len(), TICKS);
for block in [TICKS, 1, 7] {
let chained = scripted_run(TICKS, block);
assert_eq!(chained.len(), TICKS, "blocks of {block}");
for (tick, (a, b)) in reference.iter().zip(&chained).enumerate() {
assert!(
a == b,
"blocks of {block}: tick {tick} differs from the single pass"
);
}
}
}