Skip to main content

steel_core/entity/ai/goal/
leap_at_target.rs

1use glam::DVec3;
2
3use super::reduced_tick_delay;
4use super::selector::{Goal, GoalControls};
5use crate::entity::{PathfinderMob, SharedEntity};
6
7const MIN_LEAP_DISTANCE_SQR: f64 = 4.0;
8const MAX_LEAP_DISTANCE_SQR: f64 = 16.0;
9const HORIZONTAL_TARGET_EPSILON_SQR: f64 = 1.0e-7;
10const HORIZONTAL_LEAP_SCALE: f64 = 0.4;
11const EXISTING_MOMENTUM_SCALE: f64 = 0.2;
12const LEAP_CHANCE_TICKS: i32 = 5;
13
14pub struct LeapAtTargetGoal {
15    target: Option<SharedEntity>,
16    yd: f32,
17}
18
19impl LeapAtTargetGoal {
20    #[must_use]
21    pub(crate) const fn new(yd: f32) -> Self {
22        Self { target: None, yd }
23    }
24}
25
26impl Goal for LeapAtTargetGoal {
27    fn controls(&self) -> GoalControls {
28        GoalControls::JUMP | GoalControls::MOVE
29    }
30
31    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
32        if mob.has_controlling_passenger() {
33            return false;
34        }
35
36        let Some(target) = mob.target() else {
37            return false;
38        };
39
40        let distance_sqr = mob.position().distance_squared(target.position());
41        if !(MIN_LEAP_DISTANCE_SQR..=MAX_LEAP_DISTANCE_SQR).contains(&distance_sqr) {
42            return false;
43        }
44
45        if !mob.on_ground() {
46            return false;
47        }
48
49        if rand::random_range(0..reduced_tick_delay(LEAP_CHANCE_TICKS)) != 0 {
50            return false;
51        }
52
53        self.target = Some(target);
54        true
55    }
56
57    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
58        !mob.on_ground()
59    }
60
61    fn start(&mut self, mob: &dyn PathfinderMob) {
62        let Some(target) = &self.target else {
63            return;
64        };
65
66        let movement = mob.velocity();
67        let mut delta = target.position() - mob.position();
68        delta.y = 0.0;
69        if delta.length_squared() > HORIZONTAL_TARGET_EPSILON_SQR {
70            delta = delta.normalize() * HORIZONTAL_LEAP_SCALE + movement * EXISTING_MOMENTUM_SCALE;
71        }
72
73        mob.set_velocity(DVec3::new(delta.x, f64::from(self.yd), delta.z));
74    }
75
76    fn stop(&mut self, _mob: &dyn PathfinderMob) {
77        self.target = None;
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::sync::{Arc, Weak};
84
85    use glam::DVec3;
86    use steel_registry::{init_vanilla_registry, vanilla_entities};
87
88    use super::*;
89    use crate::entity::{Entity, Mob, entities::PigEntity};
90
91    fn pig(id: i32, position: DVec3) -> PigEntity {
92        PigEntity::new(&vanilla_entities::PIG, id, position, Weak::new())
93    }
94
95    fn shared_pig(id: i32, position: DVec3) -> SharedEntity {
96        Arc::new(pig(id, position))
97    }
98
99    fn set_target(mob: &PigEntity, target: &SharedEntity) {
100        assert!(mob.set_target(Some(target)));
101    }
102
103    fn assert_vec3_close(left: DVec3, right: DVec3) {
104        assert!(
105            (left - right).abs().cmple(DVec3::splat(1.0e-12)).all(),
106            "expected {left:?} to be close to {right:?}"
107        );
108    }
109
110    #[test]
111    fn leap_at_target_goal_uses_jump_and_move_controls() {
112        let goal = LeapAtTargetGoal::new(0.4);
113
114        assert_eq!(goal.controls(), GoalControls::JUMP | GoalControls::MOVE);
115    }
116
117    #[test]
118    fn leap_at_target_goal_requires_target() {
119        init_vanilla_registry();
120        let mut goal = LeapAtTargetGoal::new(0.4);
121        let mob = pig(1, DVec3::ZERO);
122        mob.base().set_on_ground(true);
123
124        assert!(!goal.can_use(&mob));
125    }
126
127    #[test]
128    fn leap_at_target_goal_uses_vanilla_distance_window() {
129        init_vanilla_registry();
130        let mut goal = LeapAtTargetGoal::new(0.4);
131        let mob = pig(1, DVec3::ZERO);
132        mob.base().set_on_ground(true);
133
134        let close_target = shared_pig(2, DVec3::new(1.0, 0.0, 0.0));
135        set_target(&mob, &close_target);
136        assert!(!goal.can_use(&mob));
137
138        let far_target = shared_pig(3, DVec3::new(5.0, 0.0, 0.0));
139        set_target(&mob, &far_target);
140        assert!(!goal.can_use(&mob));
141    }
142
143    #[test]
144    fn leap_at_target_goal_requires_ground() {
145        init_vanilla_registry();
146        let mut goal = LeapAtTargetGoal::new(0.4);
147        let mob = pig(1, DVec3::ZERO);
148        let target = shared_pig(2, DVec3::new(2.0, 0.0, 0.0));
149        set_target(&mob, &target);
150
151        assert!(!goal.can_use(&mob));
152    }
153
154    #[test]
155    fn leap_at_target_goal_applies_vanilla_leap_velocity() {
156        init_vanilla_registry();
157        let mut goal = LeapAtTargetGoal::new(0.42);
158        let mob = pig(1, DVec3::ZERO);
159        mob.base().set_on_ground(true);
160        mob.set_velocity(DVec3::new(1.0, 0.0, 0.0));
161        let target = shared_pig(2, DVec3::new(4.0, 0.0, 0.0));
162        set_target(&mob, &target);
163
164        goal.target = Some(target);
165        goal.start(&mob);
166
167        assert_vec3_close(mob.velocity(), DVec3::new(0.6, f64::from(0.42_f32), 0.0));
168    }
169
170    #[test]
171    fn leap_at_target_goal_continues_until_grounded() {
172        init_vanilla_registry();
173        let mut goal = LeapAtTargetGoal::new(0.4);
174        let mob = pig(1, DVec3::ZERO);
175
176        assert!(goal.can_continue_to_use(&mob));
177
178        mob.base().set_on_ground(true);
179        assert!(!goal.can_continue_to_use(&mob));
180    }
181}