steel_core/entity/ai/goal/
nearest_attackable_target.rs1use glam::DVec3;
2
3use super::reduced_tick_delay;
4use super::selector::{Goal, GoalControls};
5use super::target_goal::{TargetGoalBase, follow_distance};
6use crate::entity::ai::targeting::TargetingConditions;
7use crate::entity::{LivingEntity, PathfinderMob, SharedEntity};
8use crate::world::World;
9
10const DEFAULT_RANDOM_INTERVAL: i32 = 10;
11
12enum TargetSearch {
13 Players,
14 Entities,
15}
16
17pub(crate) struct NearestAttackableTargetGoal {
18 target_goal: TargetGoalBase,
19 random_interval: i32,
20 target: Option<SharedEntity>,
21 target_conditions: TargetingConditions,
22 search: TargetSearch,
23}
24
25impl NearestAttackableTargetGoal {
26 #[must_use]
27 pub(crate) fn new(
28 must_see: bool,
29 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
30 ) -> Self {
31 Self::new_with_interval(DEFAULT_RANDOM_INTERVAL, must_see, false, selector)
32 }
33
34 #[must_use]
35 pub(crate) fn new_with_interval(
36 random_interval: i32,
37 must_see: bool,
38 must_reach: bool,
39 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
40 ) -> Self {
41 Self::new_with_options(
42 random_interval,
43 must_see,
44 must_reach,
45 selector,
46 TargetSearch::Entities,
47 )
48 }
49
50 #[must_use]
51 pub(crate) fn new_for_players(
52 must_see: bool,
53 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
54 ) -> Self {
55 Self::new_for_players_with_interval(DEFAULT_RANDOM_INTERVAL, must_see, false, selector)
56 }
57
58 #[must_use]
59 pub(crate) fn new_for_players_with_interval(
60 random_interval: i32,
61 must_see: bool,
62 must_reach: bool,
63 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
64 ) -> Self {
65 Self::new_with_options(
66 random_interval,
67 must_see,
68 must_reach,
69 selector,
70 TargetSearch::Players,
71 )
72 }
73
74 fn new_with_options(
75 random_interval: i32,
76 must_see: bool,
77 must_reach: bool,
78 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
79 search: TargetSearch,
80 ) -> Self {
81 Self {
82 target_goal: TargetGoalBase::new(must_see, must_reach),
83 random_interval: reduced_tick_delay(random_interval),
84 target: None,
85 target_conditions: TargetingConditions::for_combat().selector(selector),
86 search,
87 }
88 }
89
90 fn find_target(&mut self, mob: &dyn PathfinderMob) {
91 let Some(world) = mob.level() else {
92 self.target = None;
93 return;
94 };
95
96 let follow = follow_distance(mob);
97 let position = mob.position();
98 let origin = DVec3::new(position.x, mob.get_eye_y(), position.z);
99 let target_conditions = self.target_conditions.clone().range(follow);
100 let level = world.as_ref();
101
102 self.target = match self.search {
103 TargetSearch::Players => level
104 .nearest_player(origin, follow, |player| {
105 target_conditions.test(level, Some(mob), player)
106 })
107 .map(|player| -> SharedEntity { player }),
108 TargetSearch::Entities => {
109 let search_box = mob.bounding_box().inflate_xyz(follow, follow, follow);
110 level.nearest_entity_in_aabb_matching(&search_box, origin, |entity| {
111 entity
112 .as_living_entity()
113 .is_some_and(|target| target_conditions.test(level, Some(mob), target))
114 })
115 }
116 };
117 }
118
119 pub(crate) fn set_target(&mut self, target: Option<SharedEntity>) {
120 self.target = target;
121 }
122}
123
124impl Goal for NearestAttackableTargetGoal {
125 fn controls(&self) -> GoalControls {
126 GoalControls::TARGET
127 }
128
129 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
130 if self.random_interval > 0 && rand::random_range(0..self.random_interval) != 0 {
131 return false;
132 }
133
134 self.find_target(mob);
135 self.target.is_some()
136 }
137
138 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
139 self.target_goal.can_continue_to_use(mob)
140 }
141
142 fn start(&mut self, mob: &dyn PathfinderMob) {
143 let _ = mob.set_target(self.target.as_ref());
144 self.target_goal.start();
145 }
146
147 fn stop(&mut self, mob: &dyn PathfinderMob) {
148 self.target_goal.stop(mob);
149 self.target = None;
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use std::sync::Arc;
156
157 use glam::DVec3;
158 use steel_registry::{init_vanilla_registry, vanilla_entities};
159 use steel_utils::ChunkPos;
160
161 use super::*;
162 use crate::behavior::init_behaviors;
163 use crate::entity::entities::{CowEntity, PigEntity};
164 use crate::entity::{Entity, Mob};
165 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
166
167 fn animal_fixture(
168 name: &'static str,
169 ) -> (Arc<World>, Arc<PigEntity>, Arc<PigEntity>, Arc<CowEntity>) {
170 init_vanilla_registry();
171 init_behaviors();
172 let world = fresh_test_world(name);
173 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
174
175 let hunter = Arc::new(PigEntity::new(
176 &vanilla_entities::PIG,
177 1,
178 DVec3::new(8.0, 65.0, 8.0),
179 Arc::downgrade(&world),
180 ));
181 let nearer_pig = Arc::new(PigEntity::new(
182 &vanilla_entities::PIG,
183 2,
184 DVec3::new(9.0, 65.0, 8.0),
185 Arc::downgrade(&world),
186 ));
187 let farther_cow = Arc::new(CowEntity::new(
188 &vanilla_entities::COW,
189 3,
190 DVec3::new(10.0, 65.0, 8.0),
191 Arc::downgrade(&world),
192 ));
193
194 for entity in [
195 Arc::clone(&hunter) as SharedEntity,
196 Arc::clone(&nearer_pig) as SharedEntity,
197 Arc::clone(&farther_cow) as SharedEntity,
198 ] {
199 world
200 .try_add_entity(entity)
201 .expect("test entity should attach to the loaded chunk");
202 }
203
204 (world, hunter, nearer_pig, farther_cow)
205 }
206
207 #[test]
208 fn selects_nearest_living_entity_matching_selector() {
209 let (_world, hunter, nearer_pig, _farther_cow) =
210 animal_fixture("nearest_attackable_selector");
211 let mut goal =
212 NearestAttackableTargetGoal::new_with_interval(0, false, false, |target, _| {
213 target.as_animal().is_some()
214 });
215
216 assert!(goal.can_use(hunter.as_ref()));
217 goal.start(hunter.as_ref());
218
219 let Some(target) = hunter.target() else {
220 panic!("selector goal should assign a target");
221 };
222 assert_eq!(target.uuid(), nearer_pig.uuid());
223 }
224
225 #[test]
226 fn selector_can_reject_all_candidates() {
227 let (_world, hunter, _nearer_pig, _farther_cow) =
228 animal_fixture("nearest_attackable_selector_rejects");
229 let mut goal = NearestAttackableTargetGoal::new(false, |_, _| false);
230
231 assert!(!goal.can_use(hunter.as_ref()));
232 }
233}