wowlab_engine_combat/systems/cooldowns/
rates.rs1use super::*;
2
3fn current_cooldown_rate_mult(state: &CombatState, buf: &DenseBuffer, spell: &SpellData) -> f64 {
4 let mut rate = if spell.cooldown.cooldown_hasted {
5 current_haste_mult(&crate::CombatView::new(state, buf))
6 } else {
7 1.0
8 };
9 let category_cooldown = spell.is_charged() || spell.cooldown.charge_category.is_some();
10
11 for_each_active_aura_effect(state, buf, |stacks, effect| match effect {
12 BuffEffect::RechargeRatePercent(percent, spells) if spells.contains(&spell.spell_id) => {
13 rate *= 1.0 + percent * stacks / HUNDRED;
14 }
15 BuffEffect::CooldownRechargeRateFromEffect {
16 percent,
17 source_spell_id,
18 effect_index,
19 } if !category_cooldown => {
20 let source = wowlab_types::sim::SpellIdx::from_raw(*source_spell_id);
21 let target = wowlab_types::sim::SpellIdx::from_raw(spell.spell_id);
22
23 if state
24 .config
25 .game_data
26 .effect_affects_spell(source, *effect_index, target)
27 {
28 rate *= 1.0 + percent * stacks / HUNDRED;
29 }
30 }
31 BuffEffect::ChargeRechargeRateCategory {
32 percent, category, ..
33 } if category_cooldown
34 && spell
35 .cooldown
36 .charge_category
37 .is_some_and(|spell_category| spell_category.raw() == *category) =>
38 {
39 rate *= 1.0 + percent * stacks / HUNDRED;
40 }
41 _ => {}
42 });
43
44 rate.max(f64::EPSILON)
45}
46
47pub(super) fn effective_recharge_ms(
48 state: &CombatState,
49 buf: &DenseBuffer,
50 spell: &SpellData,
51) -> (u32, f64) {
52 let rate = current_cooldown_rate_mult(state, buf, spell);
53 let duration = wowlab_types::numeric::f64_to_u32_saturating_trunc(
54 f64::from(spell.cooldown.recharge_ms) / rate,
55 );
56
57 (
58 duration.max(u32::from(spell.cooldown.recharge_ms > 0)),
59 rate,
60 )
61}
62
63pub(super) fn effective_cooldown_duration_ms(
64 state: &CombatState,
65 buf: &DenseBuffer,
66 spell: &SpellData,
67 base_duration_ms: u32,
68) -> (u32, f64) {
69 let rate = current_cooldown_rate_mult(state, buf, spell);
70 let mut duration_mult = 1.0;
71
72 for_each_active_aura_effect(state, buf, |stacks, effect| {
73 if let BuffEffect::CooldownDurationPercent {
74 percent,
75 source_spell_id,
76 effect_index,
77 } = effect
78 {
79 let source = wowlab_types::sim::SpellIdx::from_raw(*source_spell_id);
80 let target = wowlab_types::sim::SpellIdx::from_raw(spell.spell_id);
81 let affected =
82 state
83 .config
84 .game_data
85 .effect_affects_spell(source, *effect_index, target);
86
87 if affected {
88 let points = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
89
90 duration_mult *= (1.0 + percent * points / HUNDRED).max(0.0);
91 }
92 }
93 });
94 let duration = wowlab_types::numeric::f64_to_u32_saturating_trunc(
95 f64::from(base_duration_ms) * duration_mult / rate,
96 );
97
98 (duration, rate)
99}
100
101pub(super) fn effective_cooldown_ms(
102 state: &CombatState,
103 buf: &DenseBuffer,
104 spell: &SpellData,
105) -> (u32, f64) {
106 effective_cooldown_duration_ms(state, buf, spell, spell.cooldown.cooldown_duration_ms)
107}
108
109pub(super) fn effective_category_cooldown_ms(
110 state: &CombatState,
111 buf: &DenseBuffer,
112 spell: &SpellData,
113) -> u32 {
114 effective_cooldown_duration_ms(
115 state,
116 buf,
117 spell,
118 spell.cooldown.category_cooldown_duration_ms,
119 )
120 .0
121}
122
123fn reproject_deadline(deadline_s: f64, now_s: f64, old_rate: f64, new_rate: f64) -> Option<f64> {
124 (deadline_s > now_s).then(|| now_s + (deadline_s - now_s) * old_rate / new_rate)
125}
126
127#[derive(Clone, Copy)]
128struct ActiveCooldownDeadlines {
129 private: SimTime,
130 charge: Option<(wowlab_types::data::CooldownCategoryId, SimTime)>,
131}
132
133fn update_recharge_slot(
134 buf: &mut DenseBuffer,
135 spell: &SpellData,
136 now_s: f64,
137 old_rate: f64,
138 new_rate: f64,
139 deadlines: ActiveCooldownDeadlines,
140) -> (Option<f64>, Option<f64>) {
141 let Some(cd) = buf.cooldown_mut(spell.idx()) else {
142 return (None, None);
143 };
144
145 if spell.is_charged() {
146 let base_recharge_s = f64::from(spell.cooldown.recharge_ms) / MS_PER_SECOND;
147
148 cd.duration = base_recharge_s / new_rate;
149 cd.recharge_time = cd.duration;
150 let next_at = (cd.current_charges < cd.max_charges)
151 .then(|| reproject_deadline(cd.next_charge_at, now_s, old_rate, new_rate))
152 .flatten();
153
154 if let Some(next_at) = next_at {
155 cd.next_charge_at = next_at;
156 }
157
158 return (None, next_at);
159 }
160
161 let base_duration_s = f64::from(spell.cooldown.cooldown_duration_ms) / MS_PER_SECOND;
162
163 cd.duration = base_duration_s / new_rate;
164 let private = reproject_deadline(deadlines.private.as_secs_f64(), now_s, old_rate, new_rate);
165 let charge = deadlines.charge.and_then(|(_, ready_at)| {
166 reproject_deadline(ready_at.as_secs_f64(), now_s, old_rate, new_rate)
167 });
168
169 (private, charge)
170}
171
172fn sync_spell_recharge_rate(
173 state: &mut CombatState,
174 buf: &mut DenseBuffer,
175 spell: &SpellData,
176 local: LocalSpellIdx,
177 now: SimTime,
178) {
179 let now_s = now.as_secs_f64();
180 let i = local.as_usize();
181 let new_rate = current_cooldown_rate_mult(state, buf, spell);
182 let old_rate = state
183 .runtime
184 .recharge_rate_mults
185 .get(i)
186 .copied()
187 .unwrap_or(1.0)
188 .max(f64::EPSILON);
189
190 if let Some(rate) = state.runtime.recharge_rate_mults.get_mut(i) {
191 *rate = new_rate;
192 }
193
194 let private_ready_at = state.runtime.pools.cooldown_ready_at.ready_at(&local);
195 let charge_ready_at = spell.cooldown.charge_category.map(|category| {
196 (
197 category,
198 state
199 .runtime
200 .pools
201 .charge_category_ready_at
202 .ready_at(&category),
203 )
204 });
205 let deadlines = ActiveCooldownDeadlines {
206 private: private_ready_at,
207 charge: charge_ready_at,
208 };
209 let (private_rescheduled, charge_rescheduled) =
210 update_recharge_slot(buf, spell, now_s, old_rate, new_rate, deadlines);
211
212 if !spell.is_charged() {
213 if let Some(ready_at) = private_rescheduled {
214 state
215 .runtime
216 .pools
217 .cooldown_ready_at
218 .start(local, SimTime::from_secs_f64(ready_at));
219 state.schedule(Event::CooldownReady {
220 t: SimTime::from_secs_f64(ready_at),
221 cooldown_key: spell.idx(),
222 });
223 }
224
225 if let (Some((category, _)), Some(ready_at)) = (charge_ready_at, charge_rescheduled) {
226 state
227 .runtime
228 .pools
229 .charge_category_ready_at
230 .start(category, SimTime::from_secs_f64(ready_at));
231 let members = charge_pool_members(state, local);
232
233 for member in members.iter().copied() {
234 state.schedule(Event::CooldownReady {
235 t: SimTime::from_secs_f64(ready_at),
236 cooldown_key: spell_at(state, member).idx(),
237 });
238 }
239
240 project_members(state, buf, &members);
241 } else {
242 project_ready_at(state, buf, local);
243 }
244 }
245
246 if spell.is_charged() {
247 let members = charge_pool_members(state, local);
248
249 sync_charge_pool(state, buf, local, &members);
250 project_members(state, buf, &members);
251 }
252
253 if spell.is_charged() {
254 if let Some(next_at) = charge_rescheduled {
255 state.schedule(Event::CooldownReady {
256 t: SimTime::from_secs_f64(next_at),
257 cooldown_key: spell.idx(),
258 });
259 }
260 }
261}
262
263pub(crate) fn sync_recharge_rates(state: &mut CombatState, buf: &mut DenseBuffer, now: SimTime) {
264 for i in 0..state.defs.spells.len() {
265 let Some(spell) = state.defs.spells.get(i).copied() else {
266 continue;
267 };
268
269 if !spell.cooldown.has_cooldown {
270 continue;
271 }
272
273 let local = LocalSpellIdx::new(u8::try_from(i).expect("spell count validated below 256"));
274
275 if (spell.is_charged() || spell.cooldown.charge_category.is_some())
276 && charge_pool_owner(state, local) != local
277 {
278 continue;
279 }
280
281 sync_spell_recharge_rate(state, buf, &spell, local, now);
282 }
283}