1use super::{
2 ActorId, AuraKey, Event, HookCtx, LocalAuraIdx, SimTime, SpellGroup, SpellGroupRule,
3 application_state, async_stacks, attempt_weapon_enchants, aura_keys_for_actor_application,
4 aura_keys_for_application, combine_rolling_multiplier, context_aura_key,
5 emit_aura_refresh_event, expire_aura_instance, expire_conflicting_auras,
6 initialize_or_revive_periodic, is_rolling_periodic, process_health_threshold_application,
7 procs, refresh_absorbs, refresh_pet_actions_after_aura_change, refresh_snapshot,
8 rolling_ticks_left, sync_exclusive_group_enabled, synchronize_aura_dependents,
9 update_aura_slot,
10};
11
12pub(super) fn schedule_aura_expiry(ctx: &mut HookCtx<'_>, key: AuraKey) {
13 let expires_at = ctx
14 .buf
15 .aura(key)
16 .map_or(ctx.now, |a| SimTime::from_secs_f64(a.expires_at));
17
18 ctx.state.schedule(Event::AuraExpire {
19 t: expires_at,
20 key,
21 target: ctx
22 .effect_target
23 .map_or(ctx.target, |affected| match affected {
24 ActorId::Enemy(target) => Some(target),
25 ActorId::Player | ActorId::Pet(_) | ActorId::External => None,
26 }),
27 });
28}
29
30pub(super) fn enqueue_aura_apply_effects(ctx: &mut HookCtx<'_>, aura: &crate::state::AuraData) {
31 if aura.apply_effects.len == 0 {
32 return;
33 }
34
35 ctx.state
36 .runtime
37 .deferred_work
38 .push_effect(crate::state::PendingEffectProgram {
39 effects: aura.apply_effects,
40 profile_spell_id: if aura.apply_profile_spell_id == 0 {
41 aura.aura_id
42 } else {
43 aura.apply_profile_spell_id
44 },
45 repeat: 1,
46 source: ctx.source,
47 target: ctx.target.or_else(|| ctx.state.current_target()),
48 effect_target: ctx.effect_target,
49 flags: ctx.source_damage_flags,
50 originating_proc_driver_spell_id: if ctx
51 .source_damage_flags
52 .contains(crate::DamageFlags::PROC)
53 {
54 ctx.driver_spell_id()
55 } else {
56 0
57 },
58 });
59}
60
61pub(super) fn reset_aura_proc_charges(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
62 let configured_procs = ctx
63 .state
64 .defs
65 .impact_effect_procs
66 .iter()
67 .copied()
68 .enumerate()
69 .filter(|(_, proc)| proc.charge_aura == Some(local) && !proc.uses_stacks_for_charges)
70 .map(|(index, proc)| {
71 (
72 index,
73 procs::effective_proc_charges(ctx.state, ctx.buf, ctx.source, proc),
74 )
75 });
76 let resets = configured_procs
77 .filter(|(_, charges)| *charges > 0)
78 .collect::<Vec<_>>();
79
80 for (index, charges) in resets {
81 if let Some(remaining) = ctx.state.runtime.procs.impact_effect_charges.get_mut(index) {
82 *remaining = charges;
83 }
84 }
85}
86
87pub(super) fn enqueue_aura_application_proc(
88 ctx: &mut HookCtx<'_>,
89 aura: &crate::state::AuraData,
90 key: AuraKey,
91) {
92 let kind = if ctx.source_damage_flags.contains(crate::DamageFlags::PROC) {
93 crate::state::ProcSourceKind::Proc
94 } else {
95 crate::state::ProcSourceKind::Action
96 };
97 let originating_proc_driver_spell_id = if kind == crate::state::ProcSourceKind::Proc {
98 ctx.driver_spell_id()
99 } else {
100 0
101 };
102
103 ctx.state
104 .runtime
105 .deferred_work
106 .push_aura_application(crate::state::AuraApplicationEvent {
107 aura_spell_id: aura.aura_id,
108 source: key.source(),
109 affected: key.affected(),
110 target: ctx.target,
111 provenance: crate::state::ProcProvenance::from_spell(
114 &ctx.state.config.game_data,
115 aura.aura_id,
116 kind,
117 ),
118 originating_proc_driver_spell_id,
119 });
120}
121
122pub(super) fn finish_aura_application(
123 ctx: &mut HookCtx<'_>,
124 request: application_state::AuraSlotRequest<'_>,
125 is_fresh: bool,
126) {
127 let application_state::AuraSlotRequest { aura, key, .. } = request;
128
129 if is_fresh {
130 process_health_threshold_application(ctx.state, key.affected(), &aura.effects, ctx.now);
131 }
132
133 enqueue_aura_application_proc(ctx, aura, key);
134}
135
136pub(super) fn apply_spell_group_rule(
137 ctx: &mut HookCtx<'_>,
138 local: LocalAuraIdx,
139 group: SpellGroup,
140) {
141 match group.rule {
142 SpellGroupRule::Exclusive => {
143 expire_conflicting_auras(ctx, local, group.group_idx);
144 sync_exclusive_group_enabled(ctx, local, group.group_idx);
145 }
146 }
147}
148
149pub fn apply_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
151 apply_aura_with_duration(ctx, local, None);
152}
153
154pub(crate) fn apply_aura_with_rolling_multiplier(
156 ctx: &mut HookCtx<'_>,
157 local: LocalAuraIdx,
158 rolling_multiplier: f64,
159) {
160 apply_aura_with_options(ctx, local, None, rolling_multiplier);
161}
162
163pub fn apply_aura_with_duration(
165 ctx: &mut HookCtx<'_>,
166 local: LocalAuraIdx,
167 duration_ms: Option<u32>,
168) {
169 apply_aura_with_options(ctx, local, duration_ms, 1.0);
170}
171
172pub(super) fn apply_aura_with_options(
173 ctx: &mut HookCtx<'_>,
174 local: LocalAuraIdx,
175 duration_ms: Option<u32>,
176 rolling_multiplier: f64,
177) {
178 if !rolling_multiplier.is_finite() || rolling_multiplier <= 0.0 {
179 return;
180 }
181
182 let aura = ctx.state.defs.auras[local.as_usize()];
184
185 if aura_application_blocked(ctx, &aura) {
186 return;
187 }
188
189 apply_aura_unblocked(ctx, local, duration_ms, rolling_multiplier);
190
191 if aura.application_followup_aura_id != 0
192 && let Some(followup) = ctx.state.aura_local(aura.application_followup_aura_id)
193 {
194 apply_aura_unblocked(ctx, followup, None, 1.0);
195 }
196}
197
198pub(super) fn aura_application_blocked(ctx: &HookCtx<'_>, aura: &crate::state::AuraData) -> bool {
199 for aura_id in aura.application_excluded_auras {
200 if aura_id == 0 {
201 continue;
202 }
203
204 let Some(local) = ctx.state.aura_local(aura_id) else {
205 continue;
206 };
207 let Some(key) = context_aura_key(ctx, local) else {
208 continue;
209 };
210
211 if ctx
212 .buf
213 .aura(key)
214 .is_some_and(|slot| slot.is_active(ctx.now.as_secs_f64()))
215 {
216 return true;
217 }
218 }
219
220 false
221}
222
223pub(super) fn apply_aura_unblocked(
224 ctx: &mut HookCtx<'_>,
225 local: LocalAuraIdx,
226 duration_ms: Option<u32>,
227 rolling_multiplier: f64,
228) {
229 let aura = ctx.state.defs.auras[local.as_usize()];
231
232 if let Some(group) = aura.spell_group {
233 apply_spell_group_rule(ctx, local, group);
234 }
235
236 let unscaled_dur_ms = duration_ms.unwrap_or_else(|| {
237 crate::systems::buffs::effective_aura_duration_ms(
238 &ctx.view().combat(),
239 aura.aura_id,
240 aura.base_duration_ms,
241 )
242 });
243 let base_dur_ms = if aura.duration_hasted && unscaled_dur_ms > 0 {
244 if let Some(periodic) = aura.periodic {
245 let base_period_ms = crate::systems::buffs::effective_periodic_tick_ms(
246 &ctx.view().combat(),
247 aura.aura_id,
248 periodic.tick_interval_ms,
249 );
250 let period_ms = crate::systems::buffs::effective_periodic_interval_ms(
251 &ctx.view().combat(),
252 aura.aura_id,
253 periodic.tick_interval_ms,
254 periodic.hasted_ticks,
255 );
256
257 fixed_tick_periodic_duration_ms(unscaled_dur_ms, base_period_ms, period_ms)
258 } else {
259 let haste = crate::systems::buffs::current_haste_mult(&ctx.view().combat());
260
261 wowlab_types::numeric::f64_to_u32_saturating_round(f64::from(unscaled_dur_ms) / haste)
262 .max(1)
263 }
264 } else {
265 unscaled_dur_ms
266 };
267
268 let application_keys = ctx.effect_target.map_or_else(
269 || aura_keys_for_application(ctx.state, local, ctx.source, ctx.target),
270 |affected| aura_keys_for_actor_application(ctx.state, local, ctx.source, affected),
271 );
272
273 let mut applied = false;
274
275 for key in application_keys.into_iter().flatten() {
276 applied |= apply_aura_instance(ctx, local, &aura, key, base_dur_ms, rolling_multiplier);
277 }
278
279 if applied && ctx.source == ActorId::Player {
280 attempt_weapon_enchants(ctx);
281 }
282}
283
284pub(super) fn fixed_tick_periodic_duration_ms(
285 unscaled_duration_ms: u32,
286 base_period_ms: u32,
287 period_ms: u32,
288) -> u32 {
289 unscaled_duration_ms
290 .checked_div(base_period_ms)
291 .unwrap_or_default()
292 .max(1)
293 .saturating_mul(period_ms)
294}
295
296fn effective_aura_base_duration_ms(
297 ctx: &HookCtx<'_>,
298 aura_id: u32,
299 affected: ActorId,
300 base_duration_ms: u32,
301) -> (f64, u32) {
302 let multiplier =
303 crate::systems::buffs::aura_time_rate_multiplier(ctx.view().for_actor(affected), aura_id);
304 let duration_ms = if base_duration_ms == 0 {
305 0
306 } else {
307 wowlab_types::numeric::f64_to_u32_saturating_round(f64::from(base_duration_ms) * multiplier)
308 .max(1)
309 };
310
311 (multiplier, duration_ms)
312}
313
314pub(super) fn apply_aura_instance(
316 ctx: &mut HookCtx<'_>,
317 local: LocalAuraIdx,
318 aura: &crate::state::AuraData,
319 key: AuraKey,
320 base_dur_ms: u32,
321 rolling_multiplier: f64,
322) -> bool {
323 let (time_rate_multiplier, base_dur_ms) =
324 effective_aura_base_duration_ms(ctx, aura.aura_id, key.affected(), base_dur_ms);
325
326 ctx.buf.ensure_aura_slot(key);
327
328 if aura.refresh_behavior == wowlab_types::data::RefreshBehavior::None
329 && ctx
330 .buf
331 .aura(key)
332 .is_some_and(|slot| slot.is_active(ctx.now.as_secs_f64()))
333 {
334 return false;
335 }
336
337 let base_dur_ms = if let Some(control) = aura.crowd_control {
338 let duration = if base_dur_ms == 0 {
339 crate::state::CrowdControlDuration::Permanent
340 } else {
341 crate::state::CrowdControlDuration::Finite(SimTime::from_millis(base_dur_ms))
342 };
343
344 match ctx.state.apply_crowd_control_duration(
345 key.affected(),
346 control.kind,
347 control.diminishing,
348 duration,
349 ctx.now,
350 ) {
351 crate::state::CrowdControlApplication::Applied {
352 duration: crate::state::CrowdControlDuration::Finite(duration),
353 ..
354 } => duration.as_millis(),
355 crate::state::CrowdControlApplication::Applied {
356 duration: crate::state::CrowdControlDuration::Permanent,
357 ..
358 } => 0,
359 crate::state::CrowdControlApplication::Immune => return false,
360 }
361 } else {
362 base_dur_ms
363 };
364 let base_dur = SimTime::from_millis(base_dur_ms);
365 let is_permanent = base_dur_ms == 0;
366 let slot_request = application_state::AuraSlotRequest {
367 aura,
368 key,
369 base_dur_ms,
370 base_dur,
371 is_permanent,
372 };
373
374 if retrigger_unticked_reverse_aura(ctx, local, aura, key) {
375 enqueue_aura_application_proc(ctx, aura, key);
376
377 return true;
378 }
379
380 let previous_expiry = ctx
381 .buf
382 .aura(key)
383 .filter(|slot| slot.is_occupied())
384 .map(|slot| slot.expires_at);
385
386 if aura.async_stacks && !is_permanent {
387 let is_fresh = async_stacks::apply_async_stacks(ctx, local, key, base_dur_ms, aura.doses);
388
389 initialize_or_revive_periodic(ctx, slot_request, is_fresh, previous_expiry);
390 refresh_snapshot(ctx, slot_request);
391 refresh_absorbs(ctx, slot_request);
392
393 if aura.shapeshift_form > 0 {
394 ctx.buf.player_mut().shapeshift_form = aura.shapeshift_form;
395 tracing::trace!(
396 aura_id = aura.aura_id,
397 form = aura.shapeshift_form,
398 "applied shapeshift form"
399 );
400 }
401
402 synchronize_aura_dependents(ctx);
403 refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
404 enqueue_aura_apply_effects(ctx, aura);
405 reset_aura_proc_charges(ctx, local);
406 enqueue_aura_application_proc(ctx, aura, key);
407
408 return true;
409 }
410
411 let rolling_old_ticks_left = if is_rolling_periodic(aura) {
412 rolling_ticks_left(ctx, key)
413 } else {
414 0.0
415 };
416 let Some(is_fresh) = update_aura_slot(ctx, slot_request) else {
417 return false;
418 };
419
420 if let Some(slot) = ctx.buf.aura_mut(key) {
421 slot.time_rate_multiplier = time_rate_multiplier;
422 }
423
424 initialize_or_revive_periodic(
425 ctx,
426 slot_request,
427 is_fresh
428 || aura.refresh_behavior == wowlab_types::data::RefreshBehavior::Clip
429 || aura.tick_behavior == crate::state::AuraTickBehavior::Clip,
430 previous_expiry,
431 );
432 refresh_snapshot(ctx, slot_request);
433 refresh_absorbs(ctx, slot_request);
434 combine_rolling_multiplier(
435 ctx,
436 slot_request,
437 rolling_old_ticks_left,
438 rolling_multiplier,
439 );
440
441 if ctx.state.defs.stealth_aura_id == Some(aura.aura_id) {
442 ctx.buf.player_mut().stealthed = 1;
443 }
444
445 if aura.shapeshift_form > 0 {
446 ctx.buf.player_mut().shapeshift_form = aura.shapeshift_form;
447 tracing::trace!(
448 aura_id = aura.aura_id,
449 form = aura.shapeshift_form,
450 "applied shapeshift form"
451 );
452 }
453
454 synchronize_aura_dependents(ctx);
455 schedule_aura_expiry(ctx, key);
456 refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
457 enqueue_aura_apply_effects(ctx, aura);
458 reset_aura_proc_charges(ctx, local);
459
460 finish_aura_application(ctx, slot_request, is_fresh);
461
462 true
463}
464
465pub(super) fn retrigger_unticked_reverse_aura(
466 ctx: &mut HookCtx<'_>,
467 local: LocalAuraIdx,
468 aura: &crate::state::AuraData,
469 key: AuraKey,
470) -> bool {
471 if !aura.reverse || aura.tick_behavior != crate::state::AuraTickBehavior::None {
472 return false;
473 }
474
475 let Some(current_stacks) = ctx
476 .buf
477 .aura(key)
478 .filter(|slot| slot.is_active(ctx.now.as_secs_f64()))
479 .map(|slot| slot.stacks)
480 else {
481 return false;
482 };
483
484 let change = i32::from(aura.doses.max(1));
485
486 if current_stacks <= change {
487 expire_aura_instance(ctx, local, key);
488 } else {
489 let stacks = if let Some(slot) = ctx.buf.aura_mut(key) {
490 slot.stacks -= change;
491
492 wowlab_types::numeric::i32_to_u8_saturating(slot.stacks)
493 } else {
494 return true;
495 };
496
497 emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
498 synchronize_aura_dependents(ctx);
499 refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
500 }
501
502 true
503}