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_math::trig;
6use steel_utils::locks::SyncMutex;
7
8use crate::entity::Entity;
9
10const MIN_BOOST_TIME: i32 = 140;
11const BOOST_TIME_BOUND: i32 = 841;
12const BOOST_FACTOR_SCALE: f32 = 1.15;
13
14/// Runtime state for vanilla `ItemBasedSteering`.
15#[derive(Debug, Default)]
16pub struct ItemBasedSteering {
17    boosting: bool,
18    boost_time: i32,
19}
20
21impl ItemBasedSteering {
22    /// Creates default item-based steering state.
23    #[must_use]
24    pub const fn new() -> Self {
25        Self {
26            boosting: false,
27            boost_time: 0,
28        }
29    }
30
31    /// Marks a boost as active without rolling a new duration, used when
32    /// boost state arrives from synced or loaded data.
33    pub const fn on_synced(&mut self) {
34        self.boosting = true;
35        self.boost_time = 0;
36    }
37
38    /// Starts a boost if one isn't already active, returning a random total
39    /// duration in ticks.
40    pub fn boost(&mut self) -> Option<i32> {
41        if self.boosting {
42            return None;
43        }
44
45        self.boosting = true;
46        self.boost_time = 0;
47        Some(rand::random_range(0..BOOST_TIME_BOUND) + MIN_BOOST_TIME)
48    }
49
50    /// Advances the active boost by one tick, ending it once past `boost_time_total`.
51    pub const fn tick_boost(&mut self, boost_time_total: i32) {
52        if !self.boosting {
53            return;
54        }
55
56        let previous_boost_time = self.boost_time;
57        self.boost_time += 1;
58        if previous_boost_time > boost_time_total {
59            self.boosting = false;
60        }
61    }
62
63    /// Returns the current speed multiplier while boosting, ramping via a
64    /// sine curve over the boost duration; 1.0 when not boosting.
65    #[must_use]
66    pub fn boost_factor(&self, boost_time_total: i32) -> f32 {
67        if !self.boosting || boost_time_total <= 0 {
68            return 1.0;
69        }
70
71        let angle = (self.boost_time as f32 / boost_time_total as f32) * PI;
72        1.0 + BOOST_FACTOR_SCALE * trig::sin(angle)
73    }
74
75    /// Returns whether a boost is currently active.
76    #[must_use]
77    pub const fn is_boosting(&self) -> bool {
78        self.boosting
79    }
80
81    /// Returns ticks elapsed since the current boost started.
82    #[must_use]
83    pub const fn boost_time(&self) -> i32 {
84        self.boost_time
85    }
86}
87
88/// Entity behavior for vehicles that gain a temporary speed boost from an
89/// item, such as a saddled pig steered with a carrot on a stick.
90pub trait ItemSteerable: Entity {
91    /// Returns the shared runtime steering state.
92    fn item_based_steering(&self) -> &SyncMutex<ItemBasedSteering>;
93
94    /// Returns the synced total boost duration in ticks for the currently active boost.
95    fn boost_time_total(&self) -> i32;
96
97    /// Sets the synced total boost duration in ticks for the currently active boost.
98    fn set_boost_time_total(&self, boost_time_total: i32);
99
100    /// Attempts to start an item-steering boost.
101    fn boost(&self) -> bool {
102        let boost_time_total = {
103            let mut steering = self.item_based_steering().lock();
104            steering.boost()
105        };
106        let Some(boost_time_total) = boost_time_total else {
107            return false;
108        };
109
110        self.set_boost_time_total(boost_time_total);
111        true
112    }
113
114    /// Advances the active item-steering boost.
115    fn tick_boost(&self) {
116        let boost_time_total = self.boost_time_total();
117        self.item_based_steering()
118            .lock()
119            .tick_boost(boost_time_total);
120    }
121
122    /// Returns the current speed multiplier while boosting; 1.0 when not boosting.
123    fn boost_factor(&self) -> f32 {
124        let boost_time_total = self.boost_time_total();
125        self.item_based_steering()
126            .lock()
127            .boost_factor(boost_time_total)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::ItemBasedSteering;
134
135    #[test]
136    fn boost_starts_once_and_returns_vanilla_total_range() {
137        let mut steering = ItemBasedSteering::new();
138
139        let Some(total) = steering.boost() else {
140            panic!("first boost should start");
141        };
142
143        assert!((140..=980).contains(&total));
144        assert!(steering.is_boosting());
145        assert_eq!(steering.boost_time(), 0);
146        assert!(steering.boost().is_none());
147    }
148
149    #[test]
150    fn tick_boost_uses_vanilla_post_increment_expiry() {
151        let mut steering = ItemBasedSteering::new();
152
153        steering.on_synced();
154        steering.tick_boost(2);
155        assert_eq!(steering.boost_time(), 1);
156        assert!(steering.is_boosting());
157
158        steering.tick_boost(2);
159        assert_eq!(steering.boost_time(), 2);
160        assert!(steering.is_boosting());
161
162        steering.tick_boost(2);
163        assert_eq!(steering.boost_time(), 3);
164        assert!(steering.is_boosting());
165
166        steering.tick_boost(2);
167        assert_eq!(steering.boost_time(), 4);
168        assert!(!steering.is_boosting());
169    }
170}