Skip to main content

wowlab_engine_combat/systems/cooldowns/
rates.rs

1use 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}