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