Skip to main content

steel_core/entity/ai/goal/
random_pos.rs

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}