steel_core/entity/
item_based_steering.rs1use 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#[derive(Debug, Default)]
16pub struct ItemBasedSteering {
17 boosting: bool,
18 boost_time: i32,
19}
20
21impl ItemBasedSteering {
22 #[must_use]
24 pub const fn new() -> Self {
25 Self {
26 boosting: false,
27 boost_time: 0,
28 }
29 }
30
31 pub const fn on_synced(&mut self) {
34 self.boosting = true;
35 self.boost_time = 0;
36 }
37
38 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 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 #[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 #[must_use]
77 pub const fn is_boosting(&self) -> bool {
78 self.boosting
79 }
80
81 #[must_use]
83 pub const fn boost_time(&self) -> i32 {
84 self.boost_time
85 }
86}
87
88pub trait ItemSteerable: Entity {
91 fn item_based_steering(&self) -> &SyncMutex<ItemBasedSteering>;
93
94 fn boost_time_total(&self) -> i32;
96
97 fn set_boost_time_total(&self, boost_time_total: i32);
99
100 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 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 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}