Skip to main content

steel_core/entity/mob/
pathfinder.rs

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}