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(|| BlockPos::containing(position.x, position.y + 8.0, position.z));
72
73    mob.move_to_pos(
74        DVec3::new(
75            f64::from(destination_pos.x()),
76            f64::from(destination_pos.y() + 1),
77            f64::from(destination_pos.z()),
78        ),
79        AIR_NAVIGATION_SPEED_MODIFIER,
80    );
81}
82
83fn first_air_position(level: &dyn LevelReader, position: DVec3) -> Option<BlockPos> {
84    let (min, max) = air_search_bounds(position);
85    first_matching_pos_in_closed_box(min, max, |pos| gives_air(level, pos))
86}
87
88fn air_search_bounds(position: DVec3) -> (BlockPos, BlockPos) {
89    (
90        BlockPos::new(
91            fast_floor(position.x - AIR_SEARCH_HORIZONTAL_RADIUS),
92            fast_floor(position.y),
93            fast_floor(position.z - AIR_SEARCH_HORIZONTAL_RADIUS),
94        ),
95        BlockPos::new(
96            fast_floor(position.x + AIR_SEARCH_HORIZONTAL_RADIUS),
97            fast_floor(position.y + AIR_SEARCH_VERTICAL_ABOVE),
98            fast_floor(position.z + AIR_SEARCH_HORIZONTAL_RADIUS),
99        ),
100    )
101}
102
103fn first_matching_pos_in_closed_box(
104    min: BlockPos,
105    max: BlockPos,
106    mut predicate: impl FnMut(BlockPos) -> bool,
107) -> Option<BlockPos> {
108    for z in min.z()..=max.z() {
109        for y in min.y()..=max.y() {
110            for x in min.x()..=max.x() {
111                let pos = BlockPos::new(x, y, z);
112                if predicate(pos) {
113                    return Some(pos);
114                }
115            }
116        }
117    }
118    None
119}
120
121fn gives_air(level: &dyn LevelReader, pos: BlockPos) -> bool {
122    let state = level.get_block_state(pos);
123    (!state.has_fluid() || state.get_block() == &vanilla_blocks::BUBBLE_COLUMN)
124        && state.is_pathfindable(PathComputationType::Land)
125}
126
127#[cfg(test)]
128mod tests {
129    use std::sync::Weak;
130
131    use glam::DVec3;
132    use steel_registry::{init_vanilla_registry, vanilla_entities};
133
134    use super::*;
135    use crate::entity::entities::PigEntity;
136    use crate::entity::{Entity, LivingEntity, LivingTravelInput};
137
138    #[test]
139    fn breath_air_goal_uses_move_and_look_controls() {
140        let goal = BreathAirGoal::new();
141
142        assert_eq!(goal.controls(), GoalControls::MOVE | GoalControls::LOOK);
143        assert!(!goal.is_interruptable());
144    }
145
146    #[test]
147    fn breath_air_goal_uses_vanilla_air_threshold() {
148        init_vanilla_registry();
149        let mut goal = BreathAirGoal::new();
150        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
151
152        mob.set_air_supply(BREATH_AIR_THRESHOLD);
153        assert!(!goal.can_use(&mob));
154
155        mob.set_air_supply(BREATH_AIR_THRESHOLD - 1);
156        assert!(goal.can_use(&mob));
157        assert!(goal.can_continue_to_use(&mob));
158    }
159
160    #[test]
161    fn breath_air_goal_tick_applies_travel_input_to_velocity() {
162        init_vanilla_registry();
163        let mut goal = BreathAirGoal::new();
164        let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
165        mob.set_travel_input(LivingTravelInput::new(1.0, 0.0, 0.0));
166
167        goal.tick(&mob);
168
169        assert!(mob.velocity().length_squared() > 0.0);
170    }
171
172    #[test]
173    fn air_search_bounds_match_vanilla_offsets() {
174        let (min, max) = air_search_bounds(DVec3::new(-0.25, 64.9, 0.25));
175
176        assert_eq!(min, BlockPos::new(-2, 64, -1));
177        assert_eq!(max, BlockPos::new(0, 72, 1));
178    }
179
180    #[test]
181    fn closed_box_scan_uses_vanilla_x_then_y_then_z_order() {
182        let min = BlockPos::new(10, 20, 30);
183        let max = BlockPos::new(12, 22, 32);
184        let expected = BlockPos::new(11, 20, 30);
185        let later_y = BlockPos::new(10, 21, 30);
186        let later_z = BlockPos::new(10, 20, 31);
187
188        let found = first_matching_pos_in_closed_box(min, max, |pos| {
189            pos == expected || pos == later_y || pos == later_z
190        });
191
192        assert_eq!(found, Some(expected));
193    }
194}