Skip to main content

steel_core/entity/ai/
sensing.rs

1//! Per-mob sensing caches.
2
3use rustc_hash::FxHashSet;
4
5/// Vanilla `Sensing` line-of-sight cache.
6#[derive(Debug, Default)]
7pub(crate) struct Sensing {
8    seen: FxHashSet<i32>,
9    unseen: FxHashSet<i32>,
10}
11
12impl Sensing {
13    /// Creates an empty sensing cache.
14    #[must_use]
15    pub(crate) fn new() -> Self {
16        Self::default()
17    }
18
19    /// Clears per-tick sensing results.
20    pub(crate) fn tick(&mut self) {
21        self.seen.clear();
22        self.unseen.clear();
23    }
24
25    /// Returns cached line-of-sight for `target_id`, computing it on first use.
26    pub(crate) fn has_line_of_sight(
27        &mut self,
28        target_id: i32,
29        test: impl FnOnce() -> bool,
30    ) -> bool {
31        if self.seen.contains(&target_id) {
32            return true;
33        }
34        if self.unseen.contains(&target_id) {
35            return false;
36        }
37
38        let has_line_of_sight = test();
39        if has_line_of_sight {
40            self.seen.insert(target_id);
41        } else {
42            self.unseen.insert(target_id);
43        }
44        has_line_of_sight
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use std::sync::atomic::{AtomicUsize, Ordering};
51
52    use super::*;
53
54    #[test]
55    fn sensing_caches_seen_targets_until_tick() {
56        let calls = AtomicUsize::new(0);
57        let mut sensing = Sensing::new();
58
59        assert!(sensing.has_line_of_sight(1, || {
60            calls.fetch_add(1, Ordering::Relaxed);
61            true
62        }));
63        assert!(sensing.has_line_of_sight(1, || {
64            calls.fetch_add(1, Ordering::Relaxed);
65            false
66        }));
67        assert_eq!(calls.load(Ordering::Relaxed), 1);
68
69        sensing.tick();
70        assert!(!sensing.has_line_of_sight(1, || {
71            calls.fetch_add(1, Ordering::Relaxed);
72            false
73        }));
74        assert_eq!(calls.load(Ordering::Relaxed), 2);
75    }
76
77    #[test]
78    fn sensing_caches_unseen_targets_until_tick() {
79        let calls = AtomicUsize::new(0);
80        let mut sensing = Sensing::new();
81
82        assert!(!sensing.has_line_of_sight(1, || {
83            calls.fetch_add(1, Ordering::Relaxed);
84            false
85        }));
86        assert!(!sensing.has_line_of_sight(1, || {
87            calls.fetch_add(1, Ordering::Relaxed);
88            true
89        }));
90
91        assert_eq!(calls.load(Ordering::Relaxed), 1);
92    }
93}