Skip to main content

steel_core/entity/ai/goal/
door_interact.rs

1use 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}