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