1use crate::world::block::is_solid;
8use crate::world::handle::WorldHandle;
9
10#[derive(Debug, Clone, Copy)]
12pub struct Aabb {
13 pub min_x: f64,
15 pub min_y: f64,
17 pub min_z: f64,
19 pub max_x: f64,
21 pub max_y: f64,
23 pub max_z: f64,
25}
26
27impl Aabb {
28 pub fn from_entity(x: f64, y: f64, z: f64, width: f32, height: f32) -> Self {
33 let hw = f64::from(width) / 2.0;
34 let h = f64::from(height);
35 Self {
36 min_x: x - hw,
37 min_y: y,
38 min_z: z - hw,
39 max_x: x + hw,
40 max_y: y + h,
41 max_z: z + hw,
42 }
43 }
44
45 pub fn offset(&self, dx: f64, dy: f64, dz: f64) -> Self {
47 Self {
48 min_x: self.min_x + dx,
49 min_y: self.min_y + dy,
50 min_z: self.min_z + dz,
51 max_x: self.max_x + dx,
52 max_y: self.max_y + dy,
53 max_z: self.max_z + dz,
54 }
55 }
56
57 fn overlaps_block(&self, bx: i32, by: i32, bz: i32) -> bool {
59 let bx = bx as f64;
60 let by = by as f64;
61 let bz = bz as f64;
62 self.max_x > bx
63 && self.min_x < bx + 1.0
64 && self.max_y > by
65 && self.min_y < by + 1.0
66 && self.max_z > bz
67 && self.min_z < bz + 1.0
68 }
69}
70
71pub fn check_overlap(world: &dyn WorldHandle, aabb: &Aabb) -> bool {
77 let min_bx = aabb.min_x.floor() as i32;
78 let min_by = aabb.min_y.floor() as i32;
79 let min_bz = aabb.min_z.floor() as i32;
80 let max_bx = aabb.max_x.ceil() as i32;
81 let max_by = aabb.max_y.ceil() as i32;
82 let max_bz = aabb.max_z.ceil() as i32;
83
84 for bx in min_bx..max_bx {
85 for by in min_by..max_by {
86 for bz in min_bz..max_bz {
87 if is_solid(world.get_block(bx, by, bz)) && aabb.overlaps_block(bx, by, bz) {
88 return true;
89 }
90 }
91 }
92 }
93 false
94}
95
96#[derive(Debug, Clone)]
98pub struct RayHit {
99 pub block_x: i32,
101 pub block_y: i32,
103 pub block_z: i32,
105 pub distance: f64,
107}
108
109pub fn ray_cast(
114 world: &dyn WorldHandle,
115 origin: (f64, f64, f64),
116 direction: (f64, f64, f64),
117 max_distance: f64,
118) -> Option<RayHit> {
119 let (origin_x, origin_y, origin_z) = origin;
120 let (dir_x, dir_y, dir_z) = direction;
121 let step = 0.1;
122 let steps = (max_distance / step) as usize;
123 let len = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z).sqrt();
124 if len < 1e-10 {
125 return None;
126 }
127 let (nx, ny, nz) = (dir_x / len, dir_y / len, dir_z / len);
128
129 for i in 0..=steps {
130 let d = i as f64 * step;
131 let x = origin_x + nx * d;
132 let y = origin_y + ny * d;
133 let z = origin_z + nz * d;
134 let bx = x.floor() as i32;
135 let by = y.floor() as i32;
136 let bz = z.floor() as i32;
137
138 if is_solid(world.get_block(bx, by, bz)) {
139 return Some(RayHit {
140 block_x: bx,
141 block_y: by,
142 block_z: bz,
143 distance: d,
144 });
145 }
146 }
147 None
148}
149
150pub fn resolve_movement(
156 world: &dyn WorldHandle,
157 aabb: &Aabb,
158 dx: f64,
159 dy: f64,
160 dz: f64,
161) -> (f64, f64, f64) {
162 let mut resolved_dy = dy;
163 let mut resolved_dx = dx;
164 let mut resolved_dz = dz;
165
166 if resolved_dy != 0.0 {
168 let test = aabb.offset(0.0, resolved_dy, 0.0);
169 if check_overlap(world, &test) {
170 if resolved_dy < 0.0 {
172 resolved_dy = (aabb.min_y.floor() - aabb.min_y).max(resolved_dy);
174 if check_overlap(world, &aabb.offset(0.0, resolved_dy, 0.0)) {
176 resolved_dy = 0.0;
177 }
178 } else {
179 resolved_dy = (aabb.max_y.ceil() - aabb.max_y).min(resolved_dy);
181 if check_overlap(world, &aabb.offset(0.0, resolved_dy, 0.0)) {
182 resolved_dy = 0.0;
183 }
184 }
185 }
186 }
187
188 let aabb_after_y = aabb.offset(0.0, resolved_dy, 0.0);
189
190 if resolved_dx != 0.0 {
192 let test = aabb_after_y.offset(resolved_dx, 0.0, 0.0);
193 if check_overlap(world, &test) {
194 resolved_dx = 0.0;
195 }
196 }
197
198 if resolved_dz != 0.0 {
200 let test = aabb_after_y.offset(resolved_dx, 0.0, resolved_dz);
201 if check_overlap(world, &test) {
202 resolved_dz = 0.0;
203 }
204 }
205
206 (resolved_dx, resolved_dy, resolved_dz)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::world::block;
213 use crate::world::block_entity::BlockEntity;
214
215 struct FlatMock;
220
221 impl WorldHandle for FlatMock {
222 fn get_block(&self, _x: i32, y: i32, _z: i32) -> u16 {
223 match y {
224 -64 => block::BEDROCK,
225 -63 | -62 => block::DIRT,
226 -61 => block::GRASS_BLOCK,
227 _ => block::AIR,
228 }
229 }
230 fn set_block(&self, _x: i32, _y: i32, _z: i32, _state: u16) {}
231 fn get_block_entity(&self, _x: i32, _y: i32, _z: i32) -> Option<BlockEntity> {
232 None
233 }
234 fn set_block_entity(&self, _x: i32, _y: i32, _z: i32, _entity: BlockEntity) {}
235 fn mark_chunk_dirty(&self, _cx: i32, _cz: i32) {}
236 fn persist_chunk(&self, _cx: i32, _cz: i32) {}
237 fn dirty_chunks(&self) -> Vec<(i32, i32)> {
238 Vec::new()
239 }
240 fn check_overlap(&self, aabb: &Aabb) -> bool {
241 check_overlap(self, aabb)
242 }
243 fn ray_cast(
244 &self,
245 origin: (f64, f64, f64),
246 direction: (f64, f64, f64),
247 max_distance: f64,
248 ) -> Option<RayHit> {
249 ray_cast(self, origin, direction, max_distance)
250 }
251 fn resolve_movement(&self, aabb: &Aabb, dx: f64, dy: f64, dz: f64) -> (f64, f64, f64) {
252 resolve_movement(self, aabb, dx, dy, dz)
253 }
254 }
255
256 fn test_world() -> FlatMock {
257 FlatMock
258 }
259
260 #[test]
261 fn aabb_from_entity() {
262 let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
263 assert!(aabb.min_x < 0.0, "min_x should be negative");
265 assert!((aabb.min_y - (-60.0)).abs() < 1e-6);
266 assert!((aabb.max_y - (-58.2)).abs() < 1e-4);
267 }
268
269 #[test]
270 fn check_overlap_detects_solid() {
271 let world = test_world();
272 let aabb = Aabb::from_entity(0.0, -62.0, 0.0, 0.6, 1.8);
274 assert!(check_overlap(&world, &aabb));
275 }
276
277 #[test]
278 fn check_overlap_no_collision_in_air() {
279 let world = test_world();
280 let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
282 assert!(!check_overlap(&world, &aabb));
283 }
284
285 #[test]
286 fn ray_cast_finds_ground() {
287 let world = test_world();
288 let hit = ray_cast(&world, (0.5, -50.0, 0.5), (0.0, -1.0, 0.0), 20.0);
290 assert!(hit.is_some());
291 let hit = hit.unwrap();
292 assert_eq!(hit.block_y, -61); }
294
295 #[test]
296 fn ray_cast_misses_in_air() {
297 let world = test_world();
298 let hit = ray_cast(&world, (0.5, -50.0, 0.5), (1.0, 0.0, 0.0), 5.0);
300 assert!(hit.is_none());
301 }
302
303 #[test]
304 fn resolve_movement_stops_at_ground() {
305 let world = test_world();
306 let aabb = Aabb::from_entity(0.0, -60.0, 0.0, 0.6, 1.8);
308 let (dx, dy, dz) = resolve_movement(&world, &aabb, 0.0, -1.0, 0.0);
309 assert_eq!(dx, 0.0);
310 assert_eq!(dy, 0.0); assert_eq!(dz, 0.0);
312 }
313
314 #[test]
315 fn resolve_movement_allows_free_fall() {
316 let world = test_world();
317 let aabb = Aabb::from_entity(0.0, -50.0, 0.0, 0.6, 1.8);
319 let (_, dy, _) = resolve_movement(&world, &aabb, 0.0, -0.5, 0.0);
320 assert!((dy - (-0.5)).abs() < f64::EPSILON);
321 }
322
323 #[test]
324 fn resolve_movement_stops_horizontal() {
325 let world = test_world();
326 let aabb = Aabb::from_entity(0.0, -62.0, 0.0, 0.6, 1.8);
328 let (dx, _, _) = resolve_movement(&world, &aabb, 1.0, 0.0, 0.0);
329 assert_eq!(dx, 0.0);
330 }
331}