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: &(impl LevelReader + ?Sized),
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: &(impl LevelReader + ?Sized), 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: &(impl LevelReader + ?Sized), 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: &(impl LevelReader + ?Sized),
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: &(impl LevelReader + ?Sized),
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    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
743
744    use super::{NavigationPathRequest, NavigationTickContext, PathNavigation};
745    use crate::behavior::init_behaviors;
746    use crate::entity::ai::node::Node;
747    use crate::entity::ai::path::{Path, PathType, PathfindingMalus};
748    use crate::entity::ai::walk::{MobPathSettings, WalkNodeEvaluator};
749    use crate::world::LevelReader;
750
751    struct GridLevel {
752        default_state: BlockStateId,
753        states: Vec<(BlockPos, BlockStateId)>,
754        sky_positions: Vec<BlockPos>,
755    }
756
757    impl GridLevel {
758        fn new(default_state: BlockStateId) -> Self {
759            Self {
760                default_state,
761                states: Vec::new(),
762                sky_positions: Vec::new(),
763            }
764        }
765
766        fn with(mut self, pos: BlockPos, state: BlockStateId) -> Self {
767            self.states.push((pos, state));
768            self
769        }
770
771        fn with_sky(mut self, pos: BlockPos) -> Self {
772            self.sky_positions.push(pos);
773            self
774        }
775    }
776
777    impl LevelReader for GridLevel {
778        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
779            self.states
780                .iter()
781                .find_map(|(state_pos, state)| (*state_pos == pos).then_some(*state))
782                .unwrap_or(self.default_state)
783        }
784
785        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
786            0
787        }
788
789        fn can_see_sky(&self, pos: BlockPos) -> bool {
790            self.sky_positions.contains(&pos)
791        }
792
793        fn min_y(&self) -> i32 {
794            OverworldNoiseSettings::MIN_Y
795        }
796
797        fn height(&self) -> i32 {
798            OverworldNoiseSettings::HEIGHT
799        }
800
801        fn sea_level(&self) -> i32 {
802            OverworldNoiseSettings::SEA_LEVEL
803        }
804    }
805
806    fn node_with_path_type(x: i32, y: i32, z: i32, path_type: PathType) -> Node {
807        let mut node = Node::new(x, y, z);
808        node.path_type = path_type;
809        node
810    }
811
812    fn tick_context(mob_position: DVec3) -> NavigationTickContext {
813        NavigationTickContext {
814            ground_mob_position: mob_position,
815            mob_position,
816            mob_bounding_box_width: 0.9,
817            mob_speed: 0.25,
818            game_time: 0,
819        }
820    }
821
822    fn tick_context_standing_in_water(mob_position: DVec3) -> NavigationTickContext {
823        NavigationTickContext {
824            ground_mob_position: DVec3::new(
825                mob_position.x,
826                mob_position.y.floor() + 1.0,
827                mob_position.z,
828            ),
829            mob_position,
830            mob_bounding_box_width: 0.9,
831            mob_speed: 0.25,
832            game_time: 0,
833        }
834    }
835
836    fn tick_context_with_time(
837        mob_position: DVec3,
838        mob_speed: f32,
839        game_time: i64,
840    ) -> NavigationTickContext {
841        NavigationTickContext {
842            ground_mob_position: mob_position,
843            mob_position,
844            mob_bounding_box_width: 0.9,
845            mob_speed,
846            game_time,
847        }
848    }
849
850    fn empty_level() -> GridLevel {
851        init_vanilla_registry();
852        GridLevel::new(REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR))
853    }
854
855    fn move_to(
856        navigation: &mut PathNavigation,
857        path: Path,
858        speed_modifier: f64,
859        mob_position: DVec3,
860    ) -> bool {
861        let level = empty_level();
862        navigation.move_to(&level, path, speed_modifier, mob_position)
863    }
864
865    #[test]
866    fn path_navigation_tracks_can_float_flag() {
867        let mut navigation = PathNavigation::new();
868
869        assert!(!navigation.can_float());
870
871        navigation.set_can_float(true);
872
873        assert!(navigation.can_float());
874    }
875
876    #[test]
877    fn path_navigation_tracks_can_open_doors_flag() {
878        let mut navigation = PathNavigation::new();
879
880        assert!(!navigation.can_open_doors());
881
882        navigation.set_can_open_doors(true);
883
884        assert!(navigation.can_open_doors());
885    }
886
887    #[test]
888    fn path_navigation_tracks_can_walk_over_fences_flag() {
889        let mut navigation = PathNavigation::new();
890
891        assert!(!navigation.can_walk_over_fences());
892
893        navigation.set_can_walk_over_fences(true);
894
895        assert!(navigation.can_walk_over_fences());
896    }
897
898    #[test]
899    fn path_navigation_tracks_avoid_sun_flag() {
900        let mut navigation = PathNavigation::new();
901
902        assert!(!navigation.avoid_sun());
903
904        navigation.set_avoid_sun(true);
905
906        assert!(navigation.avoid_sun());
907    }
908
909    #[test]
910    fn path_navigation_tracks_can_path_to_targets_below_surface_flag() {
911        let mut navigation = PathNavigation::new();
912
913        assert!(!navigation.can_path_to_targets_below_surface());
914
915        navigation.set_can_path_to_targets_below_surface(true);
916
917        assert!(navigation.can_path_to_targets_below_surface());
918    }
919
920    #[test]
921    fn avoid_sun_trims_path_before_first_sky_node() {
922        let level = GridLevel::new(BlockStateId(0)).with_sky(BlockPos::new(2, 64, 0));
923        let mut path = Path::new(
924            vec![
925                Node::new(0, 64, 0),
926                Node::new(1, 64, 0),
927                Node::new(2, 64, 0),
928                Node::new(3, 64, 0),
929            ],
930            BlockPos::new(3, 64, 0),
931            true,
932        );
933        let mut navigation = PathNavigation::new();
934        navigation.set_avoid_sun(true);
935
936        navigation.trim_path_for_avoid_sun(&level, DVec3::new(0.5, 64.0, 0.5), &mut path);
937
938        assert_eq!(path.node_count(), 2);
939        assert_eq!(path.node_pos(1), Some(BlockPos::new(1, 64, 0)));
940    }
941
942    #[test]
943    fn avoid_sun_keeps_path_when_mob_is_already_under_sky() {
944        let level = GridLevel::new(BlockStateId(0))
945            .with_sky(BlockPos::new(0, 64, 0))
946            .with_sky(BlockPos::new(1, 64, 0));
947        let mut path = Path::new(
948            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
949            BlockPos::new(1, 64, 0),
950            true,
951        );
952        let mut navigation = PathNavigation::new();
953        navigation.set_avoid_sun(true);
954
955        navigation.trim_path_for_avoid_sun(&level, DVec3::new(0.5, 64.0, 0.5), &mut path);
956
957        assert_eq!(path.node_count(), 2);
958    }
959
960    #[test]
961    fn move_to_trims_path_over_cauldrons() {
962        init_vanilla_registry();
963
964        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
965        let cauldron = REGISTRY
966            .blocks
967            .get_default_state_id(&vanilla_blocks::CAULDRON);
968        let level = GridLevel::new(air).with(BlockPos::new(0, 64, 0), cauldron);
969        let path = Path::new(
970            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
971            BlockPos::new(1, 64, 0),
972            true,
973        );
974        let mut navigation = PathNavigation::new();
975
976        assert!(navigation.move_to(&level, path, 1.0, DVec3::new(0.5, 64.0, 0.5)));
977
978        assert_eq!(
979            navigation.path().and_then(|path| path.node_pos(0)),
980            Some(BlockPos::new(0, 65, 0))
981        );
982        assert_eq!(
983            navigation.path().and_then(|path| path.node_pos(1)),
984            Some(BlockPos::new(1, 65, 0))
985        );
986    }
987
988    #[test]
989    fn move_to_trims_avoid_sun_path() {
990        let level = empty_level().with_sky(BlockPos::new(2, 64, 0));
991        let path = Path::new(
992            vec![
993                Node::new(0, 64, 0),
994                Node::new(1, 64, 0),
995                Node::new(2, 64, 0),
996            ],
997            BlockPos::new(2, 64, 0),
998            true,
999        );
1000        let mut navigation = PathNavigation::new();
1001        navigation.set_avoid_sun(true);
1002
1003        assert!(navigation.move_to(&level, path, 1.0, DVec3::new(0.5, 64.0, 0.5)));
1004
1005        assert_eq!(navigation.path().map(Path::node_count), Some(2));
1006    }
1007
1008    #[test]
1009    fn standing_in_water_still_retires_the_waypoint_underfoot() {
1010        let path = Path::new(
1011            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1012            BlockPos::new(1, 64, 0),
1013            true,
1014        );
1015        let mut navigation = PathNavigation::new();
1016        let standing_in_water = DVec3::new(0.5, 64.0, 0.5);
1017
1018        assert!(move_to(&mut navigation, path, 1.0, standing_in_water));
1019
1020        let target = navigation.next_move_target(tick_context_standing_in_water(standing_in_water));
1021
1022        let Some((target, _)) = target else {
1023            panic!("navigation should target the next path node");
1024        };
1025        assert_eq!(
1026            target,
1027            DVec3::new(1.5, 64.0, 0.5),
1028            "the node underfoot should have retired and handed over to the next one"
1029        );
1030        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1031    }
1032
1033    #[test]
1034    fn move_to_path_targets_next_node_after_current_node_is_reached() {
1035        let path = Path::new(
1036            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1037            BlockPos::new(1, 64, 0),
1038            true,
1039        );
1040        let mut navigation = PathNavigation::new();
1041
1042        assert!(move_to(
1043            &mut navigation,
1044            path,
1045            1.25,
1046            DVec3::new(0.5, 64.0, 0.5)
1047        ));
1048
1049        let target = navigation.next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)));
1050
1051        let Some((target, speed)) = target else {
1052            panic!("navigation should target the next path node");
1053        };
1054        assert_eq!(target, DVec3::new(1.5, 64.0, 0.5));
1055        assert_eq!(speed.to_bits(), 1.25_f64.to_bits());
1056        assert!(!navigation.is_done());
1057    }
1058
1059    #[test]
1060    fn path_navigation_stops_after_final_node_is_reached() {
1061        let path = Path::new(vec![Node::new(0, 64, 0)], BlockPos::new(0, 64, 0), true);
1062        let mut navigation = PathNavigation::new();
1063
1064        assert!(move_to(
1065            &mut navigation,
1066            path,
1067            1.0,
1068            DVec3::new(0.5, 64.0, 0.5)
1069        ));
1070
1071        assert!(
1072            navigation
1073                .next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)))
1074                .is_none()
1075        );
1076        assert!(navigation.is_done());
1077        assert!(navigation.path().is_none());
1078        assert_eq!(navigation.target_pos(), Some(BlockPos::new(0, 64, 0)));
1079    }
1080
1081    #[test]
1082    fn path_navigation_targets_next_node_when_mob_passed_current_node_directionally() {
1083        let path = Path::new(
1084            vec![
1085                node_with_path_type(0, 64, 0, PathType::Walkable),
1086                Node::new(1, 64, 0),
1087                Node::new(2, 64, 0),
1088            ],
1089            BlockPos::new(2, 64, 0),
1090            true,
1091        );
1092        let mut navigation = PathNavigation::new();
1093
1094        assert!(move_to(
1095            &mut navigation,
1096            path,
1097            1.0,
1098            DVec3::new(0.0, 64.0, 0.5)
1099        ));
1100
1101        let target = navigation.next_move_target(tick_context(DVec3::new(1.1, 64.0, 0.5)));
1102
1103        let Some((target, _speed)) = target else {
1104            panic!("navigation should target a path node");
1105        };
1106        assert_eq!(target, DVec3::new(1.5, 64.0, 0.5));
1107        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1108    }
1109
1110    #[test]
1111    fn path_navigation_does_not_cut_corner_through_walkable_door() {
1112        let path = Path::new(
1113            vec![
1114                node_with_path_type(0, 64, 0, PathType::WalkableDoor),
1115                Node::new(1, 64, 0),
1116            ],
1117            BlockPos::new(1, 64, 0),
1118            true,
1119        );
1120        let mut navigation = PathNavigation::new();
1121
1122        assert!(move_to(
1123            &mut navigation,
1124            path,
1125            1.0,
1126            DVec3::new(0.0, 64.0, 0.5)
1127        ));
1128
1129        let target = navigation.next_move_target(tick_context(DVec3::new(1.1, 64.0, 0.5)));
1130
1131        let Some((target, _speed)) = target else {
1132            panic!("navigation should target the current path node");
1133        };
1134        assert_eq!(target, DVec3::new(0.5, 64.0, 0.5));
1135        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1136    }
1137
1138    #[test]
1139    fn path_navigation_without_update_does_not_advance_normally() {
1140        let path = Path::new(
1141            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1142            BlockPos::new(1, 64, 0),
1143            true,
1144        );
1145        let mut navigation = PathNavigation::new();
1146
1147        assert!(move_to(
1148            &mut navigation,
1149            path,
1150            1.0,
1151            DVec3::new(0.5, 64.0, 0.5)
1152        ));
1153
1154        let target = navigation
1155            .next_move_target_without_path_update(tick_context(DVec3::new(0.5, 64.0, 0.5)), false);
1156
1157        assert_eq!(
1158            target.map(|(target, _)| target),
1159            Some(DVec3::new(0.5, 64.0, 0.5))
1160        );
1161        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1162    }
1163
1164    #[test]
1165    fn path_navigation_without_update_advances_when_airborne_over_node() {
1166        let path = Path::new(
1167            vec![Node::new(0, 64, 0), Node::new(1, 64, 0)],
1168            BlockPos::new(1, 64, 0),
1169            true,
1170        );
1171        let mut navigation = PathNavigation::new();
1172
1173        assert!(move_to(
1174            &mut navigation,
1175            path,
1176            1.0,
1177            DVec3::new(0.5, 65.0, 0.5)
1178        ));
1179
1180        let target = navigation
1181            .next_move_target_without_path_update(tick_context(DVec3::new(0.5, 65.0, 0.5)), false);
1182
1183        assert_eq!(
1184            target.map(|(target, _)| target),
1185            Some(DVec3::new(1.5, 64.0, 0.5))
1186        );
1187        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1188    }
1189
1190    #[test]
1191    fn path_navigation_stops_when_stationary_past_stuck_interval() {
1192        let path = Path::new(vec![Node::new(1, 64, 0)], BlockPos::new(1, 64, 0), true);
1193        let mut navigation = PathNavigation::new();
1194        let mob_position = DVec3::new(0.0, 64.0, 0.5);
1195
1196        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1197        for game_time in 1..=101 {
1198            navigation.tick();
1199            let _ =
1200                navigation.next_move_target(tick_context_with_time(mob_position, 0.25, game_time));
1201        }
1202
1203        assert!(navigation.is_stuck());
1204        assert!(navigation.is_done());
1205    }
1206
1207    #[test]
1208    fn path_navigation_times_out_when_same_node_takes_too_long() {
1209        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1210        let mut navigation = PathNavigation::new();
1211        let mob_position = DVec3::new(1.0, 64.0, 0.5);
1212
1213        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1214        for game_time in 1..=92 {
1215            navigation.tick();
1216            let _ =
1217                navigation.next_move_target(tick_context_with_time(mob_position, 1.0, game_time));
1218        }
1219
1220        assert!(!navigation.is_stuck());
1221        assert!(navigation.is_done());
1222    }
1223
1224    #[test]
1225    fn path_recompute_request_delays_during_vanilla_cooldown() {
1226        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1227        let mut navigation = PathNavigation::new();
1228
1229        assert!(move_to(
1230            &mut navigation,
1231            path,
1232            1.0,
1233            DVec3::new(0.5, 64.0, 0.5)
1234        ));
1235
1236        assert_eq!(navigation.request_recompute_path(20, true), None);
1237        assert!(navigation.has_delayed_recomputation());
1238        assert!(navigation.path().is_some());
1239
1240        let Some(request) = navigation.take_delayed_recompute_request(21, true) else {
1241            panic!("recompute should be allowed after vanilla cooldown");
1242        };
1243        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1244        assert_eq!(request.reach_range, 0);
1245        assert_eq!(request.game_time, 21);
1246        assert!(navigation.path().is_none());
1247
1248        navigation.complete_recompute_path(
1249            Some(Path::new(
1250                vec![Node::new(1, 64, 0), Node::new(2, 64, 0)],
1251                BlockPos::new(2, 64, 0),
1252                true,
1253            )),
1254            request.game_time,
1255        );
1256        assert!(!navigation.has_delayed_recomputation());
1257        assert!(!navigation.is_done());
1258    }
1259
1260    #[test]
1261    fn path_recompute_request_waits_until_path_can_update() {
1262        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1263        let mut navigation = PathNavigation::new();
1264
1265        assert!(move_to(
1266            &mut navigation,
1267            path,
1268            1.0,
1269            DVec3::new(0.5, 64.0, 0.5)
1270        ));
1271
1272        assert_eq!(navigation.request_recompute_path(30, false), None);
1273        assert!(navigation.has_delayed_recomputation());
1274        assert_eq!(navigation.take_delayed_recompute_request(40, false), None);
1275        assert!(navigation.has_delayed_recomputation());
1276
1277        let Some(request) = navigation.take_delayed_recompute_request(40, true) else {
1278            panic!("recompute should run once path updates are allowed");
1279        };
1280        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1281    }
1282
1283    #[test]
1284    fn stop_keeps_delayed_recompute_state() {
1285        let path = Path::new(vec![Node::new(2, 64, 0)], BlockPos::new(2, 64, 0), true);
1286        let mut navigation = PathNavigation::new();
1287
1288        assert!(move_to(
1289            &mut navigation,
1290            path,
1291            1.0,
1292            DVec3::new(0.5, 64.0, 0.5)
1293        ));
1294
1295        assert_eq!(navigation.request_recompute_path(20, true), None);
1296        assert!(navigation.has_delayed_recomputation());
1297
1298        navigation.stop();
1299
1300        assert!(navigation.has_delayed_recomputation());
1301        assert_eq!(navigation.target_pos(), Some(BlockPos::new(2, 64, 0)));
1302        assert_eq!(navigation.speed_modifier().to_bits(), 1.0_f64.to_bits());
1303        let Some(request) = navigation.take_delayed_recompute_request(21, true) else {
1304            panic!("stopped navigation should keep enough state to recompute the target");
1305        };
1306        assert_eq!(request.target_pos, BlockPos::new(2, 64, 0));
1307
1308        navigation.complete_recompute_path(
1309            Some(Path::new(
1310                vec![Node::new(1, 64, 0), Node::new(2, 64, 0)],
1311                BlockPos::new(2, 64, 0),
1312                true,
1313            )),
1314            request.game_time,
1315        );
1316        assert!(!navigation.is_done());
1317        assert!(!navigation.has_delayed_recomputation());
1318        assert!(navigation.path().is_some());
1319    }
1320
1321    #[test]
1322    fn path_should_recompute_uses_vanilla_midpoint_window() {
1323        let path = Path::new(
1324            vec![Node::new(0, 64, 0), Node::new(4, 64, 0)],
1325            BlockPos::new(4, 64, 0),
1326            true,
1327        );
1328        let mut navigation = PathNavigation::new();
1329        let mob_position = DVec3::new(0.5, 64.0, 0.5);
1330
1331        assert!(move_to(&mut navigation, path, 1.0, mob_position));
1332        assert!(navigation.should_recompute_path(BlockPos::new(2, 64, 0), mob_position));
1333        assert!(!navigation.should_recompute_path(BlockPos::new(20, 64, 0), mob_position));
1334
1335        assert_eq!(navigation.request_recompute_path(1, true), None);
1336        assert!(!navigation.should_recompute_path(BlockPos::new(2, 64, 0), mob_position));
1337    }
1338
1339    #[test]
1340    fn move_to_same_path_keeps_current_progress() {
1341        let path = Path::new(
1342            vec![
1343                Node::new(0, 64, 0),
1344                Node::new(1, 64, 0),
1345                Node::new(2, 64, 0),
1346            ],
1347            BlockPos::new(2, 64, 0),
1348            true,
1349        );
1350        let same_path = Path::new(
1351            vec![
1352                Node::new(0, 64, 0),
1353                Node::new(1, 64, 0),
1354                Node::new(2, 64, 0),
1355            ],
1356            BlockPos::new(2, 64, 0),
1357            true,
1358        );
1359        let mut navigation = PathNavigation::new();
1360
1361        assert!(move_to(
1362            &mut navigation,
1363            path,
1364            1.0,
1365            DVec3::new(0.5, 64.0, 0.5)
1366        ));
1367        assert!(
1368            navigation
1369                .next_move_target(tick_context(DVec3::new(0.5, 64.0, 0.5)))
1370                .is_some()
1371        );
1372        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1373
1374        assert!(move_to(
1375            &mut navigation,
1376            same_path,
1377            1.5,
1378            DVec3::new(0.5, 64.0, 0.5)
1379        ));
1380
1381        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1382        assert_eq!(navigation.speed_modifier().to_bits(), 1.5_f64.to_bits());
1383    }
1384
1385    #[test]
1386    fn path_navigation_reuses_current_path_for_matching_target() {
1387        let path = Path::new(
1388            vec![Node::new(0, 64, 0), Node::new(4, 64, 0)],
1389            BlockPos::new(4, 64, 0),
1390            true,
1391        );
1392        let mut navigation = PathNavigation::new();
1393
1394        assert!(move_to(
1395            &mut navigation,
1396            path,
1397            1.0,
1398            DVec3::new(0.5, 64.0, 0.5)
1399        ));
1400        let level = empty_level();
1401        assert!(navigation.reuse_current_path_to_targets(
1402            &level,
1403            &[BlockPos::new(4, 64, 0)],
1404            1.25,
1405            DVec3::new(1.5, 64.0, 0.5),
1406        ));
1407
1408        assert!(!navigation.is_done());
1409        assert_eq!(navigation.speed_modifier().to_bits(), 1.25_f64.to_bits());
1410        assert_eq!(
1411            navigation.path().map(Path::target),
1412            Some(BlockPos::new(4, 64, 0))
1413        );
1414        assert_eq!(navigation.path().map(Path::next_node_index), Some(0));
1415    }
1416
1417    #[test]
1418    fn create_path_reuses_current_path_for_matching_target() {
1419        let mut path = Path::new(
1420            vec![
1421                Node::new(0, 64, 0),
1422                Node::new(1, 64, 0),
1423                Node::new(2, 64, 0),
1424            ],
1425            BlockPos::new(2, 64, 0),
1426            true,
1427        );
1428        path.set_next_node_index(1);
1429        let mut navigation = PathNavigation::new();
1430        assert!(move_to(
1431            &mut navigation,
1432            path,
1433            1.0,
1434            DVec3::new(0.5, 64.0, 0.5)
1435        ));
1436
1437        let level = GridLevel::new(BlockStateId(0));
1438        let malus = PathfindingMalus::new();
1439        let mut evaluator = WalkNodeEvaluator::new(MobPathSettings::new(
1440            1,
1441            1,
1442            1,
1443            BlockPos::new(0, 64, 0),
1444            &malus,
1445        ));
1446        let mut no_collision = |_aabb: WorldAabb| false;
1447
1448        let reused = navigation.create_path(
1449            &mut evaluator,
1450            &level,
1451            &mut no_collision,
1452            NavigationPathRequest {
1453                mob_position: BlockPos::new(0, 64, 0),
1454                targets: &[BlockPos::new(2, 64, 0)],
1455                max_path_length: 16.0,
1456                reach_range: 0,
1457            },
1458        );
1459
1460        let Some(reused) = reused else {
1461            panic!("matching active path should be reused");
1462        };
1463        assert_eq!(reused.target(), BlockPos::new(2, 64, 0));
1464        assert_eq!(reused.next_node_index(), 1);
1465        assert_eq!(navigation.path().map(Path::next_node_index), Some(1));
1466    }
1467
1468    #[test]
1469    fn path_navigation_does_not_reuse_current_path_for_different_target() {
1470        let path = Path::new(vec![Node::new(4, 64, 0)], BlockPos::new(4, 64, 0), true);
1471        let mut navigation = PathNavigation::new();
1472
1473        assert!(move_to(
1474            &mut navigation,
1475            path,
1476            1.0,
1477            DVec3::new(0.5, 64.0, 0.5)
1478        ));
1479
1480        let level = empty_level();
1481        assert!(!navigation.reuse_current_path_to_targets(
1482            &level,
1483            &[BlockPos::new(5, 64, 0)],
1484            1.25,
1485            DVec3::new(0.5, 64.0, 0.5),
1486        ));
1487        assert_eq!(navigation.speed_modifier().to_bits(), 1.0_f64.to_bits());
1488    }
1489
1490    #[test]
1491    fn path_navigation_updates_current_speed_modifier() {
1492        let mut navigation = PathNavigation::new();
1493
1494        navigation.set_speed_modifier(1.75);
1495
1496        assert_eq!(navigation.speed_modifier().to_bits(), 1.75_f64.to_bits());
1497    }
1498
1499    #[test]
1500    fn direct_target_stops_when_reached() {
1501        let mut navigation = PathNavigation::new();
1502        navigation.set_direct_target(DVec3::new(1.0, 64.0, 1.0), 0.5);
1503
1504        assert!(
1505            navigation
1506                .next_move_target(tick_context(DVec3::new(1.0, 64.0, 1.0)))
1507                .is_none()
1508        );
1509        assert!(navigation.is_done());
1510    }
1511
1512    #[test]
1513    fn create_path_finds_walkable_target_with_cached_navigation_state() {
1514        init_vanilla_registry();
1515        init_behaviors();
1516
1517        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
1518        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
1519        let level = GridLevel::new(air)
1520            .with(BlockPos::new(0, 63, 0), stone)
1521            .with(BlockPos::new(1, 63, 0), stone)
1522            .with(BlockPos::new(2, 63, 0), stone);
1523        let malus = PathfindingMalus::new();
1524        let mut evaluator = WalkNodeEvaluator::new(MobPathSettings::new(
1525            1,
1526            1,
1527            1,
1528            BlockPos::new(0, 64, 0),
1529            &malus,
1530        ));
1531        let mut navigation = PathNavigation::new();
1532        navigation.update_pathfinder_max_visited_nodes(16.0);
1533        let mut no_collision = |_aabb: WorldAabb| false;
1534
1535        let path = navigation.create_path(
1536            &mut evaluator,
1537            &level,
1538            &mut no_collision,
1539            NavigationPathRequest {
1540                mob_position: BlockPos::new(0, 64, 0),
1541                targets: &[BlockPos::new(2, 64, 0)],
1542                max_path_length: 16.0,
1543                reach_range: 0,
1544            },
1545        );
1546
1547        let Some(path) = path else {
1548            panic!("path should be found");
1549        };
1550        assert!(path.can_reach());
1551        assert_eq!(navigation.target_pos(), Some(BlockPos::new(2, 64, 0)));
1552        assert_eq!(navigation.reach_range(), 0);
1553    }
1554}