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
246pub(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}