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 #[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 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
90pub(crate) type ResourceGainProcFn = fn(&mut HookCtx<'_>, ResourceGainEvent);
92
93#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104#[non_exhaustive]
105pub enum ResourceGainProcAttempts {
106 Event,
107 PerWholeUnit,
108}
109
110#[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#[derive(Clone, Copy, Debug)]
214#[must_use]
215pub struct LandedImpactEvent {
216 pub impact: ImpactEvent,
217 pub target_ordinal: u8,
219 pub targets_hit: u8,
221}
222
223#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
282#[non_exhaustive]
283pub enum ProcSourceKind {
284 #[default]
285 Action,
286 Proc,
287 Weapon,
288}
289
290#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
319#[non_exhaustive]
320pub enum ProcTriggerSide {
321 #[default]
322 Caster,
323 Target,
324}
325
326#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
353pub enum SwingHand {
355 MainHand,
356 OffHand,
357}
358
359#[derive(Clone, Copy, Debug, Eq, PartialEq)]
361#[must_use]
362pub struct SwingEvent {
363 pub hand: SwingHand,
364 pub is_crit: bool,
365}
366
367pub(crate) type SwingHookFn = fn(&mut HookCtx<'_>, SwingEvent);
369
370pub(crate) type SwingMissChanceHookFn = fn(&HookCtx<'_>, SwingHand) -> Option<f64>;
372
373#[derive(Clone, Copy, Debug, PartialEq)]
375pub struct SwingReplacement {
376 pub action: crate::DamageActionId,
377 pub damage_multiplier: f64,
378}
379
380pub(crate) type SwingReplacementHookFn =
382 fn(&mut HookCtx<'_>, SwingHand) -> Option<SwingReplacement>;
383
384#[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 #[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#[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#[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#[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 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)]
536pub 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}