use hisab::Vec2;
use crate::steer::Obstacle;
#[must_use]
#[inline]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn collider_to_obstacle(position: [f32; 3], radius: f32) -> Obstacle {
Obstacle {
center: Vec2::new(position[0], position[2]),
radius,
}
}
#[must_use]
#[inline]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn velocity_3d_to_2d(velocity: [f32; 3]) -> Vec2 {
Vec2::new(velocity[0], velocity[2])
}
#[must_use]
#[inline]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn group_target_to_destination(target: [f32; 3]) -> Vec2 {
Vec2::new(target[0], target[2])
}
#[must_use]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn flee_point_to_repulsion(
flee_from: [f32; 3],
agent_pos: [f32; 3],
max_range: f32,
) -> (Vec2, f32) {
let dx = agent_pos[0] - flee_from[0];
let dz = agent_pos[2] - flee_from[2];
let dist = (dx * dx + dz * dz).sqrt();
let strength = if max_range > 0.0 {
(1.0 - dist / max_range).clamp(0.0, 1.0)
} else {
0.0
};
(Vec2::new(flee_from[0], flee_from[2]), strength)
}
#[must_use]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn wind_to_movement_cost(wind: [f32; 2], movement_direction: [f32; 2]) -> f32 {
let dot = wind[0] * movement_direction[0] + wind[1] * movement_direction[1];
(1.0 - dot / 10.0).clamp(0.5, 2.0)
}
#[must_use]
#[inline]
#[cfg_attr(feature = "logging", tracing::instrument)]
pub fn slope_to_speed_scale(slope_rad: f32) -> f32 {
let slope_deg = slope_rad.abs() * 180.0 / core::f32::consts::PI;
if slope_deg > 45.0 {
return 0.0; }
(1.0 - slope_deg / 60.0).clamp(0.1, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collider_to_obstacle_basic() {
let obs = collider_to_obstacle([1.0, 2.0, 3.0], 0.5);
assert!((obs.center.x - 1.0).abs() < 0.001);
assert!((obs.center.y - 3.0).abs() < 0.001);
assert!((obs.radius - 0.5).abs() < 0.001);
}
#[test]
fn velocity_drop_y() {
let v = velocity_3d_to_2d([1.0, 5.0, 3.0]);
assert!((v.x - 1.0).abs() < 0.001);
assert!((v.y - 3.0).abs() < 0.001);
}
#[test]
fn group_target_drops_y() {
let d = group_target_to_destination([10.0, 5.0, 20.0]);
assert!((d.x - 10.0).abs() < 0.001);
assert!((d.y - 20.0).abs() < 0.001);
}
#[test]
fn flee_repulsion_close() {
let (_, strength) = flee_point_to_repulsion([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 10.0);
assert!(strength > 0.5);
}
#[test]
fn flee_repulsion_far() {
let (_, strength) = flee_point_to_repulsion([0.0, 0.0, 0.0], [15.0, 0.0, 0.0], 10.0);
assert!(strength.abs() < 0.001);
}
#[test]
fn flee_repulsion_returns_center() {
let (center, _) = flee_point_to_repulsion([3.0, 1.0, 7.0], [0.0, 0.0, 0.0], 10.0);
assert!((center.x - 3.0).abs() < 0.001);
assert!((center.y - 7.0).abs() < 0.001);
}
#[test]
fn wind_tailwind_cheap() {
let cost = wind_to_movement_cost([5.0, 0.0], [1.0, 0.0]);
assert!(cost < 1.0);
}
#[test]
fn wind_headwind_costly() {
let cost = wind_to_movement_cost([-5.0, 0.0], [1.0, 0.0]);
assert!(cost > 1.0);
}
#[test]
fn wind_no_wind() {
let cost = wind_to_movement_cost([0.0, 0.0], [1.0, 0.0]);
assert!((cost - 1.0).abs() < 0.01);
}
#[test]
fn wind_perpendicular_neutral() {
let cost = wind_to_movement_cost([0.0, 5.0], [1.0, 0.0]);
assert!((cost - 1.0).abs() < 0.01);
}
#[test]
fn wind_clamps_to_bounds() {
let cost = wind_to_movement_cost([-100.0, 0.0], [1.0, 0.0]);
assert!((cost - 2.0).abs() < 0.01);
let cost = wind_to_movement_cost([100.0, 0.0], [1.0, 0.0]);
assert!((cost - 0.5).abs() < 0.01);
}
#[test]
fn slope_flat() {
assert!((slope_to_speed_scale(0.0) - 1.0).abs() < 0.01);
}
#[test]
fn slope_steep_impassable() {
let scale = slope_to_speed_scale(std::f32::consts::FRAC_PI_2); assert!(scale.abs() < 0.001);
}
#[test]
fn slope_moderate() {
let scale = slope_to_speed_scale(0.26); assert!(scale > 0.5 && scale < 1.0);
}
}