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::{TestWorld, fresh_test_world, insert_ready_full_chunk};
166
167 fn animal_fixture(
168 name: &'static str,
169 ) -> (TestWorld, Arc<PigEntity>, Arc<PigEntity>, Arc<CowEntity>) {
170 init_vanilla_registry();
171 init_behaviors();
172 let world_fixture = fresh_test_world(name);
173 let world = &world_fixture.world;
174 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
175
176 let pig_hunter = Arc::new(PigEntity::new(
177 &vanilla_entities::PIG,
178 1,
179 DVec3::new(8.0, 65.0, 8.0),
180 Arc::downgrade(world),
181 ));
182 let nearer_pig = Arc::new(PigEntity::new(
183 &vanilla_entities::PIG,
184 2,
185 DVec3::new(9.0, 65.0, 8.0),
186 Arc::downgrade(world),
187 ));
188 let farther_cow = Arc::new(CowEntity::new(
189 &vanilla_entities::COW,
190 3,
191 DVec3::new(10.0, 65.0, 8.0),
192 Arc::downgrade(world),
193 ));
194
195 let entities: [SharedEntity; 3] = [
196 Arc::<PigEntity>::clone(&pig_hunter),
197 Arc::<PigEntity>::clone(&nearer_pig),
198 Arc::<CowEntity>::clone(&farther_cow),
199 ];
200 for entity in entities {
201 world
202 .try_add_entity(entity)
203 .expect("test entity should attach to the loaded chunk");
204 }
205
206 (world_fixture, pig_hunter, nearer_pig, farther_cow)
207 }
208
209 #[test]
210 fn selects_nearest_living_entity_matching_selector() {
211 let (_world, pig_hunter, nearer_pig, _farther_cow) =
212 animal_fixture("nearest_attackable_selector");
213 let mut goal =
214 NearestAttackableTargetGoal::new_with_interval(0, false, false, |target, _| {
215 target.as_animal().is_some()
216 });
217
218 assert!(goal.can_use(pig_hunter.as_ref()));
219 goal.start(pig_hunter.as_ref());
220
221 let Some(target) = pig_hunter.target() else {
222 panic!("selector goal should assign a target");
223 };
224 assert_eq!(target.uuid(), nearer_pig.uuid());
225 }
226
227 #[test]
228 fn selector_can_reject_all_candidates() {
229 let (_world, pig_hunter, _nearer_pig, _farther_cow) =
230 animal_fixture("nearest_attackable_selector_rejects");
231 let mut goal = NearestAttackableTargetGoal::new(false, |_, _| false);
232
233 assert!(!goal.can_use(pig_hunter.as_ref()));
234 }
235}