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