steel_core/entity/ai/goal/
leap_at_target.rs1use 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}