Skip to main content

steel_core/entity/ai/goal/
flee_sun.rs

1use glam::DVec3;
2use steel_utils::BlockPos;
3
4use super::selector::{Goal, GoalControls};
5use crate::entity::PathfinderMob;
6use crate::inventory::equipment::EquipmentSlot;
7use crate::world::World;
8
9const HIDE_POS_ATTEMPTS: usize = 10;
10
11pub struct FleeSunGoal {
12    wanted_position: Option<DVec3>,
13    speed_modifier: f64,
14}
15
16impl FleeSunGoal {
17    #[must_use]
18    pub(crate) const fn new(speed_modifier: f64) -> Self {
19        Self {
20            wanted_position: None,
21            speed_modifier,
22        }
23    }
24
25    fn set_wanted_pos(&mut self, mob: &dyn PathfinderMob, level: &World) -> bool {
26        let Some(pos) = get_hide_pos(mob, level) else {
27            return false;
28        };
29
30        self.wanted_position = Some(pos);
31        true
32    }
33}
34
35impl Goal for FleeSunGoal {
36    fn controls(&self) -> GoalControls {
37        GoalControls::MOVE
38    }
39
40    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
41        if mob.target().is_some() {
42            return false;
43        }
44
45        let Some(level) = mob.level() else {
46            return false;
47        };
48
49        if !level.is_bright_outside() {
50            return false;
51        }
52        if !mob.is_on_fire() {
53            return false;
54        }
55        if !level.can_see_sky(mob.block_position()) {
56            return false;
57        }
58        if mob.has_item_in_slot(EquipmentSlot::Head) {
59            return false;
60        }
61
62        self.set_wanted_pos(mob, &level)
63    }
64
65    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
66        !mob.mob_base().navigation().lock().is_done()
67    }
68
69    fn start(&mut self, mob: &dyn PathfinderMob) {
70        let Some(wanted_position) = self.wanted_position else {
71            return;
72        };
73
74        mob.move_to_pos(wanted_position, self.speed_modifier);
75    }
76}
77
78fn get_hide_pos(mob: &dyn PathfinderMob, level: &World) -> Option<DVec3> {
79    get_hide_pos_with(
80        mob,
81        |pos| level.can_see_sky(pos),
82        |pos| mob.get_walk_target_value(pos),
83    )
84}
85
86fn get_hide_pos_with(
87    mob: &dyn PathfinderMob,
88    mut can_see_sky: impl FnMut(BlockPos) -> bool,
89    mut walk_target_value: impl FnMut(BlockPos) -> f32,
90) -> Option<DVec3> {
91    let pos = mob.block_position();
92
93    for _ in 0..HIDE_POS_ATTEMPTS {
94        let random_pos = {
95            pos.offset(
96                rand::random_range(0..20) - 10,
97                rand::random_range(0..6) - 3,
98                rand::random_range(0..20) - 10,
99            )
100        };
101        if !can_see_sky(random_pos) && walk_target_value(random_pos) < 0.0 {
102            let (x, y, z) = random_pos.get_bottom_center();
103            return Some(DVec3::new(x, y, z));
104        }
105    }
106
107    None
108}
109
110#[cfg(test)]
111mod tests {
112    use std::sync::Weak;
113
114    use glam::DVec3;
115    use steel_registry::{init_vanilla_registry, vanilla_entities};
116
117    use super::*;
118    use crate::entity::entities::PigEntity;
119
120    fn pig() -> PigEntity {
121        PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new())
122    }
123
124    #[test]
125    fn flee_sun_goal_uses_move_control() {
126        let goal = FleeSunGoal::new(1.0);
127
128        assert_eq!(goal.controls(), GoalControls::MOVE);
129    }
130
131    #[test]
132    fn flee_sun_goal_requires_world() {
133        init_vanilla_registry();
134        let mut goal = FleeSunGoal::new(1.0);
135
136        assert!(!goal.can_use(&pig()));
137    }
138
139    #[test]
140    fn flee_sun_hide_pos_accepts_sheltered_negative_walk_target() {
141        init_vanilla_registry();
142        let mob = pig();
143        let mut visited = Vec::new();
144
145        let pos = get_hide_pos_with(
146            &mob,
147            |pos| {
148                visited.push(pos);
149                false
150            },
151            |_| -1.0,
152        );
153
154        assert_eq!(visited.len(), 1);
155        let (x, y, z) = visited[0].get_bottom_center();
156        assert_eq!(pos, Some(DVec3::new(x, y, z)));
157    }
158
159    #[test]
160    fn flee_sun_hide_pos_rejects_sky_exposed_positions() {
161        init_vanilla_registry();
162        let mob = pig();
163        let mut visited = Vec::new();
164
165        let pos = get_hide_pos_with(
166            &mob,
167            |pos| {
168                visited.push(pos);
169                true
170            },
171            |_| -1.0,
172        );
173
174        assert!(pos.is_none());
175        assert_eq!(visited.len(), HIDE_POS_ATTEMPTS);
176    }
177
178    #[test]
179    fn flee_sun_hide_pos_rejects_non_negative_walk_targets() {
180        init_vanilla_registry();
181        let mob = pig();
182
183        assert!(get_hide_pos_with(&mob, |_| false, |_| 0.0).is_none());
184    }
185}