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: &(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}