Skip to main content

steel_core/entity/
item_based_steering.rs

1//! Vanilla item-steered vehicle helpers.
2
3use std::f32::consts::PI;
4
5use steel_utils::locks::SyncMutex;
6
7use crate::entity::Entity;
8
9const MIN_BOOST_TIME: i32 = 140;
10const BOOST_TIME_BOUND: i32 = 841;
11const BOOST_FACTOR_SCALE: f32 = 1.15;
12
13/// Runtime state for vanilla `ItemBasedSteering`.
14#[derive(Debug, Default)]
15pub struct ItemBasedSteering {
16    boosting: bool,
17    boost_time: i32,
18}
19
20impl ItemBasedSteering {
21    /// Creates default item-based steering state.
22    #[must_use]
23    pub const fn new() -> Self {
24        Self {
25            boosting: false,
26            boost_time: 0,
27        }
28    }
29
30    /// Mirrors vanilla `ItemBasedSteering.onSynced`.
31    pub const fn on_synced(&mut self) {
32        self.boosting = true;
33        self.boost_time = 0;
34    }
35
36    /// Mirrors vanilla `ItemBasedSteering.boost`.
37    pub fn boost(&mut self) -> Option<i32> {
38        if self.boosting {
39            return None;
40        }
41
42        self.boosting = true;
43        self.boost_time = 0;
44        Some(rand::random_range(0..BOOST_TIME_BOUND) + MIN_BOOST_TIME)
45    }
46
47    /// Mirrors vanilla `ItemBasedSteering.tickBoost`.
48    pub const fn tick_boost(&mut self, boost_time_total: i32) {
49        if !self.boosting {
50            return;
51        }
52
53        let previous_boost_time = self.boost_time;
54        self.boost_time += 1;
55        if previous_boost_time > boost_time_total {
56            self.boosting = false;
57        }
58    }
59
60    /// Mirrors vanilla `ItemBasedSteering.boostFactor`.
61    #[must_use]
62    pub fn boost_factor(&self, boost_time_total: i32) -> f32 {
63        if !self.boosting || boost_time_total <= 0 {
64            return 1.0;
65        }
66
67        1.0 + BOOST_FACTOR_SCALE * ((self.boost_time as f32 / boost_time_total as f32) * PI).sin()
68    }
69
70    /// Returns whether a boost is currently active.
71    #[must_use]
72    pub const fn is_boosting(&self) -> bool {
73        self.boosting
74    }
75
76    /// Returns vanilla `ItemBasedSteering.boostTime`.
77    #[must_use]
78    pub const fn boost_time(&self) -> i32 {
79        self.boost_time
80    }
81}
82
83/// Entity behavior for vanilla `ItemSteerable`.
84pub trait ItemSteerable: Entity {
85    /// Returns the shared runtime steering state.
86    fn item_based_steering(&self) -> &SyncMutex<ItemBasedSteering>;
87
88    /// Returns the synced vanilla `boostTimeTotal`.
89    fn boost_time_total(&self) -> i32;
90
91    /// Sets the synced vanilla `boostTimeTotal`.
92    fn set_boost_time_total(&self, boost_time_total: i32);
93
94    /// Attempts to start an item-steering boost.
95    fn boost(&self) -> bool {
96        let boost_time_total = {
97            let mut steering = self.item_based_steering().lock();
98            steering.boost()
99        };
100        let Some(boost_time_total) = boost_time_total else {
101            return false;
102        };
103
104        self.set_boost_time_total(boost_time_total);
105        true
106    }
107
108    /// Advances the active item-steering boost.
109    fn tick_boost(&self) {
110        let boost_time_total = self.boost_time_total();
111        self.item_based_steering()
112            .lock()
113            .tick_boost(boost_time_total);
114    }
115
116    /// Returns vanilla `ItemBasedSteering.boostFactor`.
117    fn boost_factor(&self) -> f32 {
118        let boost_time_total = self.boost_time_total();
119        self.item_based_steering()
120            .lock()
121            .boost_factor(boost_time_total)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::ItemBasedSteering;
128
129    #[test]
130    fn boost_starts_once_and_returns_vanilla_total_range() {
131        let mut steering = ItemBasedSteering::new();
132
133        let Some(total) = steering.boost() else {
134            panic!("first boost should start");
135        };
136
137        assert!((140..=980).contains(&total));
138        assert!(steering.is_boosting());
139        assert_eq!(steering.boost_time(), 0);
140        assert!(steering.boost().is_none());
141    }
142
143    #[test]
144    fn tick_boost_uses_vanilla_post_increment_expiry() {
145        let mut steering = ItemBasedSteering::new();
146
147        steering.on_synced();
148        steering.tick_boost(2);
149        assert_eq!(steering.boost_time(), 1);
150        assert!(steering.is_boosting());
151
152        steering.tick_boost(2);
153        assert_eq!(steering.boost_time(), 2);
154        assert!(steering.is_boosting());
155
156        steering.tick_boost(2);
157        assert_eq!(steering.boost_time(), 3);
158        assert!(steering.is_boosting());
159
160        steering.tick_boost(2);
161        assert_eq!(steering.boost_time(), 4);
162        assert!(!steering.is_boosting());
163    }
164}