Skip to main content

steel_core/entity/ai/goal/
tempt_goal.rs

1use 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}