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