Skip to main content

wowlab_engine_content/hooks/elemental_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::ResourceOps as _;
7use wowlab_engine_combat::SchedulingOps as _;
8use wowlab_engine_combat::{
9    BuffEffect, CombatSystemDraft, DamageFlags, HookCtx, ImpactEvent, ImpactProc, MasteryCtx,
10    PassiveDriverEffect, RuntimeDamageDef, try_push_effect,
11};
12use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
13use wowlab_engine_ports::HandlerParams;
14use wowlab_engine_rng::proc_chance;
15use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
16
17use crate::generated::specs::elemental_shaman::{
18    AURA_ANCESTRAL_SWIFTNESS, AURA_ASCENDANCE, AURA_ELEMENTAL_BLAST_CRIT,
19    AURA_ELEMENTAL_BLAST_HASTE, AURA_ELEMENTAL_BLAST_MASTERY, AURA_FIRE_ELEMENTAL,
20    AURA_FLAME_SHOCK, AURA_FLOWING_ELEMENTS, AURA_LAVA_SURGE, AURA_LIGHTNING_ROD,
21    AURA_LIGHTNING_SHIELD, AURA_MASTER_OF_THE_ELEMENTS, AURA_OVERCHARGE_TIER,
22    AURA_POWER_OF_THE_MAELSTROM, AURA_STORM_ELEMENTAL, AURA_STORMKEEPER, AURA_STORMS_EYE,
23    AURA_TEMPEST, AURA_UNLIMITED_POWER, AURA_WIND_GUST, EFFECT, SET_BONUS, SPELL,
24    SPELL_ELEMENTAL_BLAST, SPELL_LAVA_BURST, TALENT,
25};
26
27/// Server-side Maelstrom amounts absent from DBC.
28const LAVA_BURST_MAELSTROM: f64 = 8.0;
29const LIGHTNING_BOLT_OVERLOAD_MAELSTROM: f64 = 2.0;
30const CHAIN_LIGHTNING_OVERLOAD_MAELSTROM: f64 = 1.0;
31const LAVA_BURST_OVERLOAD_MAELSTROM: f64 = 3.0;
32
33const CHAIN_OVERLOAD_DIVISOR: f64 = 3.0;
34
35/// Stormkeeper's normal cast grants two charges (191634 e5).
36const STORMKEEPER_CAST_CHARGES: u32 = 2;
37/// Arc Discharge adds one Stormkeeper charge (455096 e1).
38const ARC_DISCHARGE_CHARGES: u32 = 1;
39const MID2_EMPOWERED_BUILDER_CHARGES: i32 = 2;
40const MID2_OVERLAP_EMPOWERED_BUILDER_CHARGES: i32 = 4;
41
42/// Lava Surge chance is `max(0, 0.6 - 1.16^(-2 * (attempts - 5)))`.
43const LAVA_SURGE_CEILING: f64 = 0.6;
44const LAVA_SURGE_DECAY_BASE: f64 = 1.16;
45const LAVA_SURGE_MIDPOINT: f64 = 5.0;
46const LAVA_SURGE_EXPONENT_SCALE: f64 = -2.0;
47
48/// 188592 e4/e5 affect mask (family flag 103), player half.
49#[rustfmt::skip]
50static EU_FIRE_PLAYER_SPELLS: [u32; 3] = [
51    SPELL::ELEMENTAL_BLAST,
52    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
53    SPELL::FLAME_SHOCK,
54];
55/// 188592 e4/e5 affect mask (family flag 103), Fire Elemental actions.
56static EU_FIRE_PET_SPELLS: [u32; 3] = [SPELL::METEOR, SPELL::IMMOLATE, SPELL::FIRE_BLAST];
57/// Family-flag-126 class mask shared by Storm Elemental unity (157299 e3/e4) and Lightning Capacitor (462862 e1/e2), player spells.
58#[rustfmt::skip]
59static FLAG_126_PLAYER_SPELLS: [u32; 11] = [
60    SPELL::LIGHTNING_BOLT,
61    SPELL::CHAIN_LIGHTNING,
62    SPELL::ELEMENTAL_BLAST,
63    SPELL::TEMPEST,
64    SPELL::EARTH_SHOCK,
65    SPELL::FLAME_SHOCK,
66    SPELL::STORMS_EYE,
67    EFFECT::LIGHTNING_BOLT_OVERLOAD.0,
68    EFFECT::CHAIN_LIGHTNING_OVERLOAD.0,
69    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
70    EFFECT::TEMPEST_OVERLOAD.0,
71];
72/// 157299 e3/e4 affect mask (family flag 126), Storm Elemental actions.
73static EU_STORM_PET_SPELLS: [u32; 3] = [SPELL::WIND_GUST, SPELL::CALL_LIGHTNING, SPELL::STORMFURY];
74/// Elemental Unity rows the summon auras declare in data but the game scripts on only with the talent.
75static EU_FIRE_MASKED_EFFECTS: [(u32, u8); 2] = [
76    EFFECT::FIRE_ELEMENTAL_UNITY,
77    EFFECT::FIRE_ELEMENTAL_UNITY_PERIODIC,
78];
79static EU_STORM_MASKED_EFFECTS: [(u32, u8); 2] = [
80    EFFECT::STORM_ELEMENTAL_UNITY,
81    EFFECT::STORM_ELEMENTAL_UNITY_PET,
82];
83/// 382028 e1 affect masks (family flags 28/44/86/98): the fire half.
84#[rustfmt::skip]
85static IFW_SPELLS: [u32; 5] = [
86    SPELL::FLAME_SHOCK,
87    SPELL::LAVA_BURST,
88    SPELL::ELEMENTAL_BLAST,
89    EFFECT::LAVA_BURST_OVERLOAD.0,
90    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
91];
92
93pub(crate) fn earthquake_hook(ctx: &mut HookCtx<'_>) {
94    if let Some(effect) = elemental_config(ctx).earthquake_ground {
95        ctx.schedule_ground_effect(effect);
96    }
97
98    ctx.consume_aura(AURA_OVERCHARGE_TIER);
99}
100
101pub(crate) fn earth_shock_hook(ctx: &mut HookCtx<'_>) {
102    ctx.consume_aura(AURA_OVERCHARGE_TIER);
103}
104
105const STORMS_EYE_CHARGES: i32 = 2;
106
107const RANDOM_STAT_ROLL_THIRD: f64 = 1.0 / 3.0;
108const RANDOM_STAT_ROLL_TWO_THIRDS: f64 = 2.0 / 3.0;
109
110use super::{
111    config::{ElementalRuntime, elemental_config, register_elemental_config},
112    elementals::{STORM_ELEMENTAL_GUARDIAN_TAG, summon_fire_elemental, summon_storm_elemental},
113};
114
115fn trigger_elemental_blast_stat(ctx: &mut HookCtx<'_>) {
116    let aura = match ctx.rng()() {
117        roll if roll < RANDOM_STAT_ROLL_THIRD => AURA_ELEMENTAL_BLAST_CRIT,
118        roll if roll < RANDOM_STAT_ROLL_TWO_THIRDS => AURA_ELEMENTAL_BLAST_HASTE,
119        _ => AURA_ELEMENTAL_BLAST_MASTERY,
120    };
121
122    ctx.apply_aura(aura);
123}
124
125/// Overload proc chance: mastery points x 168534 e1 coefficient (1.875 %/point).
126fn overload_chance(ctx: &HookCtx<'_>) -> f64 {
127    ctx.effect_points(crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::OVERLOAD_CHANCE)
128        / HUNDRED
129}
130
131/// Overload damage penalty from spell 168534 e2.
132fn overload_penalty(ctx: &HookCtx<'_>) -> f64 {
133    ctx.effect_base_points(
134        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::OVERLOAD_PENALTY,
135    ) / HUNDRED
136}
137
138fn fire_overload(
139    ctx: &mut HookCtx<'_>,
140    effect: wowlab_engine_combat::DamageEffectId,
141    maelstrom: f64,
142) {
143    fire_overload_scaled(ctx, effect, maelstrom, 1.0);
144    let feedback_chance = elemental_config(ctx).feedback_loop_chance;
145
146    if feedback_chance > 0.0 && proc_chance(ctx.rng(), feedback_chance) {
147        fire_overload_scaled(ctx, effect, maelstrom, 1.0);
148    }
149}
150
151fn fire_overload_scaled(
152    ctx: &mut HookCtx<'_>,
153    effect: wowlab_engine_combat::DamageEffectId,
154    maelstrom: f64,
155    damage_mult: f64,
156) {
157    let coef = ctx.effect_sp_coefficient(effect) * overload_penalty(ctx) * damage_mult;
158
159    ctx.deal_damage_sp(effect.action(), coef, DamageFlags::empty());
160
161    if maelstrom > 0.0 {
162        ctx.gain_resource(maelstrom);
163    }
164}
165
166// Elemental Blast never rolls Supercharge but retains mastery and Ascendance overloads.
167fn fire_elemental_blast_overloads(ctx: &mut HookCtx<'_>, damage_mult: f64) {
168    let cfg = elemental_config(ctx);
169    let fire = |ctx: &mut HookCtx<'_>| {
170        trigger_elemental_blast_stat(ctx);
171        fire_overload_scaled(
172            ctx,
173            crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::ELEMENTAL_BLAST_OVERLOAD,
174            0.0,
175            damage_mult,
176        );
177
178        if cfg.feedback_loop_chance > 0.0 && proc_chance(ctx.rng(), cfg.feedback_loop_chance) {
179            trigger_elemental_blast_stat(ctx);
180            fire_overload_scaled(
181                ctx,
182                crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::ELEMENTAL_BLAST_OVERLOAD,
183                0.0,
184                damage_mult,
185            );
186        }
187    };
188
189    let chance = overload_chance(ctx);
190
191    if proc_chance(ctx.rng(), chance) {
192        fire(ctx);
193    }
194
195    if ctx.is_aura_active(AURA_ASCENDANCE) {
196        fire(ctx);
197    }
198}
199
200fn fire_elemental_blast(ctx: &mut HookCtx<'_>, damage_mult: f64) {
201    trigger_elemental_blast_stat(ctx);
202    let coefficient = ctx.effect_sp_coefficient(
203        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::ELEMENTAL_BLAST_DAMAGE,
204    ) * damage_mult;
205
206    ctx.deal_damage_sp(
207        crate::generated::specs::elemental_shaman::DAMAGE_SPELL::ELEMENTAL_BLAST,
208        coefficient,
209        DamageFlags::empty(),
210    );
211    let cfg = elemental_config(ctx);
212
213    if cfg.eb_overload {
214        fire_elemental_blast_overloads(ctx, cfg.eb_overload_mult * damage_mult);
215    }
216}
217
218fn spend_stormkeeper_charge(ctx: &mut HookCtx<'_>) {
219    if ctx.consume_proc_counter(
220        wowlab_engine_combat::generated_identities::proc_counter::STORMKEEPER,
221        1,
222    ) == 0
223    {
224        ctx.expire_aura(AURA_STORMKEEPER);
225    }
226}
227
228fn consume_flowing_elements(ctx: &mut HookCtx<'_>) {
229    if ctx.consume_aura_stack(AURA_FLOWING_ELEMENTS) {
230        ctx.apply_aura(AURA_OVERCHARGE_TIER);
231    }
232}
233
234#[derive(Clone, Copy)]
235struct EmpoweredLightningOptions {
236    chance_divisor: f64,
237    stormkeeper_overload: bool,
238    consume_stormkeeper: bool,
239}
240
241const LIGHTNING_BOLT_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
242    chance_divisor: 1.0,
243    stormkeeper_overload: true,
244    consume_stormkeeper: true,
245};
246const TEMPEST_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
247    chance_divisor: 1.0,
248    stormkeeper_overload: false,
249    consume_stormkeeper: false,
250};
251const CHAIN_LIGHTNING_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
252    chance_divisor: CHAIN_OVERLOAD_DIVISOR,
253    stormkeeper_overload: true,
254    consume_stormkeeper: true,
255};
256
257fn empowered_lightning_cast(
258    ctx: &mut HookCtx<'_>,
259    overload: wowlab_engine_combat::DamageEffectId,
260    overload_maelstrom: f64,
261    options: EmpoweredLightningOptions,
262) {
263    let stormkeeper = ctx.is_aura_active(AURA_STORMKEEPER);
264    let cfg = elemental_config(ctx);
265
266    let chance = overload_chance(ctx) / options.chance_divisor;
267
268    if (stormkeeper && options.stormkeeper_overload) || proc_chance(ctx.rng(), chance) {
269        fire_overload(ctx, overload, overload_maelstrom);
270    }
271
272    if stormkeeper && options.consume_stormkeeper {
273        spend_stormkeeper_charge(ctx);
274    }
275
276    if ctx.consume_aura(AURA_POWER_OF_THE_MAELSTROM) {
277        fire_overload(ctx, overload, overload_maelstrom);
278
279        if cfg.fusion_of_elements_mult > 0.0 {
280            fire_elemental_blast(ctx, cfg.fusion_of_elements_mult);
281        }
282    }
283
284    if cfg.supercharge_chance > 0.0 && proc_chance(ctx.rng(), cfg.supercharge_chance) {
285        fire_overload(ctx, overload, overload_maelstrom);
286    }
287    // Ascendance adds one overload without duplicating Supercharge.
288
289    if ctx.is_aura_active(AURA_ASCENDANCE) {
290        fire_overload(ctx, overload, overload_maelstrom);
291    }
292
293    if ctx.active_guardian_count(STORM_ELEMENTAL_GUARDIAN_TAG) > 0 {
294        ctx.apply_aura(AURA_WIND_GUST);
295    }
296
297    ctx.consume_aura(AURA_MASTER_OF_THE_ELEMENTS);
298}
299
300pub(crate) fn lightning_bolt_hook(ctx: &mut HookCtx<'_>) {
301    empowered_lightning_cast(
302        ctx,
303        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LIGHTNING_BOLT_OVERLOAD,
304        LIGHTNING_BOLT_OVERLOAD_MAELSTROM,
305        LIGHTNING_BOLT_OPTIONS,
306    );
307    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS);
308    consume_flowing_elements(ctx);
309}
310
311pub(crate) fn tempest_hook(ctx: &mut HookCtx<'_>) {
312    let stormwell_maelstrom = elemental_config(ctx).stormwell_tempest_maelstrom;
313
314    if stormwell_maelstrom > 0.0
315        && elemental_config(ctx).stormwell_tempest_bug
316        && !ctx.is_aura_active(AURA_STORMKEEPER)
317    {
318        ctx.gain_resource(stormwell_maelstrom);
319    }
320
321    // Every Tempest execute decrements the buff.
322
323    ctx.consume_aura(AURA_TEMPEST);
324
325    if elemental_config(ctx).tww3_stormbringer_4pc && ctx.consume_aura_stack(AURA_STORMS_EYE) {
326        let storms_eye = ctx.effect_sp_coefficient(
327            crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::STORMS_EYE_DAMAGE,
328        );
329
330        ctx.deal_damage_sp(
331            crate::generated::specs::elemental_shaman::DAMAGE_SPELL::STORMS_EYE,
332            storms_eye,
333            DamageFlags::empty(),
334        );
335    }
336
337    empowered_lightning_cast(
338        ctx,
339        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::TEMPEST_OVERLOAD,
340        0.0,
341        TEMPEST_OPTIONS,
342    );
343
344    if elemental_config(ctx).arc_discharge {
345        ctx.apply_aura(AURA_STORMKEEPER);
346        ctx.add_proc_counter(
347            wowlab_engine_combat::generated_identities::proc_counter::STORMKEEPER,
348            ARC_DISCHARGE_CHARGES,
349        );
350    }
351}
352
353pub(crate) fn chain_lightning_hook(ctx: &mut HookCtx<'_>) {
354    empowered_lightning_cast(
355        ctx,
356        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::CHAIN_LIGHTNING_OVERLOAD,
357        CHAIN_LIGHTNING_OVERLOAD_MAELSTROM,
358        CHAIN_LIGHTNING_OPTIONS,
359    );
360    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS);
361    consume_flowing_elements(ctx);
362}
363
364pub(crate) fn lava_burst_hook(ctx: &mut HookCtx<'_>) {
365    let cfg = elemental_config(ctx);
366
367    if cfg.master_of_the_elements {
368        ctx.apply_aura(AURA_MASTER_OF_THE_ELEMENTS);
369    }
370
371    if cfg.potm_chance > 0.0 && proc_chance(ctx.rng(), cfg.potm_chance) {
372        ctx.apply_aura(AURA_POWER_OF_THE_MAELSTROM);
373    }
374
375    ctx.gain_resource(LAVA_BURST_MAELSTROM);
376    ctx.consume_aura(AURA_LAVA_SURGE);
377
378    let chance = overload_chance(ctx);
379
380    if proc_chance(ctx.rng(), chance) {
381        fire_overload(
382            ctx,
383            crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LAVA_BURST_OVERLOAD,
384            LAVA_BURST_OVERLOAD_MAELSTROM,
385        );
386    }
387
388    if ctx.is_aura_active(AURA_ASCENDANCE) {
389        fire_overload(
390            ctx,
391            crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LAVA_BURST_OVERLOAD,
392            LAVA_BURST_OVERLOAD_MAELSTROM,
393        );
394    }
395
396    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS);
397    consume_flowing_elements(ctx);
398}
399
400pub(crate) fn elemental_blast_hook(ctx: &mut HookCtx<'_>) {
401    let maelstrom_spent = ctx.last_primary_spent();
402
403    fire_elemental_blast(ctx, 1.0);
404
405    if elemental_config(ctx).unlimited_power {
406        ctx.apply_aura(AURA_UNLIMITED_POWER);
407    }
408
409    ctx.apply_aura(AURA_LIGHTNING_ROD);
410    ctx.consume_aura(AURA_MASTER_OF_THE_ELEMENTS);
411    let cfg = elemental_config(ctx);
412
413    if cfg.aftershock_chance > 0.0 && proc_chance(ctx.rng(), cfg.aftershock_chance) {
414        ctx.gain_resource(maelstrom_spent);
415    }
416
417    if maelstrom_spent > 0.0
418        && cfg.tempest_chance_on_blast > 0.0
419        && proc_chance(ctx.rng(), cfg.tempest_chance_on_blast)
420    {
421        ctx.apply_aura(AURA_TEMPEST);
422    }
423
424    ctx.consume_aura(AURA_OVERCHARGE_TIER);
425    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS);
426}
427
428pub(crate) fn stormkeeper_hook(ctx: &mut HookCtx<'_>) {
429    let cfg = elemental_config(ctx);
430
431    ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::STORMKEEPER);
432    ctx.add_proc_counter(
433        wowlab_engine_combat::generated_identities::proc_counter::STORMKEEPER,
434        STORMKEEPER_CAST_CHARGES,
435    );
436
437    if cfg.summon_storm_elemental {
438        summon_storm_elemental(ctx);
439    }
440}
441
442pub(crate) fn stormkeeper_expire_hook(ctx: &mut HookCtx<'_>) {
443    if !elemental_config(ctx).mid2_4pc {
444        return;
445    }
446
447    if ctx.is_aura_active(AURA_ASCENDANCE) {
448        if let Some(runtime) = ctx.spec_runtime_mut::<ElementalRuntime>() {
449            runtime.stormkeeper_faded_during_ascendance = true;
450        }
451    } else {
452        ctx.apply_aura_n(AURA_FLOWING_ELEMENTS, MID2_EMPOWERED_BUILDER_CHARGES);
453    }
454}
455
456pub(crate) fn ascendance_expire_hook(ctx: &mut HookCtx<'_>) {
457    if !elemental_config(ctx).mid2_4pc {
458        return;
459    }
460
461    let stormkeeper_buffered = ctx
462        .spec_runtime_mut::<ElementalRuntime>()
463        .is_some_and(|runtime| std::mem::take(&mut runtime.stormkeeper_faded_during_ascendance));
464    let stacks = if stormkeeper_buffered {
465        MID2_OVERLAP_EMPOWERED_BUILDER_CHARGES
466    } else {
467        MID2_EMPOWERED_BUILDER_CHARGES
468    };
469
470    ctx.apply_aura_n(AURA_FLOWING_ELEMENTS, stacks);
471}
472
473pub(crate) fn ascendance_hook(ctx: &mut HookCtx<'_>) {
474    let cfg = elemental_config(ctx);
475
476    if cfg.tww3_stormbringer_2pc {
477        ctx.apply_aura(AURA_TEMPEST);
478    }
479
480    if cfg.tww3_stormbringer_4pc {
481        ctx.apply_aura_n(AURA_STORMS_EYE, STORMS_EYE_CHARGES);
482    }
483
484    ctx.reset_cooldown(SPELL_LAVA_BURST);
485    ctx.apply_aura(AURA_FLAME_SHOCK);
486    let lava_burst = ctx.effect_sp_coefficient(
487        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LAVA_BURST_DAMAGE,
488    );
489
490    ctx.deal_damage_sp(
491        crate::generated::specs::elemental_shaman::DAMAGE_SPELL::LAVA_BURST,
492        lava_burst,
493        DamageFlags::empty(),
494    );
495    fire_overload(
496        ctx,
497        crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LAVA_BURST_OVERLOAD,
498        LAVA_BURST_OVERLOAD_MAELSTROM,
499    );
500
501    let chance = overload_chance(ctx);
502
503    if proc_chance(ctx.rng(), chance) {
504        fire_overload(
505            ctx,
506            crate::generated::specs::elemental_shaman::DAMAGE_EFFECT::LAVA_BURST_OVERLOAD,
507            LAVA_BURST_OVERLOAD_MAELSTROM,
508        );
509    }
510
511    if cfg.summon_fire_elemental {
512        summon_fire_elemental(ctx);
513    }
514}
515
516/// Returns Elemental Overload mastery damage from spell 168534 e3/e4.
517#[must_use]
518pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
519    if ctx.is_pet {
520        return ctx.bonus(EFFECT::MASTERY_PET_DAMAGE.1);
521    }
522
523    let effect = if ctx.is_periodic {
524        EFFECT::MASTERY_PERIODIC_DAMAGE.1
525    } else {
526        EFFECT::MASTERY_DIRECT_DAMAGE.1
527    };
528    let mut mult = if ctx.affects_spell(effect) {
529        ctx.bonus(effect)
530    } else {
531        1.0
532    };
533
534    if matches!(ctx.spell_id, SPELL::LAVA_BURST | SPELL::LAVA_BURST_OVERLOAD)
535        && ctx.is_aura_active(AURA_FLAME_SHOCK)
536    {
537        mult *= 1.0 + ctx.instance.player_crit() / HUNDRED;
538    }
539
540    mult
541}
542
543const fn flame_shock_filter(spell_id: u32) -> bool {
544    spell_id == SPELL::FLAME_SHOCK
545}
546
547const fn lightning_rod_filter(spell_id: u32) -> bool {
548    matches!(
549        spell_id,
550        SPELL::LIGHTNING_BOLT
551            | SPELL::CHAIN_LIGHTNING
552            | SPELL::TEMPEST
553            | SPELL::LIGHTNING_BOLT_OVERLOAD
554            | SPELL::CHAIN_LIGHTNING_OVERLOAD
555            | SPELL::TEMPEST_OVERLOAD
556    )
557}
558
559fn lightning_rod_fire(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
560    let cfg = elemental_config(ctx);
561
562    if cfg.lightning_rod_mult > 0.0 && ctx.is_aura_active(AURA_LIGHTNING_ROD) {
563        ctx.deal_resolved_damage(
564            crate::generated::specs::elemental_shaman::DAMAGE_SPELL::LIGHTNING_ROD_DAMAGE,
565            impact.amount * cfg.lightning_rod_mult,
566        );
567    }
568}
569
570fn lava_surge_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
571    if ctx.roll_attempt_proc(
572        wowlab_engine_combat::generated_identities::proc_counter::LAVA_SURGE_ATTEMPT,
573        |attempt| {
574            let exponent = LAVA_SURGE_EXPONENT_SCALE * (f64::from(attempt) - LAVA_SURGE_MIDPOINT);
575
576            (LAVA_SURGE_CEILING - LAVA_SURGE_DECAY_BASE.powf(exponent)).max(0.0)
577        },
578    ) {
579        ctx.reset_cooldown(SPELL_LAVA_BURST);
580        ctx.apply_aura(AURA_LAVA_SURGE);
581    }
582}
583
584/// Only Lightning Bolt Overload is hand-flagged.
585static STORMKEEPER_OVERLOAD_SPELLS: [u32; 1] = [EFFECT::LIGHTNING_BOLT_OVERLOAD.0];
586
587static MID2_SPENDER_DAMAGE_SPELLS: [u32; 3] = [
588    SPELL::EARTH_SHOCK,
589    SPELL::ELEMENTAL_BLAST,
590    EFFECT::EARTHQUAKE_DAMAGE.0,
591];
592
593fn register_conditional_multipliers(built: &mut CombatSystemDraft, params: &HandlerParams<'_>) {
594    let data = &params.game_data;
595    let mult_from = |effect: (u32, u8)| {
596        1.0 + data.base_points(SpellIdx::from_raw(effect.0), effect.1) / HUNDRED
597    };
598
599    // The elemental summon auras carry their Elemental Unity modifiers as ordinary DBC effects, but
600    // the game only scripts them on when Elemental Unity is picked, and swaps them for the talent's
601    // own effects under Primal Elementalist. Mask the data-derived rows so
602    // the talent state below is the only thing that applies them.
603
604    for effect in EU_FIRE_MASKED_EFFECTS {
605        built.mask_aura_dbc_effect(AURA_FIRE_ELEMENTAL, effect.0, effect.1);
606    }
607
608    for effect in EU_STORM_MASKED_EFFECTS {
609        built.mask_aura_dbc_effect(AURA_STORM_ELEMENTAL, effect.0, effect.1);
610    }
611
612    if params.talent_picked(TALENT::ELEMENTAL_UNITY) {
613        let pe = params.talent_picked(TALENT::PRIMAL_ELEMENTALIST);
614        let fire = if pe {
615            EFFECT::ELEMENTAL_UNITY_FIRE
616        } else {
617            EFFECT::FIRE_ELEMENTAL_UNITY
618        };
619        let storm = if pe {
620            EFFECT::ELEMENTAL_UNITY_STORM
621        } else {
622            EFFECT::STORM_ELEMENTAL_UNITY_PET
623        };
624
625        let fire_mult = mult_from(fire);
626        let storm_mult = mult_from(storm);
627
628        for spells in [
629            EU_FIRE_PLAYER_SPELLS.as_slice(),
630            EU_FIRE_PET_SPELLS.as_slice(),
631        ] {
632            built.set_aura_effect(
633                AURA_FIRE_ELEMENTAL,
634                BuffEffect::DamageMultSpells(fire_mult, spells),
635            );
636        }
637
638        for spells in [
639            FLAG_126_PLAYER_SPELLS.as_slice(),
640            EU_STORM_PET_SPELLS.as_slice(),
641        ] {
642            built.set_aura_effect(
643                AURA_STORM_ELEMENTAL,
644                BuffEffect::DamageMultSpells(storm_mult, spells),
645            );
646        }
647    }
648
649    if params.talent_picked(TALENT::LIGHTNING_CAPACITOR) {
650        built.set_aura_effect(
651            AURA_LIGHTNING_SHIELD,
652            BuffEffect::DamageMultSpells(
653                mult_from(EFFECT::LIGHTNING_CAPACITOR_DAMAGE),
654                &FLAG_126_PLAYER_SPELLS,
655            ),
656        );
657    }
658
659    if params.talent_picked(TALENT::FLAMETONGUE_WEAPON) {
660        let effect = EFFECT::IMPROVED_FLAMETONGUE_DAMAGE;
661        let pct = data.base_points(SpellIdx::from_raw(effect.0), effect.1);
662
663        built.register_passive_driver_effect(PassiveDriverEffect::new(
664            TALENT::FLAMETONGUE_WEAPON,
665            BuffEffect::DamageMultSpells(1.0 + pct / HUNDRED, &IFW_SPELLS),
666        ));
667    }
668}
669
670fn register_stormkeeper_damage(built: &mut CombatSystemDraft, params: &HandlerParams<'_>) {
671    let stormkeeper_mult = 1.0
672        + params
673            .game_data
674            .effect_base_points(EFFECT::STORMKEEPER_DAMAGE)
675            / HUNDRED;
676
677    built.patch_aura(AURA_STORMKEEPER, |aura| {
678        try_push_effect(
679            &mut aura.effects,
680            BuffEffect::DamageMultSpells(stormkeeper_mult, &STORMKEEPER_OVERLOAD_SPELLS),
681        )
682        .expect("Stormkeeper aura has room for its overload damage modifier");
683    });
684}
685
686pub(super) fn wire_elemental(
687    built: &mut CombatSystemDraft,
688    params: &HandlerParams<'_>,
689) -> Result<(), wowlab_engine_ports::EngineError> {
690    if params.talent_picked(TALENT::ELEMENTAL_BLAST) {
691        built.disable_spell(SPELL::EARTH_SHOCK);
692
693        built.patch_spell(SPELL_ELEMENTAL_BLAST, |spell| {
694            spell.damage = RuntimeDamageDef::None;
695        });
696    }
697
698    register_stormkeeper_damage(built, params);
699    register_conditional_multipliers(built, params);
700
701    if params
702        .set_bonus_auras
703        .contains(&SET_BONUS::MID2_ELEMENTAL_2PC)
704    {
705        let bonus = params
706            .game_data
707            .effect_base_points(EFFECT::MID2_SPENDER_DAMAGE);
708
709        built.register_passive_driver_effect(PassiveDriverEffect::new(
710            SET_BONUS::MID2_ELEMENTAL_2PC,
711            BuffEffect::DamageMultSpells(1.0 + bonus / HUNDRED, &MID2_SPENDER_DAMAGE_SPELLS),
712        ));
713    }
714
715    if params.talent_picked(TALENT::FLAMETONGUE_WEAPON) {
716        built.register_weapon_imbue(SPELL::FLAMETONGUE_WEAPON, "flametongue_weapon", true);
717    }
718
719    built.register_impact_proc(
720        ImpactProc::new(lava_surge_fire)
721            .with_spell_filter(flame_shock_filter)
722            .periodic_only(),
723    );
724
725    built.register_impact_proc(
726        ImpactProc::new(lightning_rod_fire)
727            .with_spell_filter(lightning_rod_filter)
728            .skip_periodic(),
729    );
730
731    register_elemental_config(built, params)?;
732
733    Ok(())
734}