1#[cfg(feature = "rotation-optimizations")]
2use super::CooldownReadinessSnapshot;
3use super::{
4 BuffEffect, CombatInstance, Event, LocalSpellIdx, MS_PER_SECOND, RECHARGE_EPSILON_S, SimTime,
5 SpellData, charge_pool_members, charge_pool_owner, consume_charge,
6 effective_category_cooldown_ms, effective_cooldown_ms, effective_recharge_ms,
7 for_each_planned_player_effect, grant_charges_state, reduce_charged_cooldown, spell_at,
8 start_one_charge_category, start_regular_category, sync_charge_pool,
9};
10
11fn projected_ready_at(
12 program: &crate::state::CombatProgram,
13 state: &CombatInstance,
14 spell_local: LocalSpellIdx,
15) -> SimTime {
16 let spell = spell_at(program, spell_local);
17 let own = state.runtime.pools.cooldown_ready_at.ready_at(spell_local);
18 let category = spell.cooldown.cooldown_category.map_or(SimTime::ZERO, |_| {
19 state
20 .runtime
21 .pools
22 .cooldown_category_ready_at
23 .ready_at(spell_local)
24 });
25 let charge_category = (!spell.is_charged())
26 .then_some(spell.cooldown.charge_category)
27 .flatten()
28 .map_or(SimTime::ZERO, |_| {
29 state
30 .runtime
31 .pools
32 .charge_category_ready_at
33 .ready_at(spell_local)
34 });
35
36 own.max(category).max(charge_category)
37}
38
39pub(super) fn project_ready_at(
40 program: &crate::state::CombatProgram,
41 state: &mut CombatInstance,
42 spell_local: LocalSpellIdx,
43) {
44 let spell = spell_at(program, spell_local);
45 let pooled = projected_ready_at(program, state, spell_local).as_secs_f64();
46
47 if let Some(cooldown) = program.buffer_cooldown_mut(state, spell_local) {
48 cooldown.ready_at = if spell.is_charged() && cooldown.current_charges <= 0 {
50 pooled.max(cooldown.next_charge_at)
51 } else {
52 pooled
53 };
54 }
55}
56
57pub(super) fn project_members(
58 program: &crate::state::CombatProgram,
59 state: &mut CombatInstance,
60 members: &[LocalSpellIdx],
61) {
62 for member in members.iter().copied() {
63 project_ready_at(program, state, member);
64 }
65}
66
67fn ignores_charge_cooldown(
68 program: &crate::state::CombatProgram,
69 state: &CombatInstance,
70 spell: &SpellData,
71) -> bool {
72 let Some(category) = spell.cooldown.charge_category else {
73 return false;
74 };
75 let mut ignored = false;
76
77 for_each_planned_player_effect(
78 program,
79 state,
80 crate::state::EffectFamily::IGNORED_CHARGE_COOLDOWN,
81 |_, effect| {
82 if matches!(
83 effect,
84 BuffEffect::IgnoreChargeCooldownCategory(effect_category)
85 if *effect_category == category.raw()
86 ) {
87 ignored = true;
88 }
89 },
90 );
91
92 ignored
93}
94
95fn start_private_cooldown(
96 program: &crate::state::CombatProgram,
97 state: &mut CombatInstance,
98 spell: &SpellData,
99 spell_local: LocalSpellIdx,
100 cooldown_ms: u32,
101 now: SimTime,
102) {
103 if spell.is_charged() || cooldown_ms == 0 {
104 state.runtime.pools.cooldown_ready_at.reset(spell_local);
105
106 return;
107 }
108
109 let ready_at = state.runtime.pools.cooldown_ready_at.start_for(
110 spell_local,
111 now,
112 SimTime::from_millis(cooldown_ms),
113 );
114
115 state.schedule(Event::CooldownReady {
116 t: ready_at,
117 spell: program.scheduled_spell(spell_local),
118 });
119}
120
121pub(crate) fn start_cooldown(
122 program: &crate::state::CombatProgram,
123 state: &mut CombatInstance,
124 spell_local: LocalSpellIdx,
125 now: SimTime,
126) -> u32 {
127 start_cooldown_in_slot(program, state, spell_local, spell_local, now)
128}
129
130pub(crate) fn start_cooldown_in_slot(
134 program: &crate::state::CombatProgram,
135 state: &mut CombatInstance,
136 recovery_local: LocalSpellIdx,
137 slot_local: LocalSpellIdx,
138 now: SimTime,
139) -> u32 {
140 let spell_local = slot_local;
141 let spell = spell_at(program, slot_local);
142 let recovery = spell_at(program, recovery_local);
143 let charge_members = charge_pool_members(program, spell_local);
144 #[cfg(feature = "rotation-optimizations")]
145 let charge_readiness_before =
146 CooldownReadinessSnapshot::capture(program, state, charge_members.as_slice(), now);
147 #[cfg(feature = "rotation-optimizations")]
148 let cooldown_category_members: &[LocalSpellIdx] = program
149 .cooldown_category_members
150 .get(spell_local.as_usize())
151 .map_or(&[], Box::as_ref);
152 #[cfg(feature = "rotation-optimizations")]
153 let category_readiness_before =
154 CooldownReadinessSnapshot::capture(program, state, cooldown_category_members, now);
155 let charge_owner = charge_pool_owner(program, spell_local);
156 let cooldown_local = if spell.is_charged() || spell.cooldown.charge_category.is_some() {
157 charge_owner
158 } else {
159 spell_local
160 };
161 let (cooldown_ms, cooldown_rate) =
162 effective_cooldown_ms(program, state, recovery_local, &recovery);
163 let category_cooldown_ms =
164 effective_category_cooldown_ms(program, state, recovery_local, &recovery);
165 let (recharge_ms, recharge_rate) =
166 effective_recharge_ms(program, state, recovery_local, &recovery);
167 let ignore_charge_cooldown = ignores_charge_cooldown(program, state, &spell);
168 let applied_ms = cooldown_ms
169 .max(category_cooldown_ms)
170 .max(if spell.cooldown.max_charges > 0 {
171 recharge_ms
172 } else {
173 0
174 });
175
176 if let Some(rate) = state
177 .runtime
178 .recharge_rate_mults
179 .get_mut(cooldown_local.as_usize())
180 {
181 *rate = if spell.is_charged() {
182 recharge_rate
183 } else {
184 cooldown_rate
185 };
186 }
187
188 start_private_cooldown(program, state, &spell, spell_local, cooldown_ms, now);
189
190 let Some(handle) = program.cooldown_binding(cooldown_local) else {
191 return applied_ms;
192 };
193 let cd = state.buffer.cooldown_mut_at(handle);
194
195 if spell.is_charged() && !ignore_charge_cooldown {
196 consume_charge(
197 &mut state.runtime.pending_events,
198 cd,
199 program.scheduled_spell(cooldown_local),
200 recharge_ms,
201 now,
202 );
203 } else {
204 cd.duration = f64::from(cooldown_ms) / MS_PER_SECOND;
205 }
206
207 if spell.is_charged() {
208 sync_charge_pool(program, state, charge_owner, charge_members.as_slice());
209 project_members(program, state, charge_members.as_slice());
210 }
211
212 if !ignore_charge_cooldown {
213 start_one_charge_category(
214 program,
215 state,
216 &spell,
217 charge_members.as_slice(),
218 recharge_ms,
219 now,
220 );
221 }
222
223 start_regular_category(
224 program,
225 state,
226 spell_local,
227 &spell,
228 category_cooldown_ms,
229 now,
230 );
231 project_ready_at(program, state, spell_local);
232
233 #[cfg(feature = "rotation-optimizations")]
234 {
235 charge_readiness_before.record(program, state, charge_members.as_slice(), now);
236 category_readiness_before.record(program, state, cooldown_category_members, now);
237 }
238
239 applied_ms
240}
241
242pub(crate) fn reduce_cooldown(
244 program: &crate::state::CombatProgram,
245 state: &mut CombatInstance,
246 spell_local: LocalSpellIdx,
247 amount_ms: u32,
248 now: SimTime,
249) {
250 let spell = spell_at(program, spell_local);
251 let members = charge_pool_members(program, spell_local);
252 #[cfg(feature = "rotation-optimizations")]
253 let readiness_before =
254 CooldownReadinessSnapshot::capture(program, state, members.as_slice(), now);
255 #[cfg(feature = "rotation-optimizations")]
256 let cooldown_category_members: &[LocalSpellIdx] = program
257 .cooldown_category_members
258 .get(spell_local.as_usize())
259 .map_or(&[], Box::as_ref);
260 #[cfg(feature = "rotation-optimizations")]
261 let category_readiness_before =
262 CooldownReadinessSnapshot::capture(program, state, cooldown_category_members, now);
263 let owner = charge_pool_owner(program, spell_local);
264 let reduction_s = f64::from(amount_ms) / MS_PER_SECOND;
265
266 if !spell.is_charged() {
267 let ready_at_before = state.runtime.pools.cooldown_ready_at.ready_at(spell_local);
268 let ready_at_after = state.runtime.pools.cooldown_ready_at.reduce(
269 spell_local,
270 SimTime::from_millis(amount_ms),
271 now,
272 );
273 let mut category_ready_at_before = SimTime::ZERO;
274 let mut category_ready_at_after = SimTime::ZERO;
275 let mut category_reduced = false;
276 let mut charge_category_ready_at_before = SimTime::ZERO;
277 let mut charge_category_ready_at_after = SimTime::ZERO;
278 let mut charge_category_reduced = false;
279
280 if spell.cooldown.charge_category.is_some() {
281 charge_category_ready_at_before = state
282 .runtime
283 .pools
284 .charge_category_ready_at
285 .ready_at(spell_local);
286 charge_category_ready_at_after = state.runtime.pools.charge_category_ready_at.reduce(
287 spell_local,
288 SimTime::from_millis(amount_ms),
289 now,
290 );
291
292 for member in members.as_slice().iter().copied() {
293 state
294 .runtime
295 .pools
296 .charge_category_ready_at
297 .start(member, charge_category_ready_at_after);
298 }
299
300 charge_category_reduced = true;
301
302 if charge_category_ready_at_after > now {
303 for member in members.as_slice().iter().copied() {
304 state.schedule(Event::CooldownReady {
305 t: charge_category_ready_at_after,
306 spell: program.scheduled_spell(member),
307 });
308 }
309 }
310
311 project_members(program, state, members.as_slice());
312 }
313
314 if spell.cooldown.cooldown_duration_ms == 0
319 && spell.cooldown.category_cooldown_duration_ms > 0
320 && spell.cooldown.cooldown_category.is_some()
321 {
322 category_ready_at_before = state
323 .runtime
324 .pools
325 .cooldown_category_ready_at
326 .ready_at(spell_local);
327 category_ready_at_after = state.runtime.pools.cooldown_category_ready_at.reduce(
328 spell_local,
329 SimTime::from_millis(amount_ms),
330 now,
331 );
332 category_reduced = true;
333
334 let category_members: &[LocalSpellIdx] = program
335 .cooldown_category_members
336 .get(spell_local.as_usize())
337 .map_or(&[], Box::as_ref);
338
339 for member in category_members.iter().copied() {
340 state
341 .runtime
342 .pools
343 .cooldown_category_ready_at
344 .start(member, category_ready_at_after);
345 }
346
347 if category_ready_at_after > now {
348 for member in category_members.iter().copied() {
349 state.schedule(Event::CooldownReady {
350 t: category_ready_at_after,
351 spell: program.scheduled_spell(member),
352 });
353 }
354 }
355
356 project_members(program, state, category_members);
357 }
358
359 project_ready_at(program, state, spell_local);
360
361 if ready_at_after > now {
362 state.schedule(Event::CooldownReady {
363 t: ready_at_after,
364 spell: program.scheduled_spell(spell_local),
365 });
366 }
367
368 tracing::trace!(
369 spell_id = spell.spell_id,
370 amount_ms,
371 ready_at_before = ready_at_before.as_secs_f64(),
372 ready_at_after = ready_at_after.as_secs_f64(),
373 category_reduced,
374 category_ready_at_before = category_ready_at_before.as_secs_f64(),
375 category_ready_at_after = category_ready_at_after.as_secs_f64(),
376 charge_category_reduced,
377 charge_category_ready_at_before = charge_category_ready_at_before.as_secs_f64(),
378 charge_category_ready_at_after = charge_category_ready_at_after.as_secs_f64(),
379 "COOLDOWN_REDUCTION"
380 );
381
382 #[cfg(feature = "rotation-optimizations")]
383 {
384 readiness_before.record(program, state, members.as_slice(), now);
385 category_readiness_before.record(program, state, cooldown_category_members, now);
386 }
387
388 return;
389 }
390
391 let (recharge_ms, _) = effective_recharge_ms(program, state, spell_local, &spell);
392 let Some(handle) = program.cooldown_binding(owner) else {
393 return;
394 };
395 let cd = state.buffer.cooldown_mut_at(handle);
396 let next_charge_at_before = cd.next_charge_at;
397 let charges_before = cd.current_charges;
398
399 reduce_charged_cooldown(
400 &mut state.runtime.pending_events,
401 cd,
402 program.scheduled_spell(owner),
403 recharge_ms,
404 reduction_s,
405 now,
406 );
407 let next_charge_at_after = cd.next_charge_at;
408 let charges_after = cd.current_charges;
409
410 sync_charge_pool(program, state, owner, members.as_slice());
411 project_members(program, state, members.as_slice());
412 tracing::trace!(
413 spell_id = spell.spell_id,
414 amount_ms,
415 next_charge_at_before,
416 next_charge_at_after,
417 charges_before,
418 charges_after,
419 "COOLDOWN_REDUCTION"
420 );
421
422 #[cfg(feature = "rotation-optimizations")]
423 readiness_before.record(program, state, members.as_slice(), now);
424}
425
426fn recharge_cooldown(
427 program: &crate::state::CombatProgram,
428 state: &mut CombatInstance,
429 spell_local: LocalSpellIdx,
430 charges: u8,
431) {
432 if charges == 0 {
433 return;
434 }
435
436 let spell = spell_at(program, spell_local);
437
438 if spell.is_charged() {
439 grant_charges_state(program, state, spell_local, charges);
440
441 return;
442 }
443
444 let Some(_) = spell.cooldown.charge_category else {
445 reset_cooldown_state(program, state, spell_local);
446
447 return;
448 };
449 let members = charge_pool_members(program, spell_local);
450
451 state
452 .runtime
453 .pools
454 .charge_category_ready_at
455 .reset(spell_local);
456
457 for member in members.as_slice() {
458 state.runtime.pools.cooldown_ready_at.reset(*member);
459 state.runtime.pools.charge_category_ready_at.reset(*member);
460 }
461
462 project_members(program, state, members.as_slice());
463}
464
465pub(crate) fn mutate_cooldown(
466 program: &crate::state::CombatProgram,
467 state: &mut CombatInstance,
468 spell_local: LocalSpellIdx,
469 operation: wowlab_engine_domain::dbc::CooldownMutation,
470 now: SimTime,
471) {
472 match operation {
473 wowlab_engine_domain::dbc::CooldownMutation::ReduceRemaining { amount_ms } => {
474 reduce_cooldown(program, state, spell_local, amount_ms, now);
475 }
476 wowlab_engine_domain::dbc::CooldownMutation::Recharge { charges } => {
477 #[cfg(feature = "rotation-optimizations")]
478 let members = charge_pool_members(program, spell_local);
479 #[cfg(feature = "rotation-optimizations")]
480 let readiness_before =
481 CooldownReadinessSnapshot::capture(program, state, members.as_slice(), now);
482
483 recharge_cooldown(program, state, spell_local, charges);
484
485 #[cfg(feature = "rotation-optimizations")]
486 readiness_before.record(program, state, members.as_slice(), now);
487 }
488 }
489}
490
491pub(crate) fn reset_cooldown(
492 program: &crate::state::CombatProgram,
493 state: &mut CombatInstance,
494 spell_local: LocalSpellIdx,
495 _now: SimTime,
496) {
497 #[cfg(feature = "rotation-optimizations")]
498 let members = charge_pool_members(program, spell_local);
499 #[cfg(feature = "rotation-optimizations")]
500 let readiness_before =
501 CooldownReadinessSnapshot::capture(program, state, members.as_slice(), _now);
502
503 reset_cooldown_state(program, state, spell_local);
504
505 #[cfg(feature = "rotation-optimizations")]
506 readiness_before.record(program, state, members.as_slice(), _now);
507}
508
509pub(crate) fn reset_cooldown_for_lifecycle(
510 program: &crate::state::CombatProgram,
511 state: &mut CombatInstance,
512 spell_local: LocalSpellIdx,
513) {
514 reset_cooldown_state(program, state, spell_local);
515}
516
517fn reset_cooldown_state(
518 program: &crate::state::CombatProgram,
519 state: &mut CombatInstance,
520 spell_local: LocalSpellIdx,
521) {
522 let spell = spell_at(program, spell_local);
523 let members = charge_pool_members(program, spell_local);
524 let owner = charge_pool_owner(program, spell_local);
525 let target_local = if spell.is_charged() {
526 owner
527 } else {
528 spell_local
529 };
530
531 if spell
532 .cooldown
533 .charge_category
534 .as_ref()
535 .is_some_and(|_| !spell.is_charged())
536 {
537 for member in members.as_slice() {
538 state.runtime.pools.cooldown_ready_at.reset(*member);
539 state.runtime.pools.charge_category_ready_at.reset(*member);
540 }
541 } else {
542 state.runtime.pools.cooldown_ready_at.reset(spell_local);
543 }
544
545 let Some(handle) = program.cooldown_binding(target_local) else {
546 return;
547 };
548 let cd = state.buffer.cooldown_mut_at(handle);
549
550 if spell.is_charged() {
553 let recharge_s = f64::from(spell.cooldown.recharge_ms) / MS_PER_SECOND;
554
555 cd.duration = recharge_s;
556 cd.current_charges = i32::from(spell.cooldown.max_charges);
557 cd.max_charges = i32::from(spell.cooldown.max_charges);
558 cd.next_charge_at = 0.0;
559 cd.recharge_time = recharge_s;
560 } else {
561 cd.duration = f64::from(spell.cooldown.cooldown_duration_ms) / MS_PER_SECOND;
562 cd.max_charges = i32::from(spell.cooldown.max_charges);
563 }
564
565 if spell.is_charged() {
566 sync_charge_pool(program, state, owner, members.as_slice());
567 project_members(program, state, members.as_slice());
568 } else if spell.cooldown.charge_category.is_some() {
569 project_members(program, state, members.as_slice());
570 } else {
571 project_ready_at(program, state, spell_local);
572 }
573}
574
575pub(crate) fn check_recharge(
576 program: &crate::state::CombatProgram,
577 state: &mut CombatInstance,
578 spell_local: LocalSpellIdx,
579 now: SimTime,
580) {
581 let spell = spell_at(program, spell_local);
582
583 if !spell.is_charged() {
584 return;
585 }
586
587 let members = charge_pool_members(program, spell_local);
588 let owner = charge_pool_owner(program, spell_local);
589
590 if spell_local != owner {
591 return;
592 }
593
594 #[cfg(feature = "rotation-optimizations")]
595 let readiness_before =
596 CooldownReadinessSnapshot::capture(program, state, members.as_slice(), now);
597
598 let (recharge_ms, recharge_rate) = effective_recharge_ms(program, state, spell_local, &spell);
599 let recharge_duration_s = f64::from(recharge_ms) / MS_PER_SECOND;
600
601 if let Some(rate) = state.runtime.recharge_rate_mults.get_mut(owner.as_usize()) {
602 *rate = recharge_rate;
603 }
604
605 let scheduled_spell = program.scheduled_spell(owner);
606
607 let Some(handle) = program.cooldown_binding(owner) else {
608 return;
609 };
610 let cd = state.buffer.cooldown_mut_at(handle);
611
612 let charges = cd.current_charges;
613 let max = cd.max_charges;
615
616 if charges >= max {
617 return;
618 }
619
620 if now.as_secs_f64() + RECHARGE_EPSILON_S < cd.next_charge_at {
621 return;
622 }
623
624 let new_charges = charges + 1;
625 let next_at = if new_charges < max {
627 cd.duration = recharge_duration_s;
628 cd.recharge_time = recharge_duration_s;
629 let next_ready = now.saturating_add(SimTime::from_millis(recharge_ms));
630
631 state.runtime.pending_events.push(Event::CooldownReady {
632 t: next_ready,
633 spell: scheduled_spell,
634 });
635
636 next_ready.as_secs_f64()
637 } else {
638 0.0
639 };
640
641 cd.current_charges = new_charges;
642 cd.next_charge_at = next_at;
643 sync_charge_pool(program, state, owner, members.as_slice());
644
645 #[cfg(feature = "rotation-optimizations")]
646 readiness_before.record(program, state, members.as_slice(), now);
647}