1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_math::fast_floor;
5use steel_registry::blocks::block_state_ext::BlockStateExt as _;
6use steel_registry::{vanilla_attributes, vanilla_blocks};
7use steel_utils::{BlockPos, ChunkPos};
8
9use super::{Mob, TARGET_REACH_DISTANCE_SQR};
10use crate::entity::ai::goal::{Goal, GoalControl};
11use crate::entity::ai::navigation::{
12 NavigationPathRequest, NavigationRecomputeRequest, NavigationTickContext,
13};
14use crate::entity::ai::path::Path;
15use crate::entity::ai::walk::{MobPathSettings, WalkNodeEvaluator};
16use crate::entity::{Entity, LivingEntity, SharedEntity};
17use crate::physics::WorldCollisionProvider;
18use crate::world::{LevelReader, World};
19
20pub(super) const LEASH_CLOSE_RANGE_WANTED_DISTANCE: f64 = 2.0;
21
22pub(super) fn tick_path_navigation_target<M: Mob + ?Sized>(
23 mob: &M,
24 world: &Arc<World>,
25 game_time: i64,
26 can_update_path: bool,
27) {
28 let (target, speed_modifier) = {
29 let mut navigation = mob.mob_base().navigation().lock();
30 let ground_mob_position =
31 ground_navigation_temp_mob_pos(mob, world.as_ref(), navigation.can_float());
32 let context = NavigationTickContext {
33 ground_mob_position,
34 mob_position: mob.position(),
35 mob_bounding_box_width: mob.bounding_box().width(),
36 mob_speed: mob.get_speed(),
37 game_time,
38 };
39 let next_target = if can_update_path {
40 navigation.next_move_target(context)
41 } else {
42 navigation.next_move_target_without_path_update(context, mob.on_ground())
43 };
44 let Some(target) = next_target else {
45 return;
46 };
47 target
48 };
49
50 let target_pos = BlockPos::containing(target.x, target.y, target.z);
51 let ground_y = if world.get_block_state(target_pos.below()).is_air() {
52 target.y
53 } else {
54 WalkNodeEvaluator::floor_level(world.as_ref(), target_pos)
55 };
56 mob.set_wanted_position(DVec3::new(target.x, ground_y, target.z), speed_modifier);
57}
58
59fn ground_navigation_temp_mob_pos<M: Mob + ?Sized>(
60 mob: &M,
61 world: &World,
62 can_float: bool,
63) -> DVec3 {
64 let position = mob.position();
65 DVec3::new(
66 position.x,
67 f64::from(ground_navigation_surface_y(mob, world, can_float)),
68 position.z,
69 )
70}
71
72fn ground_navigation_surface_y<M: Mob + ?Sized>(mob: &M, world: &World, can_float: bool) -> i32 {
73 if !mob.is_in_water() || !can_float {
74 return fast_floor(mob.position().y + 0.5);
75 }
76
77 let position = mob.position();
78 let block_y = mob.block_position().y();
79 let mut surface = block_y;
80 let mut state = world.get_block_state(BlockPos::containing(
81 position.x,
82 f64::from(surface),
83 position.z,
84 ));
85 let mut steps = 0;
86 while state.get_block() == &vanilla_blocks::WATER {
87 surface += 1;
88 state = world.get_block_state(BlockPos::containing(
89 position.x,
90 f64::from(surface),
91 position.z,
92 ));
93 steps += 1;
94 if steps > 16 {
95 return block_y;
96 }
97 }
98
99 surface
100}
101
102pub trait PathfinderMob: Mob {
103 fn controlled_pathfinder_vehicle(&self) -> Option<SharedEntity> {
104 let vehicle = self.controlled_mob_vehicle()?;
105 vehicle.as_pathfinder_mob()?;
106 Some(vehicle)
107 }
108
109 fn get_walk_target_value(&self, pos: BlockPos) -> f32 {
111 if let Some(monster) = self.as_monster() {
112 monster.get_walk_target_value_monster(pos)
113 } else if let Some(animal) = self.as_animal() {
114 animal.animal_walk_target_value(pos)
115 } else {
116 0.0
117 }
118 }
119
120 fn can_update_path(&self) -> bool {
121 self.on_ground() || self.is_in_water() || self.is_in_lava() || self.is_passenger()
122 }
123
124 fn can_path_to_targets_below_surface(&self) -> bool {
125 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
126 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
127 {
128 return pathfinder.can_path_to_targets_below_surface();
129 }
130
131 self.mob_base()
132 .navigation()
133 .lock()
134 .can_path_to_targets_below_surface()
135 }
136
137 fn can_reach_living_target(&self, target: &dyn LivingEntity) -> bool {
138 let target_pos = target.block_position();
139 self.create_path_to(target_pos, 0)
140 .is_some_and(|path| path_end_node_can_reach_target(&path, target_pos))
141 }
142
143 fn tick_pathfinder_path_navigation(&self) {
144 let Some(world) = self.level() else {
145 return;
146 };
147 let game_time = world.game_time();
148 let recompute_request = {
149 let mut navigation = self.mob_base().navigation().lock();
150 navigation.tick();
151 navigation.take_delayed_recompute_request(game_time, self.can_update_path())
152 };
153 if let Some(request) = recompute_request {
154 self.recompute_path(request);
155 }
156
157 tick_path_navigation_target(self, &world, game_time, self.can_update_path());
158 }
159
160 fn tick_pathfinder_goal_selectors(&self, entity: &SharedEntity)
161 where
162 Self: Sized,
163 {
164 let id_based_tick_count = self.tick_count().wrapping_add(self.id());
165 let mut target_selector = self.mob_base().target_selector().lock();
166 let mut goal_selector = self.mob_base().goal_selector().lock();
167 if id_based_tick_count % 2 != 0 && self.tick_count() > 1 {
168 target_selector.tick_running_goals(self, entity, false);
169 goal_selector.tick_running_goals(self, entity, false);
170 } else {
171 target_selector.tick(self, entity);
172 goal_selector.tick(self, entity);
173 }
174 }
175
176 fn is_stable_destination(&self, pos: BlockPos) -> bool {
177 self.level()
178 .is_some_and(|world| world.get_block_state(pos.below()).is_solid_render())
179 }
180
181 fn create_path_to(&self, target: BlockPos, reach_range: i32) -> Option<Path> {
182 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
183 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
184 {
185 return pathfinder.create_path_to(target, reach_range);
186 }
187
188 let world = self.level()?;
189 if !world.has_full_chunk(ChunkPos::from_block_pos(target)) {
190 return None;
191 }
192
193 let target = path_target_for_mob(self, world.as_ref(), target);
194 let targets = [target];
195 self.create_path_to_targets(&world, &targets, reach_range)
196 }
197
198 fn recompute_path(&self, request: NavigationRecomputeRequest) {
199 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
200 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
201 {
202 pathfinder.recompute_path(request);
203 return;
204 }
205
206 let path = self.create_path_to(request.target_pos, request.reach_range);
207 self.mob_base()
208 .navigation()
209 .lock()
210 .complete_recompute_path(path, request.game_time);
211 }
212
213 fn move_to_pos(&self, target: DVec3, speed_modifier: f64) -> bool {
214 self.move_to_pos_with_reach(target, 1, speed_modifier)
215 }
216
217 fn move_to_pos_with_reach(&self, target: DVec3, reach_range: i32, speed_modifier: f64) -> bool {
218 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
219 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
220 {
221 return pathfinder.move_to_pos_with_reach(target, reach_range, speed_modifier);
222 }
223
224 let target_pos = BlockPos::containing(target.x, target.y, target.z);
225 let Some(world) = self.level() else {
226 self.mob_base().navigation().lock().stop();
227 return false;
228 };
229 if !world.has_full_chunk(ChunkPos::from_block_pos(target_pos)) {
230 self.mob_base().navigation().lock().stop();
231 return false;
232 }
233
234 let target_pos = path_target_for_mob(self, world.as_ref(), target_pos);
235 let targets = [target_pos];
236 if self
237 .mob_base()
238 .navigation()
239 .lock()
240 .reuse_current_path_to_targets(
241 world.as_ref(),
242 &targets,
243 speed_modifier,
244 self.position(),
245 )
246 {
247 return true;
248 }
249
250 let path = self.create_path_to_targets(&world, &targets, reach_range);
251 self.move_to_path(path, speed_modifier)
252 }
253
254 fn move_to_path(&self, path: Option<Path>, speed_modifier: f64) -> bool {
255 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
256 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
257 {
258 return pathfinder.move_to_path(path, speed_modifier);
259 }
260
261 let Some(world) = self.level() else {
262 self.mob_base().navigation().lock().stop();
263 return false;
264 };
265 let mut navigation = self.mob_base().navigation().lock();
266 let Some(path) = path else {
267 navigation.stop();
268 return false;
269 };
270
271 navigation.move_to(world.as_ref(), path, speed_modifier, self.position())
272 }
273
274 fn is_path_finding(&self) -> bool {
275 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
276 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
277 {
278 return pathfinder.is_path_finding();
279 }
280
281 !self.mob_base().navigation().lock().is_done()
282 }
283
284 fn is_panicking(&self) -> bool {
285 self.mob_base()
286 .goal_selector()
287 .lock()
288 .has_running_panic_goal()
289 }
290
291 fn should_stay_close_to_leash_holder(&self) -> bool {
292 true
293 }
294
295 fn follow_leash_speed(&self) -> f64 {
296 1.0
297 }
298
299 fn when_leashed_to_pathfinder(&self, holder: &dyn Entity) {
300 let radius = self.leash_elastic_distance() as i32 - 1;
301 self.set_home_to(holder.block_position(), radius);
302 holder.notify_leash_holder(self.as_entity_event_source());
303 }
304
305 fn close_range_leash_behaviour_pathfinder(&self, holder: &dyn Entity) {
310 if !self.should_stay_close_to_leash_holder() || self.is_panicking() {
311 return;
312 }
313
314 self.mob_base()
315 .goal_selector()
316 .lock()
317 .enable_control(GoalControl::Move);
318 let distance_to = (holder.position() - self.position()).length();
319 let delta = (holder.position() - self.position()).normalize_or_zero()
320 * (distance_to - LEASH_CLOSE_RANGE_WANTED_DISTANCE).max(0.0);
321 self.move_to_pos(self.position() + delta, self.follow_leash_speed());
322 }
323
324 fn remove_all_goals(&self, predicate: impl FnMut(&dyn Goal) -> bool)
327 where
328 Self: Sized,
329 {
330 self.mob_base()
331 .goal_selector()
332 .lock()
333 .remove_all_goals(self, predicate);
334 }
335
336 fn remove_free_will(&self)
337 where
338 Self: Sized,
339 {
340 self.remove_all_goals(|_| true);
341 }
343
344 fn create_path_to_targets(
345 &self,
346 world: &Arc<World>,
347 targets: &[BlockPos],
348 reach_range: i32,
349 ) -> Option<Path> {
350 if let Some(vehicle) = self.controlled_pathfinder_vehicle()
351 && let Some(pathfinder) = vehicle.as_pathfinder_mob()
352 {
353 return pathfinder.create_path_to_targets(world, targets, reach_range);
354 }
355
356 if targets.is_empty()
357 || self.position().y < f64::from(world.min_y())
358 || !self.can_update_path()
359 {
360 return None;
361 }
362
363 let follow_range = self
364 .attributes()
365 .lock()
366 .required_value(vanilla_attributes::FOLLOW_RANGE);
367 let max_path_length = {
368 let mut navigation = self.mob_base().navigation().lock();
369 navigation.update_pathfinder_max_visited_nodes(follow_range);
370 navigation.max_path_length(follow_range)
371 };
372
373 let mob_position = self.block_position();
374 let settings = MobPathSettings::from_mob(self);
375 let mut evaluator = WalkNodeEvaluator::new(settings);
376 let collision_world =
377 WorldCollisionProvider::for_path_navigation(world, self.as_entity_event_source());
378 let mut collision = |aabb| {
379 collision_world.has_entity_context_collision(
380 aabb,
381 self.position().y,
382 self.is_descending(),
383 )
384 };
385
386 self.mob_base().navigation().lock().create_path(
387 &mut evaluator,
388 world.as_ref(),
389 &mut collision,
390 NavigationPathRequest {
391 mob_position,
392 targets,
393 max_path_length,
394 reach_range,
395 },
396 )
397 }
398}
399
400pub(super) fn path_end_node_can_reach_target(path: &Path, target: BlockPos) -> bool {
401 let Some(end_node) = path.end_node() else {
402 return false;
403 };
404 let dx = end_node.x - target.x();
405 let dz = end_node.z - target.z();
406 f64::from(dx * dx + dz * dz) <= TARGET_REACH_DISTANCE_SQR
407}
408
409fn path_target_for_mob<M: PathfinderMob + ?Sized, L: LevelReader>(
410 mob: &M,
411 level: &L,
412 target: BlockPos,
413) -> BlockPos {
414 if mob.can_path_to_targets_below_surface() {
415 target
416 } else {
417 find_ground_path_target_surface(level, target)
418 }
419}
420
421pub(super) fn find_ground_path_target_surface(
422 level: &impl LevelReader,
423 mut pos: BlockPos,
424) -> BlockPos {
425 if level.get_block_state(pos).is_air() {
426 let mut column_pos = pos.below();
427 while column_pos.y() >= level.min_y() && level.get_block_state(column_pos).is_air() {
428 column_pos = column_pos.below();
429 }
430 if column_pos.y() >= level.min_y() {
431 return column_pos.above();
432 }
433
434 column_pos = pos.at_y(pos.y() + 1);
435 while column_pos.y() < level.max_y_exclusive() && level.get_block_state(column_pos).is_air()
436 {
437 column_pos = column_pos.above();
438 }
439 pos = column_pos;
440 }
441
442 if !level.get_block_state(pos).is_solid() {
443 return pos;
444 }
445
446 let mut column_pos = pos.above();
447 while column_pos.y() < level.max_y_exclusive() && level.get_block_state(column_pos).is_solid() {
448 column_pos = column_pos.above();
449 }
450 column_pos
451}