1use std::f64::consts::{FRAC_PI_2, SQRT_2};
2
3use glam::DVec3;
4use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_utils::BlockPos;
6use steel_utils::random::{Random, legacy_random::LegacyRandom};
7
8use crate::entity::PathfinderMob;
9use crate::entity::ai::path::PathfindingContext;
10use crate::entity::ai::walk::WalkPathEvaluator;
11use crate::fluid::FluidStateExt as _;
12
13const RANDOM_POS_ATTEMPTS: usize = 10;
14
15pub(super) fn default_random_pos(
16 mob: &(impl PathfinderMob + ?Sized),
17 horizontal_dist: i32,
18 vertical_dist: i32,
19) -> Option<DVec3> {
20 let restrict = mob_restricted(mob, f64::from(horizontal_dist));
21 generate_random_pos(mob, || {
22 let direction = generate_random_direction(mob, horizontal_dist, vertical_dist);
23 default_random_pos_toward_direction(mob, f64::from(horizontal_dist), restrict, direction)
24 })
25}
26
27pub(super) fn default_random_pos_towards(
28 mob: &(impl PathfinderMob + ?Sized),
29 horizontal_dist: i32,
30 vertical_dist: i32,
31 towards_pos: DVec3,
32 max_xz_radians_from_dir: f64,
33) -> Option<DVec3> {
34 let dir = towards_pos - mob.position();
35 let restrict = mob_restricted(mob, f64::from(horizontal_dist));
36 generate_random_pos(mob, || {
37 let direction = {
38 let mut random = LegacyRandom::from_seed(rand::random());
39 generate_random_direction_within_radians(
40 &mut random,
41 0.0,
42 f64::from(horizontal_dist),
43 vertical_dist,
44 0,
45 dir.x,
46 dir.z,
47 max_xz_radians_from_dir,
48 )
49 }?;
50 default_random_pos_toward_direction(mob, f64::from(horizontal_dist), restrict, direction)
51 })
52}
53
54pub(super) fn default_random_pos_away(
55 mob: &(impl PathfinderMob + ?Sized),
56 horizontal_dist: i32,
57 vertical_dist: i32,
58 avoid_pos: DVec3,
59) -> Option<DVec3> {
60 let dir_away = mob.position() - avoid_pos;
61 let restrict = mob_restricted(mob, f64::from(horizontal_dist));
62 generate_random_pos(mob, || {
63 let direction = {
64 let mut random = LegacyRandom::from_seed(rand::random());
65 generate_random_direction_within_radians(
66 &mut random,
67 0.0,
68 f64::from(horizontal_dist),
69 vertical_dist,
70 0,
71 dir_away.x,
72 dir_away.z,
73 FRAC_PI_2,
74 )
75 }?;
76 default_random_pos_toward_direction(mob, f64::from(horizontal_dist), restrict, direction)
77 })
78}
79
80pub(super) fn land_random_pos(
81 mob: &(impl PathfinderMob + ?Sized),
82 horizontal_dist: i32,
83 vertical_dist: i32,
84) -> Option<DVec3> {
85 let restrict = mob_restricted(mob, f64::from(horizontal_dist));
86 generate_random_pos(mob, || {
87 let direction = generate_random_direction(mob, horizontal_dist, vertical_dist);
88 let pos =
89 land_random_pos_toward_direction(mob, f64::from(horizontal_dist), restrict, direction)?;
90 land_move_pos_up_out_of_solid(mob, pos)
91 })
92}
93
94fn generate_random_pos(
95 mob: &(impl PathfinderMob + ?Sized),
96 mut pos_supplier: impl FnMut() -> Option<BlockPos>,
97) -> Option<DVec3> {
98 let mut best_weight = f32::NEG_INFINITY;
99 let mut best_pos = None;
100
101 for _ in 0..RANDOM_POS_ATTEMPTS {
102 let Some(pos) = pos_supplier() else {
103 continue;
104 };
105 let value = mob.get_walk_target_value(pos);
106 if value > best_weight {
107 best_weight = value;
108 best_pos = Some(pos);
109 }
110 }
111
112 best_pos.map(block_bottom_center)
113}
114
115fn generate_random_direction(
116 _mob: &(impl PathfinderMob + ?Sized),
117 horizontal_dist: i32,
118 vertical_dist: i32,
119) -> BlockPos {
120 BlockPos::new(
121 rand::random_range(0..(2 * horizontal_dist + 1)) - horizontal_dist,
122 rand::random_range(0..(2 * vertical_dist + 1)) - vertical_dist,
123 rand::random_range(0..(2 * horizontal_dist + 1)) - horizontal_dist,
124 )
125}
126
127#[expect(
128 clippy::too_many_arguments,
129 reason = "vanilla random direction helper takes these values independently"
130)]
131fn generate_random_direction_within_radians(
132 random: &mut impl Random,
133 min_horizontal_dist: f64,
134 max_horizontal_dist: f64,
135 vertical_dist: i32,
136 flying_height: i32,
137 x_dir: f64,
138 z_dir: f64,
139 max_xz_radians_from_dir: f64,
140) -> Option<BlockPos> {
141 let y_radians_center = z_dir.atan2(x_dir) - FRAC_PI_2;
142 let y_radians =
143 y_radians_center + (2.0 * f64::from(random.next_f32()) - 1.0) * max_xz_radians_from_dir;
144 let dist = lerp(
145 random.next_f64().sqrt(),
146 min_horizontal_dist,
147 max_horizontal_dist,
148 ) * SQRT_2;
149 let xt = -dist * y_radians.sin();
150 let zt = dist * y_radians.cos();
151 if xt.abs() > max_horizontal_dist || zt.abs() > max_horizontal_dist {
152 return None;
153 }
154
155 let yt = random.next_i32_bounded(2 * vertical_dist + 1) - vertical_dist + flying_height;
156 Some(BlockPos::containing(xt, f64::from(yt), zt))
157}
158
159fn default_random_pos_toward_direction(
160 mob: &(impl PathfinderMob + ?Sized),
161 horizontal_dist: f64,
162 restrict: bool,
163 direction: BlockPos,
164) -> Option<BlockPos> {
165 let pos = generate_random_pos_toward_direction(mob, horizontal_dist, direction);
166 if !is_outside_limits(mob, pos)
167 && !is_restricted(restrict, mob, pos)
168 && mob.is_stable_destination(pos)
169 && !has_malus(mob, pos)
170 {
171 Some(pos)
172 } else {
173 None
174 }
175}
176
177fn land_random_pos_toward_direction(
178 mob: &(impl PathfinderMob + ?Sized),
179 horizontal_dist: f64,
180 restrict: bool,
181 direction: BlockPos,
182) -> Option<BlockPos> {
183 let pos = generate_random_pos_toward_direction(mob, horizontal_dist, direction);
184 if !is_outside_limits(mob, pos)
185 && !is_restricted(restrict, mob, pos)
186 && mob.is_stable_destination(pos)
187 {
188 Some(pos)
189 } else {
190 None
191 }
192}
193
194fn generate_random_pos_toward_direction(
195 mob: &(impl PathfinderMob + ?Sized),
196 horizontal_dist: f64,
197 direction: BlockPos,
198) -> BlockPos {
199 let mut xt = f64::from(direction.x());
200 let mut zt = f64::from(direction.z());
201 let position = mob.position();
202 if mob.has_home() && horizontal_dist > 1.0 {
203 let center = mob.home_position();
204 if position.x > f64::from(center.x()) {
205 xt -= rand::random_range(0.0..horizontal_dist / 2.0);
206 } else {
207 xt += rand::random_range(0.0..horizontal_dist / 2.0);
208 }
209
210 if position.z > f64::from(center.z()) {
211 zt -= rand::random_range(0.0..horizontal_dist / 2.0);
212 } else {
213 zt += rand::random_range(0.0..horizontal_dist / 2.0);
214 }
215 }
216
217 BlockPos::containing(
218 xt + position.x,
219 f64::from(direction.y()) + position.y,
220 zt + position.z,
221 )
222}
223
224fn land_move_pos_up_out_of_solid(
225 mob: &(impl PathfinderMob + ?Sized),
226 pos: BlockPos,
227) -> Option<BlockPos> {
228 let pos = move_up_out_of_solid(mob, pos)?;
229 if !is_water(mob, pos) && !has_malus(mob, pos) {
230 Some(pos)
231 } else {
232 None
233 }
234}
235
236fn move_up_out_of_solid(mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> Option<BlockPos> {
237 if !is_solid(mob, pos) {
238 return Some(pos);
239 }
240
241 let world = mob.level()?;
242 let mut pos = pos.above();
243 while pos.y() <= world.get_max_y() && is_solid(mob, pos) {
244 pos = pos.above();
245 }
246 Some(pos)
247}
248
249fn mob_restricted(mob: &(impl PathfinderMob + ?Sized), horizontal_dist: f64) -> bool {
250 mob.has_home()
251 && block_center_distance_sqr(mob.home_position(), mob.position())
252 < (f64::from(mob.home_radius()) + horizontal_dist + 1.0).powi(2)
253}
254
255fn is_outside_limits(mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> bool {
256 mob.level()
257 .is_none_or(|world| world.is_outside_build_height(pos.y()))
258}
259
260fn is_restricted(restrict: bool, mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> bool {
261 restrict && !mob.is_within_home_pos(pos)
262}
263
264fn is_water(mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> bool {
265 mob.level()
266 .is_none_or(|world| world.get_block_state(pos).get_fluid_state().is_water())
267}
268
269fn has_malus(mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> bool {
270 let Some(world) = mob.level() else {
271 return true;
272 };
273 let mut context = PathfindingContext::new(world.as_ref(), mob.block_position());
274 let path_type = WalkPathEvaluator::path_type_static(&mut context, pos);
275 mob.get_pathfinding_malus(path_type) != 0.0
276}
277
278fn is_solid(mob: &(impl PathfinderMob + ?Sized), pos: BlockPos) -> bool {
279 mob.level()
280 .is_some_and(|world| world.get_block_state(pos).is_solid())
281}
282
283fn block_bottom_center(pos: BlockPos) -> DVec3 {
284 let (x, y, z) = pos.get_bottom_center();
285 DVec3::new(x, y, z)
286}
287
288fn block_center_distance_sqr(pos: BlockPos, target: DVec3) -> f64 {
289 let (x, y, z) = pos.get_center();
290 DVec3::new(x, y, z).distance_squared(target)
291}
292
293fn lerp(delta: f64, start: f64, end: f64) -> f64 {
294 start + delta * (end - start)
295}
296
297#[cfg(test)]
298mod tests {
299 use steel_utils::random::legacy_random::LegacyRandom;
300
301 use super::*;
302
303 #[test]
304 fn random_direction_within_radians_matches_vanilla_seed() {
305 let mut random = LegacyRandom::from_seed(0);
306
307 let direction = generate_random_direction_within_radians(
308 &mut random,
309 0.0,
310 16.0,
311 7,
312 0,
313 1.0,
314 0.0,
315 FRAC_PI_2,
316 );
317
318 assert_eq!(direction, Some(BlockPos::new(15, -5, 13)));
319 }
320
321 #[test]
322 fn random_direction_within_radians_points_away_from_avoid_pos() {
323 let mut random = LegacyRandom::from_seed(0);
324
325 let direction = generate_random_direction_within_radians(
326 &mut random,
327 0.0,
328 16.0,
329 7,
330 0,
331 -1.0,
332 0.0,
333 FRAC_PI_2,
334 );
335
336 assert_eq!(direction, Some(BlockPos::new(-16, -5, -14)));
337 }
338
339 #[test]
340 fn random_direction_within_radians_rejects_points_outside_square() {
341 let mut random = LegacyRandom::from_seed(0);
342
343 let direction =
344 generate_random_direction_within_radians(&mut random, 1.0, 1.0, 0, 0, 1.0, 0.0, 0.0);
345
346 assert_eq!(direction, None);
347 }
348}