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::{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}