use crate::world::block::is_solid;
use crate::world::handle::WorldHandle;
#[derive(Debug, Clone, Copy)]
pub struct Aabb {
pub min_x: f64,
pub min_y: f64,
pub min_z: f64,
pub max_x: f64,
pub max_y: f64,
pub max_z: f64,
}
impl Aabb {
pub fn from_entity(x: f64, y: f64, z: f64, width: f32, height: f32) -> Self {
let hw = f64::from(width) / 2.0;
let h = f64::from(height);
Self {
min_x: x - hw,
min_y: y,
min_z: z - hw,
max_x: x + hw,
max_y: y + h,
max_z: z + hw,
}
}
pub fn offset(&self, dx: f64, dy: f64, dz: f64) -> Self {
Self {
min_x: self.min_x + dx,
min_y: self.min_y + dy,
min_z: self.min_z + dz,
max_x: self.max_x + dx,
max_y: self.max_y + dy,
max_z: self.max_z + dz,
}
}
fn overlaps_block(&self, bx: i32, by: i32, bz: i32) -> bool {
let bx = bx as f64;
let by = by as f64;
let bz = bz as f64;
self.max_x > bx
&& self.min_x < bx + 1.0
&& self.max_y > by
&& self.min_y < by + 1.0
&& self.max_z > bz
&& self.min_z < bz + 1.0
}
}
pub fn check_overlap(world: &dyn WorldHandle, aabb: &Aabb) -> bool {
let min_bx = aabb.min_x.floor() as i32;
let min_by = aabb.min_y.floor() as i32;
let min_bz = aabb.min_z.floor() as i32;
let max_bx = aabb.max_x.ceil() as i32;
let max_by = aabb.max_y.ceil() as i32;
let max_bz = aabb.max_z.ceil() as i32;
for bx in min_bx..max_bx {
for by in min_by..max_by {
for bz in min_bz..max_bz {
if is_solid(world.get_block(bx, by, bz)) && aabb.overlaps_block(bx, by, bz) {
return true;
}
}
}
}
false
}
#[derive(Debug, Clone)]
pub struct RayHit {
pub block_x: i32,
pub block_y: i32,
pub block_z: i32,
pub distance: f64,
}
pub fn ray_cast(
world: &dyn WorldHandle,
origin: (f64, f64, f64),
direction: (f64, f64, f64),
max_distance: f64,
) -> Option<RayHit> {
let (origin_x, origin_y, origin_z) = origin;
let (dir_x, dir_y, dir_z) = direction;
let step = 0.1;
let steps = (max_distance / step) as usize;
let len = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z).sqrt();
if len < 1e-10 {
return None;
}
let (nx, ny, nz) = (dir_x / len, dir_y / len, dir_z / len);
for i in 0..=steps {
let d = i as f64 * step;
let x = origin_x + nx * d;
let y = origin_y + ny * d;
let z = origin_z + nz * d;
let bx = x.floor() as i32;
let by = y.floor() as i32;
let bz = z.floor() as i32;
if is_solid(world.get_block(bx, by, bz)) {
return Some(RayHit {
block_x: bx,
block_y: by,
block_z: bz,
distance: d,
});
}
}
None
}
pub fn resolve_movement(
world: &dyn WorldHandle,
aabb: &Aabb,
dx: f64,
dy: f64,
dz: f64,
) -> (f64, f64, f64) {
let mut resolved_dy = dy;
let mut resolved_dx = dx;
let mut resolved_dz = dz;
if resolved_dy != 0.0 {
let test = aabb.offset(0.0, resolved_dy, 0.0);
if check_overlap(world, &test) {
if resolved_dy < 0.0 {
resolved_dy = (aabb.min_y.floor() - aabb.min_y).max(resolved_dy);
if check_overlap(world, &aabb.offset(0.0, resolved_dy, 0.0)) {
resolved_dy = 0.0;
}
} else {
resolved_dy = (aabb.max_y.ceil() - aabb.max_y).min(resolved_dy);
if check_overlap(world, &aabb.offset(0.0, resolved_dy, 0.0)) {
resolved_dy = 0.0;
}
}
}
}
let aabb_after_y = aabb.offset(0.0, resolved_dy, 0.0);
if resolved_dx != 0.0 {
let test = aabb_after_y.offset(resolved_dx, 0.0, 0.0);
if check_overlap(world, &test) {
resolved_dx = 0.0;
}
}
if resolved_dz != 0.0 {
let test = aabb_after_y.offset(resolved_dx, 0.0, resolved_dz);
if check_overlap(world, &test) {
resolved_dz = 0.0;
}
}
(resolved_dx, resolved_dy, resolved_dz)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::block;
use crate::world::block_entity::BlockEntity;
struct FlatMock;
impl WorldHandle for FlatMock {
fn get_block(&self, _x: i32, y: i32, _z: i32) -> u16 {
match y {
-64 => block::BEDROCK,
-63 | -62 => block::DIRT,
-61 => block::GRASS_BLOCK,
_ => block::AIR,
}
}
fn set_block(&self, _x: i32, _y: i32, _z: i32, _state: u16) {}
fn get_block_entity(&self, _x: i32, _y: i32, _z: i32) -> Option<BlockEntity> {
None
}
fn set_block_entity(&self, _x: i32, _y: i32, _z: i32, _entity: BlockEntity) {}
fn mark_chunk_dirty(&self, _cx: i32, _cz: i32) {}
fn persist_chunk(&self, _cx: i32, _cz: i32) {}
fn dirty_chunks(&self) -> Vec<(i32, i32)> {
Vec::new()
}
fn check_overlap(&self, aabb: &Aabb) -> bool {
check_overlap(self, aabb)
}
fn ray_cast(
&self,
origin: (f64, f64, f64),
direction: (f64, f64, f64),
max_distance: f64,
) -> Option<RayHit> {
ray_cast(self, origin, direction, max_distance)
}
fn resolve_movement(&self, aabb: &Aabb, dx: f64, dy: f64, dz: f64) -> (f64, f64, f64) {
resolve_movement(self, aabb, dx, dy, dz)
}
}
fn test_world() -> FlatMock {
FlatMock
}
#[test]
fn aabb_from_entity() {
let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
assert!(aabb.min_x < 0.0, "min_x should be negative");
assert!((aabb.min_y - (-60.0)).abs() < 1e-6);
assert!((aabb.max_y - (-58.2)).abs() < 1e-4);
}
#[test]
fn check_overlap_detects_solid() {
let world = test_world();
let aabb = Aabb::from_entity(0.0, -62.0, 0.0, 0.6, 1.8);
assert!(check_overlap(&world, &aabb));
}
#[test]
fn check_overlap_no_collision_in_air() {
let world = test_world();
let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
assert!(!check_overlap(&world, &aabb));
}
#[test]
fn ray_cast_finds_ground() {
let world = test_world();
let hit = ray_cast(&world, (0.5, -50.0, 0.5), (0.0, -1.0, 0.0), 20.0);
assert!(hit.is_some());
let hit = hit.unwrap();
assert_eq!(hit.block_y, -61); }
#[test]
fn ray_cast_misses_in_air() {
let world = test_world();
let hit = ray_cast(&world, (0.5, -50.0, 0.5), (1.0, 0.0, 0.0), 5.0);
assert!(hit.is_none());
}
#[test]
fn resolve_movement_stops_at_ground() {
let world = test_world();
let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
let (dx, dy, dz) = resolve_movement(&world, &aabb, 0.0, -1.0, 0.0);
assert_eq!(dx, 0.0);
assert_eq!(dy, 0.0); assert_eq!(dz, 0.0);
}
#[test]
fn resolve_movement_allows_free_fall() {
let world = test_world();
let aabb = Aabb::from_entity(0.0, -50.0, 0.0, 0.6, 1.8);
let (_, dy, _) = resolve_movement(&world, &aabb, 0.0, -0.5, 0.0);
assert!((dy - (-0.5)).abs() < f64::EPSILON);
}
#[test]
fn resolve_movement_stops_horizontal() {
let world = test_world();
let aabb = Aabb::from_entity(0.0, -62.0, 0.0, 0.6, 1.8);
let (dx, _, _) = resolve_movement(&world, &aabb, 1.0, 0.0, 0.0);
assert_eq!(dx, 0.0);
}
}