steel_core/entity/
item_based_steering.rs1use 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#[derive(Debug, Default)]
15pub struct ItemBasedSteering {
16 boosting: bool,
17 boost_time: i32,
18}
19
20impl ItemBasedSteering {
21 #[must_use]
23 pub const fn new() -> Self {
24 Self {
25 boosting: false,
26 boost_time: 0,
27 }
28 }
29
30 pub const fn on_synced(&mut self) {
32 self.boosting = true;
33 self.boost_time = 0;
34 }
35
36 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 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 #[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 #[must_use]
72 pub const fn is_boosting(&self) -> bool {
73 self.boosting
74 }
75
76 #[must_use]
78 pub const fn boost_time(&self) -> i32 {
79 self.boost_time
80 }
81}
82
83pub trait ItemSteerable: Entity {
85 fn item_based_steering(&self) -> &SyncMutex<ItemBasedSteering>;
87
88 fn boost_time_total(&self) -> i32;
90
91 fn set_boost_time_total(&self, boost_time_total: i32);
93
94 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 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 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}