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::{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}