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