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: &(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}