steel_core/entity/ai/goal/
follow_parent.rs1use crate::entity::ai::goal::selector::{Goal, GoalControls};
2use crate::entity::{PathfinderMob, SharedEntity};
3
4use super::reduced_tick_delay;
5
6const HORIZONTAL_SCAN_RANGE: f64 = 8.0;
7const VERTICAL_SCAN_RANGE: f64 = 4.0;
8const DONT_FOLLOW_IF_CLOSER_THAN_SQR: f64 = 9.0;
9const STOP_FOLLOW_IF_FARTHER_THAN_SQR: f64 = 256.0;
10
11pub struct FollowParentGoal {
12 parent: Option<SharedEntity>,
13 speed_modifier: f64,
14 time_to_recalc_path: i32,
15}
16
17impl FollowParentGoal {
18 #[must_use]
19 pub(crate) const fn new(speed_modifier: f64) -> Self {
20 Self {
21 parent: None,
22 speed_modifier,
23 time_to_recalc_path: 0,
24 }
25 }
26}
27
28impl Goal for FollowParentGoal {
29 fn controls(&self) -> GoalControls {
30 GoalControls::EMPTY
31 }
32
33 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
34 let Some(animal) = mob.as_animal() else {
35 return false;
36 };
37 if animal.get_age() >= 0 {
38 return false;
39 }
40
41 let Some(world) = mob.level() else {
42 return false;
43 };
44 let search_box = mob.bounding_box().inflate_xyz(
45 HORIZONTAL_SCAN_RANGE,
46 VERTICAL_SCAN_RANGE,
47 HORIZONTAL_SCAN_RANGE,
48 );
49 let parent = world.nearest_entity_in_aabb_matching(&search_box, mob.position(), |entity| {
50 entity.uuid() != mob.uuid()
51 && entity.entity_type() == mob.entity_type()
52 && entity
53 .as_animal()
54 .is_some_and(|candidate| candidate.get_age() >= 0)
55 });
56 let Some(parent) = parent else {
57 return false;
58 };
59 if mob.position().distance_squared(parent.position()) < DONT_FOLLOW_IF_CLOSER_THAN_SQR {
60 return false;
61 }
62
63 self.parent = Some(parent);
64 true
65 }
66
67 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
68 let Some(animal) = mob.as_animal() else {
69 return false;
70 };
71 if animal.get_age() >= 0 {
72 return false;
73 }
74
75 let Some(parent) = &self.parent else {
76 return false;
77 };
78 if !parent.is_alive() {
79 return false;
80 }
81
82 let distance_sqr = mob.position().distance_squared(parent.position());
83 !(distance_sqr < DONT_FOLLOW_IF_CLOSER_THAN_SQR
84 || distance_sqr > STOP_FOLLOW_IF_FARTHER_THAN_SQR)
85 }
86
87 fn start(&mut self, _mob: &dyn PathfinderMob) {
88 self.time_to_recalc_path = 0;
89 }
90
91 fn stop(&mut self, _mob: &dyn PathfinderMob) {
92 self.parent = None;
93 }
94
95 fn tick(&mut self, mob: &dyn PathfinderMob) {
96 self.time_to_recalc_path -= 1;
97 if self.time_to_recalc_path > 0 {
98 return;
99 }
100 self.time_to_recalc_path = reduced_tick_delay(10);
101
102 let Some(parent) = &self.parent else {
103 return;
104 };
105 mob.move_to_pos(parent.position(), self.speed_modifier);
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn follow_parent_goal_claims_no_controls_like_vanilla() {
115 let goal = FollowParentGoal::new(1.1);
116
117 assert_eq!(goal.controls(), GoalControls::EMPTY);
118 }
119}