Skip to main content

steel_core/entity/ai/goal/
breath_air.rs

1use glam::DVec3;
2use steel_math::fast_floor;
3use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_registry::vanilla_blocks;
5use steel_utils::BlockPos;
6
7use super::selector::{Goal, GoalControls};
8use crate::behavior::BlockStateBehaviorExt as _;
9use crate::entity::PathfinderMob;
10use crate::entity::ai::path::PathComputationType;
11use crate::physics::MoverType;
12use crate::world::LevelReader;
13
14const BREATH_AIR_THRESHOLD: i32 = 140;
15const AIR_SEARCH_HORIZONTAL_RADIUS: f64 = 1.0;
16const AIR_SEARCH_VERTICAL_ABOVE: f64 = 8.0;
17const BREATH_MOVE_SPEED: f32 = 0.02;
18const AIR_NAVIGATION_SPEED_MODIFIER: f64 = 1.0;
19
20pub struct BreathAirGoal;
21
22impl BreathAirGoal {
23    #[must_use]
24    pub(crate) const fn new() -> Self {
25        Self
26    }
27}
28
29impl Goal for BreathAirGoal {
30    fn controls(&self) -> GoalControls {
31        GoalControls::MOVE | GoalControls::LOOK
32    }
33
34    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
35        mob.air_supply() < BREATH_AIR_THRESHOLD
36    }
37
38    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
39        self.can_use(mob)
40    }
41
42    fn is_interruptable(&self) -> bool {
43        false
44    }
45
46    fn start(&mut self, mob: &dyn PathfinderMob) {
47        find_air_position(mob);
48        mob.mob_base().navigation().lock().stop();
49    }
50
51    fn tick(&mut self, mob: &dyn PathfinderMob) {
52        find_air_position(mob);
53        let input = mob.travel_input();
54        mob.move_relative(
55            BREATH_MOVE_SPEED,
56            DVec3::new(
57                f64::from(input.sideways()),
58                f64::from(input.vertical()),
59                f64::from(input.forward()),
60            ),
61        );
62        mob.move_entity(MoverType::SelfMovement, mob.velocity());
63    }
64}
65
66fn find_air_position(mob: &dyn PathfinderMob) {
67    let position = mob.position();
68    let destination_pos = mob
69        .level()
70        .and_then(|world| first_air_position(world.as_ref(), position))
71        .unwrap_or_else(|| {
72            BlockPos::containing(
73                position.x,
74                position.y + AIR_SEARCH_VERTICAL_ABOVE,
75                position.z,
76            )
77        });
78
79    mob.move_to_pos(
80        DVec3::new(
81            f64::from(destination_pos.x()),
82            f64::from(destination_pos.y() + 1),
83            f64::from(destination_pos.z()),
84        ),
85        AIR_NAVIGATION_SPEED_MODIFIER,
86    );
87}
88
89fn first_air_position(level: &dyn LevelReader, position: DVec3) -> Option<BlockPos> {
90    let (min, max) = air_search_bounds(position);
91    first_matching_pos_in_closed_box(min, max, |pos| gives_air(level, pos))
92}
93
94fn air_search_bounds(position: DVec3) -> (BlockPos, BlockPos) {
95    (
96        BlockPos::new(
97            fast_floor(position.x - AIR_SEARCH_HORIZONTAL_RADIUS),
98            fast_floor(position.y),
99            fast_floor(position.z - AIR_SEARCH_HORIZONTAL_RADIUS),
100        ),
101        BlockPos::new(
102            fast_floor(position.x + AIR_SEARCH_HORIZONTAL_RADIUS),
103            fast_floor(position.y + AIR_SEARCH_VERTICAL_ABOVE),
104            fast_floor(position.z + AIR_SEARCH_HORIZONTAL_RADIUS),
105        ),
106    )
107}
108
109fn first_matching_pos_in_closed_box(
110    min: BlockPos,
111    max: BlockPos,
112    mut predicate: impl FnMut(BlockPos) -> bool,
113) -> Option<BlockPos> {
114    for z in min.z()..=max.z() {
115        for y in min.y()..=max.y() {
116            for x in min.x()..=max.x() {
117                let pos = BlockPos::new(x, y, z);
118                if predicate(pos) {
119                    return Some(pos);
120                }
121            }
122        }
123    }
124    None
125}
126
127fn gives_air(level: &dyn LevelReader, pos: BlockPos) -> bool {
128    let state = level.get_block_state(pos);
129    (!state.has_fluid() || state.get_block() == &vanilla_blocks::BUBBLE_COLUMN)
130        && state.is_pathfindable(PathComputationType::Land)
131}
132
133#[cfg(test)]
134mod tests {
135    use std::sync::Weak;
136
137    use glam::DVec3;
138    use steel_registry::{init_vanilla_registry, vanilla_entities};
139
140    use super::*;
141    use crate::entity::entities::PigEntity;
142    use crate::entity::{Entity, LivingEntity, LivingTravelInput};
143
144    #[test]
145    fn breath_air_goal_uses_move_and_look_controls() {
146        let goal = BreathAirGoal::new();
147
148        assert_eq!(goal.controls(), GoalControls::MOVE | GoalControls::LOOK);
149        assert!(!goal.is_interruptable());
150    }
151
152    #[test]
153    fn breath_air_goal_uses_vanilla_air_threshold() {
154        init_vanilla_registry();
155        let mut goal = BreathAirGoal::new();
156        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
157
158        mob.set_air_supply(BREATH_AIR_THRESHOLD);
159        assert!(!goal.can_use(&mob));
160
161        mob.set_air_supply(BREATH_AIR_THRESHOLD - 1);
162        assert!(goal.can_use(&mob));
163        assert!(goal.can_continue_to_use(&mob));
164    }
165
166    #[test]
167    fn breath_air_goal_tick_applies_travel_input_to_velocity() {
168        init_vanilla_registry();
169        let mut goal = BreathAirGoal::new();
170        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
171        mob.set_travel_input(LivingTravelInput::new(1.0, 0.0, 0.0));
172
173        goal.tick(&mob);
174
175        assert!(mob.velocity().length_squared() > 0.0);
176    }
177
178    #[test]
179    fn air_search_bounds_match_vanilla_offsets() {
180        let (min, max) = air_search_bounds(DVec3::new(-0.25, 64.9, 0.25));
181
182        assert_eq!(min, BlockPos::new(-2, 64, -1));
183        assert_eq!(max, BlockPos::new(0, 72, 1));
184    }
185
186    #[test]
187    fn closed_box_scan_uses_vanilla_x_then_y_then_z_order() {
188        let min = BlockPos::new(10, 20, 30);
189        let max = BlockPos::new(12, 22, 32);
190        let expected = BlockPos::new(11, 20, 30);
191        let later_y = BlockPos::new(10, 21, 30);
192        let later_z = BlockPos::new(10, 20, 31);
193
194        let found = first_matching_pos_in_closed_box(min, max, |pos| {
195            pos == expected || pos == later_y || pos == later_z
196        });
197
198        assert_eq!(found, Some(expected));
199    }
200}