Skip to main content

steel_core/entity/ai/goal/
hurt_by_target.rs

1use 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            //TODO tamable animal owner check is required here
89            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}