Skip to main content

wowlab_engine_combat/state/defs/
procs.rs

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