steel_core/entity/ai/goal/
random_swimming.rs1use glam::DVec3;
2use steel_utils::BlockPos;
3
4use super::random_pos::default_random_pos;
5use super::random_stroll::RandomStrollGoal;
6use super::selector::{Goal, GoalControls};
7use crate::behavior::BlockStateBehaviorExt as _;
8use crate::entity::PathfinderMob;
9use crate::entity::ai::path::PathComputationType;
10
11const RANDOM_SWIMMING_HORIZONTAL_RANGE: i32 = 10;
12const RANDOM_SWIMMING_VERTICAL_RANGE: i32 = 7;
13const RANDOM_SWIMMING_RETRIES: i32 = 10;
14
15pub struct RandomSwimmingGoal {
16 stroll: RandomStrollGoal,
17}
18
19impl RandomSwimmingGoal {
20 #[must_use]
21 pub(crate) const fn new(speed_modifier: f64, interval: i32) -> Self {
22 Self {
23 stroll: RandomStrollGoal::with_interval(speed_modifier, interval),
24 }
25 }
26}
27
28impl Goal for RandomSwimmingGoal {
29 fn controls(&self) -> GoalControls {
30 self.stroll.controls()
31 }
32
33 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
34 self.stroll.can_use_with_position(mob, random_swimmable_pos)
35 }
36
37 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
38 self.stroll.can_continue_to_use(mob)
39 }
40
41 fn start(&mut self, mob: &dyn PathfinderMob) {
42 self.stroll.start(mob);
43 }
44
45 fn stop(&mut self, mob: &dyn PathfinderMob) {
46 self.stroll.stop(mob);
47 }
48}
49
50fn random_swimmable_pos(mob: &dyn PathfinderMob) -> Option<DVec3> {
51 retry_random_swimmable_pos(
52 default_random_pos(
53 mob,
54 RANDOM_SWIMMING_HORIZONTAL_RANGE,
55 RANDOM_SWIMMING_VERTICAL_RANGE,
56 ),
57 || {
58 default_random_pos(
59 mob,
60 RANDOM_SWIMMING_HORIZONTAL_RANGE,
61 RANDOM_SWIMMING_VERTICAL_RANGE,
62 )
63 },
64 |pos| is_water_pathfindable(mob, pos),
65 )
66}
67
68fn retry_random_swimmable_pos(
69 mut target_pos: Option<DVec3>,
70 mut next_pos: impl FnMut() -> Option<DVec3>,
71 mut is_water_pathfindable: impl FnMut(DVec3) -> bool,
72) -> Option<DVec3> {
73 let mut count = 0;
74 loop {
75 let pos = target_pos?;
76 if is_water_pathfindable(pos) || count >= RANDOM_SWIMMING_RETRIES {
77 return Some(pos);
78 }
79
80 count += 1;
81 target_pos = next_pos();
82 }
83}
84
85fn is_water_pathfindable(mob: &dyn PathfinderMob, pos: DVec3) -> bool {
86 let Some(world) = mob.level() else {
87 return false;
88 };
89 world
90 .get_block_state(BlockPos::containing(pos.x, pos.y, pos.z))
91 .is_pathfindable(PathComputationType::Water)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn random_swimming_goal_uses_random_stroll_controls() {
100 let goal = RandomSwimmingGoal::new(1.0, 120);
101
102 assert_eq!(goal.controls(), GoalControls::MOVE);
103 }
104
105 #[test]
106 fn random_swimmable_retry_accepts_first_pathfindable_pos() {
107 let result = retry_random_swimmable_pos(Some(DVec3::new(1.0, 2.0, 3.0)), || None, |_| true);
108
109 assert_eq!(result, Some(DVec3::new(1.0, 2.0, 3.0)));
110 }
111
112 #[test]
113 fn random_swimmable_retry_uses_next_candidate_until_pathfindable() {
114 let mut next_x = 1.0;
115 let result = retry_random_swimmable_pos(
116 Some(DVec3::ZERO),
117 || {
118 let pos = DVec3::new(next_x, 0.0, 0.0);
119 next_x += 1.0;
120 Some(pos)
121 },
122 |pos| pos.x.to_bits() == 3.0_f64.to_bits(),
123 );
124
125 assert_eq!(result, Some(DVec3::new(3.0, 0.0, 0.0)));
126 }
127
128 #[test]
129 fn random_swimmable_retry_returns_last_candidate_after_vanilla_retry_limit() {
130 let mut next_x = 1.0;
131 let result = retry_random_swimmable_pos(
132 Some(DVec3::ZERO),
133 || {
134 let pos = DVec3::new(next_x, 0.0, 0.0);
135 next_x += 1.0;
136 Some(pos)
137 },
138 |_| false,
139 );
140
141 assert_eq!(result, Some(DVec3::new(10.0, 0.0, 0.0)));
142 }
143}