Skip to main content

wowlab_engine_content/hooks/enhancement_shaman/
hooks.rs

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
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/// 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.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 attacks disable only Windfury self-procs.
260        windfury: direct_melee
261            && main_hand_bound
262            && action != crate::generated::specs::enhancement_shaman::DAMAGE_SPELL::WINDFURY_ATTACK,
263        flametongue,
264        // trigger_flametongue_weapon only rolls this rider for Windfury attack 25504.
265        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    // Crash Lightning procs once per Stormstrike variant, including echoes.
493
494    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    // Consume the prior proc before the child hits roll fresh Stormsurges;
515    // new procs must survive for the next Stormstrike.
516
517    ctx.consume_aura(AURA_STORMSURGE);
518    stormstrike_hits(ctx, 1.0, stormblast_mult);
519
520    // Doom Winds' direct lightning does not consume Maelstrom Weapon stacks.
521    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    // Triggered Chain Lightning variants do not consume Whirling Air.
554    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
582/// Applies Lava Lash hook effects.
583pub(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    // An already-active Hot Hand fires a second Splitstream before extension.
597
598    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    // Lava Lash applies Lashing Flames 334168 e1 but does not benefit from it.
615
616    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    // A newly spawned Searing Totem participates in the same Lava Lash volley.
629
630    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    // 453406 e1 (+100% direct on Sundering) is folded by the generic live-aura path;
930    // only the Searing Totem half of the buff is content.
931
932    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/// Returns Enhanced Elements mastery damage from effect 1.
994#[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;