Skip to main content

steel_core/entity/ai/goal/
nearest_attackable_target.rs

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