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