Skip to main content

wowlab_engine_combat/systems/procs/
impact.rs

1use super::{
2    AuraApplicationEvent, CombatCtx, HookCtx, ImpactActorFilter, ImpactEvent, ImpactFilter,
3    ImpactProc, PeriodicImpactFilter, PeriodicImpactPolicy, ProcDriver, ProcEventKind, ProcHitMask,
4    ProcPhaseMask, ProcSourceKind, ProcTriggerSide, ScratchDisposition,
5    fire_accumulating_impact_procs, fire_impact_effect_procs, with_scratch,
6};
7
8pub(crate) fn weapon_enchants_on_player_impact(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
9    attempt_weapon_enchants(ctx);
10}
11
12pub(crate) fn attempt_weapon_enchants(ctx: &mut HookCtx<'_>) {
13    for index in 0..ctx.state.defs.weapon_enchant_bindings.len() {
14        let Some(&binding) = ctx.state.defs.weapon_enchant_bindings.get(index) else {
15            continue;
16        };
17
18        if ctx.roll_enchant_rppm(binding.enchantment_id) {
19            if let Some(local) = ctx.state.aura_local(binding.buff_spell_id) {
20                ctx.apply_aura(local.raw());
21            }
22        }
23    }
24}
25
26fn impact_proc_matches(proc: &ImpactProc, impact: ImpactEvent) -> bool {
27    proc_policy_allows(impact, proc.driver)
28        && impact_filter_matches(
29            ImpactFilter {
30                actor: proc.actor_filter,
31                spell: proc.spell_filter,
32                periodic: PeriodicImpactFilter::from(PeriodicImpactPolicy {
33                    periodic_only: proc.periodic_only,
34                    skip_periodic: proc.skip_periodic,
35                }),
36                physical_only: false,
37                crit_only: proc.crit_only,
38            },
39            impact,
40        )
41}
42
43pub(super) fn proc_policy_allows(impact: ImpactEvent, driver: ProcDriver) -> bool {
44    let source = impact.provenance;
45
46    if driver.policy.only_proc_from_class_abilities && !source.policy.allow_class_ability_procs {
47        return false;
48    }
49
50    if matches!(source.kind, ProcSourceKind::Proc) && !driver.policy.can_proc_from_procs {
51        return false;
52    }
53
54    match driver.trigger_side {
55        ProcTriggerSide::Target => {
56            !source.policy.suppress_target_procs || driver.policy.can_proc_from_suppressed_target
57        }
58        ProcTriggerSide::Caster => {
59            !source.policy.suppress_caster_procs
60                || (source.policy.enable_procs_from_suppressed
61                    && driver.policy.can_proc_from_suppressed)
62        }
63    }
64}
65
66fn impact_actor_matches(filter: ImpactActorFilter, impact: ImpactEvent) -> bool {
67    match filter {
68        ImpactActorFilter::Player => matches!(impact.source, wowlab_types::sim::ActorId::Player),
69        ImpactActorFilter::Companion => matches!(impact.source, wowlab_types::sim::ActorId::Pet(_)),
70        ImpactActorFilter::Any => true,
71    }
72}
73
74pub(super) fn impact_filter_matches(filter: ImpactFilter, impact: ImpactEvent) -> bool {
75    if !impact_actor_matches(filter.actor, impact) {
76        return false;
77    }
78
79    if !filter.periodic.matches(impact.is_periodic) {
80        return false;
81    }
82
83    if filter.physical_only && !impact.is_physical {
84        return false;
85    }
86
87    if filter.crit_only && !impact.is_crit() {
88        return false;
89    }
90
91    filter.spell.is_none_or(|matches| matches(impact.spell_id))
92}
93
94fn fire_impact_proc(ctx: &mut CombatCtx<'_>, proc: ImpactProc, impact: ImpactEvent) {
95    if !impact_proc_matches(&proc, impact) || !wowlab_engine_rng::proc_chance(ctx.rng, proc.chance)
96    {
97        return;
98    }
99
100    let mut hook_ctx = HookCtx::new(
101        crate::context::HookCtxServices {
102            state: ctx.state,
103            buf: ctx.buf,
104            sink: ctx.sink,
105            rng: ctx.rng,
106        },
107        crate::context::HookCtxRequest::for_target(ctx.now, impact.target)
108            .with_source(impact.source),
109    )
110    .with_driver_spell(impact.spell_id)
111    .with_source_damage_flags(crate::DamageFlags::PROC);
112
113    (proc.fire)(&mut hook_ctx, impact);
114}
115
116pub(crate) fn fire_landed_impact_batch(ctx: &mut CombatCtx<'_>, landed_impacts: &[ImpactEvent]) {
117    let Ok(targets_hit) = u8::try_from(landed_impacts.len()) else {
118        debug_assert!(false, "damage-impact batches are bounded by u8");
119
120        return;
121    };
122
123    if targets_hit == 0 || ctx.state.defs.landed_impact_procs.is_empty() {
124        return;
125    }
126
127    let original = std::mem::take(&mut ctx.state.defs.landed_impact_procs);
128
129    for (ordinal, impact) in landed_impacts.iter().copied().enumerate() {
130        let target_ordinal = u8::try_from(ordinal).expect("landed impact batch is bounded by u8");
131
132        for proc in original.iter().copied() {
133            if !proc_policy_allows(impact, proc.driver)
134                || !impact_filter_matches(
135                    ImpactFilter {
136                        actor: proc.actor_filter,
137                        spell: proc.spell_filter,
138                        periodic: PeriodicImpactFilter::from(PeriodicImpactPolicy {
139                            periodic_only: proc.periodic_only,
140                            skip_periodic: proc.skip_periodic,
141                        }),
142                        physical_only: false,
143                        crit_only: proc.crit_only,
144                    },
145                    impact,
146                )
147                || !wowlab_engine_rng::proc_chance(ctx.rng, proc.chance)
148            {
149                continue;
150            }
151
152            let event = crate::state::LandedImpactEvent {
153                impact,
154                target_ordinal,
155                targets_hit,
156            };
157            let mut hook_ctx = HookCtx::new(
158                crate::context::HookCtxServices {
159                    state: ctx.state,
160                    buf: ctx.buf,
161                    sink: ctx.sink,
162                    rng: ctx.rng,
163                },
164                crate::context::HookCtxRequest::for_target(ctx.now, impact.target)
165                    .with_source(impact.source),
166            )
167            .with_driver_spell(impact.spell_id)
168            .with_source_damage_flags(crate::DamageFlags::PROC);
169
170            (proc.fire)(&mut hook_ctx, event);
171        }
172    }
173
174    ctx.state.defs.landed_impact_procs = original;
175}
176
177pub(crate) fn fire_impact_procs(ctx: &mut CombatCtx<'_>, impact: ImpactEvent) {
178    // docref:start procs-impact-short-circuit
179    if ctx.state.defs.impact_procs.is_empty()
180        && ctx.state.defs.accumulating_impact_procs.is_empty()
181        && ctx.state.defs.impact_effect_procs.is_empty()
182    {
183        return;
184    }
185    // docref:end procs-impact-short-circuit
186
187    if impact.kind == ProcEventKind::Spell
188        && impact.phase == ProcPhaseMask::HIT
189        && impact.hit_mask.contains(ProcHitMask::LANDED)
190        && impact.amount > 0.0
191    {
192        // `mem::take` + restore avoids the per-damage-event `Vec::clone`: 33% faster, 5 allocs/cast -> 0.
193        let original = std::mem::take(&mut ctx.state.defs.impact_procs);
194        let scratch = std::mem::take(&mut ctx.state.runtime.scratch.impacts);
195        let (original, scratch) = with_scratch(
196            original,
197            scratch,
198            ScratchDisposition::PreserveSource,
199            |entries| {
200                for proc in entries.iter().copied() {
201                    fire_impact_proc(ctx, proc, impact);
202                }
203            },
204        );
205
206        ctx.state.defs.impact_procs = original;
207        ctx.state.runtime.scratch.impacts = scratch;
208
209        fire_accumulating_impact_procs(ctx, impact);
210    }
211
212    fire_impact_effect_procs(ctx, impact, None);
213}
214
215pub(crate) fn fire_aura_application_procs(ctx: &mut CombatCtx<'_>, event: AuraApplicationEvent) {
216    let target = match event.affected {
217        wowlab_types::sim::ActorId::Enemy(target) => target,
218        _ => event.target.unwrap_or(ctx.target),
219    };
220    let impact = ImpactEvent {
221        kind: ProcEventKind::AuraApplication,
222        phase: ProcPhaseMask::HIT,
223        hit_mask: ProcHitMask::LANDED,
224        spell_id: event.aura_spell_id,
225        amount: 0.0,
226        is_periodic: false,
227        is_physical: false,
228        swing_hand: None,
229        swing_speed_ms: 0,
230        source: event.source,
231        target,
232        provenance: event.provenance,
233    };
234
235    fire_impact_effect_procs(
236        ctx,
237        impact,
238        (event.originating_proc_driver_spell_id != 0)
239            .then_some(event.originating_proc_driver_spell_id),
240    );
241}
242use crate::{AuraOps as _, ProcOps as _};