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