steel_core/entity/ai/
targeting.rs1use std::sync::Arc;
2
3use steel_utils::types::Difficulty;
4
5use crate::entity::LivingEntity;
6use crate::world::World;
7
8const MIN_VISIBILITY_DISTANCE_FOR_INVISIBLE_TARGET: f64 = 2.0;
9
10pub(crate) type TargetingSelector = Arc<dyn Fn(&dyn LivingEntity, &World) -> bool + Send + Sync>;
11
12#[derive(Clone)]
13pub(crate) struct TargetingConditions {
14 is_combat: bool,
15 range: f64,
16 check_line_of_sight: bool,
17 test_invisible: bool,
18 selector: Option<TargetingSelector>,
19}
20
21impl TargetingConditions {
22 #[must_use]
23 pub(crate) const fn for_combat() -> Self {
24 Self::new(true)
25 }
26
27 #[must_use]
28 pub(crate) const fn for_non_combat() -> Self {
29 Self::new(false)
30 }
31
32 const fn new(is_combat: bool) -> Self {
33 Self {
34 is_combat,
35 range: -1.0,
36 check_line_of_sight: true,
37 test_invisible: true,
38 selector: None,
39 }
40 }
41
42 #[must_use]
43 pub(crate) const fn range(mut self, range: f64) -> Self {
44 self.range = range;
45 self
46 }
47
48 #[must_use]
49 pub(crate) const fn ignore_line_of_sight(mut self) -> Self {
50 self.check_line_of_sight = false;
51 self
52 }
53
54 #[must_use]
55 pub(crate) const fn ignore_invisibility_testing(mut self) -> Self {
56 self.test_invisible = false;
57 self
58 }
59
60 #[must_use]
61 pub(crate) fn selector(
62 mut self,
63 selector: impl Fn(&dyn LivingEntity, &World) -> bool + Send + Sync + 'static,
64 ) -> Self {
65 self.selector = Some(Arc::new(selector));
66 self
67 }
68
69 #[must_use]
70 pub(crate) fn test(
71 &self,
72 world: &World,
73 targeter: Option<&dyn LivingEntity>,
74 target: &dyn LivingEntity,
75 ) -> bool {
76 if targeter.is_some_and(|targeter| targeter.uuid() == target.uuid()) {
77 return false;
78 }
79 if !target.can_be_seen_by_anyone() {
80 return false;
81 }
82 if let Some(selector) = &self.selector
83 && !selector(target, world)
84 {
85 return false;
86 }
87
88 let Some(targeter) = targeter else {
89 return !self.is_combat
90 || target.can_be_seen_as_enemy() && world.difficulty() != Difficulty::Peaceful;
91 };
92
93 if self.is_combat && (!targeter.can_attack(target) || targeter.is_allied_to(target)) {
94 return false;
95 }
96
97 if self.range > 0.0 {
98 let modifier = if self.test_invisible {
99 target.get_visibility_percent(Some(targeter))
100 } else {
101 1.0
102 };
103 let visibility_distance =
104 (self.range * modifier).max(MIN_VISIBILITY_DISTANCE_FOR_INVISIBLE_TARGET);
105 if targeter.position().distance_squared(target.position())
106 > visibility_distance * visibility_distance
107 {
108 return false;
109 }
110 }
111
112 if self.check_line_of_sight
113 && let Some(pathfinder) = targeter.as_pathfinder_mob()
114 && !pathfinder.has_line_of_sight_cached(target)
115 {
116 return false;
117 }
118
119 true
120 }
121}
122
123impl Default for TargetingConditions {
124 fn default() -> Self {
125 Self::for_combat()
126 }
127}