Skip to main content

wowlab_engine_combat/builder/
impact_effect_proc_builder.rs

1use wowlab_engine_domain::dbc::EffectLookup;
2
3use super::def::BuilderSpellEffect;
4use crate::state::{
5    ImpactActorFilter, ImpactChanceScale, ImpactProcFn, ImpactSource, LocalRppmIdx, ProcDriver,
6    ProcHitMask, ProcPhaseMask, ProcTriggerSide, ResourcePool,
7};
8
9#[derive(Clone, Debug)]
10#[must_use]
11#[expect(
12    clippy::struct_excessive_bools,
13    reason = "independent proc phase, periodic, critical-hit, and charge policies are orthogonal"
14)]
15pub struct ImpactEffectProcDefinition {
16    pub(crate) driver_spell_id: u32,
17    pub(crate) driver_effect_index: u8,
18    pub(crate) driver: ProcDriver,
19    pub(crate) actor_filter: ImpactActorFilter,
20    pub(crate) source: ImpactSource,
21    pub(crate) chance: f64,
22    pub(crate) chance_scale: ImpactChanceScale,
23    pub(crate) icd_ms: u32,
24    pub(crate) target_icd_ms: u32,
25    pub(crate) phase_mask: ProcPhaseMask,
26    pub(crate) hit_mask: ProcHitMask,
27    pub(crate) charges: u8,
28    pub(crate) uses_stacks_for_charges: bool,
29    pub(crate) periodic_only: bool,
30    pub(crate) skip_periodic: bool,
31    pub(crate) physical_only: bool,
32    pub(crate) crit_only: bool,
33    pub(crate) profile_spell_id: u32,
34    pub(crate) effects: Vec<BuilderSpellEffect>,
35    pub(crate) fire: Option<ImpactProcFn>,
36}
37
38impl ImpactEffectProcDefinition {
39    pub fn new(source: ImpactSource) -> Self {
40        Self {
41            driver_spell_id: 0,
42            driver_effect_index: 0,
43            driver: ProcDriver::default(),
44            actor_filter: ImpactActorFilter::Player,
45            source,
46            chance: 1.0,
47            chance_scale: ImpactChanceScale::Fixed,
48            icd_ms: 0,
49            target_icd_ms: 0,
50            phase_mask: ProcPhaseMask::HIT,
51            hit_mask: ProcHitMask::LANDED,
52            charges: 0,
53            uses_stacks_for_charges: false,
54            periodic_only: false,
55            skip_periodic: false,
56            physical_only: false,
57            crit_only: false,
58            profile_spell_id: 0,
59            effects: Vec::new(),
60            fire: None,
61        }
62    }
63
64    pub fn driver(mut self, driver: ProcDriver) -> Self {
65        self.driver = driver;
66
67        self
68    }
69
70    pub fn dbc_driver(mut self, spell_id: u32, effect_index: u8) -> Self {
71        self.driver_spell_id = spell_id;
72        self.driver_effect_index = effect_index;
73
74        self
75    }
76
77    /// Retain a DBC proc driver coordinate and derive its runtime policy and limits.
78    pub fn dbc_driver_from_data(mut self, lookup: EffectLookup<'_>) -> Self {
79        let game_data = lookup.data;
80        let driver = lookup.effect.spell;
81        let spell_id = driver.as_u32();
82        let effect_index = lookup.effect.effect_index;
83
84        self.driver_spell_id = spell_id;
85        self.driver_effect_index = effect_index;
86        self.driver = ProcDriver::from_spell(game_data, spell_id, ProcTriggerSide::Caster);
87        self.charges = game_data.proc_charges(driver).unwrap_or(0);
88        self.icd_ms = game_data.proc_category_recovery_ms(driver).unwrap_or(0);
89
90        self
91    }
92
93    pub fn profile_spell(mut self, spell_id: u32) -> Self {
94        self.profile_spell_id = spell_id;
95
96        self
97    }
98
99    pub fn actor_filter(mut self, actor_filter: ImpactActorFilter) -> Self {
100        self.actor_filter = actor_filter;
101
102        self
103    }
104
105    pub fn chance(mut self, chance: f64) -> Self {
106        self.chance = chance;
107
108        self
109    }
110
111    pub fn weapon_speed_scaled(mut self, baseline_ms: u32) -> Self {
112        self.chance_scale = ImpactChanceScale::WeaponSpeed { baseline_ms };
113
114        self
115    }
116
117    pub fn rppm(mut self, tracker: LocalRppmIdx) -> Self {
118        self.chance_scale = ImpactChanceScale::Rppm(tracker);
119
120        self
121    }
122
123    /// Configure a shuffled proc deck with an exact number of successful entries.
124    ///
125    /// # Panics
126    ///
127    /// Panics when the deck is empty or has more successful entries than total entries.
128    pub fn shuffled(mut self, success_entries: u32, total_entries: u32) -> Self {
129        assert!(
130            total_entries > 0 && success_entries <= total_entries,
131            "shuffled proc deck requires 0 <= successes ({success_entries}) <= total entries ({total_entries}) and a non-empty deck"
132        );
133        self.chance_scale = ImpactChanceScale::Shuffled {
134            success_entries,
135            total_entries,
136        };
137
138        self
139    }
140
141    pub fn accumulated(mut self, cap: u32, initial_count: u32) -> Self {
142        self.chance_scale = ImpactChanceScale::Accumulated { cap, initial_count };
143
144        self
145    }
146
147    pub fn icd_ms(mut self, icd_ms: u32) -> Self {
148        self.icd_ms = icd_ms;
149
150        self
151    }
152
153    pub fn target_icd_ms(mut self, target_icd_ms: u32) -> Self {
154        self.target_icd_ms = target_icd_ms;
155
156        self
157    }
158
159    pub fn phase_mask(mut self, phase_mask: ProcPhaseMask) -> Self {
160        self.phase_mask = phase_mask;
161
162        self
163    }
164
165    pub fn hit_mask(mut self, hit_mask: ProcHitMask) -> Self {
166        self.hit_mask = hit_mask;
167
168        self
169    }
170
171    pub fn charges(mut self, charges: u8) -> Self {
172        self.charges = charges;
173
174        self
175    }
176
177    pub fn use_stacks_for_charges(mut self) -> Self {
178        self.uses_stacks_for_charges = true;
179
180        self
181    }
182
183    pub fn periodic_only(mut self) -> Self {
184        self.periodic_only = true;
185
186        self
187    }
188
189    pub fn skip_periodic(mut self) -> Self {
190        self.skip_periodic = true;
191
192        self
193    }
194
195    pub fn physical_only(mut self) -> Self {
196        self.physical_only = true;
197
198        self
199    }
200
201    pub fn crit_only(mut self) -> Self {
202        self.crit_only = true;
203
204        self
205    }
206
207    pub fn fire(mut self, fire: ImpactProcFn) -> Self {
208        self.fire = Some(fire);
209
210        self
211    }
212
213    pub fn gain(mut self, amount: f64) -> Self {
214        self.effects.push(BuilderSpellEffect::Energize {
215            effect: None,
216            pool: ResourcePool::Primary,
217            amount,
218        });
219
220        self
221    }
222
223    pub fn secondary_gain(mut self, amount: f64) -> Self {
224        self.effects.push(BuilderSpellEffect::Energize {
225            effect: None,
226            pool: ResourcePool::Secondary,
227            amount,
228        });
229
230        self
231    }
232
233    pub fn add_aura_stack(mut self, aura: crate::LocalAuraIdx) -> Self {
234        self.effects
235            .push(BuilderSpellEffect::AddAuraStack { aura_local: aura });
236
237        self
238    }
239}