Skip to main content

wowlab_engine_combat/context/hook/
rppm.rs

1use wowlab_engine_rng as stochastic;
2use wowlab_types::sim::SimTime;
3
4use super::{ProcOps, proc_counters};
5use crate::{context::HookCtx, state::LocalRppmIdx, systems};
6
7impl ProcOps for HookCtx<'_> {
8    fn roll_rppm(&mut self, idx: LocalRppmIdx) -> bool {
9        let now_secs = self.now.as_secs_f64();
10        let scaling = systems::current_rppm_scaling(&self.view().combat());
11
12        if let Some(tracker) = self
13            .instance
14            .runtime
15            .procs
16            .rppm_trackers
17            .get_mut(idx.as_usize())
18        {
19            stochastic::roll_rppm(tracker, now_secs, scaling, self.rng)
20        } else {
21            false
22        }
23    }
24
25    /// Add one trigger event to an accumulated-threshold proc; `increment_scale` multiplies the tracker's uniform roll (plain callers pass `1.0`), returning `true` when the threshold procs.
26    fn roll_threshold(
27        &mut self,
28        idx: crate::state::LocalThresholdIdx,
29        increment_scale: f64,
30    ) -> bool {
31        let Some(tracker) = self
32            .instance
33            .runtime
34            .procs
35            .threshold_trackers
36            .get_mut(idx.as_usize())
37        else {
38            return false;
39        };
40
41        if tracker.accumulated.is_nan() {
42            tracker.accumulated = (self.rng)();
43        }
44
45        let increment = (self.rng)() * tracker.increment_max * increment_scale;
46
47        stochastic::roll_threshold(tracker, increment)
48    }
49
50    /// Roll a shared proc system's RPPM tracker by its DBC driver spell id.
51    ///
52    /// The driver also owns its optional DBC proc-category recovery.
53    ///
54    /// Attempts inside that window do not advance the tracker. Successful rolls start recovery.
55    fn roll_system_rppm(&mut self, binding: crate::SystemRppmBinding) -> bool {
56        let Some(resolved) = self
57            .program
58            .system_rppm_bindings
59            .get(binding.as_usize())
60            .copied()
61            .flatten()
62        else {
63            return false;
64        };
65        let Some(ready_at) = self
66            .instance
67            .runtime
68            .procs
69            .system_rppm_ready_at
70            .get(binding.as_usize())
71            .copied()
72        else {
73            return false;
74        };
75
76        if self.now < ready_at || !self.roll_rppm(resolved.tracker) {
77            return false;
78        }
79
80        if resolved.recovery_ms > 0 {
81            // BOUNDS: the binding was read successfully from the same finalized dense table above.
82            self.instance.runtime.procs.system_rppm_ready_at[binding.as_usize()] = self
83                .now
84                .saturating_add(SimTime::from_millis(resolved.recovery_ms));
85        }
86
87        true
88    }
89
90    /// Whether a DBC proc driver is outside its `ProcCategoryRecovery` window.
91    fn proc_category_ready(&self, binding: crate::ProcCategoryBinding) -> bool {
92        self.instance
93            .runtime
94            .procs
95            .proc_category_ready_at_dense
96            .get(binding.as_usize())
97            .is_none_or(|ready_at| self.now >= *ready_at)
98    }
99
100    /// Start the DBC `ProcCategoryRecovery` window for a proc driver.
101    fn start_proc_category_cooldown(&mut self, binding: crate::ProcCategoryBinding) {
102        let recovery_ms = self
103            .program
104            .proc_category_recovery_ms
105            .get(binding.as_usize())
106            .copied()
107            .unwrap_or(0);
108
109        if recovery_ms == 0 {
110            return;
111        }
112
113        if let Some(ready_at) = self
114            .instance
115            .runtime
116            .procs
117            .proc_category_ready_at_dense
118            .get_mut(binding.as_usize())
119        {
120            *ready_at = self.now.saturating_add(SimTime::from_millis(recovery_ms));
121        }
122    }
123
124    fn roll_accumulating_proc(
125        &mut self,
126        binding: crate::ProcCounterBinding,
127        chance_per_attempt: f64,
128    ) -> bool {
129        self.roll_attempt_proc(binding, |attempt| chance_per_attempt * f64::from(attempt))
130    }
131
132    /// Roll a driver-scoped proc whose chance is a function of its current attempt number.
133    fn roll_attempt_proc(
134        &mut self,
135        binding: crate::ProcCounterBinding,
136        chance_for_attempt: impl FnOnce(u32) -> f64,
137    ) -> bool {
138        let Some(attempts) = self
139            .instance
140            .runtime
141            .procs
142            .proc_counters
143            .get_mut(binding.as_usize())
144        else {
145            return false;
146        };
147        let attempt = attempts.saturating_add(1);
148
149        if stochastic::proc_chance(self.rng, chance_for_attempt(attempt)) {
150            *attempts = 0;
151
152            true
153        } else {
154            *attempts = attempt;
155
156            false
157        }
158    }
159
160    /// Add to a driver-scoped event counter and return its new value.
161    fn add_proc_counter(&mut self, binding: crate::ProcCounterBinding, amount: u32) -> u32 {
162        proc_counters::add(self, binding, amount)
163    }
164
165    /// Clear a driver-scoped event counter.
166    fn reset_proc_counter(&mut self, binding: crate::ProcCounterBinding) {
167        proc_counters::reset(self, binding);
168    }
169
170    /// Read a driver-scoped event counter without changing it.
171    fn proc_counter(&self, binding: crate::ProcCounterBinding) -> u32 {
172        proc_counters::get(self, binding)
173    }
174
175    /// Mark one or more driver-scoped flags and return `true` only on their first occurrence.
176    fn mark_proc_flags_once(&mut self, binding: crate::ProcCounterBinding, flags: u32) -> bool {
177        proc_counters::mark_once(self, binding, flags)
178    }
179
180    /// Consume from a driver-scoped event counter and return the remaining value.
181    fn consume_proc_counter(&mut self, binding: crate::ProcCounterBinding, amount: u32) -> u32 {
182        proc_counters::consume(self, binding, amount)
183    }
184
185    /// Advance a driver-scoped event counter and return `true` at the threshold.
186    fn advance_proc_counter_by(
187        &mut self,
188        binding: crate::ProcCounterBinding,
189        amount: u32,
190        threshold: u32,
191    ) -> bool {
192        proc_counters::advance(self, binding, amount, threshold)
193    }
194
195    /// Advance a driver-scoped event counter by one and return `true` at the threshold.
196    fn advance_proc_counter(&mut self, binding: crate::ProcCounterBinding, threshold: u32) -> bool {
197        self.advance_proc_counter_by(binding, 1, threshold)
198    }
199}