steel_core/entity/ai/goal/
hurt_by_target.rs1use steel_registry::vanilla_game_rules::UNIVERSAL_ANGER;
2use steel_utils::{DowncastTypeKey, WorldAabb};
3
4use super::selector::{Goal, GoalControls};
5use super::target_goal::{TargetGoalBase, follow_distance};
6use crate::entity::ai::targeting::TargetingConditions;
7use crate::entity::{PathfinderMob, SharedEntity};
8
9const HURT_BY_UNSEEN_MEMORY_TICKS: i32 = 300;
10const ALERT_RANGE_Y: f64 = 10.0;
11
12pub(crate) struct HurtByTargetGoal {
13 target_goal: TargetGoalBase,
14 targeting: TargetingConditions,
15 timestamp: i32,
16 ignore_damage_types: Vec<DowncastTypeKey>,
17 alert_same_type: bool,
18 ignore_alert_types: Vec<DowncastTypeKey>,
19}
20
21impl HurtByTargetGoal {
22 #[must_use]
23 pub(crate) const fn new() -> Self {
24 Self {
25 target_goal: TargetGoalBase::new(true, false),
26 targeting: TargetingConditions::for_combat()
27 .ignore_line_of_sight()
28 .ignore_invisibility_testing(),
29 timestamp: 0,
30 ignore_damage_types: Vec::new(),
31 alert_same_type: false,
32 ignore_alert_types: Vec::new(),
33 }
34 }
35
36 #[must_use]
37 pub(crate) fn with_ignored_damage_types(
38 mut self,
39 types: impl IntoIterator<Item = DowncastTypeKey>,
40 ) -> Self {
41 self.ignore_damage_types = types.into_iter().collect();
42 self
43 }
44
45 #[must_use]
46 pub(crate) fn set_alert_others(
47 mut self,
48 except_types: impl IntoIterator<Item = DowncastTypeKey>,
49 ) -> Self {
50 self.alert_same_type = true;
51 self.ignore_alert_types = except_types.into_iter().collect();
52 self
53 }
54
55 fn alert_others(&self, mob: &dyn PathfinderMob, hurt_by_mob: &SharedEntity) {
56 let Some(world) = mob.level() else {
57 return;
58 };
59
60 let within = follow_distance(mob);
61 let position = mob.position();
62 let search_box = WorldAabb::new(
63 position.x,
64 position.y,
65 position.z,
66 position.x + 1.0,
67 position.y + 1.0,
68 position.z + 1.0,
69 )
70 .inflate_xyz(within, ALERT_RANGE_Y, within);
71 let mob_type_key = mob.downcast_type_key();
72 let mob_uuid = mob.uuid();
73
74 for entity in world.get_entities_in_aabb_matching(&search_box, |entity| {
75 if entity.uuid() == mob_uuid
76 || entity.downcast_type_key() != mob_type_key
77 || self
78 .ignore_alert_types
79 .contains(&entity.downcast_type_key())
80 {
81 return false;
82 }
83
84 let Some(other) = entity.as_mob() else {
85 return false;
86 };
87
88 other.target().is_none() && !other.is_allied_to(hurt_by_mob.as_ref())
90 }) {
91 if let Some(other) = entity.as_mob() {
92 let _ = other.set_target(Some(hurt_by_mob));
93 }
94 }
95 }
96}
97
98impl Default for HurtByTargetGoal {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl Goal for HurtByTargetGoal {
105 fn controls(&self) -> GoalControls {
106 GoalControls::TARGET
107 }
108
109 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
110 let timestamp = mob.last_hurt_by_mob_timestamp();
111 if timestamp == self.timestamp {
112 return false;
113 }
114
115 let Some(hurt_by_mob) = mob.last_hurt_by_mob() else {
116 return false;
117 };
118 if self
119 .ignore_damage_types
120 .contains(&hurt_by_mob.downcast_type_key())
121 {
122 return false;
123 }
124
125 if hurt_by_mob.as_player().is_some()
126 && mob
127 .level()
128 .is_some_and(|world| world.get_game_rule(&UNIVERSAL_ANGER))
129 {
130 return false;
131 }
132
133 self.target_goal
134 .can_attack(mob, hurt_by_mob.as_living_entity(), &self.targeting)
135 }
136
137 fn can_continue_to_use(&mut self, mob: &dyn PathfinderMob) -> bool {
138 self.target_goal.can_continue_to_use(mob)
139 }
140
141 fn start(&mut self, mob: &dyn PathfinderMob) {
142 let Some(hurt_by_mob) = mob.last_hurt_by_mob() else {
143 return;
144 };
145
146 let _ = mob.set_target(Some(&hurt_by_mob));
147 self.target_goal.set_target_mob(mob.target());
148 self.timestamp = mob.last_hurt_by_mob_timestamp();
149 self.target_goal
150 .set_unseen_memory_ticks(HURT_BY_UNSEEN_MEMORY_TICKS);
151
152 if self.alert_same_type {
153 self.alert_others(mob, &hurt_by_mob);
154 }
155
156 self.target_goal.start();
157 }
158
159 fn stop(&mut self, mob: &dyn PathfinderMob) {
160 self.target_goal.stop(mob);
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use std::sync::Arc;
167
168 use glam::DVec3;
169 use steel_registry::{init_vanilla_registry, vanilla_entities};
170
171 use super::*;
172 use crate::behavior::init_behaviors;
173 use crate::entity::entities::{CowEntity, PigEntity};
174 use crate::entity::{Entity, LivingEntity, Mob};
175 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
176 use steel_utils::ChunkPos;
177
178 #[test]
179 fn targets_attacker_and_alerts_unassigned_same_type_mobs() {
180 init_vanilla_registry();
181 init_behaviors();
182 let world = fresh_test_world("hurt_by_target_goal");
183 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
184
185 let hunter = Arc::new(PigEntity::new(
186 &vanilla_entities::PIG,
187 1,
188 DVec3::new(8.0, 65.0, 8.0),
189 Arc::downgrade(&world),
190 ));
191 let ally = Arc::new(PigEntity::new(
192 &vanilla_entities::PIG,
193 2,
194 DVec3::new(9.0, 65.0, 8.0),
195 Arc::downgrade(&world),
196 ));
197 let attacker = Arc::new(CowEntity::new(
198 &vanilla_entities::COW,
199 3,
200 DVec3::new(10.0, 65.0, 8.0),
201 Arc::downgrade(&world),
202 ));
203
204 let hunter_entity: SharedEntity = hunter.clone();
205 let ally_entity: SharedEntity = ally.clone();
206 let attacker_entity: SharedEntity = attacker.clone();
207 for entity in [hunter_entity, ally_entity, attacker_entity.clone()] {
208 world
209 .try_add_entity(entity)
210 .expect("test entity should attach to the loaded chunk");
211 }
212
213 hunter.advance_tick_count();
214 hunter.set_last_hurt_by_mob(Some(&attacker_entity));
215 let mut goal = HurtByTargetGoal::new().set_alert_others([]);
216
217 assert!(goal.can_use(hunter.as_ref()));
218 goal.start(hunter.as_ref());
219
220 let Some(hunter_target) = hunter.target() else {
221 panic!("hurt-by-target goal should assign the attacker");
222 };
223 let Some(ally_target) = ally.target() else {
224 panic!("alerted same-type mob should receive the attacker");
225 };
226 assert_eq!(hunter_target.uuid(), attacker.uuid());
227 assert_eq!(ally_target.uuid(), attacker.uuid());
228 }
229}