wowlab_engine_combat/systems/
cooldowns.rs1use 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
17const 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;