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 #[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 #[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#[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#[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
89pub(crate) type ResourceGainProcFn = fn(&mut HookCtx<'_>, ResourceGainEvent);
91
92#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum ResourceGainProcAttempts {
105 Event,
106 PerWholeUnit,
107}
108
109#[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#[derive(Clone, Copy, Debug)]
178#[must_use]
179pub struct LandedImpactEvent {
180 pub impact: ImpactEvent,
181 pub target_ordinal: u8,
183 pub targets_hit: u8,
185}
186
187#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
246#[non_exhaustive]
247pub enum ProcSourceKind {
248 #[default]
249 Action,
250 Proc,
251 Weapon,
252}
253
254#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
283#[non_exhaustive]
284pub enum ProcTriggerSide {
285 #[default]
286 Caster,
287 Target,
288}
289
290#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
317pub enum SwingHand {
319 MainHand,
320 OffHand,
321}
322
323#[derive(Clone, Copy, Debug, Eq, PartialEq)]
325#[must_use]
326pub struct SwingEvent {
327 pub hand: SwingHand,
328 pub is_crit: bool,
329}
330
331pub(crate) type SwingHookFn = fn(&mut HookCtx<'_>, SwingEvent);
333
334pub(crate) type SwingMissChanceHookFn = fn(&HookCtx<'_>, SwingHand) -> Option<f64>;
336
337#[derive(Clone, Copy, Debug, PartialEq)]
339pub struct SwingReplacement {
340 pub spell_id: u32,
341 pub damage_multiplier: f64,
342}
343
344pub(crate) type SwingReplacementHookFn =
346 fn(&mut HookCtx<'_>, SwingHand) -> Option<SwingReplacement>;
347
348#[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 #[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#[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#[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#[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 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)]
500pub 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}