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