Skip to main content

wowlab_engine_combat/systems/buffs/
timing.rs

1use wowlab_engine_domain::dbc::ModifierPropertyKind;
2
3use super::{
4    ActorId, ActorView, BuffEffect, CombatView, DEFAULT_GCD_MS, DEFAULT_PERIODIC_INTERVAL_MS,
5    GCD_FLOOR_MS, HUNDRED, ModifierOperation, current_attack_haste_mult, current_haste_mult,
6    dbc_effect_points, for_each_active_spell_modifier_for_action,
7};
8
9fn spell_timing_percent(
10    view: &CombatView<'_>,
11    target: crate::state::program::LocalDamageActionIdx,
12    property: ModifierPropertyKind,
13    select: impl Fn(&BuffEffect) -> Option<(f64, bool)>,
14) -> f64 {
15    let mut percent = 0.0;
16
17    for_each_active_spell_modifier_for_action(
18        ActorView::new(view.program, view.instance, ActorId::Player),
19        target,
20        property,
21        |stacks, effect, _binding| {
22            if let Some((value, per_stack)) = select(effect) {
23                percent += value * if per_stack { stacks } else { 1.0 };
24            }
25        },
26    );
27
28    percent
29}
30
31pub(crate) fn data_driven_timing_modifiers(
32    view: &CombatView<'_>,
33    target: crate::state::program::LocalDamageActionIdx,
34    property: ModifierPropertyKind,
35    select: impl Fn(&BuffEffect) -> Option<(f64, u32, u8, ModifierOperation)>,
36) -> (f64, f64) {
37    data_driven_actor_timing_modifiers(view.for_actor(ActorId::Player), target, property, select)
38}
39
40pub(crate) fn data_driven_actor_timing_modifiers(
41    view: ActorView<'_>,
42    target: crate::state::program::LocalDamageActionIdx,
43    property: ModifierPropertyKind,
44    select: impl Fn(&BuffEffect) -> Option<(f64, u32, u8, ModifierOperation)>,
45) -> (f64, f64) {
46    let ActorView {
47        program,
48        instance: state,
49        ..
50    } = view;
51    let mut flat = 0.0;
52    let mut percent = 0.0;
53
54    for_each_active_spell_modifier_for_action(view, target, property, |stacks, effect, binding| {
55        let Some((value, _source_spell_id, _effect_index, operation)) = select(effect) else {
56            return;
57        };
58
59        if !binding
60            .source_effect
61            .is_some_and(|source| program.damage.effect_affects_action(source, target))
62        {
63            return;
64        }
65
66        let points = dbc_effect_points(
67            program,
68            state,
69            binding
70                .source_effect
71                .expect("matched source binding exists"),
72            stacks,
73        );
74
75        match operation {
76            ModifierOperation::Flat => flat += value * points,
77            ModifierOperation::Percent => percent += value * points,
78            _ => {}
79        }
80    });
81
82    (flat, percent)
83}
84
85pub(crate) fn effective_cast_time_ms(
86    view: &CombatView<'_>,
87    local: crate::state::LocalSpellIdx,
88    base_cast_time: u32,
89) -> u32 {
90    if base_cast_time == 0 {
91        return 0;
92    }
93
94    let target = view.program.damage.spell_action(local);
95    let configured_percent = spell_timing_percent(
96        view,
97        target,
98        ModifierPropertyKind::CastTime,
99        |effect| match effect {
100            BuffEffect::CastTimePercent(value, _) => Some((*value, false)),
101            BuffEffect::CastTimePercentPerStack(value, _) => Some((*value, true)),
102            _ => None,
103        },
104    );
105    let (flat, data_percent) =
106        data_driven_timing_modifiers(view, target, ModifierPropertyKind::CastTime, |effect| {
107            match effect {
108                BuffEffect::SpellCastTimeFromEffect {
109                    value,
110                    source_spell_id,
111                    effect_index,
112                    operation,
113                } => Some((*value, *source_spell_id, *effect_index, *operation)),
114                _ => None,
115            }
116        });
117    let percent = configured_percent + data_percent;
118    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
119    let haste_mult = current_haste_mult(view);
120
121    if flat.abs() > f64::EPSILON || percent.abs() > f64::EPSILON {
122        tracing::trace!(
123            spell_id = view.program.spell(local).spell_id,
124            base_cast_time,
125            configured_percent,
126            data_percent,
127            flat,
128            haste_mult,
129            "cast time modifiers resolved"
130        );
131    }
132
133    wowlab_types::numeric::f64_to_u32_saturating_trunc(
134        (f64::from(base_cast_time) + flat).max(0.0) * timing_mult / haste_mult,
135    )
136}
137
138pub(crate) fn effective_gcd_ms(
139    view: &CombatView<'_>,
140    local: crate::state::LocalSpellIdx,
141    base_gcd: u32,
142) -> u32 {
143    if base_gcd < GCD_FLOOR_MS {
144        return base_gcd;
145    }
146
147    let target = view.program.damage.spell_action(local);
148    let mut percent = spell_timing_percent(
149        view,
150        target,
151        ModifierPropertyKind::GlobalCooldown,
152        |effect| match effect {
153            BuffEffect::GcdPercent(value, _) => Some((*value, false)),
154            _ => None,
155        },
156    );
157    let (flat, data_percent) = data_driven_timing_modifiers(
158        view,
159        target,
160        ModifierPropertyKind::GlobalCooldown,
161        |effect| match effect {
162            BuffEffect::SpellGcdFromEffect {
163                value,
164                source_spell_id,
165                effect_index,
166                operation,
167            } => Some((*value, *source_spell_id, *effect_index, *operation)),
168            _ => None,
169        },
170    );
171
172    percent += data_percent;
173    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
174    let haste_mult = match view.program.spell(local).gcd_haste_type {
175        wowlab_types::combat::GcdHasteType::SpellCastSpeed => current_haste_mult(view),
176        wowlab_types::combat::GcdHasteType::AttackHaste => current_attack_haste_mult(view),
177        _ => 1.0,
178    };
179    let scaled = wowlab_types::numeric::f64_to_u32_saturating_trunc(
180        (f64::from(base_gcd) + flat).max(0.0) * timing_mult / haste_mult,
181    );
182
183    scaled.clamp(GCD_FLOOR_MS, DEFAULT_GCD_MS)
184}
185
186pub(crate) fn effective_aura_duration_ms(
187    view: &CombatView<'_>,
188    local: crate::state::LocalAuraIdx,
189    base_duration_ms: u32,
190) -> u32 {
191    if base_duration_ms == 0 {
192        return 0;
193    }
194
195    let target = view.program.damage.aura_action(local);
196    let (flat, percent) =
197        data_driven_timing_modifiers(view, target, ModifierPropertyKind::Duration, |effect| {
198            match effect {
199                BuffEffect::SpellDurationFromEffect {
200                    value,
201                    source_spell_id,
202                    effect_index,
203                    operation,
204                } => Some((*value, *source_spell_id, *effect_index, *operation)),
205                _ => None,
206            }
207        });
208    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
209
210    wowlab_types::numeric::f64_to_u32_saturating_round(
211        (f64::from(base_duration_ms) + flat).max(0.0) * timing_mult,
212    )
213}
214
215pub(crate) fn effective_periodic_tick_ms(
216    view: &CombatView<'_>,
217    local: crate::state::LocalAuraIdx,
218    base_tick_ms: u32,
219) -> u32 {
220    let base_tick_ms = if base_tick_ms == 0 {
221        DEFAULT_PERIODIC_INTERVAL_MS
222    } else {
223        base_tick_ms
224    };
225    let target = view.program.damage.aura_action(local);
226    let (flat, percent) =
227        data_driven_timing_modifiers(view, target, ModifierPropertyKind::TickTime, |effect| {
228            match effect {
229                BuffEffect::SpellTickTimeFromEffect {
230                    value,
231                    source_spell_id,
232                    effect_index,
233                    operation,
234                } => Some((*value, *source_spell_id, *effect_index, *operation)),
235                _ => None,
236            }
237        });
238    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
239
240    wowlab_types::numeric::f64_to_u32_saturating_round(
241        (f64::from(base_tick_ms) + flat).max(1.0) * timing_mult,
242    )
243    .max(1)
244}
245
246/// Resolve the integer wall-clock cadence used by `TrinityCore`'s periodic timer.
247pub(crate) fn effective_periodic_interval_ms(
248    view: &CombatView<'_>,
249    local: crate::state::LocalAuraIdx,
250    base_tick_ms: u32,
251    hasted: bool,
252) -> u32 {
253    effective_periodic_interval_for_action(
254        view,
255        view.program.damage.aura_action(local),
256        base_tick_ms,
257        hasted,
258    )
259}
260
261pub(crate) fn effective_periodic_interval_for_action(
262    view: &CombatView<'_>,
263    action: crate::state::program::LocalDamageActionIdx,
264    base_tick_ms: u32,
265    hasted: bool,
266) -> u32 {
267    let base_tick_ms = if base_tick_ms == 0 {
268        DEFAULT_PERIODIC_INTERVAL_MS
269    } else {
270        base_tick_ms
271    };
272    let (flat, percent) =
273        data_driven_timing_modifiers(view, action, ModifierPropertyKind::TickTime, |effect| {
274            match effect {
275                BuffEffect::SpellTickTimeFromEffect {
276                    value,
277                    source_spell_id,
278                    effect_index,
279                    operation,
280                } => Some((*value, *source_spell_id, *effect_index, *operation)),
281                _ => None,
282            }
283        });
284    let timing_mult = (1.0 + percent / HUNDRED).max(0.0);
285    let tick_ms = wowlab_types::numeric::f64_to_u32_saturating_round(
286        (f64::from(base_tick_ms) + flat).max(1.0) * timing_mult,
287    )
288    .max(1);
289
290    if hasted {
291        wowlab_types::numeric::f64_to_u32_saturating_trunc(
292            f64::from(tick_ms) / current_haste_mult(view),
293        )
294        .max(1)
295    } else {
296        tick_ms
297    }
298}