use alice_physics::character::{CharacterConfig, CharacterController, PushImpulse};
use alice_physics::math::{Fix128, Vec3Fix};
use alice_physics::solver::RigidBody;
struct ExpectedDefaults {
radius: f64,
height: f64,
push_force: f64,
}
const EXPECTED_DEFAULTS: ExpectedDefaults = ExpectedDefaults {
radius: 0.3,
height: 1.8,
push_force: 5.0,
};
fn assert_close(label: &str, actual: f64, expected: f64, tol: f64) {
let diff = (actual - expected).abs();
assert!(
diff <= tol,
"[character] FAIL {label}: actual={actual} expected={expected} diff={diff} tol={tol}"
);
println!("[character] OK {label}: actual={actual:.6} expected={expected:.6}");
}
fn demo_new_default() {
println!("[character] --- new_default ---");
let start = Vec3Fix::from_int(2, 5, -3);
let cc = CharacterController::new_default(start);
assert_close("new_default.position.x", cc.position.x.to_f64(), 2.0, 1e-9);
assert_close("new_default.position.y", cc.position.y.to_f64(), 5.0, 1e-9);
assert_close("new_default.position.z", cc.position.z.to_f64(), -3.0, 1e-9);
assert_close("new_default.velocity.x", cc.velocity.x.to_f64(), 0.0, 1e-9);
assert_close("new_default.velocity.y", cc.velocity.y.to_f64(), 0.0, 1e-9);
assert!(
!cc.grounded,
"[character] FAIL new_default.grounded: expected false"
);
println!("[character] OK new_default.grounded: false");
assert!(
cc.ground_body_index.is_none(),
"[character] FAIL new_default.ground_body_index: expected None"
);
println!("[character] OK new_default.ground_body_index: None");
assert_close(
"new_default.platform_velocity.y",
cc.platform_velocity.y.to_f64(),
0.0,
1e-9,
);
assert_close(
"new_default.config.radius",
cc.config.radius.to_f64(),
EXPECTED_DEFAULTS.radius,
1e-9,
);
assert_close(
"new_default.config.height",
cc.config.height.to_f64(),
EXPECTED_DEFAULTS.height,
1e-9,
);
assert_close(
"new_default.config.push_force",
cc.config.push_force.to_f64(),
EXPECTED_DEFAULTS.push_force,
1e-9,
);
}
fn demo_feet_position() {
println!("[character] --- feet_position ---");
let body_y = 5.0_f64;
let cc = CharacterController::new_default(Vec3Fix::from_int(0, 5, 0));
let half_height = EXPECTED_DEFAULTS.height / 2.0;
let expected_feet_y = body_y - half_height + EXPECTED_DEFAULTS.radius;
let feet = cc.feet_position();
assert_close(
"feet_position.y (y=5)",
feet.y.to_f64(),
expected_feet_y,
1e-9,
);
assert_close("feet_position.x (unchanged)", feet.x.to_f64(), 0.0, 1e-9);
assert_close("feet_position.z (unchanged)", feet.z.to_f64(), 0.0, 1e-9);
let zero_config = CharacterConfig {
radius: Fix128::ZERO,
height: Fix128::ZERO,
..CharacterConfig::default()
};
let cc_zero = CharacterController::new(Vec3Fix::from_int(1, 7, 1), zero_config);
let feet_zero = cc_zero.feet_position();
assert_close(
"feet_position degenerate (zero height/radius)",
feet_zero.y.to_f64(),
7.0,
1e-9,
);
}
fn demo_apply_gravity() {
println!("[character] --- apply_gravity ---");
let mut cc = CharacterController::new_default(Vec3Fix::from_int(0, 5, 0));
let g = Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-10), Fix128::ZERO);
let dt = Fix128::from_ratio(1, 4);
let expected_v1 = 0.0_f64 + (-10.0_f64 * 0.25_f64);
cc.apply_gravity(g, dt);
assert_close(
"apply_gravity v1 (airborne)",
cc.velocity.y.to_f64(),
expected_v1,
1e-9,
);
let expected_v2 = expected_v1 + (-10.0_f64 * 0.25_f64);
cc.apply_gravity(g, dt);
assert_close(
"apply_gravity v2 (airborne x2)",
cc.velocity.y.to_f64(),
expected_v2,
1e-9,
);
cc.grounded = true;
cc.apply_gravity(g, dt);
assert_close(
"apply_gravity (grounded, frozen)",
cc.velocity.y.to_f64(),
expected_v2,
1e-9,
);
let mut cc2 = CharacterController::new_default(Vec3Fix::from_int(0, 5, 0));
cc2.apply_gravity(g, Fix128::ZERO);
assert_close("apply_gravity dt=0", cc2.velocity.y.to_f64(), 0.0, 1e-9);
let mut cc3 = CharacterController::new_default(Vec3Fix::from_int(0, 5, 0));
cc3.apply_gravity(Vec3Fix::ZERO, dt);
assert_close("apply_gravity g=0", cc3.velocity.y.to_f64(), 0.0, 1e-9);
}
fn demo_compute_push_impulses() {
println!("[character] --- compute_push_impulses ---");
let cc = CharacterController::new_default(Vec3Fix::from_int(0, 0, 0));
let body_radius = 0.5_f64;
let combined = EXPECTED_DEFAULTS.radius + body_radius;
let dynamic = RigidBody::new(Vec3Fix::from_f32(0.4, 0.0, 0.0), Fix128::ONE);
let bodies = vec![dynamic];
let pushes: Vec<PushImpulse> = cc.compute_push_impulses(&bodies, Fix128::from_ratio(1, 2));
assert_eq!(
pushes.len(),
1,
"[character] FAIL compute_push_impulses: expected 1 push"
);
println!("[character] OK compute_push_impulses.len: 1");
let dist = 0.4_f64;
let expected_overlap = combined - dist; let expected_normal = (1.0_f64, 0.0_f64, 0.0_f64); let expected_impulse_x = expected_normal.0 * expected_overlap * EXPECTED_DEFAULTS.push_force;
let push = pushes[0];
assert_eq!(push.body_index, 0);
println!("[character] OK compute_push_impulses.body_index: 0");
assert_close(
"compute_push_impulses.impulse.x",
push.impulse.x.to_f64(),
expected_impulse_x,
1e-6,
);
assert_close(
"compute_push_impulses.impulse.y",
push.impulse.y.to_f64(),
0.0,
1e-6,
);
let expected_point_x = 0.4_f64 - expected_normal.0 * body_radius;
assert_close(
"compute_push_impulses.point.x",
push.point.x.to_f64(),
expected_point_x,
1e-6,
);
let far = RigidBody::new(Vec3Fix::from_f32(combined as f32, 0.0, 0.0), Fix128::ONE);
let far_bodies = vec![far];
let far_pushes = cc.compute_push_impulses(&far_bodies, Fix128::from_ratio(1, 2));
assert!(
far_pushes.is_empty(),
"[character] FAIL compute_push_impulses degenerate: expected 0 pushes at exact combined distance"
);
println!("[character] OK compute_push_impulses degenerate (dist==combined): 0 pushes");
let static_body = RigidBody::new_static(Vec3Fix::ZERO);
let static_bodies = vec![static_body];
let static_pushes = cc.compute_push_impulses(&static_bodies, Fix128::from_ratio(1, 2));
assert!(
static_pushes.is_empty(),
"[character] FAIL compute_push_impulses degenerate: expected 0 pushes for static body"
);
println!("[character] OK compute_push_impulses degenerate (static body): 0 pushes");
let manual = PushImpulse {
body_index: 7,
impulse: Vec3Fix::from_int(1, 0, 0),
point: Vec3Fix::ZERO,
};
println!(
"[character] OK PushImpulse direct construction: body_index={}",
manual.body_index
);
}
fn demo_get_platform_velocity() {
println!("[character] --- get_platform_velocity ---");
let fresh = CharacterController::new_default(Vec3Fix::from_int(0, 100, 0));
assert_close(
"get_platform_velocity (fresh, no platform)",
fresh.get_platform_velocity().x.to_f64(),
0.0,
1e-9,
);
let mut cc = CharacterController::new_default(Vec3Fix::from_int(0, 1, 0));
let mut platform = RigidBody::new_kinematic(Vec3Fix::ZERO);
let expected_platform_vx = 5.0_f64;
platform.velocity = Vec3Fix::from_int(5, 0, 0);
let bodies = vec![platform];
cc.move_and_slide(Vec3Fix::ZERO, &bodies, &[]);
assert!(
cc.grounded,
"[character] FAIL get_platform_velocity: expected grounded on platform"
);
assert_close(
"get_platform_velocity (on platform)",
cc.get_platform_velocity().x.to_f64(),
expected_platform_vx,
1e-9,
);
cc.move_and_slide(Vec3Fix::ZERO, &bodies, &[]);
assert_close(
"get_platform_velocity (left platform, reset)",
cc.get_platform_velocity().x.to_f64(),
0.0,
1e-9,
);
}
fn main() {
println!("ALICE-Physics Character Controller");
println!("===================================");
demo_new_default();
demo_feet_position();
demo_apply_gravity();
demo_compute_push_impulses();
demo_get_platform_velocity();
println!();
println!("[character] all closed-form checks passed");
}