steel_core/entity/ai/goal/
panic_goal.rs1use 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}