steel_core/entity/ai/goal/
door_interact.rs1use steel_registry::blocks::block_state_ext::BlockStateExt as _;
2use steel_registry::blocks::properties::BlockStateProperties;
3use steel_utils::BlockPos;
4
5use super::selector::{Goal, GoalControls};
6use crate::behavior::BLOCK_BEHAVIORS;
7use crate::entity::PathfinderMob;
8use crate::entity::ai::path::Path;
9
10const DOOR_REACH_DISTANCE_SQR: f64 = 2.25;
11const PATH_NODE_SCAN_AHEAD: usize = 2;
12
13pub(super) struct DoorInteractGoal {
14 door_pos: BlockPos,
15 has_door: bool,
16 passed: bool,
17 door_open_dir_x: f32,
18 door_open_dir_z: f32,
19}
20
21impl DoorInteractGoal {
22 #[must_use]
23 pub(super) const fn new() -> Self {
24 Self {
25 door_pos: BlockPos::ZERO,
26 has_door: false,
27 passed: false,
28 door_open_dir_x: 0.0,
29 door_open_dir_z: 0.0,
30 }
31 }
32
33 #[must_use]
34 pub(super) const fn door_pos(&self) -> BlockPos {
35 self.door_pos
36 }
37
38 #[must_use]
39 pub(super) const fn has_door(&self) -> bool {
40 self.has_door
41 }
42
43 pub(super) fn is_open(&mut self, mob: &dyn PathfinderMob) -> bool {
44 if !self.has_door {
45 return false;
46 }
47
48 let Some(world) = mob.level() else {
49 return false;
50 };
51 let state = world.get_block_state(self.door_pos);
52 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
53 if !behavior.is_wooden_door(state) {
54 self.has_door = false;
55 return false;
56 }
57
58 state.get_value(&BlockStateProperties::OPEN)
59 }
60
61 pub(super) fn set_open(&self, mob: &dyn PathfinderMob, open: bool) {
62 if !self.has_door {
63 return;
64 }
65
66 let Some(world) = mob.level() else {
67 return;
68 };
69 let state = world.get_block_state(self.door_pos);
70 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
71 behavior.set_door_open(
72 state,
73 &world,
74 self.door_pos,
75 Some(mob.as_entity_event_source()),
76 open,
77 );
78 }
79
80 pub(super) fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
81 if !mob.horizontal_collision() {
82 return false;
83 }
84
85 let Some(world) = mob.level() else {
86 return false;
87 };
88
89 let navigation = mob.mob_base().navigation().lock();
90 let Some(path) = navigation.path() else {
91 return false;
92 };
93 if path.is_done() {
94 return false;
95 }
96
97 self.find_door_in_path(mob, path, |pos| {
98 let state = world.get_block_state(pos);
99 BLOCK_BEHAVIORS
100 .get_behavior(state.get_block())
101 .is_wooden_door(state)
102 })
103 }
104
105 pub(super) const fn can_continue_to_use(&self) -> bool {
106 !self.passed
107 }
108
109 pub(super) fn start(&mut self, mob: &dyn PathfinderMob) {
110 self.passed = false;
111 self.door_open_dir_x = (f64::from(self.door_pos.x()) + 0.5 - mob.position().x) as f32;
112 self.door_open_dir_z = (f64::from(self.door_pos.z()) + 0.5 - mob.position().z) as f32;
113 }
114
115 pub(super) fn tick(&mut self, mob: &dyn PathfinderMob) {
116 let new_door_dir_x = (f64::from(self.door_pos.x()) + 0.5 - mob.position().x) as f32;
117 let new_door_dir_z = (f64::from(self.door_pos.z()) + 0.5 - mob.position().z) as f32;
118 let dot = self
119 .door_open_dir_x
120 .mul_add(new_door_dir_x, self.door_open_dir_z * new_door_dir_z);
121 if dot < 0.0 {
122 self.passed = true;
123 }
124 }
125
126 fn find_door_in_path(
127 &mut self,
128 mob: &dyn PathfinderMob,
129 path: &Path,
130 mut is_wooden_door: impl FnMut(BlockPos) -> bool,
131 ) -> bool {
132 let limit = path
133 .next_node_index()
134 .saturating_add(PATH_NODE_SCAN_AHEAD)
135 .min(path.node_count());
136 for index in 0..limit {
137 let Some(node) = path.node(index) else {
138 continue;
139 };
140 let door_pos = BlockPos::new(node.x, node.y + 1, node.z);
141 if distance_to_door_sqr(mob, door_pos) > DOOR_REACH_DISTANCE_SQR {
142 continue;
143 }
144
145 self.door_pos = door_pos;
146 self.has_door = is_wooden_door(door_pos);
147 if self.has_door {
148 return true;
149 }
150 }
151
152 self.door_pos = mob.block_position().above();
153 self.has_door = is_wooden_door(self.door_pos);
154 self.has_door
155 }
156}
157
158impl Goal for DoorInteractGoal {
159 fn controls(&self) -> GoalControls {
160 GoalControls::EMPTY
161 }
162
163 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
164 Self::can_use(self, mob)
165 }
166
167 fn can_continue_to_use(&mut self, _mob: &dyn PathfinderMob) -> bool {
168 Self::can_continue_to_use(self)
169 }
170
171 fn start(&mut self, mob: &dyn PathfinderMob) {
172 Self::start(self, mob);
173 }
174
175 fn requires_update_every_tick(&self) -> bool {
176 true
177 }
178
179 fn tick(&mut self, mob: &dyn PathfinderMob) {
180 Self::tick(self, mob);
181 }
182}
183
184fn distance_to_door_sqr(mob: &dyn PathfinderMob, door_pos: BlockPos) -> f64 {
185 let position = mob.position();
186 let dx = f64::from(door_pos.x()) - position.x;
187 let dz = f64::from(door_pos.z()) - position.z;
188 dx.mul_add(dx, dz * dz)
189}
190
191#[cfg(test)]
192mod tests {
193 use std::sync::Weak;
194
195 use glam::DVec3;
196 use steel_registry::{init_vanilla_registry, vanilla_entities};
197
198 use super::*;
199 use crate::entity::ai::node::Node;
200 use crate::entity::entities::PigEntity;
201 use crate::entity::{Entity, EntityGroundContact, EntityMovementFlags};
202
203 fn pig(position: DVec3) -> PigEntity {
204 PigEntity::new(&vanilla_entities::PIG, 1, position, Weak::new())
205 }
206
207 fn set_horizontal_collision(mob: &PigEntity) {
208 mob.base().set_movement_flags(
209 EntityMovementFlags::new().with_horizontal_collision(true),
210 EntityGroundContact::airborne(),
211 );
212 }
213
214 #[test]
215 fn door_interact_goal_claims_no_controls_like_vanilla() {
216 let goal = DoorInteractGoal::new();
217
218 assert_eq!(goal.controls(), GoalControls::EMPTY);
219 assert!(goal.requires_update_every_tick());
220 }
221
222 #[test]
223 fn door_interact_goal_requires_horizontal_collision() {
224 init_vanilla_registry();
225 let mut goal = DoorInteractGoal::new();
226 let mob = pig(DVec3::ZERO);
227
228 assert!(!goal.can_use(&mob));
229 }
230
231 #[test]
232 fn door_interact_path_scan_uses_current_node_and_one_ahead() {
233 init_vanilla_registry();
234 let mut goal = DoorInteractGoal::new();
235 let mob = pig(DVec3::ZERO);
236 let path = Path::new(
237 vec![Node::new(8, 0, 0), Node::new(1, 0, 0), Node::new(0, 0, 1)],
238 BlockPos::new(0, 0, 1),
239 true,
240 );
241
242 assert!(goal.find_door_in_path(&mob, &path, |pos| pos == BlockPos::new(1, 1, 0)));
243 assert_eq!(goal.door_pos(), BlockPos::new(1, 1, 0));
244 assert!(goal.has_door());
245 }
246
247 #[test]
248 fn door_interact_path_scan_uses_above_mob_as_fallback() {
249 init_vanilla_registry();
250 let mut goal = DoorInteractGoal::new();
251 let mob = pig(DVec3::ZERO);
252 let path = Path::new(vec![Node::new(8, 0, 0)], BlockPos::new(8, 0, 0), true);
253
254 assert!(goal.find_door_in_path(&mob, &path, |pos| pos == BlockPos::new(0, 1, 0)));
255 assert_eq!(goal.door_pos(), BlockPos::new(0, 1, 0));
256 }
257
258 #[test]
259 fn door_interact_tick_marks_passed_after_crossing_door_plane() {
260 init_vanilla_registry();
261 let mut goal = DoorInteractGoal::new();
262 let mob = pig(DVec3::ZERO);
263 goal.door_pos = BlockPos::new(1, 1, 0);
264 goal.start(&mob);
265
266 mob.base().set_position_local(DVec3::new(2.0, 0.0, 0.0));
267 goal.tick(&mob);
268
269 assert!(!goal.can_continue_to_use());
270 }
271
272 #[test]
273 fn door_interact_can_use_requires_world_after_collision() {
274 init_vanilla_registry();
275 let mut goal = DoorInteractGoal::new();
276 let mob = pig(DVec3::ZERO);
277 set_horizontal_collision(&mob);
278
279 assert!(!goal.can_use(&mob));
280 }
281}