Skip to main content

steel_core/entity/ai/goal/
follow_parent.rs

1use 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}