Skip to main content

steel_core/entity/ai/goal/
follow_mob.rs

1use glam::DVec3;
2
3use super::reduced_tick_delay;
4use super::selector::{Goal, GoalControls};
5use crate::entity::ai::path::PathType;
6use crate::entity::{Mob, PathfinderMob, SharedEntity};
7
8type FollowMobPredicate = Box<dyn Fn(&dyn PathfinderMob, &dyn Mob) -> bool + Send + Sync>;
9
10pub struct FollowMobGoal {
11    following_mob: Option<SharedEntity>,
12    follow_predicate: FollowMobPredicate,
13    speed_modifier: f64,
14    time_to_recalc_path: i32,
15    stop_distance: f32,
16    old_water_cost: f32,
17    area_size: f32,
18}
19
20impl FollowMobGoal {
21    #[must_use]
22    pub(crate) fn new(
23        speed_modifier: f64,
24        stop_distance: f32,
25        area_size: f32,
26        follow_predicate: impl Fn(&dyn PathfinderMob, &dyn Mob) -> bool + Send + Sync + 'static,
27    ) -> Self {
28        Self {
29            following_mob: None,
30            follow_predicate: Box::new(follow_predicate),
31            speed_modifier,
32            time_to_recalc_path: 0,
33            stop_distance,
34            old_water_cost: 0.0,
35            area_size,
36        }
37    }
38
39    fn stop_distance_sqr(&self) -> f64 {
40        f64::from(self.stop_distance * self.stop_distance)
41    }
42
43    fn should_back_away(&self, mob: &dyn PathfinderMob, following_mob: &SharedEntity) -> bool {
44        let mob_position = mob.position();
45        let following_position = following_mob.position();
46        let delta = mob_position - following_position;
47        let distance_sqr = delta.length_squared();
48        if distance_sqr <= f64::from(self.stop_distance) {
49            return true;
50        }
51
52        following_mob.as_mob().is_some_and(|following_mob| {
53            following_mob
54                .mob_base()
55                .controls()
56                .lock()
57                .look_control
58                .wanted_position()
59                == mob_position
60        })
61    }
62}
63
64impl Goal for FollowMobGoal {
65    fn controls(&self) -> GoalControls {
66        GoalControls::MOVE | GoalControls::LOOK
67    }
68
69    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
70        let Some(world) = mob.level() else {
71            return false;
72        };
73
74        let search_box = mob.bounding_box().inflate(f64::from(self.area_size));
75        let mut candidates = world.get_entities_in_aabb_matching(&search_box, |entity| {
76            if entity.uuid() == mob.uuid() {
77                return false;
78            }
79            let Some(candidate_mob) = entity.as_mob() else {
80                return false;
81            };
82            !candidate_mob.is_invisible() && (self.follow_predicate)(mob, candidate_mob)
83        });
84
85        let Some(following_mob) = candidates.drain(..).next() else {
86            return false;
87        };
88        self.following_mob = Some(following_mob);
89        true
90    }
91
92    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
93        let Some(following_mob) = &self.following_mob else {
94            return false;
95        };
96
97        !mob.mob_base().navigation().lock().is_done()
98            && mob.position().distance_squared(following_mob.position()) > self.stop_distance_sqr()
99    }
100
101    fn start(&mut self, mob: &dyn PathfinderMob) {
102        self.time_to_recalc_path = 0;
103        self.old_water_cost = mob.get_pathfinding_malus(PathType::Water);
104        mob.set_pathfinding_malus(PathType::Water, 0.0);
105    }
106
107    fn stop(&mut self, mob: &dyn PathfinderMob) {
108        self.following_mob = None;
109        mob.mob_base().navigation().lock().stop();
110        mob.set_pathfinding_malus(PathType::Water, self.old_water_cost);
111    }
112
113    fn tick(&mut self, mob: &dyn PathfinderMob) {
114        let Some(following_mob) = &self.following_mob else {
115            return;
116        };
117        if mob.is_leashed() {
118            return;
119        }
120
121        let following_position = following_mob.position();
122        mob.mob_base().controls().lock().look_control.set_look_at(
123            DVec3::new(
124                following_position.x,
125                following_mob.get_eye_y(),
126                following_position.z,
127            ),
128            10.0,
129            mob.max_head_x_rot(),
130        );
131
132        self.time_to_recalc_path -= 1;
133        if self.time_to_recalc_path > 0 {
134            return;
135        }
136        self.time_to_recalc_path = reduced_tick_delay(10);
137
138        let mob_position = mob.position();
139        let delta = mob_position - following_position;
140        let distance_sqr = delta.length_squared();
141        if distance_sqr > self.stop_distance_sqr() {
142            mob.move_to_pos(following_position, self.speed_modifier);
143            return;
144        }
145
146        mob.mob_base().navigation().lock().stop();
147        if self.should_back_away(mob, following_mob) {
148            mob.move_to_pos(
149                DVec3::new(
150                    mob_position.x + delta.x,
151                    mob_position.y,
152                    mob_position.z + delta.z,
153                ),
154                self.speed_modifier,
155            );
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use std::sync::{Arc, Weak};
163
164    use glam::DVec3;
165    use steel_registry::{init_vanilla_registry, vanilla_entities};
166
167    use super::*;
168    use crate::entity::entities::PigEntity;
169
170    #[test]
171    fn follow_mob_goal_uses_move_and_look_controls() {
172        let goal = FollowMobGoal::new(1.0, 3.0, 7.0, |_, _| true);
173
174        assert_eq!(goal.controls(), GoalControls::MOVE | GoalControls::LOOK);
175    }
176
177    #[test]
178    fn follow_mob_goal_requires_world() {
179        init_vanilla_registry();
180        let mut goal = FollowMobGoal::new(1.0, 3.0, 7.0, |_, _| true);
181        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
182
183        assert!(!goal.can_use(&mob));
184    }
185
186    #[test]
187    fn follow_mob_goal_temporarily_removes_water_malus() {
188        init_vanilla_registry();
189        let mut goal = FollowMobGoal::new(1.0, 3.0, 7.0, |_, _| true);
190        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
191        mob.set_pathfinding_malus(PathType::Water, 4.0);
192
193        goal.start(&mob);
194
195        assert_eq!(
196            mob.get_pathfinding_malus(PathType::Water).to_bits(),
197            0.0_f32.to_bits()
198        );
199
200        goal.stop(&mob);
201
202        assert_eq!(
203            mob.get_pathfinding_malus(PathType::Water).to_bits(),
204            4.0_f32.to_bits()
205        );
206    }
207
208    #[test]
209    fn follow_mob_goal_stops_when_no_navigation_is_running() {
210        init_vanilla_registry();
211        let mut goal = FollowMobGoal::new(1.0, 3.0, 7.0, |_, _| true);
212        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
213        goal.following_mob = Some(Arc::new(PigEntity::new(
214            &vanilla_entities::PIG,
215            2,
216            DVec3::new(4.0, 0.0, 0.0),
217            Weak::new(),
218        )));
219
220        assert!(!goal.can_continue_to_use(&mob));
221    }
222
223    #[test]
224    fn follow_mob_goal_looks_at_following_mob() {
225        init_vanilla_registry();
226        let mut goal = FollowMobGoal::new(1.0, 3.0, 7.0, |_, _| true);
227        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
228        let following_mob: SharedEntity = Arc::new(PigEntity::new(
229            &vanilla_entities::PIG,
230            2,
231            DVec3::new(4.0, 0.0, 0.0),
232            Weak::new(),
233        ));
234        goal.following_mob = Some(Arc::clone(&following_mob));
235
236        goal.tick(&mob);
237
238        let wanted_position = mob
239            .mob_base()
240            .controls()
241            .lock()
242            .look_control
243            .wanted_position();
244        assert_eq!(wanted_position.x.to_bits(), 4.0_f64.to_bits());
245        assert_eq!(
246            wanted_position.y.to_bits(),
247            following_mob.get_eye_y().to_bits()
248        );
249        assert_eq!(wanted_position.z.to_bits(), 0.0_f64.to_bits());
250    }
251}