Skip to main content

wowlab_engine_combat/context/hook/
rppm.rs

1use wowlab_engine_rng as stochastic;
2use wowlab_types::sim::{SimTime, SpellIdx};
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.state.defs.rppm_trackers.get_mut(idx.as_usize()) {
13            stochastic::roll_rppm(tracker, now_secs, scaling, self.rng)
14        } else {
15            false
16        }
17    }
18
19    /// 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.
20    fn roll_threshold(
21        &mut self,
22        idx: crate::state::LocalThresholdIdx,
23        increment_scale: f64,
24    ) -> bool {
25        let Some(tracker) = self.state.defs.threshold_trackers.get_mut(idx.as_usize()) else {
26            return false;
27        };
28
29        if tracker.accumulated.is_nan() {
30            tracker.accumulated = (self.rng)();
31        }
32
33        let increment = (self.rng)() * tracker.increment_max * increment_scale;
34
35        stochastic::roll_threshold(tracker, increment)
36    }
37
38    fn roll_item_rppm(&mut self, item_id: u32) -> bool {
39        let Some(&idx) = self.state.index.item_rppm_indices.get(&item_id) else {
40            return false;
41        };
42
43        self.roll_rppm(idx)
44    }
45
46    /// Roll a shared proc system's RPPM tracker by its DBC driver spell id.
47    ///
48    /// The driver also owns its optional DBC proc-category recovery.
49    ///
50    /// Attempts inside that window do not advance the tracker. Successful rolls start recovery.
51    fn roll_system_rppm(&mut self, driver_spell_id: u32) -> bool {
52        if !self.proc_category_ready(driver_spell_id) {
53            return false;
54        }
55
56        let Some(&idx) = self.state.index.system_rppm_indices.get(&driver_spell_id) else {
57            return false;
58        };
59
60        if !self.roll_rppm(idx) {
61            return false;
62        }
63
64        self.start_proc_category_cooldown(driver_spell_id);
65
66        true
67    }
68
69    /// Whether a DBC proc driver is outside its `ProcCategoryRecovery` window.
70    fn proc_category_ready(&self, driver_spell_id: u32) -> bool {
71        self.state
72            .runtime
73            .procs
74            .proc_category_ready_at
75            .get(&driver_spell_id)
76            .is_none_or(|ready_at| self.now >= *ready_at)
77    }
78
79    /// Start the DBC `ProcCategoryRecovery` window for a proc driver.
80    fn start_proc_category_cooldown(&mut self, driver_spell_id: u32) {
81        let recovery_ms = self
82            .state
83            .config
84            .game_data
85            .proc_category_recovery_ms(SpellIdx::from_raw(driver_spell_id))
86            .unwrap_or(0);
87
88        if recovery_ms == 0 {
89            return;
90        }
91
92        self.state.runtime.procs.proc_category_ready_at.insert(
93            driver_spell_id,
94            self.now.saturating_add(SimTime::from_millis(recovery_ms)),
95        );
96    }
97
98    fn roll_enchant_rppm(&mut self, enchantment_id: u32) -> bool {
99        let Some(&idx) = self.state.index.enchant_rppm_indices.get(&enchantment_id) else {
100            return false;
101        };
102
103        self.roll_rppm(idx)
104    }
105
106    fn roll_accumulating_proc(&mut self, driver_spell_id: u32, chance_per_attempt: f64) -> bool {
107        self.roll_attempt_proc(driver_spell_id, |attempt| {
108            chance_per_attempt * f64::from(attempt)
109        })
110    }
111
112    /// Roll a driver-scoped proc whose chance is a function of its current attempt number.
113    fn roll_attempt_proc(
114        &mut self,
115        driver_spell_id: u32,
116        chance_for_attempt: impl FnOnce(u32) -> f64,
117    ) -> bool {
118        let attempts = self
119            .state
120            .runtime
121            .procs
122            .proc_attempts
123            .get(&driver_spell_id)
124            .copied()
125            .unwrap_or(0);
126        let attempt = attempts.saturating_add(1);
127
128        if stochastic::proc_chance(self.rng, chance_for_attempt(attempt)) {
129            self.state
130                .runtime
131                .procs
132                .proc_attempts
133                .remove(&driver_spell_id);
134
135            true
136        } else {
137            self.state
138                .runtime
139                .procs
140                .proc_attempts
141                .insert(driver_spell_id, attempt);
142
143            false
144        }
145    }
146
147    /// Add to a driver-scoped event counter and return its new value.
148    fn add_proc_counter(&mut self, driver_spell_id: u32, amount: u32) -> u32 {
149        proc_counters::add(self, driver_spell_id, amount)
150    }
151
152    /// Clear a driver-scoped event counter.
153    fn reset_proc_counter(&mut self, driver_spell_id: u32) {
154        proc_counters::reset(self, driver_spell_id);
155    }
156
157    /// Read a driver-scoped event counter without changing it.
158    fn proc_counter(&self, driver_spell_id: u32) -> u32 {
159        proc_counters::get(self, driver_spell_id)
160    }
161
162    /// Mark one or more driver-scoped flags and return `true` only on their first occurrence.
163    fn mark_proc_flags_once(&mut self, driver_spell_id: u32, flags: u32) -> bool {
164        proc_counters::mark_once(self, driver_spell_id, flags)
165    }
166
167    /// Consume from a driver-scoped event counter and return the remaining value.
168    fn consume_proc_counter(&mut self, driver_spell_id: u32, amount: u32) -> u32 {
169        proc_counters::consume(self, driver_spell_id, amount)
170    }
171
172    /// Advance a driver-scoped event counter and return `true` at the threshold.
173    fn advance_proc_counter_by(
174        &mut self,
175        driver_spell_id: u32,
176        amount: u32,
177        threshold: u32,
178    ) -> bool {
179        proc_counters::advance(self, driver_spell_id, amount, threshold)
180    }
181
182    /// Advance a driver-scoped event counter by one and return `true` at the threshold.
183    fn advance_proc_counter(&mut self, driver_spell_id: u32, threshold: u32) -> bool {
184        self.advance_proc_counter_by(driver_spell_id, 1, threshold)
185    }
186}