Skip to main content

wowlab_engine_combat/systems/buffs/
timing.rs

1use super::*;
2
3fn spell_timing_percent(
4    view: &CombatView<'_>,
5    spell_id: u32,
6    select: impl Fn(&BuffEffect) -> Option<(f64, &'static [u32], bool)>,
7) -> f64 {
8    let mut percent = 0.0;
9
10    for_each_active_aura_effect(view.state, view.buf, |stacks, effect| {
11        if let Some((value, spells, per_stack)) = select(effect) {
12            if spells.contains(&spell_id) {
13                percent += value * if per_stack { stacks } else { 1.0 };
14            }
15        }
16    });
17
18    percent
19}
20
21pub(crate) fn data_driven_timing_modifiers(
22    view: &CombatView<'_>,
23    spell_id: u32,
24    select: impl Fn(&BuffEffect) -> Option<(f64, u32, u8, ModifierOperation)>,
25) -> (f64, f64) {
26    data_driven_actor_timing_modifiers(view.for_actor(ActorId::Player), spell_id, select)
27}
28
29pub(crate) fn data_driven_actor_timing_modifiers(
30    view: ActorView<'_>,
31    spell_id: u32,
32    select: impl Fn(&BuffEffect) -> Option<(f64, u32, u8, ModifierOperation)>,
33) -> (f64, f64) {
34    let ActorView { state, .. } = view;
35    let mut flat = 0.0;
36    let mut percent = 0.0;
37
38    for_each_active_actor_aura_effect(view, |stacks, effect| {
39        let Some((value, source_spell_id, effect_index, operation)) = select(effect) else {
40            return;
41        };
42
43        if !state.config.game_data.effect_affects_spell(
44            wowlab_types::sim::SpellIdx::from_raw(source_spell_id),
45            effect_index,
46            wowlab_types::sim::SpellIdx::from_raw(spell_id),
47        ) {
48            return;
49        }
50
51        let points = dbc_effect_points(state, source_spell_id, effect_index, stacks);
52
53        match operation {
54            ModifierOperation::Flat => flat += value * points,
55            ModifierOperation::Percent => percent += value * points,
56            _ => {}
57        }
58    });
59
60    (flat, percent)
61}
62
63pub(crate) fn effective_cast_time_ms(
64    view: &CombatView<'_>,
65    spell_id: u32,
66    base_cast_time: u32,
67) -> u32 {
68    if base_cast_time == 0 {
69        return 0;
70    }
71
72    let configured_percent = spell_timing_percent(view, spell_id, |effect| match effect {
73        BuffEffect::CastTimePercent(value, spells) => Some((*value, *spells, false)),
74        BuffEffect::CastTimePercentPerStack(value, spells) => Some((*value, *spells, true)),
75        _ => None,
76    });
77    let (flat, data_percent) =
78        data_driven_timing_modifiers(view, spell_id, |effect| match effect {
79            BuffEffect::SpellCastTimeFromEffect {
80                value,
81                source_spell_id,
82                effect_index,
83                operation,
84            } => Some((*value, *source_spell_id, *effect_index, *operation)),
85            _ => None,
86        });
87    let percent = configured_percent + data_percent;
88    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
89    let haste_mult = current_haste_mult(view);
90
91    if flat.abs() > f64::EPSILON || percent.abs() > f64::EPSILON {
92        tracing::trace!(
93            spell_id,
94            base_cast_time,
95            configured_percent,
96            data_percent,
97            flat,
98            haste_mult,
99            "cast time modifiers resolved"
100        );
101    }
102
103    wowlab_types::numeric::f64_to_u32_saturating_trunc(
104        (f64::from(base_cast_time) + flat).max(0.0) * timing_mult / haste_mult,
105    )
106}
107
108pub(crate) fn effective_gcd_ms(view: &CombatView<'_>, spell_id: u32, base_gcd: u32) -> u32 {
109    if base_gcd < GCD_FLOOR_MS {
110        return base_gcd;
111    }
112
113    let mut percent = spell_timing_percent(view, spell_id, |effect| match effect {
114        BuffEffect::GcdPercent(value, spells) => Some((*value, *spells, false)),
115        _ => None,
116    });
117    let (flat, data_percent) =
118        data_driven_timing_modifiers(view, spell_id, |effect| match effect {
119            BuffEffect::SpellGcdFromEffect {
120                value,
121                source_spell_id,
122                effect_index,
123                operation,
124            } => Some((*value, *source_spell_id, *effect_index, *operation)),
125            _ => None,
126        });
127
128    percent += data_percent;
129    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
130    let haste_mult =
131        view.state
132            .spell_data(spell_id)
133            .map_or(1.0, |(_, spell)| match spell.gcd_haste_type {
134                wowlab_types::combat::GcdHasteType::SpellCastSpeed => current_haste_mult(view),
135                wowlab_types::combat::GcdHasteType::AttackHaste => current_attack_haste_mult(view),
136                _ => 1.0,
137            });
138    let scaled = wowlab_types::numeric::f64_to_u32_saturating_trunc(
139        (f64::from(base_gcd) + flat).max(0.0) * timing_mult / haste_mult,
140    );
141
142    scaled.clamp(GCD_FLOOR_MS, DEFAULT_GCD_MS)
143}
144
145pub(crate) fn effective_aura_duration_ms(
146    view: &CombatView<'_>,
147    aura_id: u32,
148    base_duration_ms: u32,
149) -> u32 {
150    if base_duration_ms == 0 {
151        return 0;
152    }
153
154    let (flat, percent) = data_driven_timing_modifiers(view, aura_id, |effect| match effect {
155        BuffEffect::SpellDurationFromEffect {
156            value,
157            source_spell_id,
158            effect_index,
159            operation,
160        } => Some((*value, *source_spell_id, *effect_index, *operation)),
161        _ => None,
162    });
163    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
164
165    wowlab_types::numeric::f64_to_u32_saturating_round(
166        (f64::from(base_duration_ms) + flat).max(0.0) * timing_mult,
167    )
168}
169
170pub(crate) fn effective_periodic_tick_ms(
171    view: &CombatView<'_>,
172    aura_id: u32,
173    base_tick_ms: u32,
174) -> u32 {
175    let base_tick_ms = if base_tick_ms == 0 {
176        DEFAULT_PERIODIC_INTERVAL_MS
177    } else {
178        base_tick_ms
179    };
180    let (flat, percent) = data_driven_timing_modifiers(view, aura_id, |effect| match effect {
181        BuffEffect::SpellTickTimeFromEffect {
182            value,
183            source_spell_id,
184            effect_index,
185            operation,
186        } => Some((*value, *source_spell_id, *effect_index, *operation)),
187        _ => None,
188    });
189    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
190
191    wowlab_types::numeric::f64_to_u32_saturating_round(
192        (f64::from(base_tick_ms) + flat).max(1.0) * timing_mult,
193    )
194    .max(1)
195}
196
197/// Resolve the integer wall-clock cadence used by `TrinityCore`'s periodic timer.
198pub(crate) fn effective_periodic_interval_ms(
199    view: &CombatView<'_>,
200    aura_id: u32,
201    base_tick_ms: u32,
202    hasted: bool,
203) -> u32 {
204    let tick_ms = effective_periodic_tick_ms(view, aura_id, base_tick_ms);
205
206    if hasted {
207        wowlab_types::numeric::f64_to_u32_saturating_trunc(
208            f64::from(tick_ms) / current_haste_mult(view),
209        )
210        .max(1)
211    } else {
212        tick_ms
213    }
214}