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