steel_core/entity/ai/goal/
tempt_goal.rs1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_registry::item_stack::ItemStack;
5use steel_registry::vanilla_attributes;
6
7use super::reduced_tick_delay;
8use super::selector::{Goal, GoalControls};
9use crate::entity::ai::targeting::TargetingConditions;
10use crate::entity::{Entity, LivingEntity, PathfinderMob};
11use crate::player::Player;
12
13const DEFAULT_STOP_DISTANCE: f64 = 2.5;
14
15type TemptItemPredicate = Box<dyn Fn(&ItemStack) -> bool + Send + Sync>;
16
17pub struct TemptGoal {
18 player: Option<Arc<Player>>,
19 player_position: DVec3,
20 player_yaw: f32,
21 player_pitch: f32,
22 speed_modifier: f64,
23 calm_down: i32,
24 is_running: bool,
25 items: TemptItemPredicate,
26 can_scare: bool,
27 stop_distance: f64,
28}
29
30impl TemptGoal {
31 #[must_use]
32 pub(crate) fn new(
33 speed_modifier: f64,
34 items: impl Fn(&ItemStack) -> bool + Send + Sync + 'static,
35 can_scare: bool,
36 ) -> Self {
37 Self::with_stop_distance(speed_modifier, items, can_scare, DEFAULT_STOP_DISTANCE)
38 }
39
40 #[must_use]
41 pub(crate) fn with_stop_distance(
42 speed_modifier: f64,
43 items: impl Fn(&ItemStack) -> bool + Send + Sync + 'static,
44 can_scare: bool,
45 stop_distance: f64,
46 ) -> Self {
47 Self {
48 player: None,
49 player_position: DVec3::ZERO,
50 player_yaw: 0.0,
51 player_pitch: 0.0,
52 speed_modifier,
53 calm_down: 0,
54 is_running: false,
55 items: Box::new(items),
56 can_scare,
57 stop_distance,
58 }
59 }
60
61 #[must_use]
62 pub const fn is_running(&self) -> bool {
63 self.is_running
64 }
65
66 fn should_follow(&self, player: &dyn LivingEntity) -> bool {
67 player.is_holding(&mut |item_stack| (self.items)(item_stack))
68 }
69
70 const fn can_scare(&self) -> bool {
71 self.can_scare
72 }
73
74 const fn targeting_conditions(range: f64) -> TargetingConditions {
75 TargetingConditions::for_non_combat()
76 .ignore_line_of_sight()
77 .range(range)
78 }
79
80 fn update_player_scare_state(&mut self, mob: &dyn PathfinderMob) -> bool {
81 let Some(player) = &self.player else {
82 return false;
83 };
84
85 if mob.position().distance_squared(player.position()) < 36.0 {
86 if player.position().distance_squared(self.player_position) > 0.01 {
87 return false;
88 }
89
90 let (yaw, pitch) = player.rotation();
91 if (pitch - self.player_pitch).abs() > 5.0 || (yaw - self.player_yaw).abs() > 5.0 {
92 return false;
93 }
94 } else {
95 self.player_position = player.position();
96 }
97
98 let (yaw, pitch) = player.rotation();
99 self.player_yaw = yaw;
100 self.player_pitch = pitch;
101 true
102 }
103}
104
105impl Goal for TemptGoal {
106 fn controls(&self) -> GoalControls {
107 GoalControls::MOVE | GoalControls::LOOK
108 }
109
110 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
111 if self.calm_down > 0 {
112 self.calm_down -= 1;
113 return false;
114 }
115
116 let Some(world) = mob.level() else {
117 return false;
118 };
119 let range = mob
120 .attributes()
121 .lock()
122 .required_value(vanilla_attributes::TEMPT_RANGE);
123 let targeting_conditions = Self::targeting_conditions(range);
124 self.player = world.nearest_player(mob.position(), range, |player| {
125 targeting_conditions.test(world.as_ref(), Some(mob), player)
126 && self.should_follow(player)
127 });
128 self.player.is_some()
129 }
130
131 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
132 if self.can_scare() && !self.update_player_scare_state(mob) {
133 return false;
134 }
135
136 self.can_use(mob)
137 }
138
139 fn start(&mut self, _mob: &dyn PathfinderMob) {
140 if let Some(player) = &self.player {
141 self.player_position = player.position();
142 }
143 self.is_running = true;
144 }
145
146 fn stop(&mut self, mob: &dyn PathfinderMob) {
147 self.player = None;
148 mob.mob_base().navigation().lock().stop();
149 self.calm_down = reduced_tick_delay(100);
150 self.is_running = false;
151 }
152
153 fn tick(&mut self, mob: &dyn PathfinderMob) {
154 let Some(player) = &self.player else {
155 return;
156 };
157
158 let player_position = player.position();
159 mob.mob_base().controls().lock().look_control.set_look_at(
160 DVec3::new(player_position.x, player.get_eye_y(), player_position.z),
161 mob.max_head_y_rot() + 20.0,
162 mob.max_head_x_rot(),
163 );
164
165 if mob.position().distance_squared(player_position)
166 < self.stop_distance * self.stop_distance
167 {
168 mob.mob_base().navigation().lock().stop();
169 } else {
170 mob.move_to_pos(player_position, self.speed_modifier);
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use std::sync::Weak;
178
179 use steel_registry::item_stack::ItemStack;
180 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
181
182 use super::*;
183 use crate::entity::entities::PigEntity;
184 use crate::inventory::equipment::EquipmentSlot;
185
186 #[test]
187 fn tempt_goal_uses_move_and_look_controls() {
188 let goal = TemptGoal::new(1.2, |_| false, false);
189
190 assert_eq!(goal.controls(), GoalControls::MOVE | GoalControls::LOOK);
191 assert!(!goal.is_running());
192 }
193
194 #[test]
195 fn tempt_goal_should_follow_checks_both_hands() {
196 init_vanilla_registry();
197 let goal = TemptGoal::new(
198 1.2,
199 |item_stack| item_stack.is(&vanilla_items::CARROT),
200 false,
201 );
202 let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
203
204 assert!(!goal.should_follow(&pig));
205
206 pig.with_equipment_slot_mut(EquipmentSlot::OffHand, &mut |item_stack| {
207 *item_stack = ItemStack::new(&vanilla_items::CARROT);
208 });
209
210 assert!(goal.should_follow(&pig));
211 }
212}