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