Skip to main content

steel_core/entity/ai/goal/
target_goal.rs

1use super::reduced_tick_delay;
2use crate::entity::ai::targeting::TargetingConditions;
3use crate::entity::{LivingEntity, Mob, PathfinderMob, SharedEntity};
4use steel_registry::vanilla_attributes;
5
6const DEFAULT_UNSEEN_MEMORY_TICKS: i32 = 60;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9enum ReachCache {
10    Empty,
11    CanReach,
12    CantReach,
13}
14
15pub(super) struct TargetGoalBase {
16    must_see: bool,
17    must_reach: bool,
18    reach_cache: ReachCache,
19    reach_cache_time: i32,
20    unseen_ticks: i32,
21    target_mob: Option<SharedEntity>,
22    unseen_memory_ticks: i32,
23}
24
25impl TargetGoalBase {
26    #[must_use]
27    pub(super) const fn new(must_see: bool, must_reach: bool) -> Self {
28        Self {
29            must_see,
30            must_reach,
31            reach_cache: ReachCache::Empty,
32            reach_cache_time: 0,
33            unseen_ticks: 0,
34            target_mob: None,
35            unseen_memory_ticks: DEFAULT_UNSEEN_MEMORY_TICKS,
36        }
37    }
38
39    pub(super) const fn set_unseen_memory_ticks(&mut self, unseen_memory_ticks: i32) {
40        self.unseen_memory_ticks = unseen_memory_ticks;
41    }
42
43    pub(super) fn set_target_mob(&mut self, target_mob: Option<SharedEntity>) {
44        self.target_mob = target_mob;
45    }
46
47    pub(super) fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
48        let Some(target) = mob.target().or_else(|| self.target_mob.clone()) else {
49            return false;
50        };
51        let Some(target_living) = target.as_living_entity() else {
52            return false;
53        };
54
55        if !Mob::can_attack(mob, target_living) || mob.is_allied_to(target_living) {
56            return false;
57        }
58
59        let follow_distance = follow_distance(mob);
60        if mob.position().distance_squared(target.position()) > follow_distance * follow_distance {
61            return false;
62        }
63
64        if self.must_see && !self.update_unseen_ticks(mob, target_living) {
65            return false;
66        }
67
68        mob.set_target(Some(&target))
69    }
70
71    pub(super) const fn start(&mut self) {
72        self.reach_cache = ReachCache::Empty;
73        self.reach_cache_time = 0;
74        self.unseen_ticks = 0;
75    }
76
77    pub(super) fn stop(&mut self, mob: &dyn PathfinderMob) {
78        mob.set_target(None);
79        self.target_mob = None;
80    }
81
82    pub(super) fn can_attack(
83        &mut self,
84        mob: &dyn PathfinderMob,
85        target: Option<&dyn LivingEntity>,
86        target_conditions: &TargetingConditions,
87    ) -> bool {
88        let Some(target) = target else {
89            return false;
90        };
91        let Some(world) = mob.level() else {
92            return false;
93        };
94
95        if !target_conditions.test(world.as_ref(), Some(mob), target) {
96            return false;
97        }
98        if !mob.is_within_home_pos(target.block_position()) {
99            return false;
100        }
101
102        if self.must_reach && !self.can_reach(mob, target) {
103            return false;
104        }
105
106        true
107    }
108
109    fn update_unseen_ticks(&mut self, mob: &dyn PathfinderMob, target: &dyn LivingEntity) -> bool {
110        if mob.has_line_of_sight_cached(target) {
111            self.unseen_ticks = 0;
112            return true;
113        }
114
115        self.unseen_ticks += 1;
116        self.unseen_ticks <= reduced_tick_delay(self.unseen_memory_ticks)
117    }
118
119    fn can_reach(&mut self, mob: &dyn PathfinderMob, target: &dyn LivingEntity) -> bool {
120        self.reach_cache_time -= 1;
121        if self.reach_cache_time <= 0 {
122            self.reach_cache = ReachCache::Empty;
123        }
124
125        if self.reach_cache == ReachCache::Empty {
126            self.reach_cache = if self.check_reach(mob, target) {
127                ReachCache::CanReach
128            } else {
129                ReachCache::CantReach
130            };
131        }
132
133        self.reach_cache == ReachCache::CanReach
134    }
135
136    fn check_reach(&mut self, mob: &dyn PathfinderMob, target: &dyn LivingEntity) -> bool {
137        self.reach_cache_time = reduced_tick_delay(10 + rand::random_range(0..5));
138        mob.can_reach_living_target(target)
139    }
140}
141
142fn follow_distance(mob: &dyn PathfinderMob) -> f64 {
143    mob.attributes()
144        .lock()
145        .required_value(vanilla_attributes::FOLLOW_RANGE)
146}
147
148#[cfg(test)]
149mod tests {
150    use std::sync::{Arc, Weak};
151
152    use glam::DVec3;
153    use steel_registry::{init_vanilla_registry, vanilla_entities};
154
155    use super::*;
156    use crate::entity::ai::targeting::TargetingConditions;
157    use crate::entity::{Mob, entities::PigEntity};
158
159    fn pig(id: i32, position: DVec3) -> Arc<PigEntity> {
160        Arc::new(PigEntity::new(
161            &vanilla_entities::PIG,
162            id,
163            position,
164            Weak::new(),
165        ))
166    }
167
168    fn target_living(target: &SharedEntity) -> &dyn LivingEntity {
169        let Some(living) = target.as_living_entity() else {
170            panic!("test target should be a living entity");
171        };
172        living
173    }
174
175    #[test]
176    fn target_goal_base_continues_with_existing_mob_target() {
177        init_vanilla_registry();
178        let mob = pig(1, DVec3::ZERO);
179        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
180        assert!(mob.set_target(Some(&target)));
181        let mut goal = TargetGoalBase::new(false, false);
182
183        goal.start();
184
185        assert!(goal.can_continue_to_use(mob.as_ref()));
186        let Some(stored_target) = mob.target() else {
187            panic!("target should remain set");
188        };
189        assert_eq!(stored_target.uuid(), target.uuid());
190    }
191
192    #[test]
193    fn target_goal_base_restores_stored_target_while_continuing() {
194        init_vanilla_registry();
195        let mob = pig(1, DVec3::ZERO);
196        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
197        let mut goal = TargetGoalBase::new(false, false);
198        goal.set_target_mob(Some(target.clone()));
199
200        assert!(mob.target().is_none());
201        assert!(goal.can_continue_to_use(mob.as_ref()));
202
203        let Some(stored_target) = mob.target() else {
204            panic!("stored target should be copied onto the mob");
205        };
206        assert_eq!(stored_target.uuid(), target.uuid());
207    }
208
209    #[test]
210    fn target_goal_base_forgets_unseen_target_after_memory_ticks() {
211        init_vanilla_registry();
212        let mob = pig(1, DVec3::ZERO);
213        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
214        assert!(mob.set_target(Some(&target)));
215        let mut goal = TargetGoalBase::new(true, false);
216        goal.set_unseen_memory_ticks(2);
217        goal.start();
218
219        assert!(goal.can_continue_to_use(mob.as_ref()));
220        assert!(!goal.can_continue_to_use(mob.as_ref()));
221    }
222
223    #[test]
224    fn target_goal_base_stop_clears_mob_and_stored_target() {
225        init_vanilla_registry();
226        let mob = pig(1, DVec3::ZERO);
227        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
228        assert!(mob.set_target(Some(&target)));
229        let mut goal = TargetGoalBase::new(false, false);
230        goal.set_target_mob(Some(target));
231
232        goal.stop(mob.as_ref());
233
234        assert!(mob.target().is_none());
235        assert!(goal.target_mob.is_none());
236    }
237
238    #[test]
239    fn target_goal_base_can_attack_requires_world() {
240        init_vanilla_registry();
241        let mob = pig(1, DVec3::ZERO);
242        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
243        let mut goal = TargetGoalBase::new(false, false);
244        let target_conditions = TargetingConditions::for_combat().ignore_line_of_sight();
245
246        assert!(!goal.can_attack(
247            mob.as_ref(),
248            Some(target_living(&target)),
249            &target_conditions
250        ));
251    }
252
253    #[test]
254    fn target_goal_base_caches_unreachable_targets() {
255        init_vanilla_registry();
256        let mob = pig(1, DVec3::ZERO);
257        let target: SharedEntity = pig(2, DVec3::new(2.0, 0.0, 0.0));
258        let mut goal = TargetGoalBase::new(false, true);
259
260        assert!(!goal.can_reach(mob.as_ref(), target_living(&target)));
261        assert_eq!(goal.reach_cache, ReachCache::CantReach);
262        let first_reach_cache_time = goal.reach_cache_time;
263
264        assert!(!goal.can_reach(mob.as_ref(), target_living(&target)));
265        assert_eq!(goal.reach_cache, ReachCache::CantReach);
266        assert_eq!(goal.reach_cache_time, first_reach_cache_time - 1);
267    }
268}