Skip to main content

wowlab_engine_combat/state/defs/
procs.rs

1use super::{
2    ActorId, CombatProgram, EnemyIdx, HookCtx, LocalAuraIdx, LocalRppmIdx, ResolvedGameData,
3    SpellAttributeKind, SpellEffectRange, SpellIdx, spell_attribute_is,
4};
5use crate::state::ResourceGainSource;
6
7pub type ImpactProcFn = fn(&mut HookCtx<'_>, ImpactEvent);
8pub type LandedImpactProcFn = fn(&mut HookCtx<'_>, LandedImpactEvent);
9
10bitflags::bitflags! {
11    /// Spellcast phases eligible to trigger a proc.
12    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
13    pub struct ProcPhaseMask: u8 {
14        const CAST = 1 << 0;
15        const HIT = 1 << 1;
16        const FINISH = 1 << 2;
17    }
18}
19
20impl Default for ProcPhaseMask {
21    fn default() -> Self {
22        Self::HIT
23    }
24}
25
26bitflags::bitflags! {
27    /// Hit outcomes eligible to trigger a proc.
28    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
29    pub struct ProcHitMask: u16 {
30        const NORMAL = 1 << 0;
31        const CRITICAL = 1 << 1;
32        const MISS = 1 << 2;
33        const DODGE = 1 << 3;
34        const PARRY = 1 << 4;
35        const GLANCE = 1 << 5;
36        const BLOCK = 1 << 6;
37        const ABSORB = 1 << 7;
38        const LANDED = 1 << 8;
39    }
40}
41
42impl ProcHitMask {
43    #[must_use]
44    pub const fn from_hit_result(result: wowlab_types::combat::HitResult) -> Self {
45        use wowlab_types::combat::HitResult;
46
47        match result {
48            HitResult::Hit => Self::NORMAL.union(Self::LANDED),
49            HitResult::Crit => Self::CRITICAL.union(Self::LANDED),
50            HitResult::Miss => Self::MISS,
51            HitResult::Dodge => Self::DODGE,
52            HitResult::Parry => Self::PARRY,
53            HitResult::Glance => Self::GLANCE.union(Self::LANDED),
54            HitResult::Block | HitResult::CritBlock => Self::BLOCK.union(Self::LANDED),
55        }
56    }
57}
58
59impl Default for ProcHitMask {
60    fn default() -> Self {
61        Self::LANDED
62    }
63}
64
65/// Event family carried through proc matching.
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
67#[non_exhaustive]
68pub enum ProcEventKind {
69    #[default]
70    Spell,
71    AuraApplication,
72    Heartbeat,
73    Kill,
74    Dispel,
75}
76
77/// One completed aura application eligible for caster-side DBC proc callbacks.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79#[must_use]
80pub(crate) struct AuraApplicationEvent {
81    pub aura_spell_id: u32,
82    pub action: crate::state::program::LocalDamageActionIdx,
83    pub source: ActorId,
84    pub affected: ActorId,
85    pub target: Option<EnemyIdx>,
86    pub provenance: ProcProvenance,
87    pub originating_proc_driver_spell_id: u32,
88}
89
90/// Hook fired after an effective resource gain.
91pub(crate) type ResourceGainProcFn = fn(&mut HookCtx<'_>, ResourceGainEvent);
92
93/// One completed resource gain, including the amount rejected by the resource cap.
94#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct ResourceGainEvent {
96    pub actual: f64,
97    pub wasted: f64,
98    pub source: ResourceGainSource,
99    pub secondary: bool,
100}
101
102/// How many proc attempts a resource-gain event contributes.
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104#[non_exhaustive]
105pub enum ResourceGainProcAttempts {
106    Event,
107    PerWholeUnit,
108}
109
110/// A pseudo-random proc driven by effective resource gains.
111#[derive(Clone, Copy, Debug)]
112#[must_use]
113pub struct ResourceGainProc {
114    pub prd_constant: f64,
115    pub attempts: u32,
116    pub attempt_mode: ResourceGainProcAttempts,
117    pub secondary: bool,
118    pub fire: ResourceGainProcFn,
119}
120
121impl ResourceGainProc {
122    pub fn prd(average_rate: f64, fire: ResourceGainProcFn) -> Self {
123        Self {
124            prd_constant: wowlab_engine_rng::prd_constant(average_rate),
125            attempts: 0,
126            attempt_mode: ResourceGainProcAttempts::Event,
127            secondary: false,
128            fire,
129        }
130    }
131
132    pub const fn per_whole_unit(mut self) -> Self {
133        self.attempt_mode = ResourceGainProcAttempts::PerWholeUnit;
134
135        self
136    }
137
138    pub const fn secondary(mut self) -> Self {
139        self.secondary = true;
140
141        self
142    }
143}
144
145#[derive(Clone, Copy, Debug)]
146#[must_use]
147pub struct ImpactEvent {
148    pub kind: ProcEventKind,
149    pub phase: ProcPhaseMask,
150    pub hit_mask: ProcHitMask,
151    pub spell_id: u32,
152    pub(crate) action: Option<crate::state::program::LocalDamageActionIdx>,
153    pub amount: f64,
154    pub is_periodic: bool,
155    pub is_physical: bool,
156    pub swing_hand: Option<SwingHand>,
157    pub swing_speed_ms: u32,
158    pub source: ActorId,
159    pub target: EnemyIdx,
160    pub provenance: ProcProvenance,
161}
162
163#[derive(Clone, Copy, Debug)]
164pub struct ImpactEventData {
165    pub kind: ProcEventKind,
166    pub phase: ProcPhaseMask,
167    pub hit_mask: ProcHitMask,
168    pub amount: f64,
169    pub source: ActorId,
170    pub target: EnemyIdx,
171    pub provenance: ProcProvenance,
172}
173
174impl ImpactEvent {
175    #[cfg(feature = "test-support")]
176    pub(crate) const fn bound(
177        action: crate::DamageActionId,
178        local: crate::state::program::LocalDamageActionIdx,
179        data: ImpactEventData,
180    ) -> Self {
181        Self {
182            kind: data.kind,
183            phase: data.phase,
184            hit_mask: data.hit_mask,
185            spell_id: action.spell_id(),
186            action: Some(local),
187            amount: data.amount,
188            is_periodic: false,
189            is_physical: false,
190            swing_hand: None,
191            swing_speed_ms: 0,
192            source: data.source,
193            target: data.target,
194            provenance: data.provenance,
195        }
196    }
197
198    #[must_use]
199    pub const fn is_crit(self) -> bool {
200        self.hit_mask.contains(ProcHitMask::CRITICAL)
201    }
202
203    #[must_use]
204    pub const fn is_pet(self) -> bool {
205        matches!(self.source, ActorId::Pet(_))
206    }
207}
208
209/// One landed member of a fully resolved damage-impact batch.
210///
211/// Unlike [`ImpactEvent`], this event is emitted after all selected targets roll.
212/// Fully absorbed hits remain landed.
213#[derive(Clone, Copy, Debug)]
214#[must_use]
215pub struct LandedImpactEvent {
216    pub impact: ImpactEvent,
217    /// Zero-based position among landed targets.
218    pub target_ordinal: u8,
219    /// Number of targets whose attack-table result landed.
220    pub targets_hit: u8,
221}
222
223/// DBC policy carried by a triggering action or registered proc driver.
224#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
225#[must_use]
226#[expect(
227    clippy::struct_excessive_bools,
228    reason = "proc policy is a direct set of independent DBC attributes"
229)]
230pub struct ProcPolicy {
231    pub suppress_caster_procs: bool,
232    pub suppress_target_procs: bool,
233    pub can_proc_from_procs: bool,
234    pub can_proc_from_suppressed_target: bool,
235    pub enable_procs_from_suppressed: bool,
236    pub can_proc_from_suppressed: bool,
237    pub only_proc_from_class_abilities: bool,
238    pub allow_class_ability_procs: bool,
239}
240
241impl ProcPolicy {
242    pub fn from_attributes(attributes: &[i32]) -> Self {
243        Self {
244            suppress_caster_procs: spell_attribute_is(
245                attributes,
246                SpellAttributeKind::SuppressCasterProcs,
247            ),
248            suppress_target_procs: spell_attribute_is(
249                attributes,
250                SpellAttributeKind::SuppressTargetProcs,
251            ),
252            can_proc_from_procs: spell_attribute_is(
253                attributes,
254                SpellAttributeKind::CanProcFromProcs,
255            ),
256            can_proc_from_suppressed_target: spell_attribute_is(
257                attributes,
258                SpellAttributeKind::CanProcFromSuppressedTarget,
259            ),
260            enable_procs_from_suppressed: spell_attribute_is(
261                attributes,
262                SpellAttributeKind::EnableProcsFromSuppressed,
263            ),
264            can_proc_from_suppressed: spell_attribute_is(
265                attributes,
266                SpellAttributeKind::CanProcFromSuppressed,
267            ),
268            only_proc_from_class_abilities: spell_attribute_is(
269                attributes,
270                SpellAttributeKind::OnlyProcFromClassAbilities,
271            ),
272            allow_class_ability_procs: spell_attribute_is(
273                attributes,
274                SpellAttributeKind::AllowClassAbilityProcs,
275            ),
276        }
277    }
278}
279
280/// Whether an impact came from an ordinary action, another proc, or a weapon swing.
281#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
282#[non_exhaustive]
283pub enum ProcSourceKind {
284    #[default]
285    Action,
286    Proc,
287    Weapon,
288}
289
290/// Triggering-action identity and policy carried by each impact.
291#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
292#[must_use]
293pub struct ProcProvenance {
294    pub trigger_spell_id: u32,
295    pub kind: ProcSourceKind,
296    pub policy: ProcPolicy,
297}
298
299impl ProcProvenance {
300    pub fn from_spell(
301        game_data: &ResolvedGameData,
302        trigger_spell_id: u32,
303        kind: ProcSourceKind,
304    ) -> Self {
305        let attributes = game_data
306            .spell_attributes(SpellIdx::from_raw(trigger_spell_id))
307            .unwrap_or_default();
308
309        Self {
310            trigger_spell_id,
311            kind,
312            policy: ProcPolicy::from_attributes(attributes),
313        }
314    }
315}
316
317/// Which actor-side callback owns a registered proc.
318#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
319#[non_exhaustive]
320pub enum ProcTriggerSide {
321    #[default]
322    Caster,
323    Target,
324}
325
326/// Policy and callback ownership for a registered proc driver.
327#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
328#[must_use]
329pub struct ProcDriver {
330    pub policy: ProcPolicy,
331    pub trigger_side: ProcTriggerSide,
332}
333
334impl ProcDriver {
335    pub fn from_spell(
336        game_data: &ResolvedGameData,
337        driver_spell_id: u32,
338        trigger_side: ProcTriggerSide,
339    ) -> Self {
340        let attributes = game_data
341            .spell_attributes(SpellIdx::from_raw(driver_spell_id))
342            .unwrap_or_default();
343
344        Self {
345            policy: ProcPolicy::from_attributes(attributes),
346            trigger_side,
347        }
348    }
349}
350
351/// Which hand produced an auto-attack swing.
352#[derive(Clone, Copy, Debug, Eq, PartialEq)]
353// #t(rust_non_exhaustive_on_public) two hands is a fixed game rule; hooks match exhaustively
354pub enum SwingHand {
355    MainHand,
356    OffHand,
357}
358
359/// One completed auto-attack swing supplied to content hooks.
360#[derive(Clone, Copy, Debug, Eq, PartialEq)]
361#[must_use]
362pub struct SwingEvent {
363    pub hand: SwingHand,
364    pub is_crit: bool,
365}
366
367/// Hook fired after an auto-attack swing lands.
368pub(crate) type SwingHookFn = fn(&mut HookCtx<'_>, SwingEvent);
369
370/// Hook that conditionally overrides the default dual-wield white-miss chance.
371pub(crate) type SwingMissChanceHookFn = fn(&HookCtx<'_>, SwingHand) -> Option<f64>;
372
373/// Data-driven spell selected by a hook to replace the current landed auto attack.
374#[derive(Clone, Copy, Debug, PartialEq)]
375pub struct SwingReplacement {
376    pub action: crate::DamageActionId,
377    pub damage_multiplier: f64,
378}
379
380/// Hook fired after the white-hit roll and before damage; `Some` suppresses white damage.
381pub(crate) type SwingReplacementHookFn =
382    fn(&mut HookCtx<'_>, SwingHand) -> Option<SwingReplacement>;
383
384/// A registered per-damage-impact proc.
385#[derive(Clone, Copy, Debug)]
386pub struct ImpactProc {
387    pub driver: ProcDriver,
388    pub actor_filter: ImpactActorFilter,
389    pub chance: f64,
390    pub fire: ImpactProcFn,
391    pub spell_filter: Option<fn(u32) -> bool>,
392    pub periodic_only: bool,
393    pub skip_periodic: bool,
394    pub crit_only: bool,
395}
396
397impl ImpactProc {
398    /// Creates a player-driven impact proc with unit chance and no eligibility restrictions.
399    #[must_use]
400    pub fn new(fire: ImpactProcFn) -> Self {
401        Self {
402            driver: ProcDriver::default(),
403            actor_filter: ImpactActorFilter::Player,
404            chance: 1.0,
405            fire,
406            spell_filter: None,
407            periodic_only: false,
408            skip_periodic: false,
409            crit_only: false,
410        }
411    }
412
413    #[must_use]
414    pub fn with_driver(mut self, driver: ProcDriver) -> Self {
415        self.driver = driver;
416
417        self
418    }
419
420    #[must_use]
421    pub fn with_actor_filter(mut self, actor_filter: ImpactActorFilter) -> Self {
422        self.actor_filter = actor_filter;
423
424        self
425    }
426
427    #[must_use]
428    pub fn with_chance(mut self, chance: f64) -> Self {
429        self.chance = chance;
430
431        self
432    }
433
434    #[must_use]
435    pub fn with_spell_filter(mut self, spell_filter: fn(u32) -> bool) -> Self {
436        self.spell_filter = Some(spell_filter);
437
438        self
439    }
440
441    #[must_use]
442    pub fn periodic_only(mut self) -> Self {
443        self.periodic_only = true;
444
445        self
446    }
447
448    #[must_use]
449    pub fn skip_periodic(mut self) -> Self {
450        self.skip_periodic = true;
451
452        self
453    }
454
455    #[must_use]
456    pub fn crit_only(mut self) -> Self {
457        self.crit_only = true;
458
459        self
460    }
461}
462
463/// A registered callback for landed members of a resolved damage-impact batch.
464#[derive(Clone, Copy, Debug)]
465pub struct LandedImpactProc {
466    pub driver: ProcDriver,
467    pub actor_filter: ImpactActorFilter,
468    pub chance: f64,
469    pub fire: LandedImpactProcFn,
470    pub spell_filter: Option<fn(u32) -> bool>,
471    pub periodic_only: bool,
472    pub skip_periodic: bool,
473    pub crit_only: bool,
474}
475
476/// A registered per-impact proc driven by randomized contributions to a threshold accumulator.
477#[derive(Clone, Copy, Debug)]
478pub struct AccumulatingImpactProc {
479    pub driver: ProcDriver,
480    pub actor_filter: ImpactActorFilter,
481    pub threshold: f64,
482    pub accumulator: f64,
483    pub contribution: fn(&CombatProgram, ImpactEvent) -> f64,
484    pub fire: ImpactProcFn,
485    pub spell_filter: Option<fn(u32) -> bool>,
486    pub periodic_only: bool,
487    pub skip_periodic: bool,
488    pub crit_only: bool,
489}
490
491/// Actor whose impact is eligible to drive a proc.
492#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
493#[non_exhaustive]
494pub enum ImpactActorFilter {
495    #[default]
496    Player,
497    Companion,
498    Any,
499}
500
501#[derive(Clone, Copy, Debug, Eq, PartialEq)]
502#[non_exhaustive]
503pub enum ImpactSource {
504    Spell(u32),
505    AutoAttack(SwingHand),
506    /// DBC `SpellAuraOptions.ProcTypeMask` matched against outgoing damage impacts.
507    ProcTypeMask(u64),
508    Heartbeat,
509    Kill,
510    Dispel,
511}
512
513#[derive(Clone, Copy, Debug, Eq, PartialEq)]
514#[non_exhaustive]
515pub enum ImpactChanceScale {
516    Fixed,
517    WeaponSpeed {
518        baseline_ms: u32,
519    },
520    Rppm(LocalRppmIdx),
521    Shuffled {
522        success_entries: u32,
523        total_entries: u32,
524    },
525    Accumulated {
526        cap: u32,
527        initial_count: u32,
528    },
529}
530
531#[derive(Clone, Copy, Debug)]
532#[expect(
533    clippy::struct_excessive_bools,
534    reason = "lowered proc records retain independent phase, periodic, critical-hit, and charge policies"
535)]
536// #t(rust_similar_structs) runtime proc data uses packed effect ranges after the builder definition is lowered
537pub struct ImpactEffectProc {
538    pub driver_spell_id: u32,
539    pub driver_effect_index: u8,
540    pub driver: ProcDriver,
541    pub actor_filter: ImpactActorFilter,
542    pub source: ImpactSource,
543    pub chance: f64,
544    pub chance_scale: ImpactChanceScale,
545    pub icd_ms: u32,
546    pub target_icd_ms: u32,
547    pub phase_mask: ProcPhaseMask,
548    pub hit_mask: ProcHitMask,
549    pub charges: u8,
550    pub uses_stacks_for_charges: bool,
551    pub charge_aura: Option<LocalAuraIdx>,
552    pub periodic_only: bool,
553    pub skip_periodic: bool,
554    pub physical_only: bool,
555    pub crit_only: bool,
556    pub profile_spell_id: u32,
557    pub effects: SpellEffectRange,
558    pub fire: Option<ImpactProcFn>,
559}