Skip to main content

wowlab_engine_content/hooks/enhancement_shaman/
hooks.rs

1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ProcOps as _;
4use wowlab_engine_combat::SchedulingOps as _;
5use wowlab_engine_combat::GuardianOps as _;
6use wowlab_engine_combat::AuraOps as _;
7use wowlab_engine_combat::{
8    DamageFlags, DamageOutcome, GuardianAbility, GuardianEvent, GuardianSpec, HookCtx, MasteryCtx,
9    SwingEvent, SwingHand,
10};
11use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
12use wowlab_engine_domain::{
13    constants::OFF_HAND_DAMAGE_MULT,
14    dbc::{
15        DefenseType, ResolvedGameDataSemanticExt as _, SpellAttributeKind, damage_def,
16        spell_attribute_is,
17    },
18};
19use wowlab_engine_gamedata::ResolvedGameData;
20use wowlab_engine_rng::{accumulated_proc_chance, proc_chance};
21use wowlab_types::{combat::DamageSchool, constants::HUNDRED, sim::SpellIdx};
22
23use crate::generated::specs::enhancement_shaman::{
24    AURA, AURA_AMPLIFICATION_CORE, AURA_CRACKLING_SURGE, AURA_CRASH_LIGHTNING, AURA_DOOM_WINDS,
25    AURA_FLAME_SHOCK, AURA_HOT_HAND, AURA_LASHING_FLAMES, AURA_MAELSTROM_WEAPON,
26    AURA_MOLTEN_WEAPON, AURA_STORM_UNLEASHED, AURA_STORMSURGE, AURA_SURGING_TOTEM,
27    AURA_TOTEMIC_REBOUND, AURA_WHIRLING_AIR, AURA_WHIRLING_EARTH, AURA_WHIRLING_FIRE, EFFECT,
28    REPORTED_SPELL, SPELL, SPELL_LAVA_LASH, SPELL_STORMSTRIKE, TALENT,
29};
30
31/// Stormflurry (344357) e1: echo chance.
32const STORMFLURRY_CHANCE: f64 = 0.20;
33/// Stormflurry (344357) e2: echo damage percent.
34const STORMFLURRY_MULT: f64 = 0.40;
35/// Doom Winds (466772) e1: +100% Windfury proc chance while active.
36const DOOM_WINDS_WF_PROC_MULT: f64 = 2.0;
37/// `SimC`'s accumulated RNG base chance for Imbuement Mastery.
38const IMBUEMENT_MASTERY_CHANCE: f64 = 0.07;
39const LIVELY_TOTEMS_CHANCE: f64 = 0.06;
40/// Maelstrom Weapon 187880: 20% melee proc with spell 384149 e2's stack cap.
41const MW_CONSUME_CAP: i32 = 10;
42/// Maelstrom Weapon talent 187880 e3: +20% damage per stack consumed.
43const MW_DMG_PER_STACK: f64 = 0.20;
44/// Whirling Air (453409 e3): three guaranteed bolts on the next direct lightning cast.
45const WHIRLING_AIR_BOLTS: u32 = 3;
46/// Earthsurge e1 multiplier is 150%.
47const EARTHSURGE_MULT: f64 = 1.5;
48/// Whirling Fire (453405 e1) starts or extends Hot Hand by 8 seconds.
49const WHIRLING_FIRE_HOT_HAND_MS: u32 = 8_000;
50/// Synthetic guardian tags distinguish elemental wolf actors without using game spell IDs.
51const 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;
63/// Ride the Lightning 289874 e2 always unleashes Chain Lightning.
64const RIDE_THE_LIGHTNING_MULT: f64 = 1.0;
65
66/// Disables DBC damage children that an affected spell's cast hook owns.
67pub(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
73        .game_data()
74        .proc_chance(SpellIdx::from_raw(EFFECT::WINDFURY_PROC.0))
75        .unwrap_or(0.0);
76    let mastery_bonus = ctx.effect_points(EFFECT::WINDFURY_MASTERY) / HUNDRED;
77    let mut chance = base + mastery_bonus;
78
79    if ctx.is_aura_active(AURA_DOOM_WINDS.raw()) {
80        chance *= DOOM_WINDS_WF_PROC_MULT;
81    }
82
83    chance
84}
85
86fn stormsurge_chance(ctx: &HookCtx<'_>) -> f64 {
87    let base = ctx
88        .game_data()
89        .proc_chance(SpellIdx::from_raw(SPELL::STORMSURGE_DRIVER))
90        .unwrap_or(0.0);
91
92    base + ctx.effect_points(EFFECT::STORMSURGE_MASTERY) / HUNDRED
93}
94
95use super::{
96    config::{EnhancementConfig, EnhancementRuntime, WINDFURY_BASE_HITS, enhancement_config},
97    guardians::summon_feral_spirit,
98};
99
100fn searing_totem_pulse(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
101    let coefficient = ctx
102        .game_data()
103        .effect_sp_coefficient(EFFECT::SEARING_BOLT_DAMAGE);
104
105    ctx.deal_damage_sp(SPELL::SEARING_BOLT, coefficient, DamageFlags::empty());
106}
107
108fn summon_searing_totem(ctx: &mut HookCtx<'_>) {
109    let _ = ctx.summon_guardian(
110        GuardianSpec::new_inheriting_owner(TALENT::LIVELY_TOTEMS, SEARING_TOTEM_DURATION_MS)
111            .ability(
112                GuardianAbility::new(SEARING_TOTEM_PULSE_MS, searing_totem_pulse)
113                    .first_action_delay(SEARING_TOTEM_PULSE_MS),
114            ),
115    );
116}
117
118fn trigger_searing_volley(ctx: &mut HookCtx<'_>) {
119    let count = ctx.active_guardian_count(TALENT::LIVELY_TOTEMS);
120
121    if count == 0 {
122        return;
123    }
124
125    let coefficient = ctx
126        .game_data()
127        .effect_sp_coefficient(EFFECT::SEARING_VOLLEY_DAMAGE);
128
129    for _ in 0..count {
130        ctx.deal_damage_sp(SPELL::SEARING_VOLLEY, coefficient, DamageFlags::empty());
131    }
132}
133
134fn deal_tremor(ctx: &mut HookCtx<'_>, mult: f64) {
135    let coefficient = ctx.game_data().effect_ap_coefficient(EFFECT::TREMOR_DAMAGE);
136
137    ctx.deal_damage_ap(SPELL::TREMOR, coefficient * mult, DamageFlags::empty());
138}
139
140fn deal_surging_bolt(ctx: &mut HookCtx<'_>) {
141    if ctx.active_guardian_count(SPELL::SURGING_TOTEM) == 0 {
142        return;
143    }
144
145    let coefficient = ctx
146        .game_data()
147        .effect_ap_coefficient(EFFECT::SURGING_BOLT_DAMAGE);
148
149    ctx.deal_damage_ap(SPELL::SURGING_BOLT, coefficient, DamageFlags::PET);
150}
151
152fn trigger_totemic_rebound(ctx: &mut HookCtx<'_>, forced: bool, delay_ms: u32) {
153    if ctx.active_guardian_count(SPELL::SURGING_TOTEM) == 0 {
154        return;
155    }
156
157    if !forced && !ctx.roll_system_rppm(EFFECT::TOTEMIC_REBOUND_RPPM_DRIVER.0) {
158        return;
159    }
160
161    ctx.apply_aura(AURA_TOTEMIC_REBOUND.raw());
162    ctx.schedule_hook(delay_ms, deal_surging_bolt);
163}
164
165fn trigger_direct_lightning_totemic(ctx: &mut HookCtx<'_>) {
166    trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
167
168    if ctx.is_aura_active(AURA_WHIRLING_AIR.raw()) {
169        for bolt in 0..WHIRLING_AIR_BOLTS {
170            let delay_ms = SURGING_BOLT_DELAY_MS + bolt * WHIRLING_AIR_BOLT_SPACING_MS;
171
172            trigger_totemic_rebound(ctx, true, delay_ms);
173        }
174
175        ctx.consume_aura(AURA_WHIRLING_AIR.raw());
176    }
177}
178
179fn roll_stormsurge(ctx: &mut HookCtx<'_>) {
180    let chance = stormsurge_chance(ctx);
181
182    apply_stormsurge_roll(ctx, chance, AURA_STORMSURGE.raw(), SPELL_STORMSTRIKE.raw());
183}
184
185fn apply_stormsurge_roll(ctx: &mut HookCtx<'_>, chance: f64, aura: u8, strike: u8) {
186    let success = proc_chance(ctx.rng(), chance);
187
188    tracing::trace!(chance, success, "STORMSURGE_ROLL");
189
190    if success {
191        ctx.apply_aura(aura);
192        ctx.reset_cooldown(strike);
193    }
194}
195
196#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
197#[expect(
198    clippy::struct_excessive_bools,
199    reason = "each flag preserves one independent SimC landed-melee proc-family capability"
200)]
201struct LandedMeleeProcFamily {
202    windfury: bool,
203    flametongue: bool,
204    imbuement_mastery: bool,
205    maelstrom_weapon: bool,
206}
207
208impl LandedMeleeProcFamily {
209    const fn player_attack(main_hand: bool) -> Self {
210        Self {
211            windfury: main_hand,
212            flametongue: true,
213            imbuement_mastery: false,
214            maelstrom_weapon: true,
215        }
216    }
217}
218
219fn spell_landed_melee_proc_family(
220    game_data: &ResolvedGameData,
221    spell_id: u32,
222) -> LandedMeleeProcFamily {
223    let spell = SpellIdx::from_raw(spell_id);
224    let attributes = game_data.spell_attributes(spell).unwrap_or_default();
225    let melee = game_data.defense_type(spell) == Some(DefenseType::Melee as i32);
226    let direct_melee = melee && damage_def(game_data, spell).is_some();
227    let main_hand_bound = spell_attribute_is(attributes, SpellAttributeKind::RequiresMainHand);
228    let weapon_bound =
229        main_hand_bound || spell_attribute_is(attributes, SpellAttributeKind::RequiresOffHand);
230    let flametongue = direct_melee && weapon_bound;
231
232    LandedMeleeProcFamily {
233        // SimC's windfury_attack_t explicitly disables only Windfury self-procs.
234        windfury: direct_melee && main_hand_bound && spell_id != SPELL::WINDFURY_ATTACK,
235        flametongue,
236        // trigger_flametongue_weapon only rolls this rider for Windfury attack 25504.
237        imbuement_mastery: flametongue && spell_id == SPELL::WINDFURY_ATTACK,
238        maelstrom_weapon: melee
239            && !spell_attribute_is(attributes, SpellAttributeKind::SuppressCasterProcs),
240    }
241}
242
243fn trigger_flametongue(ctx: &mut HookCtx<'_>, imbuement_mastery_eligible: bool) {
244    let ft_coef = ctx
245        .game_data()
246        .effect_ap_coefficient(EFFECT::FLAMETONGUE_ATTACK_DAMAGE);
247
248    ctx.deal_damage_ap(SPELL::FLAMETONGUE_ATTACK, ft_coef, DamageFlags::empty());
249
250    let imbuement_mastery = ctx
251        .spec_config::<EnhancementConfig>()
252        .is_some_and(|config| config.imbuement_mastery);
253
254    let mut failures = ctx
255        .spec_runtime::<EnhancementRuntime>()
256        .map(|runtime| runtime.imbuement_mastery_failures);
257    let imbuement_mastery_proc = imbuement_mastery_eligible
258        && imbuement_mastery
259        && failures.as_mut().is_some_and(|failures| {
260            accumulated_proc_chance(ctx.rng(), IMBUEMENT_MASTERY_CHANCE, failures, 0)
261        });
262
263    if let (Some(runtime), Some(failures)) =
264        (ctx.spec_runtime_mut::<EnhancementRuntime>(), failures)
265    {
266        runtime.imbuement_mastery_failures = failures;
267    }
268
269    if imbuement_mastery_proc {
270        let coef = ctx.game_data().ap_coef(
271            SpellIdx::from_raw(SPELL::FLAMETONGUE_ATTACK_IMBUEMENT_MASTERY),
272            1,
273        );
274
275        ctx.deal_damage_ap(
276            SPELL::FLAMETONGUE_ATTACK_IMBUEMENT_MASTERY,
277            coef,
278            DamageFlags::empty(),
279        );
280    }
281}
282
283fn trigger_maelstrom_weapon(ctx: &mut HookCtx<'_>) {
284    let chance = ctx
285        .game_data()
286        .proc_chance(SpellIdx::from_raw(SPELL::MAELSTROM_WEAPON_DRIVER))
287        .unwrap_or(0.0);
288
289    if proc_chance(ctx.rng(), chance) {
290        ctx.apply_aura(AURA_MAELSTROM_WEAPON.raw());
291    }
292}
293
294fn landed_melee_riders(ctx: &mut HookCtx<'_>, family: LandedMeleeProcFamily) {
295    if family.flametongue {
296        trigger_flametongue(ctx, family.imbuement_mastery);
297    }
298
299    if family.maelstrom_weapon {
300        trigger_maelstrom_weapon(ctx);
301    }
302}
303
304fn landed_melee_spell_procs(ctx: &mut HookCtx<'_>, spell_id: u32, outcome: DamageOutcome) {
305    if outcome.amount <= 0.0 {
306        return;
307    }
308
309    let family = spell_landed_melee_proc_family(ctx.game_data(), spell_id);
310
311    landed_melee_procs(ctx, family);
312}
313
314fn landed_melee_procs(ctx: &mut HookCtx<'_>, family: LandedMeleeProcFamily) {
315    let windfury_procced = if family.windfury {
316        let chance = windfury_chance(ctx);
317        let success = proc_chance(ctx.rng(), chance);
318
319        tracing::trace!(chance, success, "WINDFURY_ROLL");
320
321        success
322    } else {
323        false
324    };
325
326    if windfury_procced {
327        let coef = ctx
328            .game_data()
329            .effect_ap_coefficient(EFFECT::WINDFURY_ATTACK_DAMAGE);
330        let hits = ctx
331            .spec_config::<EnhancementConfig>()
332            .map_or(WINDFURY_BASE_HITS, |config| config.windfury_hits);
333        let child_family = spell_landed_melee_proc_family(ctx.game_data(), SPELL::WINDFURY_ATTACK);
334
335        for _ in 0..hits {
336            let outcome =
337                ctx.deal_damage_ap_result(SPELL::WINDFURY_ATTACK, coef, DamageFlags::PHYSICAL);
338
339            if outcome.amount > 0.0 {
340                landed_melee_riders(ctx, child_family);
341            }
342        }
343    }
344
345    landed_melee_riders(ctx, family);
346}
347
348fn melee_hit_procs(ctx: &mut HookCtx<'_>, main_hand: bool) {
349    landed_melee_procs(ctx, LandedMeleeProcFamily::player_attack(main_hand));
350}
351
352fn trigger_crash_lightning_proc(
353    ctx: &mut HookCtx<'_>,
354    source_spell_id: u32,
355    effect_index: u8,
356    multiplier: f64,
357) {
358    if ctx.deal_triggered_damage(
359        source_spell_id,
360        effect_index,
361        multiplier,
362        DamageFlags::empty(),
363    ) {
364        let family =
365            spell_landed_melee_proc_family(ctx.game_data(), REPORTED_SPELL::CRASH_LIGHTNING_PROC);
366
367        landed_melee_procs(ctx, family);
368    }
369}
370
371pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
372    let hand = event.hand;
373
374    let mh = matches!(hand, SwingHand::MainHand);
375
376    melee_hit_procs(ctx, mh);
377
378    let hh_chance = ctx
379        .game_data()
380        .proc_chance(SpellIdx::from_raw(SPELL::HOT_HAND_DRIVER))
381        .unwrap_or(0.0);
382
383    if proc_chance(ctx.rng(), hh_chance) {
384        ctx.apply_aura(AURA_HOT_HAND.raw());
385    }
386}
387
388fn stormstrike_hits(ctx: &mut HookCtx<'_>, mult: f64, stormblast_mult: f64) {
389    let mh_coef = ctx
390        .game_data()
391        .effect_ap_coefficient(EFFECT::STORMSTRIKE_MH_DAMAGE);
392    let oh_coef = ctx
393        .game_data()
394        .effect_ap_coefficient(EFFECT::STORMSTRIKE_OH_DAMAGE);
395    let main_hand =
396        ctx.deal_damage_ap_result(SPELL::STORMSTRIKE_MH, mh_coef * mult, DamageFlags::PHYSICAL);
397
398    if stormblast_mult > 0.0 {
399        let _ = ctx.deal_damage_from_result(
400            SPELL::STORMBLAST,
401            main_hand,
402            stormblast_mult,
403            DamageFlags::empty(),
404        );
405    }
406
407    roll_stormsurge(ctx);
408    melee_hit_procs(ctx, true);
409    let off_hand = ctx.deal_damage_ap_result(
410        SPELL::STORMSTRIKE_OFFHAND,
411        oh_coef * mult * OFF_HAND_DAMAGE_MULT,
412        DamageFlags::PHYSICAL,
413    );
414
415    if stormblast_mult > 0.0 {
416        let _ = ctx.deal_damage_from_result(
417            SPELL::STORMBLAST,
418            off_hand,
419            stormblast_mult,
420            DamageFlags::empty(),
421        );
422    }
423
424    roll_stormsurge(ctx);
425    melee_hit_procs(ctx, false);
426
427    // Crash Lightning procs once per Stormstrike variant, including echoes.
428
429    if ctx.is_aura_active(AURA_CRASH_LIGHTNING.raw()) {
430        let stacks = f64::from(ctx.aura_stacks(AURA_CRASH_LIGHTNING.raw()));
431
432        trigger_crash_lightning_proc(ctx, AURA::CRASH_LIGHTNING, 1, mult * stacks);
433    }
434
435    ride_the_lightning(ctx);
436}
437
438pub(crate) fn stormstrike_hook(ctx: &mut HookCtx<'_>) {
439    let stormblast_mult = if ctx.is_aura_active(AURA_STORMSURGE.raw()) {
440        ctx.spec_config::<EnhancementConfig>()
441            .map_or(0.0, |config| config.stormblast_mult)
442    } else {
443        0.0
444    };
445    // Consume the prior proc before the child hits roll fresh Stormsurges;
446    // new procs must survive for the next Stormstrike.
447
448    ctx.consume_aura(AURA_STORMSURGE.raw());
449    stormstrike_hits(ctx, 1.0, stormblast_mult);
450
451    // Doom Winds' direct lightning does not consume Maelstrom Weapon stacks.
452    let mw_stacks = ctx
453        .aura_stacks(AURA_MAELSTROM_WEAPON.raw())
454        .min(MW_CONSUME_CAP);
455
456    if ctx.is_aura_active(AURA_DOOM_WINDS.raw()) && mw_stacks > 0 {
457        let coefficient = ctx
458            .game_data()
459            .effect_sp_coefficient(EFFECT::LIGHTNING_BOLT_DAMAGE);
460
461        ctx.deal_damage_sp(
462            SPELL::LIGHTNING_BOLT,
463            coefficient * (1.0 + MW_DMG_PER_STACK * f64::from(mw_stacks)),
464            DamageFlags::empty(),
465        );
466        trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
467    }
468
469    if proc_chance(ctx.rng(), STORMFLURRY_CHANCE) {
470        stormstrike_hits(ctx, STORMFLURRY_MULT, stormblast_mult);
471    }
472
473    ctx.apply_aura(AURA_MAELSTROM_WEAPON.raw());
474}
475
476fn ride_the_lightning(ctx: &mut HookCtx<'_>) {
477    let coef = ctx
478        .game_data()
479        .effect_sp_coefficient(EFFECT::CHAIN_LIGHTNING_DAMAGE);
480
481    ctx.deal_damage_sp(
482        SPELL::CHAIN_LIGHTNING,
483        coef * RIDE_THE_LIGHTNING_MULT,
484        DamageFlags::empty(),
485    );
486    // Triggered Chain Lightning variants do not consume Whirling Air.
487    trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
488}
489
490fn trigger_splitstream(ctx: &mut HookCtx<'_>) {
491    let splitstream_mult = ctx
492        .spec_config::<EnhancementConfig>()
493        .map_or(0.0, |config| config.splitstream_mult);
494
495    if splitstream_mult <= 0.0 {
496        return;
497    }
498
499    let splitstream = ctx
500        .game_data()
501        .effect_ap_coefficient(EFFECT::SUNDERING_SPLITSTREAM_DAMAGE);
502
503    ctx.deal_damage_ap(
504        SPELL::SUNDERING_SPLITSTREAM,
505        splitstream * splitstream_mult,
506        DamageFlags::PHYSICAL,
507    );
508    melee_hit_procs(ctx, true);
509
510    if ctx.is_aura_active(AURA_SURGING_TOTEM.raw()) {
511        deal_tremor(ctx, EARTHSURGE_MULT * splitstream_mult);
512    }
513}
514
515/// Applies Lava Lash hook effects.
516pub(crate) fn lava_lash_hook(ctx: &mut HookCtx<'_>) {
517    let ashen_catalyst_cdr_ms = ctx
518        .spec_config::<EnhancementConfig>()
519        .map_or(0, |config| config.ashen_catalyst_cdr_ms);
520
521    if ashen_catalyst_cdr_ms > 0 && ctx.is_aura_active(AURA_FLAME_SHOCK.raw()) {
522        ctx.reduce_cooldown(SPELL_LAVA_LASH.raw(), ashen_catalyst_cdr_ms);
523    }
524
525    if ctx.is_aura_active(AURA_HOT_HAND.raw()) {
526        trigger_splitstream(ctx);
527    }
528
529    // An already-active Hot Hand fires a second Splitstream before extension.
530
531    if ctx.is_aura_active(AURA_WHIRLING_FIRE.raw()) {
532        let hot_hand_active = ctx.is_aura_active(AURA_HOT_HAND.raw());
533
534        if hot_hand_active {
535            trigger_splitstream(ctx);
536        }
537
538        ctx.consume_aura(AURA_WHIRLING_FIRE.raw());
539
540        if hot_hand_active {
541            ctx.extend_aura(AURA_HOT_HAND.raw(), WHIRLING_FIRE_HOT_HAND_MS);
542        } else {
543            ctx.apply_aura(AURA_HOT_HAND.raw());
544        }
545    }
546
547    // Lava Lash applies Lashing Flames 334168 e1 but does not benefit from it.
548
549    ctx.apply_aura(AURA_LASHING_FLAMES.raw());
550
551    if ctx.is_aura_active(AURA_CRASH_LIGHTNING.raw()) {
552        let stacks = f64::from(ctx.aura_stacks(AURA_CRASH_LIGHTNING.raw()));
553
554        trigger_crash_lightning_proc(
555            ctx,
556            EFFECT::CRASH_LIGHTNING_DAMAGE.0,
557            EFFECT::CRASH_LIGHTNING_DAMAGE.1,
558            stacks,
559        );
560    }
561
562    // A newly spawned Searing Totem participates in the same Lava Lash volley.
563
564    let lively_totems = ctx
565        .spec_config::<EnhancementConfig>()
566        .is_some_and(|config| config.lively_totems);
567    let mut failures = ctx
568        .spec_runtime::<EnhancementRuntime>()
569        .map(|runtime| runtime.lively_totems_failures);
570    let lively_totems_proc = lively_totems
571        && failures.as_mut().is_some_and(|failures| {
572            accumulated_proc_chance(ctx.rng(), LIVELY_TOTEMS_CHANCE, failures, 0)
573        });
574
575    if let (Some(runtime), Some(failures)) =
576        (ctx.spec_runtime_mut::<EnhancementRuntime>(), failures)
577    {
578        runtime.lively_totems_failures = failures;
579    }
580
581    if lively_totems_proc {
582        summon_searing_totem(ctx);
583    }
584
585    trigger_searing_volley(ctx);
586
587    ride_the_lightning(ctx);
588    melee_hit_procs(ctx, false);
589}
590
591fn crash_lightning_unleashed(ctx: &mut HookCtx<'_>) {
592    let coefficient = ctx
593        .game_data()
594        .effect_ap_coefficient(EFFECT::CRASH_LIGHTNING_UNLEASHED_DAMAGE);
595
596    ctx.deal_damage_ap(
597        SPELL::CRASH_LIGHTNING_UNLEASHED,
598        coefficient,
599        DamageFlags::empty(),
600    );
601}
602
603pub(crate) fn crash_lightning_hook(ctx: &mut HookCtx<'_>) {
604    melee_hit_procs(ctx, true);
605    let repeats = ctx
606        .spec_config::<EnhancementConfig>()
607        .is_some_and(|config| config.storm_unleashed_repeats);
608
609    if repeats {
610        ctx.schedule_hook(
611            CRASH_LIGHTNING_UNLEASHED_FIRST_MS,
612            crash_lightning_unleashed,
613        );
614        ctx.schedule_hook(
615            CRASH_LIGHTNING_UNLEASHED_SECOND_MS,
616            crash_lightning_unleashed,
617        );
618    }
619}
620
621fn primordial_storm_damage_mult(ctx: &HookCtx<'_>) -> f64 {
622    let stacks = ctx
623        .spec_runtime::<EnhancementRuntime>()
624        .map_or(0, |runtime| runtime.primordial_storm_mw_stacks);
625
626    1.0 + MW_DMG_PER_STACK * f64::from(stacks)
627}
628
629fn consume_maelstrom_weapon(ctx: &mut HookCtx<'_>) -> i32 {
630    let stacks = ctx
631        .aura_stacks(AURA_MAELSTROM_WEAPON.raw())
632        .min(MW_CONSUME_CAP);
633
634    ctx.consume_aura(AURA_MAELSTROM_WEAPON.raw());
635    let cdr_per_stack = ctx
636        .spec_config::<EnhancementConfig>()
637        .map_or(0, |config| config.elemental_tempo_cdr_per_stack_ms);
638    let stack_count = wowlab_types::numeric::i32_to_u32_nonnegative(stacks);
639    let cdr_ms = cdr_per_stack.saturating_mul(stack_count);
640
641    if cdr_ms > 0 {
642        ctx.reduce_cooldown(SPELL_STORMSTRIKE.raw(), cdr_ms);
643        ctx.reduce_cooldown(SPELL_LAVA_LASH.raw(), cdr_ms);
644    }
645
646    let hot_hand_per_stack = ctx
647        .spec_config::<EnhancementConfig>()
648        .map_or(0, |config| config.totemic_momentum_hot_hand_ms);
649
650    if hot_hand_per_stack > 0 && ctx.is_aura_active(AURA_HOT_HAND.raw()) {
651        ctx.extend_aura(
652            AURA_HOT_HAND.raw(),
653            hot_hand_per_stack.saturating_mul(stack_count),
654        );
655    }
656
657    let storm_unleashed_chance = ctx
658        .spec_config::<EnhancementConfig>()
659        .map_or(0.0, |config| config.storm_unleashed_chance_per_stack);
660
661    if (0..stacks).any(|_| proc_chance(ctx.rng(), storm_unleashed_chance)) {
662        ctx.apply_aura(AURA_STORM_UNLEASHED.raw());
663    }
664
665    stacks
666}
667
668fn primordial_fire(ctx: &mut HookCtx<'_>) {
669    let coefficient = ctx
670        .game_data()
671        .effect_ap_coefficient(EFFECT::PRIMORDIAL_FIRE_DAMAGE);
672
673    let outcome = ctx.deal_damage_ap_result(
674        SPELL::PRIMORDIAL_FIRE,
675        coefficient * primordial_storm_damage_mult(ctx),
676        DamageFlags::empty(),
677    );
678
679    landed_melee_spell_procs(ctx, SPELL::PRIMORDIAL_FIRE, outcome);
680}
681
682fn primordial_frost(ctx: &mut HookCtx<'_>) {
683    let coefficient = ctx
684        .game_data()
685        .effect_ap_coefficient(EFFECT::PRIMORDIAL_FROST_DAMAGE);
686
687    let outcome = ctx.deal_damage_ap_result(
688        SPELL::PRIMORDIAL_FROST,
689        coefficient * primordial_storm_damage_mult(ctx),
690        DamageFlags::empty(),
691    );
692
693    landed_melee_spell_procs(ctx, SPELL::PRIMORDIAL_FROST, outcome);
694}
695
696fn primordial_lightning(ctx: &mut HookCtx<'_>) {
697    let coefficient = ctx
698        .game_data()
699        .effect_ap_coefficient(EFFECT::PRIMORDIAL_LIGHTNING_DAMAGE);
700
701    let outcome = ctx.deal_damage_ap_result(
702        SPELL::PRIMORDIAL_LIGHTNING,
703        coefficient * primordial_storm_damage_mult(ctx),
704        DamageFlags::empty(),
705    );
706
707    landed_melee_spell_procs(ctx, SPELL::PRIMORDIAL_LIGHTNING, outcome);
708}
709
710fn primordial_lightning_bolt(ctx: &mut HookCtx<'_>) {
711    let coefficient = ctx
712        .game_data()
713        .effect_sp_coefficient(EFFECT::LIGHTNING_BOLT_DAMAGE);
714
715    ctx.deal_damage_sp(
716        SPELL::LIGHTNING_BOLT,
717        coefficient * primordial_storm_damage_mult(ctx) * PRIMORDIAL_LIGHTNING_BOLT_MULT,
718        DamageFlags::empty(),
719    );
720    trigger_totemic_rebound(ctx, false, SURGING_BOLT_DELAY_MS);
721}
722
723pub(crate) fn primordial_storm_hook(ctx: &mut HookCtx<'_>) {
724    let stacks = consume_maelstrom_weapon(ctx);
725
726    if let Some(runtime) = ctx.spec_runtime_mut::<EnhancementRuntime>() {
727        runtime.primordial_storm_mw_stacks = stacks;
728    }
729
730    primordial_fire(ctx);
731    ctx.schedule_hook(PRIMORDIAL_FROST_DELAY_MS, primordial_frost);
732    ctx.schedule_hook(PRIMORDIAL_LIGHTNING_DELAY_MS, primordial_lightning);
733    ctx.schedule_hook(
734        PRIMORDIAL_LIGHTNING_BOLT_DELAY_MS,
735        primordial_lightning_bolt,
736    );
737}
738
739pub(crate) fn lightning_bolt_hook(ctx: &mut HookCtx<'_>) {
740    let stacks = consume_maelstrom_weapon(ctx);
741
742    let coef = ctx
743        .game_data()
744        .effect_sp_coefficient(EFFECT::LIGHTNING_BOLT_DAMAGE);
745    let mult = 1.0 + MW_DMG_PER_STACK * f64::from(stacks);
746
747    ctx.deal_damage_sp(SPELL::LIGHTNING_BOLT, coef * mult, DamageFlags::empty());
748    trigger_direct_lightning_totemic(ctx);
749}
750
751pub(crate) fn chain_lightning_hook(ctx: &mut HookCtx<'_>) {
752    trigger_direct_lightning_totemic(ctx);
753}
754
755pub(crate) fn surging_totem_hook(ctx: &mut HookCtx<'_>) {
756    let config = enhancement_config(ctx);
757    let _ = ctx.summon_guardian(
758        GuardianSpec::new_inheriting_owner(SPELL::SURGING_TOTEM, config.surging_totem_duration_ms)
759            .npc_id(config.surging_totem_npc_id)
760            .on_demise(surging_totem_demise),
761    );
762
763    ctx.apply_aura(AURA_AMPLIFICATION_CORE.raw());
764    ctx.apply_aura(AURA_WHIRLING_AIR.raw());
765    ctx.apply_aura(AURA_WHIRLING_FIRE.raw());
766    ctx.apply_aura(AURA_WHIRLING_EARTH.raw());
767    deal_tremor(ctx, 1.0);
768}
769
770fn surging_totem_demise(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
771    ctx.expire_aura(AURA_AMPLIFICATION_CORE.raw());
772    ctx.expire_aura(AURA_SURGING_TOTEM.raw());
773    ctx.expire_aura(AURA_TOTEMIC_REBOUND.raw());
774    ctx.expire_aura(AURA_WHIRLING_AIR.raw());
775    ctx.expire_aura(AURA_WHIRLING_EARTH.raw());
776    ctx.expire_aura(AURA_WHIRLING_FIRE.raw());
777}
778
779pub(crate) fn surging_totem_pulse_hook(ctx: &mut HookCtx<'_>) {
780    deal_tremor(ctx, 1.0);
781}
782
783pub(crate) fn sundering_hook(ctx: &mut HookCtx<'_>) {
784    melee_hit_procs(ctx, true);
785    summon_feral_spirit(ctx, FIRE_WOLF_GUARDIAN_TAG, AURA_MOLTEN_WEAPON.raw());
786
787    if ctx.active_guardian_count(SPELL::SURGING_TOTEM) > 0 {
788        deal_tremor(ctx, EARTHSURGE_MULT);
789    }
790
791    // 453406 e1 (+100% direct on Sundering) is folded by the generic live-aura path, matching
792    // SimC's source_eff_builder_t( buff.whirling_earth ) (sc_shaman.cpp:12812); only the
793    // Searing Totem half of the buff is content.
794
795    if ctx.is_aura_active(AURA_WHIRLING_EARTH.raw()) {
796        ctx.consume_aura(AURA_WHIRLING_EARTH.raw());
797        summon_searing_totem(ctx);
798    }
799}
800
801pub(crate) fn doom_winds_hook(ctx: &mut HookCtx<'_>) {
802    summon_feral_spirit(ctx, LIGHTNING_WOLF_GUARDIAN_TAG, AURA_CRACKLING_SURGE.raw());
803}
804
805pub(crate) fn voltaic_blaze_hook(ctx: &mut HookCtx<'_>) {
806    ctx.apply_aura_n(
807        AURA_MAELSTROM_WEAPON.raw(),
808        enhancement_config(ctx).voltaic_blaze_maelstrom_stacks,
809    );
810    trigger_searing_volley(ctx);
811}
812
813pub(crate) fn ascendance_hook(ctx: &mut HookCtx<'_>) {
814    ctx.reset_cooldown(SPELL_STORMSTRIKE.raw());
815}
816
817/// Returns Enhanced Elements mastery damage from effect 1.
818#[must_use]
819pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
820    let enhanced_elements = if [
821        DamageSchool::Fire,
822        DamageSchool::Frost,
823        DamageSchool::Nature,
824    ]
825    .into_iter()
826    .any(|school| ctx.has_school(school))
827    {
828        ctx.bonus(1)
829    } else {
830        1.0
831    };
832
833    let elemental_weapons_per_imbue = ctx
834        .state
835        .spec_config::<EnhancementConfig>()
836        .map_or(0.0, |config| config.elemental_weapons_per_imbue);
837    let modifier_effect = if ctx.is_periodic {
838        EFFECT::ELEMENTAL_WEAPONS_PERIODIC.1
839    } else {
840        EFFECT::ELEMENTAL_WEAPONS_DIRECT.1
841    };
842    let affected = ctx.state.config.game_data.effect_affects_spell(
843        SpellIdx::from_raw(EFFECT::ELEMENTAL_WEAPONS_DIRECT.0),
844        modifier_effect,
845        SpellIdx::from_raw(ctx.spell_id),
846    ) || ctx.spell_id == SPELL::STORMBLAST;
847    let active_imbues = ctx.active_weapon_imbue_count();
848    let elemental_weapons = if affected {
849        1.0 + elemental_weapons_per_imbue * wowlab_types::numeric::usize_to_f64(active_imbues)
850    } else {
851        1.0
852    };
853
854    enhanced_elements * elemental_weapons
855}
856
857#[cfg(test)]
858#[path = "hooks/tests.rs"]
859mod tests;