wowlab_engine_combat/builder/
impact_effect_proc_builder.rs1use 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 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 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}