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
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
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 windfury: direct_melee && main_hand_bound && spell_id != SPELL::WINDFURY_ATTACK,
235 flametongue,
236 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 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 ctx.consume_aura(AURA_STORMSURGE.raw());
449 stormstrike_hits(ctx, 1.0, stormblast_mult);
450
451 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 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
515pub(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 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 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 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 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#[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;