1use super::{
2 CombatCtx, CombatInstance, EffectExecution, HookCtx, ImpactChanceScale, ImpactEffectProc,
3 ImpactEffectRngState, ImpactEvent, ImpactFilter, ImpactSource, PeriodicImpactFilter,
4 PeriodicImpactPolicy, ProcEventKind, ProcPhaseMask, SimTime,
5 execute_proc_effect_range_for_actor, impact_filter_matches, proc_policy_allows,
6 proc_type_mask_matches, with_scratch,
7};
8
9const FINALIZED_IMPACT_DRIVER: &str = "nonzero finalized impact proc driver has a dense action";
10
11fn impact_effect_source_matches(
12 ctx: &CombatCtx<'_>,
13 proc: ImpactEffectProc,
14 impact: ImpactEvent,
15) -> bool {
16 match proc.source {
17 ImpactSource::Spell(source_spell_id) => {
18 impact.kind == ProcEventKind::Spell
19 && impact.swing_hand.is_none()
20 && source_spell_id == impact.spell_id
21 }
22 ImpactSource::AutoAttack(source_hand) => {
23 impact.kind == ProcEventKind::Spell && impact.swing_hand == Some(source_hand)
24 }
25 ImpactSource::ProcTypeMask(mask) => {
26 matches!(
27 impact.kind,
28 ProcEventKind::Spell | ProcEventKind::AuraApplication
29 ) && proc_type_mask_matches(
30 impact.action.map_or(0, |action| {
31 ctx.program.damage.action_at(action).defense_type
32 }),
33 mask,
34 impact,
35 )
36 }
37 ImpactSource::Heartbeat => impact.kind == ProcEventKind::Heartbeat,
38 ImpactSource::Kill => impact.kind == ProcEventKind::Kill,
39 ImpactSource::Dispel => impact.kind == ProcEventKind::Dispel,
40 }
41}
42
43fn impact_effect_matches(ctx: &CombatCtx<'_>, proc: ImpactEffectProc, impact: ImpactEvent) -> bool {
44 proc_policy_allows(impact, proc.driver)
45 && proc.phase_mask.contains(impact.phase)
46 && (impact.phase != ProcPhaseMask::HIT || proc.hit_mask.intersects(impact.hit_mask))
47 && impact_filter_matches(
48 ImpactFilter {
49 actor: proc.actor_filter,
50 spell: None,
51 periodic: PeriodicImpactFilter::from(PeriodicImpactPolicy {
52 periodic_only: proc.periodic_only,
53 skip_periodic: proc.skip_periodic,
54 }),
55 physical_only: proc.physical_only,
56 crit_only: proc.crit_only,
57 },
58 impact,
59 )
60 && impact_effect_source_matches(ctx, proc, impact)
61 && proc.charge_aura.is_none_or(|aura| {
62 crate::systems::is_aura_active(&ctx.view(), aura, impact.source, Some(impact.target))
63 })
64}
65
66fn impact_effect_runtime_ready(
67 ctx: &CombatCtx<'_>,
68 proc: ImpactEffectProc,
69 index: usize,
70 impact: ImpactEvent,
71) -> bool {
72 let charges = effective_proc_charges(ctx.program, ctx.instance, impact.source, proc, index);
73 let cooldown_ready = ctx
74 .instance
75 .runtime
76 .procs
77 .impact_effect_ready_at
78 .get(index)
79 .is_some_and(|ready_at| ctx.now.as_secs_f64() >= *ready_at);
80 let target_ready = proc.target_icd_ms == 0
81 || ctx
82 .instance
83 .runtime
84 .procs
85 .target_icd(index, impact.target)
86 .is_none_or(|ready_at| ctx.now >= ready_at);
87 let has_charges = if proc.uses_stacks_for_charges {
88 proc.charge_aura.is_some_and(|aura| {
89 crate::systems::aura_stacks(
90 ctx.program,
91 ctx.instance,
92 aura,
93 impact.source,
94 Some(impact.target),
95 ) > 0
96 })
97 } else {
98 charges == 0
99 || ctx
100 .instance
101 .runtime
102 .procs
103 .impact_effect_charges
104 .get(index)
105 .is_some_and(|remaining| *remaining > 0)
106 };
107
108 cooldown_ready && target_ready && has_charges
109}
110
111pub(in crate::systems) fn effective_proc_charges(
112 program: &crate::state::CombatProgram,
113 state: &CombatInstance,
114 source: wowlab_types::sim::ActorId,
115 proc: ImpactEffectProc,
116 index: usize,
117) -> u8 {
118 if proc.driver_spell_id == 0 {
119 return proc.charges;
120 }
121
122 let value = crate::systems::buffs::active_spell_modifier_value(
123 crate::context::ActorView::new(program, state, source),
124 program
125 .damage
126 .impact_effect_driver_action(index)
127 .expect(FINALIZED_IMPACT_DRIVER),
128 wowlab_engine_domain::dbc::ModifierPropertyKind::ProcCharges,
129 f64::from(proc.charges),
130 );
131
132 let clamped =
133 wowlab_types::numeric::f64_to_u32_saturating_round(value.max(0.0)).min(u32::from(u8::MAX));
134
135 u8::try_from(clamped).expect("proc charges are clamped to u8")
136}
137
138fn roll_rppm_impact_effect(
139 ctx: &mut CombatCtx<'_>,
140 source: wowlab_types::sim::ActorId,
141 proc: ImpactEffectProc,
142 tracker: crate::state::LocalRppmIdx,
143 index: usize,
144) -> bool {
145 let scaling = crate::systems::buffs::current_rppm_scaling(&ctx.view());
146 let base_rate = ctx
147 .instance
148 .runtime
149 .procs
150 .rppm_trackers
151 .get(tracker.as_usize())
152 .map_or(0.0, |tracker| tracker.rppm);
153 let rate = if proc.driver_spell_id == 0 {
154 base_rate
155 } else {
156 crate::systems::buffs::active_spell_modifier_value(
157 ctx.view().for_actor(source),
158 ctx.program
159 .damage
160 .impact_effect_driver_action(index)
161 .expect(FINALIZED_IMPACT_DRIVER),
162 wowlab_engine_domain::dbc::ModifierPropertyKind::ProcFrequency,
163 base_rate,
164 )
165 .max(0.0)
166 };
167 let Some(tracker) = ctx
168 .instance
169 .runtime
170 .procs
171 .rppm_trackers
172 .get_mut(tracker.as_usize())
173 else {
174 return false;
175 };
176 let base_rate = std::mem::replace(&mut tracker.rppm, rate);
177 let success = wowlab_engine_rng::roll_rppm(tracker, ctx.now.as_secs_f64(), scaling, ctx.rng);
178
179 tracker.rppm = base_rate;
180
181 success
182}
183
184fn roll_impact_effect(
185 ctx: &mut CombatCtx<'_>,
186 proc: ImpactEffectProc,
187 index: usize,
188 impact: ImpactEvent,
189) -> bool {
190 match proc.chance_scale {
191 ImpactChanceScale::Fixed => wowlab_engine_rng::proc_chance(ctx.rng, proc.chance),
192 ImpactChanceScale::WeaponSpeed { baseline_ms } => {
193 let chance = if baseline_ms == 0 {
194 0.0
195 } else {
196 proc.chance * f64::from(impact.swing_speed_ms) / f64::from(baseline_ms)
197 };
198
199 wowlab_engine_rng::proc_chance(ctx.rng, chance)
200 }
201 ImpactChanceScale::Rppm(tracker) => {
202 roll_rppm_impact_effect(ctx, impact.source, proc, tracker, index)
203 }
204 ImpactChanceScale::Shuffled { .. } => ctx
205 .instance
206 .runtime
207 .procs
208 .impact_effect_rng
209 .get_mut(index)
210 .is_some_and(|state| match state {
211 ImpactEffectRngState::Shuffled(tracker) => tracker.trigger(ctx.rng),
212 _ => false,
213 }),
214 ImpactChanceScale::Accumulated { .. } => ctx
215 .instance
216 .runtime
217 .procs
218 .impact_effect_rng
219 .get_mut(index)
220 .is_some_and(|state| match state {
221 ImpactEffectRngState::Accumulated(tracker) => !matches!(
222 tracker.trigger(ctx.rng),
223 wowlab_engine_rng::AccumulatedOutcome::Failed
224 ),
225 _ => false,
226 }),
227 }
228}
229
230fn record_impact_effect_success(
231 ctx: &mut CombatCtx<'_>,
232 proc: ImpactEffectProc,
233 index: usize,
234 impact: ImpactEvent,
235) -> bool {
236 let icd_ms = if proc.driver_spell_id == 0 {
237 proc.icd_ms
238 } else {
239 let value = crate::systems::buffs::active_spell_modifier_value(
240 ctx.view().for_actor(impact.source),
241 ctx.program
242 .damage
243 .impact_effect_driver_action(index)
244 .expect(FINALIZED_IMPACT_DRIVER),
245 wowlab_engine_domain::dbc::ModifierPropertyKind::ProcCooldown,
246 f64::from(proc.icd_ms),
247 );
248
249 wowlab_types::numeric::f64_to_u32_saturating_round(value.max(0.0))
250 };
251
252 if let Some(ready_at) = ctx
253 .instance
254 .runtime
255 .procs
256 .impact_effect_ready_at
257 .get_mut(index)
258 {
259 *ready_at = ctx
260 .now
261 .saturating_add(SimTime::from_millis(icd_ms))
262 .as_secs_f64();
263 }
264
265 if proc.target_icd_ms > 0 {
266 ctx.instance.runtime.procs.set_target_icd(
267 index,
268 impact.target,
269 ctx.now
270 .saturating_add(SimTime::from_millis(proc.target_icd_ms)),
271 );
272 }
273
274 if proc.uses_stacks_for_charges
275 || effective_proc_charges(ctx.program, ctx.instance, impact.source, proc, index) == 0
276 {
277 return false;
278 }
279
280 let Some(remaining) = ctx
281 .instance
282 .runtime
283 .procs
284 .impact_effect_charges
285 .get_mut(index)
286 else {
287 return false;
288 };
289
290 *remaining = remaining.saturating_sub(1);
291
292 *remaining == 0
293}
294
295fn consume_impact_effect_aura_stack(
296 ctx: &mut CombatCtx<'_>,
297 proc: ImpactEffectProc,
298 impact: ImpactEvent,
299) {
300 if !proc.uses_stacks_for_charges {
301 return;
302 }
303
304 let Some(aura) = proc.charge_aura else {
305 return;
306 };
307
308 let mut aura_ctx = HookCtx::new(
309 crate::context::HookCtxServices {
310 program: ctx.program,
311 instance: ctx.instance,
312 sink: ctx.sink,
313 rng: ctx.rng,
314 },
315 crate::context::HookCtxRequest::for_target(ctx.now, impact.target)
316 .with_source(impact.source),
317 );
318
319 crate::systems::consume_aura_stack(&mut aura_ctx, aura);
320}
321
322fn expire_depleted_impact_effect_aura(
323 ctx: &mut CombatCtx<'_>,
324 proc: ImpactEffectProc,
325 impact: ImpactEvent,
326) {
327 let Some(aura) = proc.charge_aura else {
328 return;
329 };
330 let mut aura_ctx = HookCtx::new(
331 crate::context::HookCtxServices {
332 program: ctx.program,
333 instance: ctx.instance,
334 sink: ctx.sink,
335 rng: ctx.rng,
336 },
337 crate::context::HookCtxRequest::for_target(ctx.now, impact.target)
338 .with_source(impact.source),
339 );
340
341 crate::systems::expire_aura(&mut aura_ctx, aura);
342}
343
344const fn impact_effect_driver_spell_id(proc: ImpactEffectProc, impact: ImpactEvent) -> u32 {
345 if proc.driver_spell_id == 0 {
346 impact.spell_id
347 } else {
348 proc.driver_spell_id
349 }
350}
351
352pub(super) fn fire_impact_effect_procs(
353 ctx: &mut CombatCtx<'_>,
354 impact: ImpactEvent,
355 excluded_driver_spell_id: Option<u32>,
356) {
357 if ctx.instance.runtime.procs.impact_effect_procs_dispatching {
358 return;
359 }
360
361 ctx.instance.runtime.procs.impact_effect_procs_dispatching = true;
362 let scratch = std::mem::take(&mut ctx.instance.runtime.scratch.impact_effects);
363 let scratch = with_scratch(&ctx.program.impact_effect_procs, scratch, |entries| {
364 for (index, proc) in entries.iter().copied().enumerate() {
365 if excluded_driver_spell_id == Some(proc.driver_spell_id)
368 || !impact_effect_matches(ctx, proc, impact)
369 || !impact_effect_runtime_ready(ctx, proc, index, impact)
370 || !roll_impact_effect(ctx, proc, index, impact)
371 {
372 continue;
373 }
374
375 let depleted = record_impact_effect_success(ctx, proc, index, impact);
376
377 consume_impact_effect_aura_stack(ctx, proc, impact);
378
379 if let Some(fire) = proc.fire {
380 let driver_spell_id = impact_effect_driver_spell_id(proc, impact);
381 let mut hook_ctx = HookCtx::new(
382 crate::context::HookCtxServices {
383 program: ctx.program,
384 instance: ctx.instance,
385 sink: ctx.sink,
386 rng: ctx.rng,
387 },
388 crate::context::HookCtxRequest::for_target(ctx.now, impact.target)
389 .with_source(impact.source),
390 )
391 .with_driver_spell(driver_spell_id)
392 .with_source_damage_flags(crate::DamageFlags::PROC);
393
394 fire(&mut hook_ctx, impact);
395 }
396
397 execute_proc_effect_range_for_actor(
398 ctx,
399 EffectExecution {
400 range: proc.effects,
401 profile_spell_id: if proc.profile_spell_id == 0 {
402 impact_effect_driver_spell_id(proc, impact)
403 } else {
404 proc.profile_spell_id
405 },
406 profile_action: ctx
407 .program
408 .damage
409 .impact_effect_profile_action(index)
410 .or(impact.action)
411 .expect("impact effect program carries a finalized profile action"),
412 flags: crate::DamageFlags::PROC,
413 },
414 None,
415 impact_effect_driver_spell_id(proc, impact),
416 );
417
418 if depleted {
419 expire_depleted_impact_effect_aura(ctx, proc, impact);
420 }
421 }
422 });
423
424 ctx.instance.runtime.scratch.impact_effects = scratch;
425 ctx.instance.runtime.procs.impact_effect_procs_dispatching = false;
426}