steel_core/entity/ai/goal/
avoid_entity.rs1use 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}