Skip to main content

wowlab_engine_combat/systems/
cooldowns.rs

1//! Cooldown start, reduction, reset, and charge-recharge logic.
2
3use wowlab_engine_ports::Event;
4use wowlab_types::{
5    constants::{HUNDRED, MS_PER_SECOND},
6    sim::SimTime,
7};
8
9use super::{
10    buffs::{dbc_effect_points, for_each_planned_player_effect},
11    current_haste_mult,
12};
13#[cfg(feature = "rotation-optimizations")]
14use crate::state::runtime::RotationMutationDirection;
15use crate::state::{BuffEffect, CombatInstance, LocalSpellIdx, SpellData};
16
17// Event times are ms-quantized but next_charge_at carries float drift; a recharge rounded onto the same ms must still count as due.
18const RECHARGE_EPSILON_S: f64 = 0.0005;
19
20mod categories;
21mod charges;
22mod rates;
23mod recharge;
24
25use categories::{start_one_charge_category, start_regular_category};
26use charges::{
27    charge_pool_members, charge_pool_owner, consume_charge, grant_charges_state,
28    reduce_charged_cooldown, spell_at, sync_charge_pool,
29};
30pub(crate) use charges::{grant_charges, refresh_maximum_charges_for};
31pub(crate) use rates::refresh_recharge_rates_for;
32use rates::{effective_category_cooldown_ms, effective_cooldown_ms, effective_recharge_ms};
33#[cfg(feature = "rotation-optimizations")]
34pub(crate) use rates::{initialize_cooldown_projections, reset_cooldown_projections};
35pub(crate) use recharge::{
36    check_recharge, mutate_cooldown, reduce_cooldown, reset_cooldown, reset_cooldown_for_lifecycle,
37    start_cooldown, start_cooldown_in_slot,
38};
39use recharge::{project_members, project_ready_at};
40
41#[cfg(feature = "rotation-optimizations")]
42const COOLDOWN_READINESS_WORD_COUNT: usize = 4;
43
44#[derive(Clone, Copy, Debug, Default)]
45#[cfg(feature = "rotation-optimizations")]
46struct CooldownReadinessSnapshot {
47    present: [u64; COOLDOWN_READINESS_WORD_COUNT],
48    ready: [u64; COOLDOWN_READINESS_WORD_COUNT],
49}
50
51#[cfg(feature = "rotation-optimizations")]
52impl CooldownReadinessSnapshot {
53    fn capture(
54        program: &crate::state::CombatProgram,
55        state: &CombatInstance,
56        members: &[LocalSpellIdx],
57        now: SimTime,
58    ) -> Self {
59        let mut snapshot = Self::default();
60
61        for &member in members {
62            let Some(ready) = cooldown_readiness(program, state, member, now) else {
63                continue;
64            };
65            let index = member.as_usize();
66            let word = index / u64::BITS as usize;
67            let bit = 1_u64 << (index % u64::BITS as usize);
68
69            let (Some(present), Some(ready_word)) =
70                (snapshot.present.get_mut(word), snapshot.ready.get_mut(word))
71            else {
72                continue;
73            };
74
75            *present |= bit;
76
77            if ready {
78                *ready_word |= bit;
79            }
80        }
81
82        snapshot
83    }
84
85    fn record(
86        self,
87        program: &crate::state::CombatProgram,
88        state: &mut CombatInstance,
89        members: &[LocalSpellIdx],
90        now: SimTime,
91    ) {
92        for &member in members {
93            let index = member.as_usize();
94            let word = index / u64::BITS as usize;
95            let bit = 1_u64 << (index % u64::BITS as usize);
96
97            let present = self.present.get(word).copied().unwrap_or(0);
98            let ready = self.ready.get(word).copied().unwrap_or(0);
99
100            if present & bit == 0 {
101                continue;
102            }
103
104            let before = ready & bit != 0;
105
106            let Some(after) = cooldown_readiness(program, state, member, now) else {
107                continue;
108            };
109
110            let direction = match (before, after) {
111                (false, true) => RotationMutationDirection::Rise,
112                (true, false) => RotationMutationDirection::Fall,
113                _ => continue,
114            };
115
116            state
117                .runtime
118                .rotation_mutations
119                .record_cooldown(member, direction, now);
120        }
121    }
122}
123
124#[cfg(feature = "rotation-optimizations")]
125fn cooldown_readiness(
126    program: &crate::state::CombatProgram,
127    state: &CombatInstance,
128    local: LocalSpellIdx,
129    now: SimTime,
130) -> Option<bool> {
131    let cooldown = program.buffer_cooldown(state, local)?;
132    let ready = if cooldown.max_charges > 1 {
133        cooldown.current_charges > 0
134    } else {
135        cooldown.ready_at <= now.as_secs_f64()
136    };
137
138    Some(ready)
139}
140
141#[cfg(test)]
142#[path = "cooldowns/dbc_tests.rs"]
143mod dbc_tests;
144
145#[cfg(test)]
146#[path = "cooldowns/effect_tests.rs"]
147mod effect_tests;
148
149#[cfg(test)]
150#[path = "cooldowns/tests.rs"]
151mod tests;