steel_core/entity/ai/goal/
look_at_player.rs1use glam::DVec3;
2
3use super::reduced_tick_delay;
4use crate::entity::ai::control::{DEFAULT_LOOK_X_MAX_ROT_ANGLE, DEFAULT_LOOK_Y_MAX_ROT_SPEED};
5use crate::entity::ai::goal::selector::{Goal, GoalControls};
6use crate::entity::ai::targeting::TargetingConditions;
7use crate::entity::{LivingEntity, PathfinderMob, SharedEntity};
8use crate::world::World;
9
10const DEFAULT_PROBABILITY: f32 = 0.02;
11
12type LookAtEntitySelector = Box<dyn Fn(&dyn LivingEntity, &World) -> bool + Send + Sync>;
13
14enum LookAtTargetType {
15 Player,
16 LivingEntity(LookAtEntitySelector),
17}
18
19pub struct LookAtPlayerGoal {
20 look_at: Option<SharedEntity>,
21 look_distance: f64,
22 look_time: i32,
23 probability: f32,
24 only_horizontal: bool,
25 controls: GoalControls,
26 look_at_type: LookAtTargetType,
27 look_at_context: TargetingConditions,
28}
29
30impl LookAtPlayerGoal {
31 #[must_use]
32 pub(crate) fn new(look_distance: f64) -> Self {
33 Self::new_with_probability(look_distance, DEFAULT_PROBABILITY)
34 }
35
36 #[must_use]
37 pub(crate) fn new_with_probability(look_distance: f64, probability: f32) -> Self {
38 Self::new_with_probability_and_horizontal(look_distance, probability, false)
39 }
40
41 #[must_use]
42 pub(crate) fn new_with_probability_and_horizontal(
43 look_distance: f64,
44 probability: f32,
45 only_horizontal: bool,
46 ) -> Self {
47 Self::new_for_players_with_controls(
48 look_distance,
49 probability,
50 only_horizontal,
51 GoalControls::LOOK,
52 )
53 }
54
55 #[must_use]
56 pub(crate) fn new_for_living_entities(
57 look_distance: f64,
58 probability: f32,
59 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
60 ) -> Self {
61 Self::new_for_living_entities_with_controls(
62 look_distance,
63 probability,
64 false,
65 GoalControls::LOOK,
66 selector,
67 )
68 }
69
70 #[must_use]
71 pub(super) fn new_for_players_with_controls(
72 look_distance: f64,
73 probability: f32,
74 only_horizontal: bool,
75 controls: GoalControls,
76 ) -> Self {
77 Self {
78 look_at: None,
79 look_distance,
80 look_time: 0,
81 probability,
82 only_horizontal,
83 controls,
84 look_at_type: LookAtTargetType::Player,
85 look_at_context: TargetingConditions::for_non_combat().range(look_distance),
86 }
87 }
88
89 #[must_use]
90 pub(super) fn new_for_living_entities_with_controls(
91 look_distance: f64,
92 probability: f32,
93 only_horizontal: bool,
94 controls: GoalControls,
95 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
96 ) -> Self {
97 Self {
98 look_at: None,
99 look_distance,
100 look_time: 0,
101 probability,
102 only_horizontal,
103 controls,
104 look_at_type: LookAtTargetType::LivingEntity(Box::new(selector)),
105 look_at_context: TargetingConditions::for_non_combat().range(look_distance),
106 }
107 }
108}
109
110impl Goal for LookAtPlayerGoal {
111 fn controls(&self) -> GoalControls {
112 self.controls
113 }
114
115 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
116 if rand::random::<f32>() >= self.probability {
117 return false;
118 }
119
120 let Some(world) = mob.level() else {
121 return false;
122 };
123
124 let position = mob.position();
125 let origin = DVec3::new(position.x, mob.get_eye_y(), position.z);
126 self.look_at = match &self.look_at_type {
127 LookAtTargetType::Player => world
128 .nearest_player(origin, self.look_distance, |player| {
129 !mob.has_indirect_passenger(player)
130 && self.look_at_context.test(world.as_ref(), Some(mob), player)
131 })
132 .map(|player| -> SharedEntity { player }),
133 LookAtTargetType::LivingEntity(selector) => {
134 let search_box =
135 mob.bounding_box()
136 .inflate_xyz(self.look_distance, 3.0, self.look_distance);
137 world.nearest_entity_in_aabb_matching(&search_box, origin, |entity| {
138 entity.as_living_entity().is_some_and(|living| {
139 selector(living, world.as_ref())
140 && self.look_at_context.test(world.as_ref(), Some(mob), living)
141 })
142 })
143 }
144 };
145
146 self.look_at.is_some()
147 }
148
149 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
150 let Some(look_at) = &self.look_at else {
151 return false;
152 };
153 if !look_at.is_alive() {
154 return false;
155 }
156 if mob.position().distance_squared(look_at.position())
157 > self.look_distance * self.look_distance
158 {
159 return false;
160 }
161
162 self.look_time > 0
163 }
164
165 fn start(&mut self, _mob: &dyn PathfinderMob) {
166 self.look_time = reduced_tick_delay(40 + rand::random_range(0..40));
167 }
168
169 fn stop(&mut self, _mob: &dyn PathfinderMob) {
170 self.look_at = None;
171 }
172
173 fn tick(&mut self, mob: &dyn PathfinderMob) {
174 let Some(look_at) = &self.look_at else {
175 return;
176 };
177 if !look_at.is_alive() {
178 return;
179 }
180
181 let position = look_at.position();
182 let target_y = if self.only_horizontal {
183 mob.get_eye_y()
184 } else {
185 look_at.get_eye_y()
186 };
187 mob.mob_base().controls().lock().look_control.set_look_at(
188 DVec3::new(position.x, target_y, position.z),
189 DEFAULT_LOOK_Y_MAX_ROT_SPEED,
190 DEFAULT_LOOK_X_MAX_ROT_ANGLE,
191 );
192 self.look_time -= 1;
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use std::sync::Weak;
199
200 use super::*;
201 use crate::entity::entities::PigEntity;
202 use glam::DVec3;
203 use steel_registry::{init_vanilla_registry, vanilla_entities};
204
205 #[test]
206 fn look_at_player_goal_claims_only_look_control() {
207 let goal = LookAtPlayerGoal::new(6.0);
208
209 assert_eq!(goal.controls(), GoalControls::LOOK);
210 }
211
212 #[test]
213 fn look_at_player_goal_can_claim_custom_controls_for_vanilla_subclasses() {
214 let goal = LookAtPlayerGoal::new_for_players_with_controls(
215 6.0,
216 1.0,
217 false,
218 GoalControls::LOOK | GoalControls::MOVE,
219 );
220
221 assert_eq!(goal.controls(), GoalControls::LOOK | GoalControls::MOVE);
222 }
223
224 #[test]
225 fn look_at_player_goal_supports_selector_based_living_targets() {
226 let goal = LookAtPlayerGoal::new_for_living_entities(8.0, 1.0, |living, _| {
227 living.entity_type() == &vanilla_entities::PIG
228 });
229
230 assert_eq!(goal.controls(), GoalControls::LOOK);
231 }
232
233 #[test]
234 fn look_at_player_goal_uses_vanilla_adjusted_look_time() {
235 init_vanilla_registry();
236 let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
237 let mut goal = LookAtPlayerGoal::new(6.0);
238
239 goal.start(&pig);
240
241 assert!(
242 (reduced_tick_delay(40)..=reduced_tick_delay(79)).contains(&goal.look_time),
243 "look_time was {}",
244 goal.look_time
245 );
246 }
247}