1use super::{
2 BuffEffect, CombatState, DenseBuffer, Event, LocalSpellIdx, MS_PER_SECOND, RECHARGE_EPSILON_S,
3 SimTime, SpellData, charge_pool_members, charge_pool_owner, consume_charge,
4 effective_category_cooldown_ms, effective_cooldown_ms, effective_recharge_ms,
5 for_each_active_aura_effect, grant_charges, reduce_charged_cooldown, spell_at,
6 start_one_charge_category, start_regular_category, sync_charge_pool,
7};
8
9fn projected_ready_at(state: &CombatState, spell_local: LocalSpellIdx) -> SimTime {
10 let spell = spell_at(state, spell_local);
11 let own = state.runtime.pools.cooldown_ready_at.ready_at(&spell_local);
12 let category = spell
13 .cooldown
14 .cooldown_category
15 .map_or(SimTime::ZERO, |category| {
16 state
17 .runtime
18 .pools
19 .cooldown_category_ready_at
20 .ready_at(&category)
21 });
22 let charge_category = (!spell.is_charged())
23 .then_some(spell.cooldown.charge_category)
24 .flatten()
25 .map_or(SimTime::ZERO, |category| {
26 state
27 .runtime
28 .pools
29 .charge_category_ready_at
30 .ready_at(&category)
31 });
32
33 own.max(category).max(charge_category)
34}
35
36pub(super) fn project_ready_at(
37 state: &CombatState,
38 buf: &mut DenseBuffer,
39 spell_local: LocalSpellIdx,
40) {
41 let spell = spell_at(state, spell_local);
42 let pooled = projected_ready_at(state, spell_local).as_secs_f64();
43
44 if let Some(cooldown) = buf.cooldown_mut(spell.idx()) {
45 cooldown.ready_at = if spell.is_charged() && cooldown.current_charges <= 0 {
47 pooled.max(cooldown.next_charge_at)
48 } else {
49 pooled
50 };
51 }
52}
53
54pub(super) fn project_members(
55 state: &CombatState,
56 buf: &mut DenseBuffer,
57 members: &[LocalSpellIdx],
58) {
59 for member in members.iter().copied() {
60 project_ready_at(state, buf, member);
61 }
62}
63
64fn ignores_charge_cooldown(state: &CombatState, buf: &DenseBuffer, spell: &SpellData) -> bool {
65 let Some(category) = spell.cooldown.charge_category else {
66 return false;
67 };
68 let mut ignored = false;
69
70 for_each_active_aura_effect(state, buf, |_, effect| {
71 if matches!(
72 effect,
73 BuffEffect::IgnoreChargeCooldownCategory(effect_category)
74 if *effect_category == category.raw()
75 ) {
76 ignored = true;
77 }
78 });
79
80 ignored
81}
82
83fn start_private_cooldown(
84 state: &mut CombatState,
85 spell: &SpellData,
86 spell_local: LocalSpellIdx,
87 cooldown_ms: u32,
88 now: SimTime,
89) {
90 if spell.is_charged() || cooldown_ms == 0 {
91 state.runtime.pools.cooldown_ready_at.reset(&spell_local);
92
93 return;
94 }
95
96 let ready_at = state.runtime.pools.cooldown_ready_at.start_for(
97 spell_local,
98 now,
99 SimTime::from_millis(cooldown_ms),
100 );
101
102 state.schedule(Event::CooldownReady {
103 t: ready_at,
104 cooldown_key: spell.idx(),
105 });
106}
107
108pub(crate) fn start_cooldown(
109 state: &mut CombatState,
110 buf: &mut DenseBuffer,
111 spell_local: LocalSpellIdx,
112 now: SimTime,
113) -> u32 {
114 start_cooldown_in_slot(state, buf, spell_local, spell_local, now)
115}
116
117pub(crate) fn start_cooldown_in_slot(
123 state: &mut CombatState,
124 buf: &mut DenseBuffer,
125 recovery_local: LocalSpellIdx,
126 slot_local: LocalSpellIdx,
127 now: SimTime,
128) -> u32 {
129 let spell_local = slot_local;
130 let spell = spell_at(state, slot_local);
131 let recovery = spell_at(state, recovery_local);
132 let charge_members = charge_pool_members(state, spell_local);
133 let charge_owner = charge_pool_owner(state, spell_local);
134 let cooldown_local = if spell.is_charged() || spell.cooldown.charge_category.is_some() {
135 charge_owner
136 } else {
137 spell_local
138 };
139 let cooldown_spell = spell_at(state, cooldown_local);
140 let idx = cooldown_spell.idx();
141 let (cooldown_ms, cooldown_rate) = effective_cooldown_ms(state, buf, &recovery);
142 let category_cooldown_ms = effective_category_cooldown_ms(state, buf, &recovery);
143 let (recharge_ms, recharge_rate) = effective_recharge_ms(state, buf, &recovery);
144 let ignore_charge_cooldown = ignores_charge_cooldown(state, buf, &spell);
145 let applied_ms = cooldown_ms
146 .max(category_cooldown_ms)
147 .max(if spell.cooldown.max_charges > 0 {
148 recharge_ms
149 } else {
150 0
151 });
152
153 if let Some(rate) = state
154 .runtime
155 .recharge_rate_mults
156 .get_mut(cooldown_local.as_usize())
157 {
158 *rate = if spell.is_charged() {
159 recharge_rate
160 } else {
161 cooldown_rate
162 };
163 }
164
165 start_private_cooldown(state, &spell, spell_local, cooldown_ms, now);
166
167 let Some(cd) = buf.cooldown_mut(idx) else {
168 return applied_ms;
169 };
170
171 if spell.is_charged() && !ignore_charge_cooldown {
172 consume_charge(state, cd, &spell, recharge_ms, now);
173 } else {
174 cd.duration = f64::from(cooldown_ms) / MS_PER_SECOND;
175 }
176
177 if spell.is_charged() {
178 sync_charge_pool(state, buf, charge_owner, &charge_members);
179 project_members(state, buf, &charge_members);
180 }
181
182 if !ignore_charge_cooldown {
183 start_one_charge_category(state, buf, &spell, &charge_members, recharge_ms, now);
184 }
185
186 start_regular_category(state, buf, &spell, category_cooldown_ms, now);
187 project_ready_at(state, buf, spell_local);
188
189 applied_ms
190}
191
192pub(crate) fn reduce_cooldown(
193 state: &mut CombatState,
194 buf: &mut DenseBuffer,
195 spell_local: LocalSpellIdx,
196 amount_ms: u32,
197 now: SimTime,
198) {
199 let spell = spell_at(state, spell_local);
200 let members = charge_pool_members(state, spell_local);
201 let owner = charge_pool_owner(state, spell_local);
202 let reduction_s = f64::from(amount_ms) / MS_PER_SECOND;
203
204 if !spell.is_charged() {
205 let ready_at_before = state.runtime.pools.cooldown_ready_at.ready_at(&spell_local);
206 let ready_at_after = state.runtime.pools.cooldown_ready_at.reduce(
207 spell_local,
208 SimTime::from_millis(amount_ms),
209 now,
210 );
211 let mut category_ready_at_before = SimTime::ZERO;
212 let mut category_ready_at_after = SimTime::ZERO;
213 let mut category_reduced = false;
214 let mut charge_category_ready_at_before = SimTime::ZERO;
215 let mut charge_category_ready_at_after = SimTime::ZERO;
216 let mut charge_category_reduced = false;
217
218 if let Some(category) = spell.cooldown.charge_category {
219 charge_category_ready_at_before = state
220 .runtime
221 .pools
222 .charge_category_ready_at
223 .ready_at(&category);
224 charge_category_ready_at_after = state.runtime.pools.charge_category_ready_at.reduce(
225 category,
226 SimTime::from_millis(amount_ms),
227 now,
228 );
229 charge_category_reduced = true;
230
231 if charge_category_ready_at_after > now {
232 for member in members.iter().copied() {
233 state.schedule(Event::CooldownReady {
234 t: charge_category_ready_at_after,
235 cooldown_key: spell_at(state, member).idx(),
236 });
237 }
238 }
239
240 project_members(state, buf, &members);
241 }
242
243 if spell.cooldown.cooldown_duration_ms == 0
248 && spell.cooldown.category_cooldown_duration_ms > 0
249 && let Some(category) = spell.cooldown.cooldown_category
250 {
251 category_ready_at_before = state
252 .runtime
253 .pools
254 .cooldown_category_ready_at
255 .ready_at(&category);
256 category_ready_at_after = state.runtime.pools.cooldown_category_ready_at.reduce(
257 category,
258 SimTime::from_millis(amount_ms),
259 now,
260 );
261 category_reduced = true;
262
263 let category_members = state.index.cooldown_categories.members(&category).to_vec();
264
265 if category_ready_at_after > now {
266 for member in category_members.iter().copied() {
267 state.schedule(Event::CooldownReady {
268 t: category_ready_at_after,
269 cooldown_key: spell_at(state, member).idx(),
270 });
271 }
272 }
273
274 project_members(state, buf, &category_members);
275 }
276
277 project_ready_at(state, buf, spell_local);
278
279 if ready_at_after > now {
280 state.schedule(Event::CooldownReady {
281 t: ready_at_after,
282 cooldown_key: spell.idx(),
283 });
284 }
285
286 tracing::trace!(
287 spell_id = spell.spell_id,
288 amount_ms,
289 ready_at_before = ready_at_before.as_secs_f64(),
290 ready_at_after = ready_at_after.as_secs_f64(),
291 category_reduced,
292 category_ready_at_before = category_ready_at_before.as_secs_f64(),
293 category_ready_at_after = category_ready_at_after.as_secs_f64(),
294 charge_category_reduced,
295 charge_category_ready_at_before = charge_category_ready_at_before.as_secs_f64(),
296 charge_category_ready_at_after = charge_category_ready_at_after.as_secs_f64(),
297 "COOLDOWN_REDUCTION"
298 );
299
300 return;
301 }
302
303 let owner_spell = spell_at(state, owner);
304 let (recharge_ms, _) = effective_recharge_ms(state, buf, &spell);
305 let Some(cd) = buf.cooldown_mut(owner_spell.idx()) else {
306 return;
307 };
308 let next_charge_at_before = cd.next_charge_at;
309 let charges_before = cd.current_charges;
310
311 reduce_charged_cooldown(state, cd, owner_spell.idx(), recharge_ms, reduction_s, now);
312 let next_charge_at_after = cd.next_charge_at;
313 let charges_after = cd.current_charges;
314
315 sync_charge_pool(state, buf, owner, &members);
316 project_members(state, buf, &members);
317 tracing::trace!(
318 spell_id = spell.spell_id,
319 amount_ms,
320 next_charge_at_before,
321 next_charge_at_after,
322 charges_before,
323 charges_after,
324 "COOLDOWN_REDUCTION"
325 );
326}
327
328fn recharge_cooldown(
329 state: &mut CombatState,
330 buf: &mut DenseBuffer,
331 spell_local: LocalSpellIdx,
332 charges: u8,
333) {
334 if charges == 0 {
335 return;
336 }
337
338 let spell = spell_at(state, spell_local);
339
340 if spell.is_charged() {
341 grant_charges(state, buf, spell_local, charges);
342
343 return;
344 }
345
346 let Some(category) = spell.cooldown.charge_category else {
347 reset_cooldown(state, buf, spell_local);
348
349 return;
350 };
351 let members = charge_pool_members(state, spell_local);
352
353 state
354 .runtime
355 .pools
356 .charge_category_ready_at
357 .reset(&category);
358
359 for member in &members {
360 state.runtime.pools.cooldown_ready_at.reset(member);
361 }
362
363 project_members(state, buf, &members);
364}
365
366pub(crate) fn mutate_cooldown(
367 state: &mut CombatState,
368 buf: &mut DenseBuffer,
369 spell_local: LocalSpellIdx,
370 operation: wowlab_engine_domain::dbc::CooldownMutation,
371 now: SimTime,
372) {
373 match operation {
374 wowlab_engine_domain::dbc::CooldownMutation::ReduceRemaining { amount_ms } => {
375 reduce_cooldown(state, buf, spell_local, amount_ms, now);
376 }
377 wowlab_engine_domain::dbc::CooldownMutation::Recharge { charges } => {
378 recharge_cooldown(state, buf, spell_local, charges);
379 }
380 }
381}
382
383pub(crate) fn reset_cooldown(
384 state: &mut CombatState,
385 buf: &mut DenseBuffer,
386 spell_local: LocalSpellIdx,
387) {
388 let spell = spell_at(state, spell_local);
389 let members = charge_pool_members(state, spell_local);
390 let owner = charge_pool_owner(state, spell_local);
391 let target_local = if spell.is_charged() {
392 owner
393 } else {
394 spell_local
395 };
396 let target_spell = spell_at(state, target_local);
397 let idx = target_spell.idx();
398
399 if let Some(category) = spell
400 .cooldown
401 .charge_category
402 .filter(|_| !spell.is_charged())
403 {
404 state
405 .runtime
406 .pools
407 .charge_category_ready_at
408 .reset(&category);
409
410 for member in &members {
411 state.runtime.pools.cooldown_ready_at.reset(member);
412 }
413 } else {
414 state.runtime.pools.cooldown_ready_at.reset(&spell_local);
415 }
416
417 let Some(cd) = buf.cooldown_mut(idx) else {
418 return;
419 };
420
421 if spell.is_charged() {
424 let recharge_s = f64::from(spell.cooldown.recharge_ms) / MS_PER_SECOND;
425
426 cd.duration = recharge_s;
427 cd.current_charges = i32::from(spell.cooldown.max_charges);
428 cd.max_charges = i32::from(spell.cooldown.max_charges);
429 cd.next_charge_at = 0.0;
430 cd.recharge_time = recharge_s;
431 } else {
432 cd.duration = f64::from(spell.cooldown.cooldown_duration_ms) / MS_PER_SECOND;
433 cd.max_charges = i32::from(spell.cooldown.max_charges);
434 }
435
436 if spell.is_charged() {
437 sync_charge_pool(state, buf, owner, &members);
438 project_members(state, buf, &members);
439 } else if spell.cooldown.charge_category.is_some() {
440 project_members(state, buf, &members);
441 } else {
442 project_ready_at(state, buf, spell_local);
443 }
444}
445
446pub(crate) fn check_recharge(
447 state: &mut CombatState,
448 buf: &mut DenseBuffer,
449 spell_local: LocalSpellIdx,
450 now: SimTime,
451) {
452 let spell = spell_at(state, spell_local);
453
454 if !spell.is_charged() {
455 return;
456 }
457
458 let members = charge_pool_members(state, spell_local);
459 let owner = charge_pool_owner(state, spell_local);
460
461 if spell_local != owner {
462 return;
463 }
464
465 let owner_spell = spell_at(state, owner);
466 let idx = owner_spell.idx();
467 let (recharge_ms, recharge_rate) = effective_recharge_ms(state, buf, &spell);
468 let recharge_duration_s = f64::from(recharge_ms) / MS_PER_SECOND;
469
470 if let Some(rate) = state.runtime.recharge_rate_mults.get_mut(owner.as_usize()) {
471 *rate = recharge_rate;
472 }
473
474 let Some(cd) = buf.cooldown_mut(idx) else {
475 return;
476 };
477
478 let charges = cd.current_charges;
479 let max = cd.max_charges;
481
482 if charges >= max {
483 return;
484 }
485
486 if now.as_secs_f64() + RECHARGE_EPSILON_S < cd.next_charge_at {
487 return;
488 }
489
490 let new_charges = charges + 1;
491 let next_at = if new_charges < max {
493 cd.duration = recharge_duration_s;
494 cd.recharge_time = recharge_duration_s;
495 let next_ready = now.saturating_add(SimTime::from_millis(recharge_ms));
496
497 state.schedule(Event::CooldownReady {
498 t: next_ready,
499 cooldown_key: idx,
500 });
501
502 next_ready.as_secs_f64()
503 } else {
504 0.0
505 };
506
507 cd.current_charges = new_charges;
508 cd.next_charge_at = next_at;
509 sync_charge_pool(state, buf, owner, &members);
510}