steel_core/entity/ai/goal/
flee_sun.rs1use 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}