wowlab_engine_combat/systems/buffs/
timing.rs1use 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
197pub(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}