1use crate::prelude::*;
2
3#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
14#[tsify(from_wasm_abi, into_wasm_abi)]
15pub struct ManaPool {
16 pub max: f64,
18 pub current: f64,
20 pub regen_per_second: f64,
22 pub updated_tick: u64,
24}
25
26impl PartialEq for ManaPool {
27 fn eq(&self, other: &Self) -> bool {
28 self.max.to_bits() == other.max.to_bits()
29 && self.current.to_bits() == other.current.to_bits()
30 && self.regen_per_second.to_bits() == other.regen_per_second.to_bits()
31 && self.updated_tick == other.updated_tick
32 }
33}
34
35impl Eq for ManaPool {}
36
37impl ManaPool {
38 pub fn full(max: f64, regen_per_second: f64, current_tick: u64) -> Self {
40 let max = if max.is_finite() && max > 0.0 {
41 max
42 } else {
43 0.0
44 };
45 let regen_per_second = if regen_per_second.is_finite() && regen_per_second > 0.0 {
46 regen_per_second
47 } else {
48 0.0
49 };
50 Self {
51 max,
52 current: max,
53 regen_per_second,
54 updated_tick: current_tick,
55 }
56 }
57
58 pub fn regen_to(&mut self, current_tick: u64) {
62 let elapsed_ms = current_tick.saturating_sub(self.updated_tick);
63 self.updated_tick = current_tick;
64 if elapsed_ms == 0 || self.regen_per_second <= 0.0 {
65 return;
66 }
67 let gained = self.regen_per_second * (elapsed_ms as f64) / 1000.0;
68 self.current = (self.current + gained).min(self.max);
69 }
70
71 pub fn try_spend(&mut self, cost: f64) -> bool {
74 if !cost.is_finite() || cost <= 0.0 {
75 return true;
76 }
77 if self.current + f64::EPSILON < cost {
78 return false;
79 }
80 self.current = (self.current - cost).max(0.0);
81 true
82 }
83
84 pub fn ms_until_affordable(&self, cost: f64) -> Option<u64> {
91 if !cost.is_finite() || cost <= self.current {
92 return Some(0);
93 }
94 if self.regen_per_second <= 0.0 || cost > self.max {
95 return None;
96 }
97 let ms = (cost - self.current) / self.regen_per_second * 1000.0;
98 Some(ms.ceil().max(0.0) as u64)
99 }
100
101 pub fn missing_fraction(&self) -> f64 {
104 if self.max <= 0.0 {
105 return 0.0;
106 }
107 ((self.max - self.current) / self.max).clamp(0.0, 1.0)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn full_pool_starts_at_max() {
117 let pool = ManaPool::full(100.0, 5.0, 42);
118 assert_eq!(pool.current, 100.0);
119 assert_eq!(pool.max, 100.0);
120 assert_eq!(pool.updated_tick, 42);
121 }
122
123 #[test]
124 fn regen_accrues_by_elapsed_milliseconds_and_clamps() {
125 let mut pool = ManaPool::full(100.0, 10.0, 0);
126 assert!(pool.try_spend(50.0));
127 assert_eq!(pool.current, 50.0);
128
129 pool.regen_to(1_000);
130 assert_eq!(pool.current, 60.0);
131
132 pool.regen_to(100_000);
133 assert_eq!(pool.current, 100.0);
134 }
135
136 #[test]
137 fn spend_rejects_insufficient_mana_without_mutating() {
138 let mut pool = ManaPool::full(10.0, 0.0, 0);
139 assert!(pool.try_spend(10.0));
140 assert_eq!(pool.current, 0.0);
141 assert!(!pool.try_spend(1.0));
142 assert_eq!(pool.current, 0.0);
143 }
144
145 #[test]
146 fn wait_time_is_the_exact_refill_time() {
147 let mut pool = ManaPool::full(100.0, 10.0, 0);
148 assert!(pool.try_spend(100.0));
149
150 assert_eq!(pool.ms_until_affordable(0.0), Some(0));
152 assert_eq!(pool.ms_until_affordable(25.0), Some(2_500));
154 assert_eq!(pool.ms_until_affordable(0.0001), Some(1));
156 assert_eq!(pool.ms_until_affordable(101.0), None);
158
159 let no_regen = ManaPool::full(100.0, 0.0, 0);
161 assert_eq!(no_regen.ms_until_affordable(150.0), None);
162 assert_eq!(no_regen.ms_until_affordable(50.0), Some(0));
163 }
164
165 #[test]
166 fn missing_fraction_tracks_spent_share() {
167 let mut pool = ManaPool::full(200.0, 0.0, 0);
168 assert_eq!(pool.missing_fraction(), 0.0);
169 assert!(pool.try_spend(50.0));
170 assert_eq!(pool.missing_fraction(), 0.25);
171 }
172}