Skip to main content

steel_core/entity/ai/
targeting.rs

1use 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}