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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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: &dyn PathfinderMob,
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::<f64>() * horizontal_dist / 2.0;
206 } else {
207 xt += rand::random::<f64>() * horizontal_dist / 2.0;
208 }
209
210 if position.z > f64::from(center.z()) {
211 zt -= rand::random::<f64>() * horizontal_dist / 2.0;
212 } else {
213 zt += rand::random::<f64>() * 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(mob: &dyn PathfinderMob, pos: BlockPos) -> Option<BlockPos> {
225 let pos = move_up_out_of_solid(mob, pos)?;
226 if !is_water(mob, pos) && !has_malus(mob, pos) {
227 Some(pos)
228 } else {
229 None
230 }
231}
232
233fn move_up_out_of_solid(mob: &dyn PathfinderMob, pos: BlockPos) -> Option<BlockPos> {
234 if !is_solid(mob, pos) {
235 return Some(pos);
236 }
237
238 let world = mob.level()?;
239 let mut pos = pos.above();
240 while pos.y() <= world.get_max_y() && is_solid(mob, pos) {
241 pos = pos.above();
242 }
243 Some(pos)
244}
245
246fn mob_restricted(mob: &dyn PathfinderMob, horizontal_dist: f64) -> bool {
247 mob.has_home()
248 && block_center_distance_sqr(mob.home_position(), mob.position())
249 < (f64::from(mob.home_radius()) + horizontal_dist + 1.0).powi(2)
250}
251
252fn is_outside_limits(mob: &dyn PathfinderMob, pos: BlockPos) -> bool {
253 mob.level()
254 .is_none_or(|world| world.is_outside_build_height(pos.y()))
255}
256
257fn is_restricted(restrict: bool, mob: &dyn PathfinderMob, pos: BlockPos) -> bool {
258 restrict && !mob.is_within_home_pos(pos)
259}
260
261fn is_water(mob: &dyn PathfinderMob, pos: BlockPos) -> bool {
262 mob.level()
263 .is_none_or(|world| world.get_block_state(pos).get_fluid_state().is_water())
264}
265
266fn has_malus(mob: &dyn PathfinderMob, pos: BlockPos) -> bool {
267 let Some(world) = mob.level() else {
268 return true;
269 };
270 let mut context = PathfindingContext::new(world.as_ref(), mob.block_position());
271 let path_type = WalkPathEvaluator::path_type_static(&mut context, pos);
272 mob.get_pathfinding_malus(path_type) != 0.0
273}
274
275fn is_solid(mob: &dyn PathfinderMob, pos: BlockPos) -> bool {
276 mob.level()
277 .is_some_and(|world| world.get_block_state(pos).is_solid())
278}
279
280fn block_bottom_center(pos: BlockPos) -> DVec3 {
281 let (x, y, z) = pos.get_bottom_center();
282 DVec3::new(x, y, z)
283}
284
285fn block_center_distance_sqr(pos: BlockPos, target: DVec3) -> f64 {
286 let (x, y, z) = pos.get_center();
287 DVec3::new(x, y, z).distance_squared(target)
288}
289
290fn lerp(delta: f64, start: f64, end: f64) -> f64 {
291 start + delta * (end - start)
292}
293
294#[cfg(test)]
295mod tests {
296 use steel_utils::random::legacy_random::LegacyRandom;
297
298 use super::*;
299
300 #[test]
301 fn random_direction_within_radians_matches_vanilla_seed() {
302 let mut random = LegacyRandom::from_seed(0);
303
304 let direction = generate_random_direction_within_radians(
305 &mut random,
306 0.0,
307 16.0,
308 7,
309 0,
310 1.0,
311 0.0,
312 FRAC_PI_2,
313 );
314
315 assert_eq!(direction, Some(BlockPos::new(15, -5, 13)));
316 }
317
318 #[test]
319 fn random_direction_within_radians_points_away_from_avoid_pos() {
320 let mut random = LegacyRandom::from_seed(0);
321
322 let direction = generate_random_direction_within_radians(
323 &mut random,
324 0.0,
325 16.0,
326 7,
327 0,
328 -1.0,
329 0.0,
330 FRAC_PI_2,
331 );
332
333 assert_eq!(direction, Some(BlockPos::new(-16, -5, -14)));
334 }
335
336 #[test]
337 fn random_direction_within_radians_rejects_points_outside_square() {
338 let mut random = LegacyRandom::from_seed(0);
339
340 let direction =
341 generate_random_direction_within_radians(&mut random, 1.0, 1.0, 0, 0, 1.0, 0.0, 0.0);
342
343 assert_eq!(direction, None);
344 }
345}