1use super::{
2 ProfiledDamageDef, add_aura_stack, apply_aura, deal_bound_profiled_damage_def, expire_aura,
3 extend_aura, gain_resource_with_procs, mutate_cooldown,
4};
5use crate::{
6 DamageFlags,
7 context::CombatCtx,
8 state::{
9 AuraMutation, ResourceGain, ResourceGainSource, ResourcePool, SpellEffectData,
10 SpellEffectRange,
11 },
12};
13
14fn execute_dispel(
15 ctx: &mut CombatCtx<'_>,
16 _profile_spell_id: u32,
17 profile_action: crate::state::program::LocalDamageActionIdx,
18 dispel_type: i32,
19 steal: bool,
20 target: wowlab_types::sim::ActorId,
21) {
22 let selected = ctx
23 .program
24 .effects
25 .dispellable_auras(target, dispel_type)
26 .iter()
27 .find(|entry| {
28 ctx.instance
29 .buffer
30 .aura_at(entry.handle)
31 .is_active(ctx.now.as_secs_f64())
32 })
33 .copied();
34 let Some(entry) = selected else {
35 return;
36 };
37 let key = entry.key;
38 let local = entry.local;
39 let base_resistance = entry.dispel_resistance_flat * entry.dispel_resistance_multiplier;
40 let resistance = super::buffs::active_spell_modifier_value(
41 ctx.view().for_actor(key.source()),
42 ctx.program.damage.aura_action(local),
43 wowlab_engine_domain::dbc::ModifierPropertyKind::DispelResistance,
44 base_resistance,
45 )
46 .clamp(0.0, wowlab_types::constants::HUNDRED);
47
48 if (ctx.rng)() * wowlab_types::constants::HUNDRED < resistance {
49 return;
50 }
51
52 let stolen_slot: Option<crate::state::AuraTransferState> = steal
53 .then(|| {
54 ctx.program
55 .aura_binding(local, key)
56 .and_then(|binding| ctx.program.capture_aura_transfer(ctx.instance, binding))
57 })
58 .flatten();
59
60 {
61 let mut aura_ctx = crate::HookCtx::new(
62 crate::context::HookCtxServices {
63 program: ctx.program,
64 instance: ctx.instance,
65 sink: ctx.sink,
66 rng: ctx.rng,
67 },
68 crate::context::HookCtxRequest::for_target(ctx.now, ctx.target)
69 .with_source(key.source()),
70 )
71 .with_effect_target(target);
72
73 expire_aura(&mut aura_ctx, local);
74 };
75
76 if let Some(slot) = stolen_slot {
77 let player_key = wowlab_types::sim::AuraKey::new(
78 key.aura(),
79 wowlab_types::sim::ActorId::Player,
80 wowlab_types::sim::ActorId::Player,
81 wowlab_types::sim::AuraOn::Player,
82 );
83
84 if let Some(binding) = ctx.program.aura_binding(local, player_key) {
85 ctx.program
86 .mutate_aura(ctx.instance, binding, AuraMutation::Transfer(slot), ctx.now);
87 }
88
89 crate::systems::auras::emit_aura_apply_event(
90 ctx.program,
91 ctx.instance,
92 ctx.sink,
93 player_key,
94 key.aura().as_u32(),
95 wowlab_types::numeric::i32_to_u8_saturating(slot.stacks()),
96 ctx.now,
97 );
98 ctx.instance
99 .schedule(wowlab_engine_ports::Event::AuraExpire {
100 t: wowlab_types::sim::SimTime::from_secs_f64(slot.expires_at()),
101 aura: ctx
102 .program
103 .aura_event(local, player_key, None)
104 .expect("active aura has a finalized event binding"),
105 });
106 }
107
108 super::procs::fire_dispel_procs(ctx, profile_action);
109}
110
111#[derive(Clone, Copy, Debug)]
112pub(crate) struct EffectExecution {
113 pub range: SpellEffectRange,
114 pub profile_spell_id: u32,
115 pub profile_action: crate::state::program::LocalDamageActionIdx,
116 pub flags: DamageFlags,
117}
118
119fn flags_for_child_profile(
120 program: &crate::state::CombatProgram,
121 _state: &crate::state::CombatInstance,
122 profile_action: crate::state::program::LocalDamageActionIdx,
123 inherited: DamageFlags,
124) -> DamageFlags {
125 let action = program.damage.action_at(profile_action);
126 let mut profile_flags = action.damage_attribute_flags;
127
128 if action.school.is_physical() {
129 profile_flags |= DamageFlags::PHYSICAL;
130 }
131
132 flags_with_profile_semantics(inherited, profile_flags)
133}
134
135fn flags_with_profile_semantics(inherited: DamageFlags, profile_flags: DamageFlags) -> DamageFlags {
137 let profile_bound = DamageFlags::PHYSICAL
138 | DamageFlags::PERIODIC
139 | DamageFlags::NO_CRIT
140 | DamageFlags::IGNORE_PLAYER_MULTIPLIERS
141 | DamageFlags::IGNORE_TARGET_MULTIPLIERS
142 | DamageFlags::IGNORE_POSITIVE_TARGET_MULTIPLIERS
143 | DamageFlags::AOE;
144
145 (inherited & !profile_bound) | profile_flags
146}
147
148fn effect_is_hostile(
149 program: &crate::state::CombatProgram,
150 state: &crate::state::CombatInstance,
151 effect: SpellEffectData,
152 remaining_depth: u8,
153) -> bool {
154 match effect {
155 SpellEffectData::Damage { .. }
156 | SpellEffectData::InterruptCast { .. }
157 | SpellEffectData::Dispel { .. } => true,
158 SpellEffectData::ApplyAura { aura_local }
159 | SpellEffectData::AddAuraStack { aura_local }
160 | SpellEffectData::RemoveAura { aura_local }
161 | SpellEffectData::ExtendAura { aura_local, .. } => program
162 .auras
163 .get(aura_local.as_usize())
164 .is_some_and(|aura| aura.on == wowlab_types::sim::AuraOn::Target),
165 SpellEffectData::Conditional { .. }
166 | SpellEffectData::Heal { .. }
167 | SpellEffectData::Energize { .. }
168 | SpellEffectData::EnergizePercent { .. }
169 | SpellEffectData::MutateCooldown { .. } => false,
170 SpellEffectData::Delayed { effects, .. } => {
171 remaining_depth > 0
172 && effect_range_is_hostile_with_depth(program, state, effects, remaining_depth - 1)
173 }
174 }
175}
176
177fn effect_range_is_hostile_with_depth(
178 program: &crate::state::CombatProgram,
179 state: &crate::state::CombatInstance,
180 range: SpellEffectRange,
181 remaining_depth: u8,
182) -> bool {
183 let start = range.start as usize;
184 let end = start.saturating_add(usize::from(range.len));
185 let Some(effects) = program
186 .spell_effects
187 .get(start..end.min(program.spell_effects.len()))
188 else {
189 return false;
190 };
191
192 effects
193 .iter()
194 .copied()
195 .any(|effect| effect_is_hostile(program, state, effect, remaining_depth))
196}
197
198pub(crate) fn effect_range_is_hostile(
199 program: &crate::state::CombatProgram,
200 state: &crate::state::CombatInstance,
201 range: SpellEffectRange,
202) -> bool {
203 const MAX_NESTED_EFFECT_DEPTH: u8 = 32;
204
205 effect_range_is_hostile_with_depth(program, state, range, MAX_NESTED_EFFECT_DEPTH)
206}
207
208fn effect_hook_ctx<'a>(
209 ctx: &'a mut CombatCtx<'_>,
210 reflected: bool,
211 effect_target: Option<wowlab_types::sim::ActorId>,
212) -> crate::HookCtx<'a> {
213 let source = ctx.source;
214 let hook = ctx.hook_ctx();
215
216 if reflected {
217 hook.with_effect_target(source)
218 } else if let Some(effect_target) = effect_target {
219 hook.with_effect_target(effect_target)
220 } else {
221 hook
222 }
223}
224
225fn hostile_effect_target(
226 ctx: &CombatCtx<'_>,
227 reflected: bool,
228 effect_target: Option<wowlab_types::sim::ActorId>,
229) -> wowlab_types::sim::ActorId {
230 if reflected {
231 ctx.source
232 } else {
233 match effect_target {
234 Some(target) => target,
235 None => wowlab_types::sim::ActorId::Enemy(ctx.target),
236 }
237 }
238}
239
240fn execute_percent_energize(
241 ctx: &mut CombatCtx<'_>,
242 profile_action: crate::state::program::LocalDamageActionIdx,
243 resource_type: wowlab_types::combat::ResourceType,
244 percent: f64,
245 effect_index: u8,
246) {
247 let Some(resource) = ctx.program.buffer_resource(ctx.instance, resource_type) else {
248 return;
249 };
250 let Some(effect) = ctx
251 .program
252 .damage
253 .action_effect(profile_action, effect_index)
254 else {
255 return;
256 };
257 let percent =
258 super::buffs::active_effect_points(ctx.view().for_actor(ctx.source), effect, percent);
259 let amount = resource.max * percent / wowlab_types::constants::HUNDRED;
260 let Some(is_secondary) =
261 super::resources::resource_pool_for_type(ctx.program, ctx.instance, resource_type)
262 else {
263 return;
264 };
265 let request = ResourceGain::modified(
266 amount,
267 ResourceGainSource::SpellEffect {
268 spell_id: ctx.program.damage.action_at(profile_action).spell.as_u32(),
269 effect_index,
270 },
271 );
272
273 gain_resource_with_procs(ctx, request, is_secondary);
274}
275
276fn execute_damage_effect(
277 ctx: &mut CombatCtx<'_>,
278 execution: EffectExecution,
279 effect_ordinal: usize,
280 effect_data: SpellEffectData,
281) {
282 let SpellEffectData::Damage {
283 effect,
284 damage,
285 base_points,
286 may_crit,
287 attribute_flags,
288 requires_main_hand,
289 requires_off_hand,
290 equipped_item_requirement,
291 } = effect_data
292 else {
293 return;
294 };
295 let missing_required_hand = (requires_main_hand && ctx.program.damage.main_hand.speed_ms == 0)
296 || (requires_off_hand && ctx.program.damage.off_hand.is_none());
297 let item_requirement_unmet = equipped_item_requirement.is_some_and(|requirement| {
298 let hand = if requires_off_hand {
299 wowlab_engine_gamedata::ResolvedDamageWeapon::OffHand
300 } else {
301 wowlab_engine_gamedata::ResolvedDamageWeapon::MainHand
302 };
303
304 let weapon = match hand {
305 wowlab_engine_gamedata::ResolvedDamageWeapon::MainHand => {
306 Some(ctx.program.damage.main_hand)
307 }
308 wowlab_engine_gamedata::ResolvedDamageWeapon::OffHand => ctx.program.damage.off_hand,
309 _ => None,
310 };
311
312 !weapon.is_some_and(|weapon| {
313 weapon.speed_ms > 0
314 && requirement.matches_class_and_subclass(weapon.item_class, weapon.subclass)
315 })
316 });
317
318 if missing_required_hand || item_requirement_unmet {
319 tracing::trace!(
320 spell_id = effect.spell_id,
321 effect_index = effect.effect_index,
322 requires_main_hand,
323 requires_off_hand,
324 ?equipped_item_requirement,
325 "SPELL_EFFECT_EQUIPMENT_REQUIREMENT_UNMET"
326 );
327
328 return;
329 }
330
331 let profile_flags = attribute_flags
332 | if may_crit {
333 DamageFlags::empty()
334 } else {
335 DamageFlags::NO_CRIT
336 };
337
338 if let Some(binding) = ctx.program.damage.effect_request(effect_ordinal) {
339 deal_bound_profiled_damage_def(
340 ctx,
341 ProfiledDamageDef {
342 binding,
343 effect,
344 geometry_effect: effect,
345 profile_spell_id: execution.profile_spell_id,
346 base_points: Some(base_points),
347 def: damage,
348 flags: flags_with_profile_semantics(execution.flags, profile_flags),
349 source_position: None,
350 destination: None,
351 },
352 );
353 }
354}
355
356fn execute_heal_effect(
357 ctx: &mut CombatCtx<'_>,
358 effect_target: Option<wowlab_types::sim::ActorId>,
359 effect_ordinal: usize,
360 effect_data: SpellEffectData,
361) {
362 let SpellEffectData::Heal {
363 base,
364 ap_coef,
365 sp_coef,
366 percent_of_max,
367 may_crit,
368 ..
369 } = effect_data
370 else {
371 return;
372 };
373 let stats = &ctx.program.config.base_stats.stats;
374 let (base, percent_of_max) = ctx
375 .program
376 .damage
377 .spell_effect_source(effect_ordinal)
378 .map_or((base, percent_of_max), |effect| {
379 (
380 super::buffs::active_effect_points(ctx.view().for_actor(ctx.source), effect, base),
381 super::buffs::active_effect_points(
382 ctx.view().for_actor(ctx.source),
383 effect,
384 percent_of_max * wowlab_types::constants::HUNDRED,
385 ) / wowlab_types::constants::HUNDRED,
386 )
387 });
388 let amount = base + ap_coef * stats.attack_power + sp_coef * stats.spell_power;
389
390 let _ = super::deal_heal(
391 ctx.program,
392 ctx.instance,
393 ctx.rng,
394 super::HealRequest {
395 source: ctx.source,
396 target: effect_target.unwrap_or(ctx.source),
397 base: amount,
398 percent_of_max,
399 school: wowlab_types::combat::DamageSchool::Holy,
400 periodic: false,
401 may_crit,
402 at: ctx.now,
403 },
404 );
405}
406
407fn execute_resource_effect(
408 ctx: &mut CombatCtx<'_>,
409 execution: EffectExecution,
410 effect_ordinal: usize,
411 effect_data: SpellEffectData,
412) -> bool {
413 match effect_data {
414 SpellEffectData::Energize { pool, amount, .. } => {
415 let amount = ctx
416 .program
417 .damage
418 .spell_effect_source(effect_ordinal)
419 .map_or(amount, |effect| {
420 super::buffs::active_effect_points(
421 ctx.view().for_actor(ctx.source),
422 effect,
423 amount,
424 )
425 });
426 let is_secondary = matches!(pool, ResourcePool::Secondary);
427 let request = ResourceGain::modified(
428 amount,
429 ResourceGainSource::Spell(execution.profile_spell_id),
430 );
431
432 gain_resource_with_procs(ctx, request, is_secondary);
433
434 true
435 }
436 SpellEffectData::EnergizePercent {
437 resource_type,
438 percent,
439 effect_index,
440 } => {
441 execute_percent_energize(
442 ctx,
443 execution.profile_action,
444 resource_type,
445 percent,
446 effect_index,
447 );
448
449 true
450 }
451 SpellEffectData::MutateCooldown {
452 spell_local,
453 operation,
454 } => {
455 mutate_cooldown(ctx.program, ctx.instance, spell_local, operation, ctx.now);
456
457 true
458 }
459 _ => false,
460 }
461}
462
463pub(crate) fn execute_effect_range(ctx: &mut CombatCtx<'_>, execution: EffectExecution) {
464 execute_effect_range_for_actor(ctx, execution, None);
465}
466
467fn effect_frame(
468 ctx: &mut CombatCtx<'_>,
469 execution: EffectExecution,
470) -> Option<crate::state::EffectProgramFrame> {
471 let hostile = effect_range_is_hostile(ctx.program, ctx.instance, execution.range);
472 let flags = if hostile {
473 super::damage_pipeline::resolve_spell_impact_flags(
474 ctx,
475 execution.profile_action,
476 execution.flags,
477 )?
478 } else {
479 execution.flags
480 };
481 let execution = EffectExecution { flags, ..execution };
482 let next = execution.range.start as usize;
483 let end = next.saturating_add(usize::from(execution.range.len));
484
485 Some(crate::state::EffectProgramFrame {
486 range: execution.range,
487 profile_spell_id: execution.profile_spell_id,
488 profile_action: Some(execution.profile_action),
489 flags: execution.flags,
490 next,
491 end,
492 reflected: flags.contains(DamageFlags::REFLECTED),
493 })
494}
495
496const fn effect_driver_spell_id(execution: EffectExecution, proc_driver_spell_id: u32) -> u32 {
497 if proc_driver_spell_id == 0 {
498 execution.profile_spell_id
499 } else {
500 proc_driver_spell_id
501 }
502}
503
504pub(crate) fn execute_effect_range_for_actor(
505 ctx: &mut CombatCtx<'_>,
506 execution: EffectExecution,
507 effect_target: Option<wowlab_types::sim::ActorId>,
508) {
509 execute_effect_range_for_actor_with_proc_driver(ctx, execution, effect_target, 0);
510}
511
512pub(crate) fn execute_proc_effect_range_for_actor(
513 ctx: &mut CombatCtx<'_>,
514 execution: EffectExecution,
515 effect_target: Option<wowlab_types::sim::ActorId>,
516 proc_driver_spell_id: u32,
517) {
518 execute_effect_range_for_actor_with_proc_driver(
519 ctx,
520 execution,
521 effect_target,
522 proc_driver_spell_id,
523 );
524}
525
526fn execute_effect_range_for_actor_with_proc_driver(
529 ctx: &mut CombatCtx<'_>,
530 execution: EffectExecution,
531 effect_target: Option<wowlab_types::sim::ActorId>,
532 proc_driver_spell_id: u32,
533) {
534 let Some(frame) = effect_frame(ctx, execution) else {
535 return;
536 };
537 let frame_base = ctx.instance.runtime.scratch.effect_program_frame_count;
538
539 ctx.instance
540 .runtime
541 .scratch
542 .push_effect_program_frame(frame);
543
544 while let Some(mut frame) = ctx
545 .instance
546 .runtime
547 .scratch
548 .pop_effect_program_frame_after(frame_base)
549 {
550 if frame.next >= frame.end {
551 continue;
552 }
553
554 let effect_ordinal = frame.next;
555 let Some(effect) = ctx.program.spell_effects.get(effect_ordinal).copied() else {
556 continue;
557 };
558
559 frame.next = frame.next.saturating_add(1);
560 let execution = EffectExecution {
561 range: frame.range,
562 profile_spell_id: frame.profile_spell_id,
563 profile_action: frame
564 .profile_action
565 .expect("runtime effect frame carries a finalized profile action"),
566 flags: frame.flags,
567 };
568 let reflected = frame.reflected;
569
570 ctx.instance
571 .runtime
572 .scratch
573 .push_effect_program_frame(frame);
574
575 if execute_resource_effect(ctx, execution, effect_ordinal, effect) {
576 continue;
577 }
578
579 match effect {
580 SpellEffectData::Conditional { predicate, effects } => {
581 let enabled = {
582 let hook = effect_hook_ctx(ctx, reflected, effect_target);
583
584 predicate(&hook)
585 };
586
587 if enabled
588 && let Some(frame) = effect_frame(
589 ctx,
590 EffectExecution {
591 range: effects,
592 ..execution
593 },
594 )
595 {
596 ctx.instance
597 .runtime
598 .scratch
599 .push_effect_program_frame(frame);
600 }
601 }
602 SpellEffectData::ApplyAura { aura_local } => {
603 let mut hook = effect_hook_ctx(ctx, reflected, effect_target)
604 .with_driver_spell(effect_driver_spell_id(execution, proc_driver_spell_id))
605 .with_source_damage_flags(execution.flags);
606
607 apply_aura(&mut hook, aura_local);
608 }
609 SpellEffectData::AddAuraStack { aura_local } => {
610 let mut hook = effect_hook_ctx(ctx, reflected, effect_target)
611 .with_driver_spell(effect_driver_spell_id(execution, proc_driver_spell_id))
612 .with_source_damage_flags(execution.flags);
613
614 add_aura_stack(&mut hook, aura_local);
615 }
616 SpellEffectData::RemoveAura { aura_local } => {
617 expire_aura(
618 &mut effect_hook_ctx(ctx, reflected, effect_target),
619 aura_local,
620 );
621 }
622 SpellEffectData::InterruptCast { lockout_ms } => {
623 let _ = super::interrupt_cast(
624 ctx.program,
625 ctx.instance,
626 hostile_effect_target(ctx, reflected, effect_target),
627 ctx.now,
628 lockout_ms,
629 );
630 }
631 SpellEffectData::Dispel { dispel_type, steal } => {
632 let target = hostile_effect_target(ctx, reflected, effect_target);
633
634 execute_dispel(
635 ctx,
636 execution.profile_spell_id,
637 execution.profile_action,
638 dispel_type,
639 steal,
640 target,
641 );
642 }
643 effect @ SpellEffectData::Damage { .. } => {
644 execute_damage_effect(ctx, execution, effect_ordinal, effect);
645 }
646 effect @ SpellEffectData::Heal { .. } => {
647 let target = if reflected {
648 Some(ctx.source)
649 } else {
650 effect_target
651 };
652
653 execute_heal_effect(ctx, target, effect_ordinal, effect);
654 }
655 SpellEffectData::Energize { .. }
656 | SpellEffectData::EnergizePercent { .. }
657 | SpellEffectData::MutateCooldown { .. } => {}
658 SpellEffectData::ExtendAura {
659 aura_local,
660 amount_ms,
661 } => extend_aura(
662 &mut effect_hook_ctx(ctx, reflected, effect_target),
663 aura_local,
664 amount_ms,
665 ),
666 SpellEffectData::Delayed {
667 delay_ms,
668 profile_spell_id,
669 effects,
670 } => {
671 let profile_action = ctx
672 .program
673 .damage
674 .delayed_profile_action(effect_ordinal)
675 .expect("delayed effect carries a finalized profile action");
676 let flags = flags_for_child_profile(
677 ctx.program,
678 ctx.instance,
679 profile_action,
680 execution.flags,
681 );
682
683 if delay_ms == 0 {
684 if let Some(frame) = effect_frame(
685 ctx,
686 EffectExecution {
687 range: effects,
688 profile_spell_id,
689 profile_action,
690 flags,
691 },
692 ) {
693 ctx.instance
694 .runtime
695 .scratch
696 .push_effect_program_frame(frame);
697 }
698
699 continue;
700 }
701
702 let source = ctx.source;
703 let hook = ctx.hook_ctx();
704 let mut hook = if reflected {
705 hook.with_effect_target(source)
706 } else if let Some(effect_target) = effect_target {
707 hook.with_effect_target(effect_target)
708 } else {
709 hook
710 };
711
712 hook.schedule_effect_program(
713 delay_ms,
714 effects,
715 profile_action,
716 flags,
717 proc_driver_spell_id,
718 );
719 }
720 }
721 }
722}
723
724#[cfg(test)]
725#[path = "effects/tests.rs"]
726mod tests;