wowlab_engine_combat/state/
resource.rs1use wowlab_types::sim::SimTime;
4
5pub(crate) const RUNE_COUNT: usize = 6;
6pub(crate) const MAX_REGENERATING_RUNES: usize = 3;
7pub(crate) const RUNE_RECHARGE_SECONDS: f64 = 10.0;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub(crate) enum RuneSlotState {
11 Full,
12 Regenerating { remaining: f64 },
13 Depleted,
14}
15
16#[derive(Clone, Debug)]
17pub(crate) struct RuneState {
18 pub(crate) slots: [RuneSlotState; RUNE_COUNT],
19 pub(crate) updated_at: SimTime,
20}
21
22impl RuneState {
23 pub(crate) fn reset_to(&mut self, full: usize, now: SimTime) {
24 self.slots = [RuneSlotState::Depleted; RUNE_COUNT];
25
26 for slot in self.slots.iter_mut().take(full.min(RUNE_COUNT)) {
27 *slot = RuneSlotState::Full;
28 }
29
30 self.updated_at = now;
31 self.start_depleted();
32 }
33
34 pub(crate) fn full_count(&self) -> usize {
35 self.slots
36 .iter()
37 .filter(|slot| matches!(slot, RuneSlotState::Full))
38 .count()
39 }
40
41 pub(crate) fn regenerating_count(&self) -> usize {
42 self.slots
43 .iter()
44 .filter(|slot| matches!(slot, RuneSlotState::Regenerating { .. }))
45 .count()
46 }
47
48 pub(crate) fn consume(&mut self, count: usize) -> bool {
49 if self.full_count() < count {
50 return false;
51 }
52
53 for _ in 0..count {
54 let Some(slot) = self
55 .slots
56 .iter_mut()
57 .find(|slot| matches!(slot, RuneSlotState::Full))
58 else {
59 return false;
60 };
61
62 *slot = RuneSlotState::Depleted;
63 }
64
65 self.start_depleted();
66
67 true
68 }
69
70 pub(crate) fn fill(&mut self, count: usize) {
71 for _ in 0..count {
72 let Some(index) = self.next_rune_to_fill() else {
73 break;
74 };
75
76 let Some(slot) = self.slots.get_mut(index) else {
77 break;
78 };
79
80 *slot = RuneSlotState::Full;
81 self.start_depleted();
82 }
83 }
84
85 pub(crate) fn advance(&mut self, elapsed_work: f64) {
86 let mut work = elapsed_work.max(0.0);
87
88 while work > f64::EPSILON {
89 let Some(next) = self
90 .slots
91 .iter()
92 .filter_map(|slot| match slot {
93 RuneSlotState::Regenerating { remaining } => Some(*remaining),
94 _ => None,
95 })
96 .min_by(f64::total_cmp)
97 else {
98 break;
99 };
100 let step = work.min(next);
101
102 for slot in &mut self.slots {
103 if let RuneSlotState::Regenerating { remaining } = slot {
104 *remaining = (*remaining - step).max(0.0);
105 }
106 }
107
108 work -= step;
109
110 for slot in &mut self.slots {
111 if matches!(slot, RuneSlotState::Regenerating { remaining } if *remaining <= f64::EPSILON)
112 {
113 *slot = RuneSlotState::Full;
114 }
115 }
116
117 self.start_depleted();
118 }
119 }
120
121 pub(crate) fn time_to_regen(&self, needed: usize, rate_multiplier: f64) -> Option<f64> {
122 if needed == 0 {
123 return Some(0.0);
124 }
125
126 if self.full_count() + needed > RUNE_COUNT || rate_multiplier <= 0.0 {
127 return None;
128 }
129
130 let mut projected = self.clone();
131 let initial_full = projected.full_count();
132 let target_full = initial_full + needed;
133 let mut elapsed_work = 0.0;
134
135 while projected.full_count() < target_full {
136 let next = projected
137 .slots
138 .iter()
139 .filter_map(|slot| match slot {
140 RuneSlotState::Regenerating { remaining } => Some(*remaining),
141 _ => None,
142 })
143 .min_by(f64::total_cmp)?;
144
145 projected.advance(next);
146 elapsed_work += next;
147 }
148
149 Some(elapsed_work / rate_multiplier)
150 }
151
152 fn next_rune_to_fill(&self) -> Option<usize> {
153 let regenerating = self
154 .slots
155 .iter()
156 .enumerate()
157 .filter_map(|(index, slot)| match slot {
158 RuneSlotState::Regenerating { remaining } => Some((index, *remaining)),
159 _ => None,
160 })
161 .min_by(|left, right| left.1.total_cmp(&right.1));
162
163 regenerating.map_or_else(
164 || {
165 self.slots
166 .iter()
167 .position(|slot| matches!(slot, RuneSlotState::Depleted))
168 },
169 |(index, _)| Some(index),
170 )
171 }
172
173 fn start_depleted(&mut self) {
174 let available = MAX_REGENERATING_RUNES.saturating_sub(self.regenerating_count());
175
176 for slot in self
177 .slots
178 .iter_mut()
179 .filter(|slot| matches!(slot, RuneSlotState::Depleted))
180 .take(available)
181 {
182 *slot = RuneSlotState::Regenerating {
183 remaining: RUNE_RECHARGE_SECONDS,
184 };
185 }
186 }
187}
188
189impl Default for RuneState {
190 fn default() -> Self {
191 Self {
192 slots: [RuneSlotState::Full; RUNE_COUNT],
193 updated_at: SimTime::ZERO,
194 }
195 }
196}
197
198#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
200#[non_exhaustive]
201#[must_use]
202pub enum ResourceGainSource {
203 #[default]
204 Unattributed,
205 AutoAttack,
206 Spell(u32),
207 SpellEffect {
208 spell_id: u32,
209 effect_index: u8,
210 },
211}
212
213#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
215#[non_exhaustive]
216#[must_use]
217pub(crate) enum ResourceGainScaling {
218 #[default]
219 Modified,
220 Unmodified,
221}
222
223#[derive(Clone, Copy, Debug, PartialEq)]
225#[must_use]
226pub(crate) struct ResourceGain {
227 pub amount: f64,
228 pub source: ResourceGainSource,
229 pub scaling: ResourceGainScaling,
230}
231
232impl ResourceGain {
233 pub(crate) const fn modified(amount: f64, source: ResourceGainSource) -> Self {
234 Self {
235 amount,
236 source,
237 scaling: ResourceGainScaling::Modified,
238 }
239 }
240
241 pub(crate) const fn unmodified(amount: f64, source: ResourceGainSource) -> Self {
242 Self {
243 amount,
244 source,
245 scaling: ResourceGainScaling::Unmodified,
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests;