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