Skip to main content

wowlab_engine_combat/state/
resource.rs

1//! Typed resource-gain requests and provenance.
2
3use 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/// Origin of a discrete resource gain.
199#[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/// Whether active resource-gain modifiers may scale a gain.
214#[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/// A discrete resource gain carried through the resource pipeline.
224#[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;