1use wowlab_engine_combat::AuraOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::DamageOps as _;
4use wowlab_engine_combat::GuardianOps as _;
5use wowlab_engine_combat::ProcOps as _;
6use wowlab_engine_combat::SchedulingOps as _;
7use wowlab_engine_combat::{
8 DamageFlags, DamageOutcome, GuardianAbility, GuardianEvent, GuardianSpec, HookCtx, MasteryCtx,
9 SwingEvent, SwingHand,
10};
11use wowlab_engine_domain::constants::OFF_HAND_DAMAGE_MULT;
12#[cfg(test)]
13use wowlab_engine_domain::dbc::{damage_def, spell_attribute_is, DefenseType, SpellAttributeKind};
14#[cfg(test)]
15use wowlab_engine_gamedata::ResolvedGameData;
16use wowlab_engine_rng::{accumulated_proc_chance, proc_chance};
17#[cfg(test)]
18use wowlab_types::sim::SpellIdx;
19use wowlab_types::{combat::DamageSchool, constants::HUNDRED};
20
21#[cfg(test)]
22use crate::generated::specs::enhancement_shaman::{AURA, REPORTED_SPELL};
23use crate::generated::specs::enhancement_shaman::{
24 AURA_AMPLIFICATION_CORE, AURA_BURNING_CORE, AURA_CRACKLING_SURGE, AURA_CRASH_LIGHTNING,
25 AURA_DOOM_WINDS, AURA_FLAME_SHOCK, AURA_HOT_HAND, AURA_LASHING_FLAMES, AURA_MAELSTROM_WEAPON,
26 AURA_MOLTEN_WEAPON, AURA_SHORT_CIRCUIT, AURA_STORMSURGE, AURA_STORM_UNLEASHED,
27 AURA_SURGING_TOTEM, AURA_TOTEMIC_REBOUND, AURA_WHIRLING_AIR, AURA_WHIRLING_EARTH,
28 AURA_WHIRLING_FIRE, SPELL, SPELL_LAVA_LASH, SPELL_STORMSTRIKE, TALENT,
29};
30
31const STORMFLURRY_CHANCE: f64 = 0.20;
33const STORMFLURRY_MULT: f64 = 0.40;
35const DOOM_WINDS_WF_PROC_MULT: f64 = 2.0;
37const IMBUEMENT_MASTERY_CHANCE: f64 = 0.07;
39const LIVELY_TOTEMS_CHANCE: f64 = 0.06;
40const MW_CONSUME_CAP: i32 = 10;
42const MW_DMG_PER_STACK: f64 = 0.20;
44const WHIRLING_AIR_BOLTS: u32 = 3;
46const EARTHSURGE_MULT: f64 = 1.5;
48const WHIRLING_FIRE_HOT_HAND_MS: u32 = 8_000;
50const FIRE_WOLF_GUARDIAN_TAG: u32 = 4_693_141;
52const LIGHTNING_WOLF_GUARDIAN_TAG: u32 = 4_693_142;
53const SEARING_TOTEM_DURATION_MS: u32 = 8_000;
54const SEARING_TOTEM_PULSE_MS: u32 = 2_000;
55const PRIMORDIAL_FROST_DELAY_MS: u32 = 300;
56const PRIMORDIAL_LIGHTNING_DELAY_MS: u32 = 600;
57const PRIMORDIAL_LIGHTNING_BOLT_DELAY_MS: u32 = 950;
58const PRIMORDIAL_LIGHTNING_BOLT_MULT: f64 = 1.5;
59const SURGING_BOLT_DELAY_MS: u32 = 300;
60const WHIRLING_AIR_BOLT_SPACING_MS: u32 = 500;
61const CRASH_LIGHTNING_UNLEASHED_FIRST_MS: u32 = 1_000;
62const CRASH_LIGHTNING_UNLEASHED_SECOND_MS: u32 = 2_000;
63const RIDE_THE_LIGHTNING_MULT: f64 = 1.0;
65
66pub(crate) const fn hook_owned_damage_trigger_disabled(_ctx: &HookCtx<'_>) -> bool {
68 false
69}
70
71fn windfury_chance(ctx: &HookCtx<'_>) -> f64 {
72 let base = ctx.action_proc_chance(
73 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::WINDFURY_PROC.action(),
74 );
75 let mastery_bonus = ctx.effect_points(
76 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::WINDFURY_MASTERY,
77 ) / HUNDRED;
78 let mut chance = base + mastery_bonus;
79
80 if ctx.is_aura_active(AURA_DOOM_WINDS) {
81 chance *= DOOM_WINDS_WF_PROC_MULT;
82 }
83
84 chance
85}
86
87fn stormsurge_chance(ctx: &HookCtx<'_>) -> f64 {
88 let base = ctx.action_proc_chance(
89 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::STORMSURGE_DRIVER,
90 );
91
92 base + ctx.effect_points(
93 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::STORMSURGE_MASTERY,
94 ) / HUNDRED
95}
96
97use super::{
98 config::{enhancement_config, EnhancementConfig, EnhancementRuntime, WINDFURY_BASE_HITS},
99 guardians::summon_feral_spirit,
100};
101
102fn searing_totem_pulse(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
103 let coefficient = ctx.effect_sp_coefficient(
104 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::SEARING_BOLT_DAMAGE,
105 );
106
107 ctx.deal_damage_sp(
108 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::SEARING_BOLT,
109 coefficient,
110 DamageFlags::empty(),
111 );
112}
113
114fn summon_searing_totem(ctx: &mut HookCtx<'_>) {
115 let _ = ctx.summon_guardian(
116 GuardianSpec::new_inheriting_owner(TALENT::LIVELY_TOTEMS, SEARING_TOTEM_DURATION_MS)
117 .ability(
118 GuardianAbility::new(SEARING_TOTEM_PULSE_MS, searing_totem_pulse)
119 .first_action_delay(SEARING_TOTEM_PULSE_MS),
120 ),
121 );
122}
123
124fn trigger_searing_volley(ctx: &mut HookCtx<'_>) {
125 let count = ctx.active_guardian_count(TALENT::LIVELY_TOTEMS);
126
127 if count == 0 {
128 return;
129 }
130
131 let coefficient = ctx.effect_sp_coefficient(
132 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::SEARING_VOLLEY_DAMAGE,
133 );
134
135 for _ in 0..count {
136 ctx.deal_damage_sp(
137 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::SEARING_VOLLEY,
138 coefficient,
139 DamageFlags::empty(),
140 );
141 }
142}
143
144fn deal_tremor(ctx: &mut HookCtx<'_>, mult: f64) {
145 let coefficient = ctx.effect_ap_coefficient(
146 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::TREMOR_DAMAGE,
147 );
148
149 ctx.deal_damage_ap(
150 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::TREMOR,
151 coefficient * mult,
152 DamageFlags::empty(),
153 );
154}
155
156fn deal_surging_bolt(ctx: &mut HookCtx<'_>) {
157 if ctx.active_guardian_count(SPELL::SURGING_TOTEM) == 0 {
158 return;
159 }
160
161 let coefficient = ctx.effect_ap_coefficient(
162 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::SURGING_BOLT_DAMAGE,
163 );
164
165 ctx.deal_damage_ap(
166 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::SURGING_BOLT,
167 coefficient,
168 DamageFlags::PET,
169 );
170}
171
172fn trigger_totemic_rebound(ctx: &mut HookCtx<'_>, forced: bool, delay_ms: u32) {
173 if ctx.active_guardian_count(SPELL::SURGING_TOTEM) == 0 {
174 return;
175 }
176
177 if !forced
178 && !ctx.roll_system_rppm(
179 wowlab_engine_combat::generated_identities::system_rppm::TOTEMIC_REBOUND,
180 )
181 {
182 return;
183 }
184
185 ctx.apply_aura(AURA_TOTEMIC_REBOUND);
186 ctx.schedule_hook(delay_ms, deal_surging_bolt);
187}
188
189fn trigger_direct_lightning_totemic(ctx: &mut HookCtx<'_>) {
190 trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
191
192 if ctx.is_aura_active(AURA_WHIRLING_AIR) {
193 for bolt in 0..WHIRLING_AIR_BOLTS {
194 let delay_ms = SURGING_BOLT_DELAY_MS + bolt * WHIRLING_AIR_BOLT_SPACING_MS;
195
196 trigger_totemic_rebound(ctx, true, delay_ms);
197 }
198
199 ctx.consume_aura(AURA_WHIRLING_AIR);
200 }
201}
202
203fn roll_stormsurge(ctx: &mut HookCtx<'_>) {
204 let chance = stormsurge_chance(ctx);
205
206 apply_stormsurge_roll(ctx, chance, AURA_STORMSURGE, SPELL_STORMSTRIKE);
207}
208
209fn apply_stormsurge_roll(
210 ctx: &mut HookCtx<'_>,
211 chance: f64,
212 aura: wowlab_engine_combat::LocalAuraIdx,
213 strike: wowlab_engine_combat::LocalSpellIdx,
214) {
215 let success = proc_chance(ctx.rng(), chance);
216
217 tracing::trace!(chance, success, "STORMSURGE_ROLL");
218
219 if success {
220 ctx.apply_aura(aura);
221 ctx.reset_cooldown(strike);
222 }
223}
224
225#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
226#[expect(
227 clippy::struct_excessive_bools,
228 reason = "each flag preserves one independent landed-melee proc-family capability"
229)]
230struct LandedMeleeProcFamily {
231 windfury: bool,
232 flametongue: bool,
233 imbuement_mastery: bool,
234 maelstrom_weapon: bool,
235}
236
237impl LandedMeleeProcFamily {
238 const fn player_attack(main_hand: bool) -> Self {
239 Self {
240 windfury: main_hand,
241 flametongue: true,
242 imbuement_mastery: false,
243 maelstrom_weapon: true,
244 }
245 }
246}
247
248fn spell_landed_melee_proc_family(
249 ctx: &HookCtx<'_>,
250 action: wowlab_engine_combat::DamageActionId,
251) -> LandedMeleeProcFamily {
252 let melee = ctx.action_is_melee(action);
253 let direct_melee = melee && ctx.action_has_direct_damage(action);
254 let main_hand_bound = ctx.action_requires_main_hand(action);
255 let weapon_bound = main_hand_bound || ctx.action_requires_off_hand(action);
256 let flametongue = direct_melee && weapon_bound;
257
258 LandedMeleeProcFamily {
259 windfury: direct_melee
261 && main_hand_bound
262 && action != crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::WINDFURY_ATTACK,
263 flametongue,
264 imbuement_mastery: flametongue
266 && action == crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::WINDFURY_ATTACK,
267 maelstrom_weapon: melee && !ctx.action_suppresses_caster_procs(action),
268 }
269}
270
271#[cfg(test)]
272fn reference_spell_landed_melee_proc_family(
273 game_data: &ResolvedGameData,
274 spell_id: u32,
275) -> LandedMeleeProcFamily {
276 let spell = SpellIdx::from_raw(spell_id);
277 let attributes = game_data.spell_attributes(spell).unwrap_or_default();
278 let melee = game_data.defense_type(spell) == Some(DefenseType::Melee as i32);
279 let direct_melee = melee && damage_def(game_data, spell).is_some();
280 let main_hand_bound = spell_attribute_is(attributes, SpellAttributeKind::RequiresMainHand);
281 let weapon_bound =
282 main_hand_bound || spell_attribute_is(attributes, SpellAttributeKind::RequiresOffHand);
283 let flametongue = direct_melee && weapon_bound;
284
285 LandedMeleeProcFamily {
286 windfury: direct_melee && main_hand_bound && spell_id != SPELL::WINDFURY_ATTACK,
287 flametongue,
288 imbuement_mastery: flametongue && spell_id == SPELL::WINDFURY_ATTACK,
289 maelstrom_weapon: melee
290 && !spell_attribute_is(attributes, SpellAttributeKind::SuppressCasterProcs),
291 }
292}
293
294fn trigger_flametongue(ctx: &mut HookCtx<'_>, imbuement_mastery_eligible: bool) {
295 let ft_coef = ctx.effect_ap_coefficient(
296 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::FLAMETONGUE_ATTACK_DAMAGE,
297 );
298
299 ctx.deal_damage_ap(
300 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::FLAMETONGUE_ATTACK,
301 ft_coef,
302 DamageFlags::empty(),
303 );
304
305 let imbuement_mastery = ctx
306 .spec_config::<EnhancementConfig>()
307 .is_some_and(|config| config.imbuement_mastery);
308
309 let mut failures = ctx
310 .spec_runtime::<EnhancementRuntime>()
311 .map(|runtime| runtime.imbuement_mastery_failures);
312 let imbuement_mastery_proc = imbuement_mastery_eligible
313 && imbuement_mastery
314 && failures.as_mut().is_some_and(|failures| {
315 accumulated_proc_chance(ctx.rng(), IMBUEMENT_MASTERY_CHANCE, failures, 0)
316 });
317
318 if let (Some(runtime), Some(failures)) =
319 (ctx.spec_runtime_mut::<EnhancementRuntime>(), failures)
320 {
321 runtime.imbuement_mastery_failures = failures;
322 }
323
324 if imbuement_mastery_proc {
325 let coef = ctx.effect_ap_coefficient(
326 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::FLAMETONGUE_IMBUEMENT_MASTERY_DAMAGE,
327 );
328
329 ctx.deal_damage_ap(
330 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::FLAMETONGUE_ATTACK_IMBUEMENT_MASTERY,
331 coef,
332 DamageFlags::empty(),
333 );
334 }
335}
336
337fn trigger_maelstrom_weapon(ctx: &mut HookCtx<'_>) {
338 let chance = ctx.action_proc_chance(
339 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::MAELSTROM_WEAPON_DRIVER,
340 );
341
342 if proc_chance(ctx.rng(), chance) {
343 ctx.apply_aura(AURA_MAELSTROM_WEAPON);
344 }
345}
346
347fn landed_melee_riders(ctx: &mut HookCtx<'_>, family: LandedMeleeProcFamily) {
348 if family.flametongue {
349 trigger_flametongue(ctx, family.imbuement_mastery);
350 }
351
352 if family.maelstrom_weapon {
353 trigger_maelstrom_weapon(ctx);
354 }
355}
356
357fn landed_melee_spell_procs(
358 ctx: &mut HookCtx<'_>,
359 action: wowlab_engine_combat::DamageActionId,
360 outcome: DamageOutcome,
361) {
362 if outcome.amount <= 0.0 {
363 return;
364 }
365
366 let family = spell_landed_melee_proc_family(ctx, action);
367
368 landed_melee_procs(ctx, family);
369}
370
371fn landed_melee_procs(ctx: &mut HookCtx<'_>, family: LandedMeleeProcFamily) {
372 let windfury_procced = if family.windfury {
373 let chance = windfury_chance(ctx);
374 let success = proc_chance(ctx.rng(), chance);
375
376 tracing::trace!(chance, success, "WINDFURY_ROLL");
377
378 success
379 } else {
380 false
381 };
382
383 if windfury_procced {
384 let coef = ctx.effect_ap_coefficient(
385 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::WINDFURY_ATTACK_DAMAGE,
386 );
387 let hits = ctx
388 .spec_config::<EnhancementConfig>()
389 .map_or(WINDFURY_BASE_HITS, |config| config.windfury_hits);
390 let child_family = spell_landed_melee_proc_family(
391 ctx,
392 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::WINDFURY_ATTACK,
393 );
394
395 for _ in 0..hits {
396 let outcome = ctx.deal_damage_ap_result(
397 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::WINDFURY_ATTACK,
398 coef,
399 DamageFlags::PHYSICAL,
400 );
401
402 if outcome.amount > 0.0 {
403 landed_melee_riders(ctx, child_family);
404 }
405 }
406 }
407
408 landed_melee_riders(ctx, family);
409}
410
411fn melee_hit_procs(ctx: &mut HookCtx<'_>, main_hand: bool) {
412 landed_melee_procs(ctx, LandedMeleeProcFamily::player_attack(main_hand));
413}
414
415fn trigger_crash_lightning_proc(
416 ctx: &mut HookCtx<'_>,
417 effect: wowlab_engine_combat::DamageEffectId,
418 multiplier: f64,
419) {
420 let snapshot = ctx
421 .spec_runtime::<EnhancementRuntime>()
422 .map_or(0.0, mid2_crash_snapshot_average);
423
424 if ctx.deal_triggered_damage(effect, multiplier * (1.0 + snapshot), DamageFlags::empty()) {
425 let family = spell_landed_melee_proc_family(
426 ctx,
427 crate::generated::specs::enhancement_shaman::DAMAGE_REPORTED::CRASH_LIGHTNING_PROC,
428 );
429
430 landed_melee_procs(ctx, family);
431 }
432}
433
434pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
435 let hand = event.hand;
436
437 let mh = matches!(hand, SwingHand::MainHand);
438
439 melee_hit_procs(ctx, mh);
440
441 let hh_chance = ctx.action_proc_chance(
442 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::HOT_HAND_DRIVER,
443 );
444
445 if proc_chance(ctx.rng(), hh_chance) {
446 ctx.apply_aura(AURA_HOT_HAND);
447 }
448}
449
450fn stormstrike_hits(ctx: &mut HookCtx<'_>, mult: f64, stormblast_mult: f64) {
451 let mh_coef = ctx.effect_ap_coefficient(
452 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::STORMSTRIKE_MH_DAMAGE,
453 );
454 let oh_coef = ctx.effect_ap_coefficient(
455 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::STORMSTRIKE_OH_DAMAGE,
456 );
457 let main_hand = ctx.deal_damage_ap_result(
458 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::STORMSTRIKE_MH,
459 mh_coef * mult,
460 DamageFlags::PHYSICAL,
461 );
462
463 if stormblast_mult > 0.0 {
464 let _ = ctx.deal_damage_from_result(
465 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::STORMBLAST,
466 main_hand,
467 stormblast_mult,
468 DamageFlags::empty(),
469 );
470 }
471
472 roll_stormsurge(ctx);
473 melee_hit_procs(ctx, true);
474 let off_hand = ctx.deal_damage_ap_result(
475 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::STORMSTRIKE_OFFHAND,
476 oh_coef * mult * OFF_HAND_DAMAGE_MULT,
477 DamageFlags::PHYSICAL,
478 );
479
480 if stormblast_mult > 0.0 {
481 let _ = ctx.deal_damage_from_result(
482 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::STORMBLAST,
483 off_hand,
484 stormblast_mult,
485 DamageFlags::empty(),
486 );
487 }
488
489 roll_stormsurge(ctx);
490 melee_hit_procs(ctx, false);
491
492 if ctx.is_aura_active(AURA_CRASH_LIGHTNING) {
495 let stacks = f64::from(ctx.aura_stacks(AURA_CRASH_LIGHTNING));
496
497 trigger_crash_lightning_proc(
498 ctx,
499 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::CRASH_LIGHTNING_DAMAGE,
500 mult * stacks,
501 );
502 }
503
504 ride_the_lightning(ctx);
505}
506
507pub(crate) fn stormstrike_hook(ctx: &mut HookCtx<'_>) {
508 let stormblast_mult = if ctx.is_aura_active(AURA_STORMSURGE) {
509 ctx.spec_config::<EnhancementConfig>()
510 .map_or(0.0, |config| config.stormblast_mult)
511 } else {
512 0.0
513 };
514 ctx.consume_aura(AURA_STORMSURGE);
518 stormstrike_hits(ctx, 1.0, stormblast_mult);
519
520 let mw_stacks = ctx.aura_stacks(AURA_MAELSTROM_WEAPON).min(MW_CONSUME_CAP);
522
523 if ctx.is_aura_active(AURA_DOOM_WINDS) && mw_stacks > 0 {
524 let coefficient = ctx.effect_sp_coefficient(
525 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::LIGHTNING_BOLT_DAMAGE,
526 );
527
528 ctx.deal_damage_sp(
529 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::LIGHTNING_BOLT,
530 coefficient * (1.0 + MW_DMG_PER_STACK * f64::from(mw_stacks)),
531 DamageFlags::empty(),
532 );
533 trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
534 }
535
536 if proc_chance(ctx.rng(), STORMFLURRY_CHANCE) {
537 stormstrike_hits(ctx, STORMFLURRY_MULT, stormblast_mult);
538 }
539
540 ctx.apply_aura(AURA_MAELSTROM_WEAPON);
541}
542
543fn ride_the_lightning(ctx: &mut HookCtx<'_>) {
544 let coef = ctx.effect_sp_coefficient(
545 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::CHAIN_LIGHTNING_DAMAGE,
546 );
547
548 ctx.deal_damage_sp(
549 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::CHAIN_LIGHTNING,
550 coef * RIDE_THE_LIGHTNING_MULT,
551 DamageFlags::empty(),
552 );
553 trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
555}
556
557fn trigger_splitstream(ctx: &mut HookCtx<'_>) {
558 let splitstream_mult = ctx
559 .spec_config::<EnhancementConfig>()
560 .map_or(0.0, |config| config.splitstream_mult);
561
562 if splitstream_mult <= 0.0 {
563 return;
564 }
565
566 let splitstream = ctx.effect_ap_coefficient(
567 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::SUNDERING_SPLITSTREAM_DAMAGE,
568 );
569
570 ctx.deal_damage_ap(
571 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::SUNDERING_SPLITSTREAM,
572 splitstream * splitstream_mult,
573 DamageFlags::PHYSICAL,
574 );
575 melee_hit_procs(ctx, true);
576
577 if ctx.is_aura_active(AURA_SURGING_TOTEM) {
578 deal_tremor(ctx, EARTHSURGE_MULT * splitstream_mult);
579 }
580}
581
582pub(crate) fn lava_lash_hook(ctx: &mut HookCtx<'_>) {
584 let ashen_catalyst_cdr_ms = ctx
585 .spec_config::<EnhancementConfig>()
586 .map_or(0, |config| config.ashen_catalyst_cdr_ms);
587
588 if ashen_catalyst_cdr_ms > 0 && ctx.is_aura_active(AURA_FLAME_SHOCK) {
589 ctx.reduce_cooldown(SPELL_LAVA_LASH, ashen_catalyst_cdr_ms);
590 }
591
592 if ctx.is_aura_active(AURA_HOT_HAND) {
593 trigger_splitstream(ctx);
594 }
595
596 if ctx.is_aura_active(AURA_WHIRLING_FIRE) {
599 let hot_hand_active = ctx.is_aura_active(AURA_HOT_HAND);
600
601 if hot_hand_active {
602 trigger_splitstream(ctx);
603 }
604
605 ctx.consume_aura(AURA_WHIRLING_FIRE);
606
607 if hot_hand_active {
608 ctx.extend_aura(AURA_HOT_HAND, WHIRLING_FIRE_HOT_HAND_MS);
609 } else {
610 ctx.apply_aura(AURA_HOT_HAND);
611 }
612 }
613
614 ctx.apply_aura(AURA_LASHING_FLAMES);
617
618 if ctx.is_aura_active(AURA_CRASH_LIGHTNING) {
619 let stacks = f64::from(ctx.aura_stacks(AURA_CRASH_LIGHTNING));
620
621 trigger_crash_lightning_proc(
622 ctx,
623 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::CRASH_LIGHTNING_DAMAGE,
624 stacks,
625 );
626 }
627
628 let lively_totems = ctx
631 .spec_config::<EnhancementConfig>()
632 .is_some_and(|config| config.lively_totems);
633 let mut failures = ctx
634 .spec_runtime::<EnhancementRuntime>()
635 .map(|runtime| runtime.lively_totems_failures);
636 let lively_totems_proc = lively_totems
637 && failures.as_mut().is_some_and(|failures| {
638 accumulated_proc_chance(ctx.rng(), LIVELY_TOTEMS_CHANCE, failures, 0)
639 });
640
641 if let (Some(runtime), Some(failures)) =
642 (ctx.spec_runtime_mut::<EnhancementRuntime>(), failures)
643 {
644 runtime.lively_totems_failures = failures;
645 }
646
647 if lively_totems_proc {
648 summon_searing_totem(ctx);
649 }
650
651 trigger_searing_volley(ctx);
652
653 ride_the_lightning(ctx);
654 melee_hit_procs(ctx, false);
655}
656
657fn crash_lightning_unleashed(ctx: &mut HookCtx<'_>) {
658 let coefficient = ctx.effect_ap_coefficient(crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::CRASH_LIGHTNING_UNLEASHED_DAMAGE);
659 let snapshot = ctx
660 .spec_runtime::<EnhancementRuntime>()
661 .map_or(0.0, |runtime| runtime.mid2_storm_unleashed_snapshot);
662
663 ctx.deal_damage_ap(
664 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::CRASH_LIGHTNING_UNLEASHED,
665 coefficient * (1.0 + snapshot),
666 DamageFlags::empty(),
667 );
668}
669
670pub(crate) fn crash_lightning_hook(ctx: &mut HookCtx<'_>) {
671 melee_hit_procs(ctx, true);
672 let config = ctx.spec_config::<EnhancementConfig>().copied().unwrap_or_default();
673 let snapshot = f64::from(ctx.aura_stacks(AURA_SHORT_CIRCUIT).max(0))
674 * config.mid2_short_circuit_per_stack;
675
676 if let Some(runtime) = ctx.spec_runtime_mut::<EnhancementRuntime>() {
677 push_mid2_crash_snapshot(runtime, snapshot);
678 runtime.mid2_storm_unleashed_snapshot = snapshot;
679 }
680
681 if config.storm_unleashed_repeats {
682 ctx.schedule_hook(
683 CRASH_LIGHTNING_UNLEASHED_FIRST_MS,
684 crash_lightning_unleashed,
685 );
686 ctx.schedule_hook(
687 CRASH_LIGHTNING_UNLEASHED_SECOND_MS,
688 crash_lightning_unleashed,
689 );
690 }
691
692 ctx.consume_aura(AURA_SHORT_CIRCUIT);
693}
694
695pub(crate) fn crash_lightning_expire_hook(ctx: &mut HookCtx<'_>) {
696 if let Some(runtime) = ctx.spec_runtime_mut::<EnhancementRuntime>()
697 && runtime.mid2_crash_snapshot_count > 0
698 {
699 runtime.mid2_crash_snapshot_head =
700 (runtime.mid2_crash_snapshot_head + 1) % runtime.mid2_crash_snapshots.len();
701 runtime.mid2_crash_snapshot_count -= 1;
702 }
703}
704
705fn push_mid2_crash_snapshot(runtime: &mut EnhancementRuntime, snapshot: f64) {
706 let capacity = runtime.mid2_crash_snapshots.len();
707 let index = (runtime.mid2_crash_snapshot_head + runtime.mid2_crash_snapshot_count) % capacity;
708 let Some(slot) = runtime.mid2_crash_snapshots.get_mut(index) else {
709 return;
710 };
711
712 *slot = snapshot;
713
714 if runtime.mid2_crash_snapshot_count < capacity {
715 runtime.mid2_crash_snapshot_count += 1;
716 } else {
717 runtime.mid2_crash_snapshot_head = (runtime.mid2_crash_snapshot_head + 1) % capacity;
718 }
719}
720
721fn mid2_crash_snapshot_average(runtime: &EnhancementRuntime) -> f64 {
722 if runtime.mid2_crash_snapshot_count == 0 {
723 return 0.0;
724 }
725
726 let total = (0..runtime.mid2_crash_snapshot_count)
727 .map(|offset| {
728 runtime.mid2_crash_snapshots
729 .get(
730 (runtime.mid2_crash_snapshot_head + offset)
731 % runtime.mid2_crash_snapshots.len(),
732 )
733 .copied()
734 .unwrap_or_default()
735 })
736 .sum::<f64>();
737
738 total / wowlab_types::numeric::usize_to_f64(runtime.mid2_crash_snapshot_count)
739}
740
741fn primordial_storm_damage_mult(ctx: &HookCtx<'_>) -> f64 {
742 let stacks = ctx
743 .spec_runtime::<EnhancementRuntime>()
744 .map_or(0, |runtime| runtime.primordial_storm_mw_stacks);
745
746 1.0 + MW_DMG_PER_STACK * f64::from(stacks)
747}
748
749fn consume_maelstrom_weapon(ctx: &mut HookCtx<'_>) -> i32 {
750 let stacks = ctx.aura_stacks(AURA_MAELSTROM_WEAPON).min(MW_CONSUME_CAP);
751
752 ctx.consume_aura(AURA_MAELSTROM_WEAPON);
753 let cdr_per_stack = ctx
754 .spec_config::<EnhancementConfig>()
755 .map_or(0, |config| config.elemental_tempo_cdr_per_stack_ms);
756 let stack_count = wowlab_types::numeric::i32_to_u32_nonnegative(stacks);
757 let cdr_ms = cdr_per_stack.saturating_mul(stack_count);
758
759 if cdr_ms > 0 {
760 ctx.reduce_cooldown(SPELL_STORMSTRIKE, cdr_ms);
761 ctx.reduce_cooldown(SPELL_LAVA_LASH, cdr_ms);
762 }
763
764 let hot_hand_per_stack = ctx
765 .spec_config::<EnhancementConfig>()
766 .map_or(0, |config| config.totemic_momentum_hot_hand_ms);
767
768 if hot_hand_per_stack > 0 && ctx.is_aura_active(AURA_HOT_HAND) {
769 ctx.extend_aura(
770 AURA_HOT_HAND,
771 hot_hand_per_stack.saturating_mul(stack_count),
772 );
773 }
774
775 let storm_unleashed_chance = ctx
776 .spec_config::<EnhancementConfig>()
777 .map_or(0.0, |config| config.storm_unleashed_chance_per_stack);
778
779 if (0..stacks).any(|_| proc_chance(ctx.rng(), storm_unleashed_chance)) {
780 ctx.apply_aura(AURA_STORM_UNLEASHED);
781 }
782
783 stacks
784}
785
786fn primordial_fire(ctx: &mut HookCtx<'_>) {
787 let coefficient = ctx.effect_ap_coefficient(
788 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::PRIMORDIAL_FIRE_DAMAGE,
789 );
790
791 let outcome = ctx.deal_damage_ap_result(
792 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_FIRE,
793 coefficient * primordial_storm_damage_mult(ctx),
794 DamageFlags::empty(),
795 );
796
797 landed_melee_spell_procs(
798 ctx,
799 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_FIRE,
800 outcome,
801 );
802}
803
804fn primordial_frost(ctx: &mut HookCtx<'_>) {
805 let coefficient = ctx.effect_ap_coefficient(
806 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::PRIMORDIAL_FROST_DAMAGE,
807 );
808
809 let outcome = ctx.deal_damage_ap_result(
810 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_FROST,
811 coefficient * primordial_storm_damage_mult(ctx),
812 DamageFlags::empty(),
813 );
814
815 landed_melee_spell_procs(
816 ctx,
817 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_FROST,
818 outcome,
819 );
820}
821
822fn primordial_lightning(ctx: &mut HookCtx<'_>) {
823 let coefficient = ctx.effect_ap_coefficient(
824 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::PRIMORDIAL_LIGHTNING_DAMAGE,
825 );
826
827 let outcome = ctx.deal_damage_ap_result(
828 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_LIGHTNING,
829 coefficient * primordial_storm_damage_mult(ctx),
830 DamageFlags::empty(),
831 );
832
833 landed_melee_spell_procs(
834 ctx,
835 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::PRIMORDIAL_LIGHTNING,
836 outcome,
837 );
838}
839
840fn primordial_lightning_bolt(ctx: &mut HookCtx<'_>) {
841 let coefficient = ctx.effect_sp_coefficient(
842 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::LIGHTNING_BOLT_DAMAGE,
843 );
844
845 ctx.deal_damage_sp(
846 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::LIGHTNING_BOLT,
847 coefficient * primordial_storm_damage_mult(ctx) * PRIMORDIAL_LIGHTNING_BOLT_MULT,
848 DamageFlags::empty(),
849 );
850 trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
851}
852
853pub(crate) fn primordial_storm_hook(ctx: &mut HookCtx<'_>) {
854 let stacks = consume_maelstrom_weapon(ctx);
855
856 if let Some(runtime) = ctx.spec_runtime_mut::<EnhancementRuntime>() {
857 runtime.primordial_storm_mw_stacks = stacks;
858 }
859
860 primordial_fire(ctx);
861 ctx.schedule_hook(PRIMORDIAL_FROST_DELAY_MS, primordial_frost);
862 ctx.schedule_hook(PRIMORDIAL_LIGHTNING_DELAY_MS, primordial_lightning);
863 ctx.schedule_hook(
864 PRIMORDIAL_LIGHTNING_BOLT_DELAY_MS,
865 primordial_lightning_bolt,
866 );
867}
868
869pub(crate) fn lightning_bolt_hook(ctx: &mut HookCtx<'_>) {
870 let stacks = consume_maelstrom_weapon(ctx);
871
872 let coef = ctx.effect_sp_coefficient(
873 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::LIGHTNING_BOLT_DAMAGE,
874 );
875 let mult = 1.0 + MW_DMG_PER_STACK * f64::from(stacks);
876
877 ctx.deal_damage_sp(
878 crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::LIGHTNING_BOLT,
879 coef * mult,
880 DamageFlags::empty(),
881 );
882 trigger_direct_lightning_totemic(ctx);
883}
884
885pub(crate) fn chain_lightning_hook(ctx: &mut HookCtx<'_>) {
886 trigger_direct_lightning_totemic(ctx);
887}
888
889pub(crate) fn surging_totem_hook(ctx: &mut HookCtx<'_>) {
890 let config = enhancement_config(ctx);
891 let _ = ctx.summon_guardian(
892 GuardianSpec::new_inheriting_owner(SPELL::SURGING_TOTEM, config.surging_totem_duration_ms)
893 .npc_id(config.surging_totem_npc_id)
894 .stat_inheritance(wowlab_engine_combat::PetStatInheritance::new(
895 config.surging_totem_ap_inheritance,
896 config.surging_totem_sp_inheritance,
897 ))
898 .on_demise(surging_totem_demise),
899 );
900
901 ctx.apply_aura(AURA_AMPLIFICATION_CORE);
902 ctx.apply_aura(AURA_WHIRLING_AIR);
903 ctx.apply_aura(AURA_WHIRLING_FIRE);
904 ctx.apply_aura(AURA_WHIRLING_EARTH);
905 deal_tremor(ctx, 1.0);
906}
907
908fn surging_totem_demise(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
909 ctx.expire_aura(AURA_AMPLIFICATION_CORE);
910 ctx.expire_aura(AURA_SURGING_TOTEM);
911 ctx.expire_aura(AURA_TOTEMIC_REBOUND);
912 ctx.expire_aura(AURA_WHIRLING_AIR);
913 ctx.expire_aura(AURA_WHIRLING_EARTH);
914 ctx.expire_aura(AURA_WHIRLING_FIRE);
915}
916
917pub(crate) fn surging_totem_pulse_hook(ctx: &mut HookCtx<'_>) {
918 deal_tremor(ctx, 1.0);
919}
920
921pub(crate) fn sundering_hook(ctx: &mut HookCtx<'_>) {
922 melee_hit_procs(ctx, true);
923 summon_feral_spirit(ctx, FIRE_WOLF_GUARDIAN_TAG, AURA_MOLTEN_WEAPON);
924
925 if ctx.active_guardian_count(SPELL::SURGING_TOTEM) > 0 {
926 deal_tremor(ctx, EARTHSURGE_MULT);
927 }
928
929 if ctx.is_aura_active(AURA_WHIRLING_EARTH) {
933 ctx.consume_aura(AURA_WHIRLING_EARTH);
934 summon_searing_totem(ctx);
935 }
936}
937
938pub(crate) fn doom_winds_hook(ctx: &mut HookCtx<'_>) {
939 summon_feral_spirit(ctx, LIGHTNING_WOLF_GUARDIAN_TAG, AURA_CRACKLING_SURGE);
940}
941
942pub(crate) fn voltaic_blaze_hook(ctx: &mut HookCtx<'_>) {
943 ctx.apply_aura_n(
944 AURA_MAELSTROM_WEAPON,
945 enhancement_config(ctx).voltaic_blaze_maelstrom_stacks,
946 );
947
948 if enhancement_config(ctx).mid2_2pc {
949 ctx.apply_aura(AURA_BURNING_CORE);
950 }
951
952 trigger_searing_volley(ctx);
953}
954
955pub(crate) fn burning_core_tick_hook(ctx: &mut HookCtx<'_>) {
956 let config = enhancement_config(ctx);
957 let primary_target = ctx.target();
958 let effect = crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::FIRE_NOVA_DAMAGE;
959 let coefficient = ctx.effect_ap_coefficient(effect);
960
961 ctx.with_resolved_spell_effect_targets(effect, effect.action(), None, |ctx, targets| {
962 for target in targets.iter().copied() {
963 let multiplier = if Some(target) == primary_target {
964 config.mid2_fire_nova_primary_mult
965 } else {
966 1.0
967 };
968
969 ctx.with_resolved_target(target, |target_ctx| {
970 target_ctx.deal_damage_ap_to_target(
971 target,
972 effect.action(),
973 coefficient * multiplier,
974 DamageFlags::AOE,
975 );
976 });
977 }
978 });
979
980 if config.mid2_fire_nova_crash_cdr_ms > 0 {
981 ctx.reduce_cooldown(
982 crate::generated::specs::enhancement_shaman::SPELL_CRASH_LIGHTNING,
983 config.mid2_fire_nova_crash_cdr_ms,
984 );
985 ctx.apply_aura(AURA_SHORT_CIRCUIT);
986 }
987}
988
989pub(crate) fn ascendance_hook(ctx: &mut HookCtx<'_>) {
990 ctx.reset_cooldown(SPELL_STORMSTRIKE);
991}
992
993#[must_use]
995pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
996 let enhanced_elements = if [
997 DamageSchool::Fire,
998 DamageSchool::Frost,
999 DamageSchool::Nature,
1000 ]
1001 .into_iter()
1002 .any(|school| ctx.has_school(school))
1003 {
1004 ctx.bonus(1)
1005 } else {
1006 1.0
1007 };
1008
1009 let elemental_weapons_per_imbue = ctx
1010 .program
1011 .spec_config::<EnhancementConfig>()
1012 .map_or(0.0, |config| config.elemental_weapons_per_imbue);
1013 let modifier_effect = if ctx.is_periodic {
1014 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::ELEMENTAL_WEAPONS_PERIODIC
1015 } else {
1016 crate::generated::specs::enhancement_shaman::DAMAGE_EFFECT::ELEMENTAL_WEAPONS_DIRECT
1017 };
1018 let affected = ctx.effect_affects_spell(modifier_effect) || ctx.spell_id == SPELL::STORMBLAST;
1019 let active_imbues = ctx.active_weapon_imbue_count();
1020 let elemental_weapons = if affected {
1021 1.0 + elemental_weapons_per_imbue * wowlab_types::numeric::usize_to_f64(active_imbues)
1022 } else {
1023 1.0
1024 };
1025
1026 enhanced_elements * elemental_weapons
1027}
1028
1029#[cfg(test)]
1030#[path = "hooks/tests.rs"]
1031mod tests;