Skip to main content

steel_core/entity/ai/goal/
avoid_entity.rs

1use steel_utils::BlockPos;
2
3use super::random_pos::default_random_pos_away;
4use super::selector::{Goal, GoalControls};
5use crate::entity::ai::path::Path;
6use crate::entity::ai::targeting::TargetingConditions;
7use crate::entity::entity_selector;
8use crate::entity::{LivingEntity, PathfinderMob, SharedEntity};
9use crate::world::World;
10
11pub struct AvoidEntityGoal {
12    to_avoid: Option<SharedEntity>,
13    path: Option<Path>,
14    max_dist: f32,
15    walk_speed_modifier: f64,
16    sprint_speed_modifier: f64,
17    avoid_entity_targeting: TargetingConditions,
18}
19
20impl AvoidEntityGoal {
21    #[must_use]
22    pub(crate) fn new(max_dist: f32, walk_speed_modifier: f64, sprint_speed_modifier: f64) -> Self {
23        Self::with_selector(
24            max_dist,
25            walk_speed_modifier,
26            sprint_speed_modifier,
27            |target, _| entity_selector::no_creative_or_spectator(target.as_entity_event_source()),
28        )
29    }
30
31    #[must_use]
32    pub(crate) fn with_selector(
33        max_dist: f32,
34        walk_speed_modifier: f64,
35        sprint_speed_modifier: f64,
36        selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
37    ) -> Self {
38        Self {
39            to_avoid: None,
40            path: None,
41            max_dist,
42            walk_speed_modifier,
43            sprint_speed_modifier,
44            avoid_entity_targeting: TargetingConditions::for_combat()
45                .range(f64::from(max_dist))
46                .selector(selector),
47        }
48    }
49}
50
51impl Goal for AvoidEntityGoal {
52    fn controls(&self) -> GoalControls {
53        GoalControls::MOVE
54    }
55
56    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
57        let Some(world) = mob.level() else {
58            return false;
59        };
60
61        let search_box =
62            mob.bounding_box()
63                .inflate_xyz(f64::from(self.max_dist), 3.0, f64::from(self.max_dist));
64        let Some(to_avoid) =
65            world.nearest_entity_in_aabb_matching(&search_box, mob.position(), |entity| {
66                entity.as_living_entity().is_some_and(|living| {
67                    self.avoid_entity_targeting
68                        .test(world.as_ref(), Some(mob), living)
69                })
70            })
71        else {
72            return false;
73        };
74
75        let Some(position) = default_random_pos_away(mob, 16, 7, to_avoid.position()) else {
76            return false;
77        };
78        if to_avoid.position().distance_squared(position)
79            < to_avoid.position().distance_squared(mob.position())
80        {
81            return false;
82        }
83
84        let path = mob.create_path_to(BlockPos::containing(position.x, position.y, position.z), 0);
85        if path.is_none() {
86            return false;
87        }
88
89        self.to_avoid = Some(to_avoid);
90        self.path = path;
91        true
92    }
93
94    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
95        !mob.mob_base().navigation().lock().is_done()
96    }
97
98    fn start(&mut self, mob: &dyn PathfinderMob) {
99        mob.move_to_path(self.path.take(), self.walk_speed_modifier);
100    }
101
102    fn stop(&mut self, _mob: &dyn PathfinderMob) {
103        self.to_avoid = None;
104        self.path = None;
105    }
106
107    fn tick(&mut self, mob: &dyn PathfinderMob, _entity: &SharedEntity) {
108        let Some(to_avoid) = &self.to_avoid else {
109            return;
110        };
111
112        let speed_modifier = if mob.position().distance_squared(to_avoid.position()) < 49.0 {
113            self.sprint_speed_modifier
114        } else {
115            self.walk_speed_modifier
116        };
117        mob.mob_base()
118            .navigation()
119            .lock()
120            .set_speed_modifier(speed_modifier);
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use std::sync::{Arc, Weak};
127
128    use glam::DVec3;
129    use steel_registry::{init_vanilla_registry, vanilla_entities};
130
131    use super::*;
132    use crate::entity::{Mob, entities::PigEntity};
133
134    #[test]
135    fn avoid_entity_goal_uses_move_control() {
136        let goal = AvoidEntityGoal::new(8.0, 1.0, 1.2);
137
138        assert_eq!(goal.controls(), GoalControls::MOVE);
139    }
140
141    #[test]
142    fn avoid_entity_default_selector_allows_non_player_living_entities() {
143        init_vanilla_registry();
144        let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
145
146        assert!(entity_selector::no_creative_or_spectator(&pig));
147    }
148
149    #[test]
150    fn avoid_entity_goal_requires_world() {
151        init_vanilla_registry();
152        let mut goal = AvoidEntityGoal::new(8.0, 1.0, 1.2);
153        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
154
155        assert!(!goal.can_use(&mob));
156    }
157
158    #[test]
159    fn avoid_entity_goal_sprints_when_close_to_avoided_entity() {
160        init_vanilla_registry();
161        let mut goal = AvoidEntityGoal::new(8.0, 1.0, 1.2);
162        let mob = Arc::new(PigEntity::new(
163            &vanilla_entities::PIG,
164            1,
165            DVec3::ZERO,
166            Weak::new(),
167        ));
168        let mob_entity: SharedEntity = Arc::<PigEntity>::clone(&mob);
169        goal.to_avoid = Some(Arc::new(PigEntity::new(
170            &vanilla_entities::PIG,
171            2,
172            DVec3::new(2.0, 0.0, 0.0),
173            Weak::new(),
174        )));
175
176        goal.tick(mob.as_ref(), &mob_entity);
177
178        assert_eq!(
179            mob.mob_base()
180                .navigation()
181                .lock()
182                .speed_modifier()
183                .to_bits(),
184            1.2_f64.to_bits()
185        );
186    }
187
188    #[test]
189    fn avoid_entity_goal_walks_when_far_from_avoided_entity() {
190        init_vanilla_registry();
191        let mut goal = AvoidEntityGoal::new(8.0, 1.0, 1.2);
192        let mob = Arc::new(PigEntity::new(
193            &vanilla_entities::PIG,
194            1,
195            DVec3::ZERO,
196            Weak::new(),
197        ));
198        let mob_entity: SharedEntity = Arc::<PigEntity>::clone(&mob);
199        goal.to_avoid = Some(Arc::new(PigEntity::new(
200            &vanilla_entities::PIG,
201            2,
202            DVec3::new(8.0, 0.0, 0.0),
203            Weak::new(),
204        )));
205
206        goal.tick(mob.as_ref(), &mob_entity);
207
208        assert_eq!(
209            mob.mob_base()
210                .navigation()
211                .lock()
212                .speed_modifier()
213                .to_bits(),
214            1.0_f64.to_bits()
215        );
216    }
217}