Skip to main content

steel_core/entity/ai/
navigation.rs

1//! Path navigation state shell.
2
3use glam::DVec3;
4use steel_math::fast_floor;
5use steel_registry::REGISTRY;
6use steel_registry::vanilla_block_tags::BlockTag;
7use steel_utils::BlockPos;
8
9use crate::entity::ai::path::{Path, PathType, PathTypeCache, PathfindingContext};
10use crate::entity::ai::pathfinder::{PathFinder, PathRequest};
11use crate::entity::ai::walk::{WalkNodeCollision, WalkNodeEvaluator};
12use crate::world::LevelReader;
13
14const DIRECT_TARGET_REACHED_DISTANCE_SQR: f64 = 2.500_000_3e-7;
15const DEFAULT_REQUIRED_PATH_LENGTH: f32 = 16.0;
16const MAX_TIME_RECOMPUTE: i64 = 20;
17const MAX_VISITED_NODES_SCALE: f32 = 16.0;
18const STUCK_CHECK_INTERVAL: i32 = 100;
19const STUCK_THRESHOLD_DISTANCE_FACTOR: f32 = 0.25;
20
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct NavigationPathRequest<'a> {
23    pub mob_position: BlockPos,
24    pub targets: &'a [BlockPos],
25    pub max_path_length: f32,
26    pub reach_range: i32,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct NavigationTickContext {
31    pub mob_position: DVec3,
32    pub mob_bounding_box_width: f64,
33    pub mob_speed: f32,
34    pub game_time: i64,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct NavigationRecomputeRequest {
39    pub target_pos: BlockPos,
40    pub reach_range: i32,
41    pub game_time: i64,
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct PathNavigation {
46    path: Option<Path>,
47    path_finder: PathFinder,
48    path_type_cache: PathTypeCache,
49    direct_target: Option<DVec3>,
50    target_pos: Option<BlockPos>,
51    speed_modifier: f64,
52    max_visited_nodes_multiplier: f32,
53    required_path_length: f32,
54    reach_range: i32,
55    tick: i32,
56    last_stuck_check: i32,
57    last_stuck_check_pos: DVec3,
58    timeout_cached_node: BlockPos,
59    timeout_timer: i64,
60    last_timeout_check: i64,
61    timeout_limit: f64,
62    has_delayed_recomputation: bool,
63    time_last_recompute: i64,
64    stuck: bool,
65    done: bool,
66    can_float: bool,
67    can_open_doors: bool,
68    can_walk_over_fences: bool,
69    avoid_sun: bool,
70    can_path_to_targets_below_surface: bool,
71}
72
73impl PathNavigation {
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            path: None,
78            path_finder: PathFinder::new(0),
79            path_type_cache: PathTypeCache::new(),
80            direct_target: None,
81            target_pos: None,
82            speed_modifier: 0.0,
83            max_visited_nodes_multiplier: 1.0,
84            required_path_length: DEFAULT_REQUIRED_PATH_LENGTH,
85            reach_range: 0,
86            tick: 0,
87            last_stuck_check: 0,
88            last_stuck_check_pos: DVec3::ZERO,
89            timeout_cached_node: BlockPos::ZERO,
90            timeout_timer: 0,
91            last_timeout_check: 0,
92            timeout_limit: 0.0,
93            has_delayed_recomputation: false,
94            time_last_recompute: 0,
95            stuck: false,
96            done: true,
97            can_float: false,
98            can_open_doors: false,
99            can_walk_over_fences: false,
100            avoid_sun: false,
101            can_path_to_targets_below_surface: false,
102        }
103    }
104
105    #[must_use]
106    pub const fn path(&self) -> Option<&Path> {
107        self.path.as_ref()
108    }
109
110    #[must_use]
111    pub const fn target_pos(&self) -> Option<BlockPos> {
112        self.target_pos
113    }
114
115    #[must_use]
116    pub const fn speed_modifier(&self) -> f64 {
117        self.speed_modifier
118    }
119
120    pub const fn set_speed_modifier(&mut self, speed_modifier: f64) {
121        self.speed_modifier = speed_modifier;
122    }
123
124    #[must_use]
125    pub const fn reach_range(&self) -> i32 {
126        self.reach_range
127    }
128
129    #[must_use]
130    pub const fn required_path_length(&self) -> f32 {
131        self.required_path_length
132    }
133
134    #[must_use]
135    pub const fn max_visited_nodes_multiplier(&self) -> f32 {
136        self.max_visited_nodes_multiplier
137    }
138
139    pub fn set_required_path_length(&mut self, required_path_length: f32, follow_range: f64) {
140        self.required_path_length = required_path_length;
141        self.update_pathfinder_max_visited_nodes(follow_range);
142    }
143
144    pub const fn reset_max_visited_nodes_multiplier(&mut self) {
145        self.max_visited_nodes_multiplier = 1.0;
146    }
147
148    pub const fn set_max_visited_nodes_multiplier(&mut self, max_visited_nodes_multiplier: f32) {
149        self.max_visited_nodes_multiplier = max_visited_nodes_multiplier;
150    }
151
152    #[must_use]
153    pub const fn is_done(&self) -> bool {
154        self.done
155    }
156
157    #[must_use]
158    pub const fn tick_count(&self) -> i32 {
159        self.tick
160    }
161
162    #[must_use]
163    pub const fn is_stuck(&self) -> bool {
164        self.stuck
165    }
166
167    #[must_use]
168    pub const fn has_delayed_recomputation(&self) -> bool {
169        self.has_delayed_recomputation
170    }
171
172    #[must_use]
173    pub const fn can_float(&self) -> bool {
174        self.can_float
175    }
176
177    pub const fn set_can_float(&mut self, can_float: bool) {
178        self.can_float = can_float;
179    }
180
181    #[must_use]
182    pub const fn can_open_doors(&self) -> bool {
183        self.can_open_doors
184    }
185
186    pub const fn set_can_open_doors(&mut self, can_open_doors: bool) {
187        self.can_open_doors = can_open_doors;
188    }
189
190    #[must_use]
191    pub const fn can_walk_over_fences(&self) -> bool {
192        self.can_walk_over_fences
193    }
194
195    pub const fn set_can_walk_over_fences(&mut self, can_walk_over_fences: bool) {
196        self.can_walk_over_fences = can_walk_over_fences;
197    }
198
199    #[must_use]
200    pub const fn avoid_sun(&self) -> bool {
201        self.avoid_sun
202    }
203
204    pub const fn set_avoid_sun(&mut self, avoid_sun: bool) {
205        self.avoid_sun = avoid_sun;
206    }
207
208    #[must_use]
209    pub const fn can_path_to_targets_below_surface(&self) -> bool {
210        self.can_path_to_targets_below_surface
211    }
212
213    pub const fn set_can_path_to_targets_below_surface(
214        &mut self,
215        can_path_to_targets_below_surface: bool,
216    ) {
217        self.can_path_to_targets_below_surface = can_path_to_targets_below_surface;
218    }
219
220    pub const fn tick(&mut self) {
221        self.tick = self.tick.wrapping_add(1);
222    }
223
224    pub fn invalidate_path_type(&mut self, pos: BlockPos) {
225        self.path_type_cache.invalidate(pos);
226    }
227
228    pub fn stop(&mut self) {
229        self.path = None;
230        self.done = true;
231    }
232
233    #[must_use]
234    pub const fn max_path_length(&self, follow_range: f64) -> f32 {
235        (follow_range as f32).max(self.required_path_length)
236    }
237
238    pub fn update_pathfinder_max_visited_nodes(&mut self, follow_range: f64) {
239        let max_visited_nodes = fast_floor(f64::from(
240            self.max_path_length(follow_range) * MAX_VISITED_NODES_SCALE,
241        ));
242        self.path_finder.set_max_visited_nodes(max_visited_nodes);
243    }
244
245    pub fn create_path(
246        &mut self,
247        evaluator: &mut WalkNodeEvaluator,
248        level: &dyn LevelReader,
249        collision: &mut impl WalkNodeCollision,
250        request: NavigationPathRequest<'_>,
251    ) -> Option<Path> {
252        if let Some(path) = self.reusable_current_path(request.targets) {
253            // Vanilla returns the current `Path` instance here. Steel's
254            // navigation API returns owned paths, so reuse copies the path
255            // state instead of running the pathfinder again.
256            return Some(path.clone());
257        }
258
259        let mut context =
260            PathfindingContext::with_cache(level, request.mob_position, &mut self.path_type_cache);
261        let path = self.path_finder.find_path(
262            evaluator,
263            &mut context,
264            collision,
265            PathRequest {
266                targets: request.targets,
267                max_path_length: request.max_path_length,
268                reach_range: request.reach_range,
269                max_visited_nodes_multiplier: self.max_visited_nodes_multiplier,
270            },
271        )?;
272        self.target_pos = Some(path.target());
273        self.reach_range = request.reach_range;
274        self.reset_stuck_timeout();
275        Some(path)
276    }
277
278    fn reusable_current_path(&self, targets: &[BlockPos]) -> Option<&Path> {
279        let path = self.path.as_ref()?;
280        if path.is_done() {
281            return None;
282        }
283
284        let target_pos = self.target_pos?;
285        targets.contains(&target_pos).then_some(path)
286    }
287
288    fn trim_path_for_avoid_sun(
289        &self,
290        level: &dyn LevelReader,
291        mob_position: DVec3,
292        path: &mut Path,
293    ) {
294        if !self.avoid_sun
295            || level.can_see_sky(BlockPos::containing(
296                mob_position.x,
297                mob_position.y + 0.5,
298                mob_position.z,
299            ))
300        {
301            return;
302        }
303
304        for index in 0..path.node_count() {
305            let Some(pos) = path.node_pos(index) else {
306                continue;
307            };
308            if level.can_see_sky(pos) {
309                path.truncate_nodes(index);
310                return;
311            }
312        }
313    }
314
315    fn trim_path(&self, level: &dyn LevelReader, mob_position: DVec3, path: &mut Path) {
316        Self::trim_path_for_cauldrons(level, path);
317        self.trim_path_for_avoid_sun(level, mob_position, path);
318    }
319
320    fn trim_path_for_cauldrons(level: &dyn LevelReader, path: &mut Path) {
321        for index in 0..path.node_count() {
322            let Some(node) = path.node(index).cloned() else {
323                continue;
324            };
325            let Some(block) = REGISTRY
326                .blocks
327                .by_state_id(level.get_block_state(node.as_block_pos()))
328            else {
329                continue;
330            };
331            if !block.has_tag(&BlockTag::CAULDRONS) {
332                continue;
333            }
334
335            let _ = path.replace_node(index, node.clone_and_move(node.x, node.y + 1, node.z));
336            let Some(next_node) = path.node(index + 1).cloned() else {
337                continue;
338            };
339            if node.y >= next_node.y {
340                let _ = path.replace_node(
341                    index + 1,
342                    node.clone_and_move(next_node.x, node.y + 1, next_node.z),
343                );
344            }
345        }
346    }
347
348    pub fn move_to(
349        &mut self,
350        level: &dyn LevelReader,
351        mut path: Path,
352        speed_modifier: f64,
353        mob_position: DVec3,
354    ) -> bool {
355        self.direct_target = None;
356        if path.node_count() == 0 {
357            self.path = None;
358            self.done = true;
359            return false;
360        }
361
362        let same_as_current = self
363            .path
364            .as_ref()
365            .is_some_and(|current| path.same_as(current));
366        if !same_as_current {
367            self.trim_path(level, mob_position, &mut path);
368            self.path = Some(path);
369        } else if let Some(mut path) = self.path.take() {
370            self.trim_path(level, mob_position, &mut path);
371            self.path = Some(path);
372        }
373        if self.path.as_ref().is_none_or(Path::is_done) {
374            self.done = true;
375            return false;
376        }
377
378        self.target_pos = self.path.as_ref().map(Path::target);
379        self.speed_modifier = speed_modifier;
380        self.last_stuck_check = self.tick;
381        self.last_stuck_check_pos = mob_position;
382        self.done = false;
383        true
384    }
385
386    pub fn reuse_current_path_to_targets(
387        &mut self,
388        level: &dyn LevelReader,
389        targets: &[BlockPos],
390        speed_modifier: f64,
391        mob_position: DVec3,
392    ) -> bool {
393        if targets.is_empty() {
394            return false;
395        }
396        if self.path.as_ref().is_none_or(Path::is_done) {
397            return false;
398        }
399
400        let Some(target_pos) = self.target_pos else {
401            return false;
402        };
403        if !targets.contains(&target_pos) {
404            return false;
405        }
406
407        if let Some(mut path) = self.path.take() {
408            self.trim_path(level, mob_position, &mut path);
409            self.path = Some(path);
410        }
411        if self.path.as_ref().is_none_or(Path::is_done) {
412            self.done = true;
413            return false;
414        }
415
416        self.direct_target = None;
417        self.speed_modifier = speed_modifier;
418        self.last_stuck_check = self.tick;
419        self.last_stuck_check_pos = mob_position;
420        self.done = false;
421        true
422    }
423
424    pub fn set_direct_target(&mut self, target: DVec3, speed_modifier: f64) {
425        self.path = None;
426        self.direct_target = Some(target);
427        self.target_pos = Some(BlockPos::new(
428            target.x.floor() as i32,
429            target.y.floor() as i32,
430            target.z.floor() as i32,
431        ));
432        self.speed_modifier = speed_modifier;
433        self.done = false;
434    }
435
436    pub fn next_move_target(&mut self, context: NavigationTickContext) -> Option<(DVec3, f64)> {
437        if self.done {
438            return None;
439        }
440
441        if self.path.is_some() {
442            return self.next_path_move_target(context);
443        }
444
445        let target = self.direct_target?;
446        if target.distance_squared(context.mob_position) < DIRECT_TARGET_REACHED_DISTANCE_SQR {
447            self.stop();
448            return None;
449        }
450
451        Some((target, self.speed_modifier))
452    }
453
454    pub fn next_move_target_without_path_update(
455        &mut self,
456        context: NavigationTickContext,
457        on_ground: bool,
458    ) -> Option<(DVec3, f64)> {
459        if self.done {
460            return None;
461        }
462
463        let path = self.path.as_mut()?;
464        let Some(target) = path_move_target(path, context.mob_bounding_box_width) else {
465            self.stop();
466            return None;
467        };
468
469        if context.mob_position.y > target.y
470            && !on_ground
471            && fast_floor(context.mob_position.x) == fast_floor(target.x)
472            && fast_floor(context.mob_position.z) == fast_floor(target.z)
473        {
474            path.advance();
475        }
476
477        if path.is_done() {
478            self.stop();
479            return None;
480        }
481
482        path_move_target(path, context.mob_bounding_box_width)
483            .map(|target| (target, self.speed_modifier))
484    }
485
486    pub fn request_recompute_path(
487        &mut self,
488        game_time: i64,
489        can_update_path: bool,
490    ) -> Option<NavigationRecomputeRequest> {
491        if game_time - self.time_last_recompute <= MAX_TIME_RECOMPUTE || !can_update_path {
492            self.has_delayed_recomputation = true;
493            return None;
494        }
495
496        let target_pos = self.target_pos?;
497        self.path = None;
498        Some(NavigationRecomputeRequest {
499            target_pos,
500            reach_range: self.reach_range,
501            game_time,
502        })
503    }
504
505    pub fn take_delayed_recompute_request(
506        &mut self,
507        game_time: i64,
508        can_update_path: bool,
509    ) -> Option<NavigationRecomputeRequest> {
510        if !self.has_delayed_recomputation {
511            return None;
512        }
513
514        self.request_recompute_path(game_time, can_update_path)
515    }
516
517    pub fn complete_recompute_path(&mut self, path: Option<Path>, game_time: i64) {
518        self.direct_target = None;
519        self.path = path;
520        self.done = self.path.as_ref().is_none_or(Path::is_done);
521        self.time_last_recompute = game_time;
522        self.has_delayed_recomputation = false;
523    }
524
525    #[must_use]
526    pub fn should_recompute_path(&self, pos: BlockPos, mob_position: DVec3) -> bool {
527        if self.has_delayed_recomputation {
528            return false;
529        }
530
531        let Some(path) = self.path.as_ref() else {
532            return false;
533        };
534        if path.is_done() || path.node_count() == 0 {
535            return false;
536        }
537
538        let Some(target) = path.end_node() else {
539            return false;
540        };
541        let middle_pos = DVec3::new(
542            f64::midpoint(f64::from(target.x), mob_position.x),
543            f64::midpoint(f64::from(target.y), mob_position.y),
544            f64::midpoint(f64::from(target.z), mob_position.z),
545        );
546        let distance = (path.node_count() - path.next_node_index()) as f64;
547        block_center(pos).distance_squared(middle_pos) < distance * distance
548    }
549
550    fn next_path_move_target(&mut self, context: NavigationTickContext) -> Option<(DVec3, f64)> {
551        {
552            let Some(path) = self.path.as_mut() else {
553                self.done = true;
554                return None;
555            };
556
557            let Some(current_node_pos) = path.next_node_pos() else {
558                self.stop();
559                return None;
560            };
561
562            let max_distance_to_waypoint = if context.mob_bounding_box_width > 0.75 {
563                context.mob_bounding_box_width / 2.0
564            } else {
565                0.75 - context.mob_bounding_box_width / 2.0
566            };
567            let x_distance =
568                (context.mob_position.x - (f64::from(current_node_pos.x()) + 0.5)).abs();
569            let y_distance = (context.mob_position.y - f64::from(current_node_pos.y())).abs();
570            let z_distance =
571                (context.mob_position.z - (f64::from(current_node_pos.z()) + 0.5)).abs();
572            let is_close_enough_to_current_node = x_distance < max_distance_to_waypoint
573                && z_distance < max_distance_to_waypoint
574                && y_distance < 1.0;
575            let should_cut_corner = path
576                .next_node()
577                .is_some_and(|node| can_cut_corner(node.path_type))
578                && should_target_next_node_in_direction(path, context.mob_position);
579            if is_close_enough_to_current_node || should_cut_corner {
580                path.advance();
581            }
582
583            if path.is_done() {
584                self.stop();
585                return None;
586            }
587        }
588
589        self.do_stuck_detection(context.mob_position, context.mob_speed, context.game_time);
590        if self.done {
591            return None;
592        }
593
594        let Some(path) = self.path.as_ref() else {
595            self.stop();
596            return None;
597        };
598
599        let target = path_move_target(path, context.mob_bounding_box_width)?;
600        Some((target, self.speed_modifier))
601    }
602
603    fn do_stuck_detection(&mut self, mob_position: DVec3, mob_speed: f32, game_time: i64) {
604        if self.tick - self.last_stuck_check > STUCK_CHECK_INTERVAL {
605            let effective_speed = if mob_speed >= 1.0 {
606                mob_speed
607            } else {
608                mob_speed * mob_speed
609            };
610            let threshold_distance =
611                effective_speed * STUCK_CHECK_INTERVAL as f32 * STUCK_THRESHOLD_DISTANCE_FACTOR;
612            if mob_position.distance_squared(self.last_stuck_check_pos)
613                < f64::from(threshold_distance * threshold_distance)
614            {
615                self.stuck = true;
616                self.stop();
617            } else {
618                self.stuck = false;
619            }
620
621            self.last_stuck_check = self.tick;
622            self.last_stuck_check_pos = mob_position;
623        }
624
625        if self.is_done() {
626            return;
627        }
628
629        let Some(current_node_pos) = self.path.as_ref().and_then(Path::next_node_pos) else {
630            return;
631        };
632        if current_node_pos == self.timeout_cached_node {
633            self.timeout_timer += game_time - self.last_timeout_check;
634        } else {
635            self.timeout_cached_node = current_node_pos;
636            let dist_to_node = mob_position.distance(block_bottom_center(current_node_pos));
637            self.timeout_limit = if mob_speed > 0.0 {
638                dist_to_node / f64::from(mob_speed) * 20.0
639            } else {
640                0.0
641            };
642        }
643
644        if self.timeout_limit > 0.0 && self.timeout_timer as f64 > self.timeout_limit * 3.0 {
645            self.timeout_path();
646        }
647
648        self.last_timeout_check = game_time;
649    }
650
651    fn timeout_path(&mut self) {
652        self.reset_stuck_timeout();
653        self.stop();
654    }
655
656    const fn reset_stuck_timeout(&mut self) {
657        self.timeout_cached_node = BlockPos::ZERO;
658        self.timeout_timer = 0;
659        self.timeout_limit = 0.0;
660        self.stuck = false;
661    }
662}
663
664fn should_target_next_node_in_direction(path: &Path, mob_position: DVec3) -> bool {
665    let next_node_index = path.next_node_index();
666    if next_node_index + 1 >= path.node_count() {
667        return false;
668    }
669
670    let Some(current_node_pos) = path.next_node_pos() else {
671        return false;
672    };
673    let current_node = block_bottom_center(current_node_pos);
674    if mob_position.distance_squared(current_node) >= 4.0 {
675        return false;
676    }
677
678    let Some(next_node_pos) = path.node_pos(next_node_index + 1) else {
679        return false;
680    };
681    let next_node = block_bottom_center(next_node_pos);
682    let mob_to_current = current_node - mob_position;
683    let mob_to_next = next_node - mob_position;
684    let mob_to_current_sqr = mob_to_current.length_squared();
685    let mob_to_next_sqr = mob_to_next.length_squared();
686    let closer_to_next_than_current = mob_to_next_sqr < mob_to_current_sqr;
687    let within_current_block = mob_to_current_sqr < 0.5;
688    if !closer_to_next_than_current && !within_current_block {
689        return false;
690    }
691
692    mob_to_next.dot(mob_to_current) < 0.0
693}
694
695fn block_bottom_center(pos: BlockPos) -> DVec3 {
696    let (x, y, z) = pos.get_bottom_center();
697    DVec3::new(x, y, z)
698}
699
700fn block_center(pos: BlockPos) -> DVec3 {
701    let (x, y, z) = pos.get_center();
702    DVec3::new(x, y, z)
703}
704
705fn path_move_target(path: &Path, mob_bounding_box_width: f64) -> Option<DVec3> {
706    path.next_node().map(|node| {
707        let offset = f64::from(fast_floor(mob_bounding_box_width + 1.0)) * 0.5;
708        DVec3::new(
709            f64::from(node.x) + offset,
710            f64::from(node.y),
711            f64::from(node.z) + offset,
712        )
713    })
714}
715
716const fn can_cut_corner(path_type: PathType) -> bool {
717    !matches!(
718        path_type,
719        PathType::FireInNeighbor | PathType::DamagingInNeighbor | PathType::WalkableDoor
720    )
721}
722
723impl Default for PathNavigation {
724    fn default() -> Self {
725        Self::new()
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use glam::DVec3;
732    use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_blocks};
733    use steel_utils::{BlockPos, BlockStateId, WorldAabb};
734
735    use super::{NavigationPathRequest, NavigationTickContext, PathNavigation};
736    use crate::behavior::init_behaviors;
737    use crate::entity::ai::node::Node;
738    use crate::entity::ai::path::{Path, PathType, PathfindingMalus};
739    use crate::entity::ai::walk::{MobPathSettings, WalkNodeEvaluator};
740    use crate::world::LevelReader;
741
742    struct GridLevel {
743        default_state: BlockStateId,
744        states: Vec<(BlockPos, BlockStateId)>,
745        sky_positions: Vec<BlockPos>,
746    }
747
748    impl GridLevel {
749        fn new(default_state: BlockStateId) -> Self {
750            Self {
751                default_state,
752                states: Vec::new(),
753                sky_positions: Vec::new(),
754            }
755        }
756
757        fn with(mut self, pos: BlockPos, state: BlockStateId) -> Self {
758            self.states.push((pos, state));
759            self
760        }
761
762        fn with_sky(mut self, pos: BlockPos) -> Self {
763            self.sky_positions.push(pos);
764            self
765        }
766    }
767
768    impl LevelReader for GridLevel {
769        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
770            self.states
771                .iter()
772                .find_map(|(state_pos, state)| (*state_pos == pos).then_some(*state))
773                .unwrap_or(self.default_state)
774        }
775
776        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
777            0
778        }
779
780        fn can_see_sky(&self, pos: BlockPos) -> bool {
781            self.sky_positions.contains(&pos)
782        }
783
784        fn min_y(&self) -> i32 {
785            -64
786        }
787
788        fn height(&self) -> i32 {
789            384
790        }
791    }
792
793    fn node_with_path_type(x: i32, y: i32, z: i32, path_type: PathType) -> Node {
794        let mut node = Node::new(x, y, z);
795        node.path_type = path_type;
796        node
797    }
798
799    fn tick_context(mob_position: DVec3) -> NavigationTickContext {
800        NavigationTickContext {
801            mob_position,
802            mob_bounding_box_width: 0.9,
803            mob_speed: 0.25,
804            game_time: 0,
805        }
806    }
807
808    fn tick_context_with_time(
809        mob_position: DVec3,
810        mob_speed: f32,
811        game_time: i64,
812    ) -> NavigationTickContext {
813        NavigationTickContext {
814            mob_position,
815            mob_bounding_box_width: 0.9,
816            mob_speed,
817            game_time,
818        }
819    }
820
821    fn empty_level() -> GridLevel {
822        init_vanilla_registry();
823        GridLevel::new(REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR))
824    }
825
826    fn move_to(
827        navigation: &mut PathNavigation,
828        path: Path,
829        speed_modifier: f64,
830        mob_position: DVec3,
831    ) -> bool {
832        let level = empty_level();
833        navigation.move_to(&level, path, speed_modifier, mob_position)
834    }
835
836    #[test]
837    fn path_navigation_tracks_can_float_flag() {
838        let mut navigation = PathNavigation::new();
839
840        assert!(!navigation.can_float());
841
842        navigation.set_can_float(true);
843
844        assert!(navigation.can_float());
845    }
846
847    #[test]
848    fn path_navigation_tracks_can_open_doors_flag() {
849        let mut navigation = PathNavigation::new();
850
851        assert!(!navigation.can_open_doors());
852
853        navigation.set_can_open_doors(true);
854
855        assert!(navigation.can_open_doors());
856    }
857
858    #[test]
859    fn path_navigation_tracks_can_walk_over_fences_flag() {
860        let mut navigation = PathNavigation::new();
861
862        assert!(!navigation.can_walk_over_fences());
863
864        navigation.set_can_walk_over_fences(true);
865
866        assert!(navigation.can_walk_over_fences());
867    }
868
869    #[test]
870    fn path_navigation_tracks_avoid_sun_flag() {
871        let mut navigation = PathNavigation::new();
872
873        assert!(!navigation.avoid_sun());
874
875        navigation.set_avoid_sun(true);
876
877        assert!(navigation.avoid_sun());
878    }
879
880    #[test]
881    fn path_navigation_tracks_can_path_to_targets_below_surface_flag() {
882        let mut navigation = PathNavigation::new();
883
884        assert!(!navigation.can_path_to_targets_below_surface());
885
886        navigation.set_can_path_to_targets_below_surface(true);
887
888        assert!(navigation.can_path_to_targets_below_surface());
889    }
890
891    #[test]
892    fn avoid_sun_trims_path_before_first_sky_node() {
893        let level = GridLevel::new(BlockStateId(0)).with_sky(BlockPos::new(2, 64, 0));
894        let mut path = Path::new(
895            vec![
896                Node::new(0, 64, 0),
897                Node::new(1, 64, 0),
898                Node::new(2, 64, 0),
899                Node::new(3, 64, 0),
900            ],
901            BlockPos::new(3, 64, 0),
902            true,
903        );
904        let mut navigation = PathNavigation::new();
905        navigation.set_avoid_sun(true);
906
907        navigation.trim_path_for_avoid_sun(&level, DVec3::new(0.5, 64.0, 0.5), &mut path);
908
909        assert_eq!(path.node_count(), 2);
910        assert_eq!(path.node_pos(1), Some(BlockPos::new(1, 64, 0)));
911    }
912
913    #[test]
914    fn avoid_sun_keeps_path_when_mob_is_already_under_sky() {
915        let level = GridLevel::new(BlockStateId(0))
916            .with_sky(BlockPos::new(0, 64, 0))
917            .with_sky(BlockPos::new(1, 64, 0));
918        let mut path = Path::new(
919            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
920            BlockPos::new(1, 64, 0),
921            true,
922        );
923        let mut navigation = PathNavigation::new();
924        navigation.set_avoid_sun(true);
925
926        navigation.trim_path_for_avoid_sun(&level, DVec3::new(0.5, 64.0, 0.5), &mut path);
927
928        assert_eq!(path.node_count(), 2);
929    }
930
931    #[test]
932    fn move_to_trims_path_over_cauldrons() {
933        init_vanilla_registry();
934
935        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
936        let cauldron = REGISTRY
937            .blocks
938            .get_default_state_id(&vanilla_blocks::CAULDRON);
939        let level = GridLevel::new(air).with(BlockPos::new(0, 64, 0), cauldron);
940        let path = Path::new(
941            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
942            BlockPos::new(1, 64, 0),
943            true,
944        );
945        let mut navigation = PathNavigation::new();
946
947        assert!(navigation.move_to(&level, path, 1.0, DVec3::new(0.5, 64.0, 0.5)));
948
949        assert_eq!(
950            navigation.path().and_then(|path| path.node_pos(0)),
951            Some(BlockPos::new(0, 65, 0))
952        );
953        assert_eq!(
954            navigation.path().and_then(|path| path.node_pos(1)),
955            Some(BlockPos::new(1, 65, 0))
956        );
957    }
958
959    #[test]
960    fn move_to_trims_avoid_sun_path() {
961        let level = empty_level().with_sky(BlockPos::new(2, 64, 0));
962        let path = Path::new(
963            vec![
964                Node::new(0, 64, 0),
965                Node::new(1, 64, 0),
966                Node::new(2, 64, 0),
967            ],
968            BlockPos::new(2, 64, 0),
969            true,
970        );
971        let mut navigation = PathNavigation::new();
972        navigation.set_avoid_sun(true);
973
974        assert!(navigation.move_to(&level, path, 1.0, DVec3::new(0.5, 64.0, 0.5)));
975
976        assert_eq!(navigation.path().map(Path::node_count), Some(2));
977    }
978
979    #[test]
980    fn move_to_path_targets_next_node_after_current_node_is_reached() {
981        let path = Path::new(
982            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
983            BlockPos::new(1, 64, 0),
984            true,
985        );
986        let mut navigation = PathNavigation::new();
987
988        assert!(move_to(
989            &mut navigation,
990            path,
991            1.25,
992            DVec3::new(0.5, 64.0, 0.5)
993        ));
994
995        let target = navigation.next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)));
996
997        let Some((target, speed)) = target else {
998            panic!("navigation should target the next path node");
999        };
1000        assert_eq!(target, DVec3::new(1.5, 64.0, 0.5));
1001        assert_eq!(speed.to_bits(), 1.25_f64.to_bits());
1002        assert!(!navigation.is_done());
1003    }
1004
1005    #[test]
1006    fn path_navigation_stops_after_final_node_is_reached() {
1007        let path = Path::new(vec![Node::new(0, 64, 0)], BlockPos::new(0, 64, 0), true);
1008        let mut navigation = PathNavigation::new();
1009
1010        assert!(move_to(
1011            &mut navigation,
1012            path,
1013            1.0,
1014            DVec3::new(0.5, 64.0, 0.5)
1015        ));
1016
1017        assert!(
1018            navigation
1019                .next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)))
1020                .is_none()
1021        );
1022        assert!(navigation.is_done());
1023        assert!(navigation.path().is_none());
1024        assert_eq!(navigation.target_pos(), Some(BlockPos::new(0, 64, 0)));
1025    }
1026
1027    #[test]
1028    fn path_navigation_targets_next_node_when_mob_passed_current_node_directionally() {
1029        let path = Path::new(
1030            vec![
1031                node_with_path_type(0, 64, 0, PathType::Walkable),
1032                Node::new(1, 64, 0),
1033                Node::new(2, 64, 0),
1034            ],
1035            BlockPos::new(2, 64, 0),
1036            true,
1037        );
1038        let mut navigation = PathNavigation::new();
1039
1040        assert!(move_to(
1041            &mut navigation,
1042            path,
1043            1.0,
1044            DVec3::new(0.0, 64.0, 0.5)
1045        ));
1046
1047        let target = navigation.next_move_target(tick_context(DVec3::new(1.1, 64.0, 0.5)));
1048
1049        let Some((target, _speed)) = target else {
1050            panic!("navigation should target a path node");
1051        };
1052        assert_eq!(target, DVec3::new(1.5, 64.0, 0.5));
1053        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1054    }
1055
1056    #[test]
1057    fn path_navigation_does_not_cut_corner_through_walkable_door() {
1058        let path = Path::new(
1059            vec![
1060                node_with_path_type(0, 64, 0, PathType::WalkableDoor),
1061                Node::new(1, 64, 0),
1062            ],
1063            BlockPos::new(1, 64, 0),
1064            true,
1065        );
1066        let mut navigation = PathNavigation::new();
1067
1068        assert!(move_to(
1069            &mut navigation,
1070            path,
1071            1.0,
1072            DVec3::new(0.0, 64.0, 0.5)
1073        ));
1074
1075        let target = navigation.next_move_target(tick_context(DVec3::new(1.1, 64.0, 0.5)));
1076
1077        let Some((target, _speed)) = target else {
1078            panic!("navigation should target the current path node");
1079        };
1080        assert_eq!(target, DVec3::new(0.5, 64.0, 0.5));
1081        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1082    }
1083
1084    #[test]
1085    fn path_navigation_without_update_does_not_advance_normally() {
1086        let path = Path::new(
1087            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1088            BlockPos::new(1, 64, 0),
1089            true,
1090        );
1091        let mut navigation = PathNavigation::new();
1092
1093        assert!(move_to(
1094            &mut navigation,
1095            path,
1096            1.0,
1097            DVec3::new(0.5, 64.0, 0.5)
1098        ));
1099
1100        let target = navigation
1101            .next_move_target_without_path_update(tick_context(DVec3::new(0.5, 64.0, 0.5)), false);
1102
1103        assert_eq!(
1104            target.map(|(target, _)| target),
1105            Some(DVec3::new(0.5, 64.0, 0.5))
1106        );
1107        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1108    }
1109
1110    #[test]
1111    fn path_navigation_without_update_advances_when_airborne_over_node() {
1112        let path = Path::new(
1113            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1114            BlockPos::new(1, 64, 0),
1115            true,
1116        );
1117        let mut navigation = PathNavigation::new();
1118
1119        assert!(move_to(
1120            &mut navigation,
1121            path,
1122            1.0,
1123            DVec3::new(0.5, 65.0, 0.5)
1124        ));
1125
1126        let target = navigation
1127            .next_move_target_without_path_update(tick_context(DVec3::new(0.5, 65.0, 0.5)), false);
1128
1129        assert_eq!(
1130            target.map(|(target, _)| target),
1131            Some(DVec3::new(1.5, 64.0, 0.5))
1132        );
1133        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1134    }
1135
1136    #[test]
1137    fn path_navigation_stops_when_stationary_past_stuck_interval() {
1138        let path = Path::new(vec![Node::new(1, 64, 0)], BlockPos::new(1, 64, 0), true);
1139        let mut navigation = PathNavigation::new();
1140        let mob_position = DVec3::new(0.0, 64.0, 0.5);
1141
1142        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1143        for game_time in 1..=101 {
1144            navigation.tick();
1145            let _ =
1146                navigation.next_move_target(tick_context_with_time(mob_position, 0.25, game_time));
1147        }
1148
1149        assert!(navigation.is_stuck());
1150        assert!(navigation.is_done());
1151    }
1152
1153    #[test]
1154    fn path_navigation_times_out_when_same_node_takes_too_long() {
1155        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1156        let mut navigation = PathNavigation::new();
1157        let mob_position = DVec3::new(1.0, 64.0, 0.5);
1158
1159        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1160        for game_time in 1..=92 {
1161            navigation.tick();
1162            let _ =
1163                navigation.next_move_target(tick_context_with_time(mob_position, 1.0, game_time));
1164        }
1165
1166        assert!(!navigation.is_stuck());
1167        assert!(navigation.is_done());
1168    }
1169
1170    #[test]
1171    fn path_recompute_request_delays_during_vanilla_cooldown() {
1172        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1173        let mut navigation = PathNavigation::new();
1174
1175        assert!(move_to(
1176            &mut navigation,
1177            path,
1178            1.0,
1179            DVec3::new(0.5, 64.0, 0.5)
1180        ));
1181
1182        assert_eq!(navigation.request_recompute_path(20, true), None);
1183        assert!(navigation.has_delayed_recomputation());
1184        assert!(navigation.path().is_some());
1185
1186        let Some(request) = navigation.take_delayed_recompute_request(21, true) else {
1187            panic!("recompute should be allowed after vanilla cooldown");
1188        };
1189        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1190        assert_eq!(request.reach_range, 0);
1191        assert_eq!(request.game_time, 21);
1192        assert!(navigation.path().is_none());
1193
1194        navigation.complete_recompute_path(
1195            Some(Path::new(
1196                vec![Node::new(1, 64, 0), Node::new(2, 64, 0)],
1197                BlockPos::new(2, 64, 0),
1198                true,
1199            )),
1200            request.game_time,
1201        );
1202        assert!(!navigation.has_delayed_recomputation());
1203        assert!(!navigation.is_done());
1204    }
1205
1206    #[test]
1207    fn path_recompute_request_waits_until_path_can_update() {
1208        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1209        let mut navigation = PathNavigation::new();
1210
1211        assert!(move_to(
1212            &mut navigation,
1213            path,
1214            1.0,
1215            DVec3::new(0.5, 64.0, 0.5)
1216        ));
1217
1218        assert_eq!(navigation.request_recompute_path(30, false), None);
1219        assert!(navigation.has_delayed_recomputation());
1220        assert_eq!(navigation.take_delayed_recompute_request(40, false), None);
1221        assert!(navigation.has_delayed_recomputation());
1222
1223        let Some(request) = navigation.take_delayed_recompute_request(40, true) else {
1224            panic!("recompute should run once path updates are allowed");
1225        };
1226        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1227    }
1228
1229    #[test]
1230    fn stop_keeps_delayed_recompute_state() {
1231        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1232        let mut navigation = PathNavigation::new();
1233
1234        assert!(move_to(
1235            &mut navigation,
1236            path,
1237            1.0,
1238            DVec3::new(0.5, 64.0, 0.5)
1239        ));
1240
1241        assert_eq!(navigation.request_recompute_path(20, true), None);
1242        assert!(navigation.has_delayed_recomputation());
1243
1244        navigation.stop();
1245
1246        assert!(navigation.has_delayed_recomputation());
1247        assert_eq!(navigation.target_pos(), Some(BlockPos::new(2, 64, 0)));
1248        assert_eq!(navigation.speed_modifier().to_bits(), 1.0_f64.to_bits());
1249        let Some(request) = navigation.take_delayed_recompute_request(21, true) else {
1250            panic!("stopped navigation should keep enough state to recompute the target");
1251        };
1252        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1253
1254        navigation.complete_recompute_path(
1255            Some(Path::new(
1256                vec![Node::new(1, 64, 0), Node::new(2, 64, 0)],
1257                BlockPos::new(2, 64, 0),
1258                true,
1259            )),
1260            request.game_time,
1261        );
1262        assert!(!navigation.is_done());
1263        assert!(!navigation.has_delayed_recomputation());
1264        assert!(navigation.path().is_some());
1265    }
1266
1267    #[test]
1268    fn path_should_recompute_uses_vanilla_midpoint_window() {
1269        let path = Path::new(
1270            vec![Node::new(0, 64, 0), Node::new(4, 64, 0)],
1271            BlockPos::new(4, 64, 0),
1272            true,
1273        );
1274        let mut navigation = PathNavigation::new();
1275        let mob_position = DVec3::new(0.5, 64.0, 0.5);
1276
1277        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1278        assert!(navigation.should_recompute_path(BlockPos::new(2, 64, 0), mob_position));
1279        assert!(!navigation.should_recompute_path(BlockPos::new(20, 64, 0), mob_position));
1280
1281        assert_eq!(navigation.request_recompute_path(1, true), None);
1282        assert!(!navigation.should_recompute_path(BlockPos::new(2, 64, 0), mob_position));
1283    }
1284
1285    #[test]
1286    fn move_to_same_path_keeps_current_progress() {
1287        let path = Path::new(
1288            vec![
1289                Node::new(0, 64, 0),
1290                Node::new(1, 64, 0),
1291                Node::new(2, 64, 0),
1292            ],
1293            BlockPos::new(2, 64, 0),
1294            true,
1295        );
1296        let same_path = Path::new(
1297            vec![
1298                Node::new(0, 64, 0),
1299                Node::new(1, 64, 0),
1300                Node::new(2, 64, 0),
1301            ],
1302            BlockPos::new(2, 64, 0),
1303            true,
1304        );
1305        let mut navigation = PathNavigation::new();
1306
1307        assert!(move_to(
1308            &mut navigation,
1309            path,
1310            1.0,
1311            DVec3::new(0.5, 64.0, 0.5)
1312        ));
1313        assert!(
1314            navigation
1315                .next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)))
1316                .is_some()
1317        );
1318        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1319
1320        assert!(move_to(
1321            &mut navigation,
1322            same_path,
1323            1.5,
1324            DVec3::new(0.5, 64.0, 0.5)
1325        ));
1326
1327        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1328        assert_eq!(navigation.speed_modifier().to_bits(), 1.5_f64.to_bits());
1329    }
1330
1331    #[test]
1332    fn path_navigation_reuses_current_path_for_matching_target() {
1333        let path = Path::new(
1334            vec![Node::new(0, 64, 0), Node::new(4, 64, 0)],
1335            BlockPos::new(4, 64, 0),
1336            true,
1337        );
1338        let mut navigation = PathNavigation::new();
1339
1340        assert!(move_to(
1341            &mut navigation,
1342            path,
1343            1.0,
1344            DVec3::new(0.5, 64.0, 0.5)
1345        ));
1346        let level = empty_level();
1347        assert!(navigation.reuse_current_path_to_targets(
1348            &level,
1349            &[BlockPos::new(4, 64, 0)],
1350            1.25,
1351            DVec3::new(1.5, 64.0, 0.5),
1352        ));
1353
1354        assert!(!navigation.is_done());
1355        assert_eq!(navigation.speed_modifier().to_bits(), 1.25_f64.to_bits());
1356        assert_eq!(
1357            navigation.path().map(Path::target),
1358            Some(BlockPos::new(4, 64, 0))
1359        );
1360        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1361    }
1362
1363    #[test]
1364    fn create_path_reuses_current_path_for_matching_target() {
1365        let mut path = Path::new(
1366            vec![
1367                Node::new(0, 64, 0),
1368                Node::new(1, 64, 0),
1369                Node::new(2, 64, 0),
1370            ],
1371            BlockPos::new(2, 64, 0),
1372            true,
1373        );
1374        path.set_next_node_index(1);
1375        let mut navigation = PathNavigation::new();
1376        assert!(move_to(
1377            &mut navigation,
1378            path,
1379            1.0,
1380            DVec3::new(0.5, 64.0, 0.5)
1381        ));
1382
1383        let level = GridLevel::new(BlockStateId(0));
1384        let malus = PathfindingMalus::new();
1385        let mut evaluator = WalkNodeEvaluator::new(MobPathSettings::new(
1386            1,
1387            1,
1388            1,
1389            BlockPos::new(0, 64, 0),
1390            &malus,
1391        ));
1392        let mut no_collision = |_aabb: WorldAabb| false;
1393
1394        let reused = navigation.create_path(
1395            &mut evaluator,
1396            &level,
1397            &mut no_collision,
1398            NavigationPathRequest {
1399                mob_position: BlockPos::new(0, 64, 0),
1400                targets: &[BlockPos::new(2, 64, 0)],
1401                max_path_length: 16.0,
1402                reach_range: 0,
1403            },
1404        );
1405
1406        let Some(reused) = reused else {
1407            panic!("matching active path should be reused");
1408        };
1409        assert_eq!(reused.target(), BlockPos::new(2, 64, 0));
1410        assert_eq!(reused.next_node_index(), 1);
1411        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1412    }
1413
1414    #[test]
1415    fn path_navigation_does_not_reuse_current_path_for_different_target() {
1416        let path = Path::new(vec![Node::new(4, 64, 0)], BlockPos::new(4, 64, 0), true);
1417        let mut navigation = PathNavigation::new();
1418
1419        assert!(move_to(
1420            &mut navigation,
1421            path,
1422            1.0,
1423            DVec3::new(0.5, 64.0, 0.5)
1424        ));
1425
1426        let level = empty_level();
1427        assert!(!navigation.reuse_current_path_to_targets(
1428            &level,
1429            &[BlockPos::new(5, 64, 0)],
1430            1.25,
1431            DVec3::new(0.5, 64.0, 0.5),
1432        ));
1433        assert_eq!(navigation.speed_modifier().to_bits(), 1.0_f64.to_bits());
1434    }
1435
1436    #[test]
1437    fn path_navigation_updates_current_speed_modifier() {
1438        let mut navigation = PathNavigation::new();
1439
1440        navigation.set_speed_modifier(1.75);
1441
1442        assert_eq!(navigation.speed_modifier().to_bits(), 1.75_f64.to_bits());
1443    }
1444
1445    #[test]
1446    fn direct_target_stops_when_reached() {
1447        let mut navigation = PathNavigation::new();
1448        navigation.set_direct_target(DVec3::new(1.0, 64.0, 1.0), 0.5);
1449
1450        assert!(
1451            navigation
1452                .next_move_target(tick_context(DVec3::new(1.0, 64.0, 1.0)))
1453                .is_none()
1454        );
1455        assert!(navigation.is_done());
1456    }
1457
1458    #[test]
1459    fn create_path_finds_walkable_target_with_cached_navigation_state() {
1460        init_vanilla_registry();
1461        init_behaviors();
1462
1463        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
1464        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
1465        let level = GridLevel::new(air)
1466            .with(BlockPos::new(0, 63, 0), stone)
1467            .with(BlockPos::new(1, 63, 0), stone)
1468            .with(BlockPos::new(2, 63, 0), stone);
1469        let malus = PathfindingMalus::new();
1470        let mut evaluator = WalkNodeEvaluator::new(MobPathSettings::new(
1471            1,
1472            1,
1473            1,
1474            BlockPos::new(0, 64, 0),
1475            &malus,
1476        ));
1477        let mut navigation = PathNavigation::new();
1478        navigation.update_pathfinder_max_visited_nodes(16.0);
1479        let mut no_collision = |_aabb: WorldAabb| false;
1480
1481        let path = navigation.create_path(
1482            &mut evaluator,
1483            &level,
1484            &mut no_collision,
1485            NavigationPathRequest {
1486                mob_position: BlockPos::new(0, 64, 0),
1487                targets: &[BlockPos::new(2, 64, 0)],
1488                max_path_length: 16.0,
1489                reach_range: 0,
1490            },
1491        );
1492
1493        let Some(path) = path else {
1494            panic!("path should be found");
1495        };
1496        assert!(path.can_reach());
1497        assert_eq!(navigation.target_pos(), Some(BlockPos::new(2, 64, 0)));
1498        assert_eq!(navigation.reach_range(), 0);
1499    }
1500}