Skip to main content

steel_core/entity/ai/goal/
panic_goal.rs

1use glam::DVec3;
2use steel_registry::blocks::block_state_ext::BlockStateExt as _;
3use steel_registry::vanilla_damage_type_tags;
4use steel_utils::BlockPos;
5
6use super::random_pos::default_random_pos;
7use super::selector::{Goal, GoalControls};
8use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext};
9use crate::entity::PathfinderMob;
10use crate::fluid::FluidStateExt as _;
11
12const WATER_CHECK_DISTANCE_VERTICAL: i32 = 1;
13
14pub struct PanicGoal {
15    wanted_position: Option<DVec3>,
16    speed_modifier: f64,
17    is_running: bool,
18}
19
20impl PanicGoal {
21    #[must_use]
22    pub(crate) const fn new(speed_modifier: f64) -> Self {
23        Self {
24            wanted_position: None,
25            speed_modifier,
26            is_running: false,
27        }
28    }
29
30    #[must_use]
31    pub const fn is_running(&self) -> bool {
32        self.is_running
33    }
34
35    fn should_panic(mob: &dyn PathfinderMob) -> bool {
36        mob.last_damage_source()
37            .is_some_and(|source| source.is(&vanilla_damage_type_tags::DamageTypeTag::PANIC_CAUSES))
38    }
39
40    fn find_random_position(&mut self, mob: &dyn PathfinderMob) -> bool {
41        let Some(position) = default_random_pos(mob, 5, 4) else {
42            return false;
43        };
44
45        self.wanted_position = Some(position);
46        true
47    }
48}
49
50impl Goal for PanicGoal {
51    fn controls(&self) -> GoalControls {
52        GoalControls::MOVE
53    }
54
55    fn is_panic_goal(&self) -> bool {
56        true
57    }
58
59    fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
60        if !Self::should_panic(mob) {
61            return false;
62        }
63
64        if mob.is_on_fire()
65            && let Some(water_pos) = look_for_water(mob, 5)
66        {
67            self.wanted_position = Some(block_pos_corner(water_pos));
68            return true;
69        }
70
71        self.find_random_position(mob)
72    }
73
74    fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
75        !mob.mob_base().navigation().lock().is_done()
76    }
77
78    fn start(&mut self, mob: &dyn PathfinderMob) {
79        if let Some(wanted_position) = self.wanted_position {
80            mob.move_to_pos(wanted_position, self.speed_modifier);
81        }
82        self.is_running = true;
83    }
84
85    fn stop(&mut self, _mob: &dyn PathfinderMob) {
86        self.is_running = false;
87    }
88}
89
90fn look_for_water(mob: &dyn PathfinderMob, xz_dist: i32) -> Option<BlockPos> {
91    let world = mob.level()?;
92    let mob_position = mob.block_position();
93    let block_state = world.get_block_state(mob_position);
94    let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
95    if !behavior
96        .get_collision_shape(
97            block_state,
98            world.as_ref(),
99            mob_position,
100            BlockCollisionContext::empty(),
101        )
102        .is_empty()
103    {
104        return None;
105    }
106
107    mob_position.find_closest_match(xz_dist, WATER_CHECK_DISTANCE_VERTICAL, |pos| {
108        world.get_block_state(pos).get_fluid_state().is_water()
109    })
110}
111
112fn block_pos_corner(pos: BlockPos) -> DVec3 {
113    DVec3::new(f64::from(pos.x()), f64::from(pos.y()), f64::from(pos.z()))
114}
115
116#[cfg(test)]
117mod tests {
118    use std::sync::Weak;
119
120    use steel_registry::{init_vanilla_registry, vanilla_damage_types, vanilla_entities};
121
122    use super::*;
123    use crate::entity::LivingEntity;
124    use crate::entity::damage::DamageSource;
125    use crate::entity::entities::PigEntity;
126    use crate::test_support::test_world;
127
128    #[test]
129    fn panic_goal_uses_move_control() {
130        let goal = PanicGoal::new(1.25);
131
132        assert_eq!(goal.controls(), GoalControls::MOVE);
133        assert!(!goal.is_running());
134    }
135
136    #[test]
137    fn panic_goal_uses_vanilla_panic_damage_tag() {
138        init_vanilla_registry();
139        let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
140
141        assert!(!PanicGoal::should_panic(&pig));
142
143        assert!(pig.hurt_server(
144            test_world(),
145            &DamageSource::environment(&vanilla_damage_types::GENERIC),
146            1.0
147        ));
148        assert!(!PanicGoal::should_panic(&pig));
149
150        assert!(pig.hurt_server(
151            test_world(),
152            &DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK),
153            2.0
154        ));
155        assert!(PanicGoal::should_panic(&pig));
156    }
157}