steel_core/entity/ai/goal/
follow_mob.rs1use 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}