Skip to main content

wowlab_engine_content/hooks/arcane_mage/
config.rs

1use wowlab_engine_combat::{BuffEffect, CombatInstance, CombatProgram, CombatSystemDraft};
2use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
3use wowlab_engine_ports::HandlerParams;
4use wowlab_engine_rng::prd_constant_capped;
5use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
6
7use super::hooks::{
8    CLEARCASTING_CONSTANT_CAP, CLEARCASTING_UNDOCUMENTED_BONUS, arcane_missiles_ticks,
9};
10use crate::{
11    generated::specs::arcane_mage::{
12        AURA, AURA_ARCANE_CHARGE, AURA_ARCANE_SALVO, AURA_ENLIGHTENED, AURA_INTUITION, EFFECT,
13        SET_BONUS, TALENT,
14    },
15    hooks::gated,
16};
17
18#[derive(Clone, Copy, Debug, Default)]
19pub(super) struct ArcaneRuntime {
20    pub(super) augury_attempts: u32,
21    pub(super) augury_icd_until_ms: u32,
22    pub(super) clearcasting_attempts: u32,
23    pub(super) touch_of_the_magi_start_damage: f64,
24}
25
26crate::hooks::define_spec_config! {
27    pub(super) struct ArcaneConfig {
28        arcane_missiles_ticks: u32 { provenance: "Arcane Missiles base ticks plus Amplification and MID2 two-piece waves.", gate: true, source: selected_value(EFFECT::AMPLIFICATION_WAVES.0, EFFECT::AMPLIFICATION_WAVES.1) + mid2_extra_waves, transform: arcane_missiles_ticks },
29        mid2_cumulative_power: bool { provenance: "MID2 Arcane four-piece enables Cumulative Power stacking from Missiles waves.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARCANE_4PC), source: true, transform: |value| value },
30        prismatic_bolt: bool { provenance: "Prismatic Bolt rank one enables its Arcane Barrage proc and replacement cast.", gate: params.talent_picked(TALENT::PRISMATIC_BOLT_1), source: true, transform: |value| value },
31        prismatic_bolt_proc_per_salvo: f64 { provenance: "Prismatic Bolt rank one chance plus rank three's per-Salvo bonus.", gate: params.talent_picked(TALENT::PRISMATIC_BOLT_1), source: fraction(EFFECT::PRISMATIC_BOLT_PROC_CHANCE.0, EFFECT::PRISMATIC_BOLT_PROC_CHANCE.1) + selected_fraction(EFFECT::PRISMATIC_BOLT_PROC_CHANCE_BONUS.0, EFFECT::PRISMATIC_BOLT_PROC_CHANCE_BONUS.1), transform: |value| value },
32        prismatic_bolt_clearcasting_chance: f64 { provenance: "Prismatic Bolt rank two Clearcasting chance.", gate: params.talent_picked(TALENT::PRISMATIC_BOLT_2), source: fraction(EFFECT::PRISMATIC_BOLT_CLEARCASTING_CHANCE.0, EFFECT::PRISMATIC_BOLT_CLEARCASTING_CHANCE.1), transform: |value| value },
33        prismatic_bolt_charges: f64 { provenance: "Prismatic Bolt 1295924 e3 Arcane Charges.", gate: params.talent_picked(TALENT::PRISMATIC_BOLT_1), source: params.game_data.effect_base_points(EFFECT::PRISMATIC_BOLT_CHARGES), transform: |value| value },
34        prismatic_bolt_reduced_targets: usize { provenance: "Prismatic Bolt 1295924 e4 reduced-AoE threshold.", gate: params.talent_picked(TALENT::PRISMATIC_BOLT_1), source: params.game_data.effect_base_points(EFFECT::PRISMATIC_BOLT_REDUCED_TARGETS), transform: wowlab_types::numeric::f64_to_usize_saturating_trunc },
35        arcane_pulse_charges_per_target: f64 { provenance: "Arcane Pulse charge grant per target from 1295977 e1.", gate: params.talent_picked(TALENT::ARCANE_PULSE), source: params.game_data.effect_base_points(EFFECT::ARCANE_PULSE_CHARGES_PER_TARGET), transform: |value| value },
36        arcane_pulse_reduced_targets: usize { provenance: "Arcane Pulse 1241462 e2 reduced-AoE target threshold.", gate: params.talent_picked(TALENT::ARCANE_PULSE), source: params.game_data.effect_base_points(EFFECT::ARCANE_PULSE_REDUCED_TARGETS), transform: wowlab_types::numeric::f64_to_usize_saturating_trunc },
37        arcane_familiar: bool { provenance: "Arcane Familiar enables its periodic assault.", gate: params.talent_picked_any(&[TALENT::ARCANE_FAMILIAR]), source: true, transform: |value| value },
38        arcane_familiar_extra_assaults: u32 { provenance: "Energized Familiar assault count beyond the baseline assault.", gate: true, source: selected_value(EFFECT::ENERGIZED_FAMILIAR_ASSAULTS.0, EFFECT::ENERGIZED_FAMILIAR_ASSAULTS.1), transform: |value: f64| wowlab_types::numeric::f64_to_u32_saturating_round(value.max(1.0)) - 1 },
39        aegwynns_technique: bool { provenance: "Aegwynn's Technique enables its familiar rider.", gate: params.talent_picked_any(&[TALENT::AEGWYNNS_TECHNIQUE]), source: true, transform: |value| value },
40        attuned_familiar_chance: f64 { provenance: "Attuned Familiar effect proc chance.", gate: params.talent_picked(TALENT::ATTUNED_FAMILIAR), source: fraction(EFFECT::ATTUNED_FAMILIAR_CHANCE.0, EFFECT::ATTUNED_FAMILIAR_CHANCE.1), transform: |value| value },
41        arcane_salvo: bool { provenance: "Either Arcane Salvo binding enables salvo stacking.", gate: params.talent_picked_any(&[TALENT::ARCANE_SALVO, TALENT::ARCANE_SALVO_DRIVER]), source: true, transform: |value| value },
42        augury_abounds: bool { provenance: "Augury Abounds enables accumulated splinter rolls.", gate: params.talent_picked_any(&[TALENT::AUGURY_ABOUNDS]), source: true, transform: |value| value },
43        charged_missiles_fraction: f64 { provenance: "Charged Missiles talent e1 damage fraction.", gate: true, source: selected_fraction(EFFECT::CHARGED_MISSILES_DAMAGE.0, EFFECT::CHARGED_MISSILES_DAMAGE.1), transform: |value| value },
44        clearcasting_prd_constant: f64 { provenance: "Clearcasting base and Illuminated Thoughts chance converted to capped PRD.", gate: true, source: clearcasting_rate, transform: |value| prd_constant_capped(value, CLEARCASTING_CONSTANT_CAP) },
45        expanded_mind: bool { provenance: "Either Expanded Mind binding enables extra Salvo stacks.", gate: params.talent_picked_any(&[TALENT::EXPANDED_MIND, TALENT::EXPANDED_MIND_DRIVER]), source: true, transform: |value| value },
46        force_of_will_salvo_per_splinter: i32 { provenance: "Force of Will barrage divisor interpreted as Salvo stacks per splinter.", gate: params.talent_picked(TALENT::FORCE_OF_WILL_DRIVER), source: params.game_data.effect_base_points(EFFECT::FORCE_OF_WILL_BARRAGE_DIVISOR), transform: wowlab_types::numeric::f64_to_i32_saturating_round },
47        enlightened_damage_fraction: f64 { provenance: "Enlightened damage effect fraction.", gate: true, source: fraction(EFFECT::ENLIGHTENED_DAMAGE.0, EFFECT::ENLIGHTENED_DAMAGE.1), transform: |value| value },
48        enlightened_regen_fraction: f64 { provenance: "Enlightened regeneration effect fraction.", gate: true, source: fraction(EFFECT::ENLIGHTENED_REGEN.0, EFFECT::ENLIGHTENED_REGEN.1), transform: |value| value },
49        energized_familiar_chance: f64 { provenance: "Energized Familiar effect proc chance.", gate: true, source: selected_fraction(EFFECT::ENERGIZED_FAMILIAR_CHANCE.0, EFFECT::ENERGIZED_FAMILIAR_CHANCE.1), transform: |value| value },
50        energized_familiar_gain_fraction: f64 { provenance: "Energized Familiar mana-gain effect fraction.", gate: params.talent_picked_any(&[TALENT::ENERGIZED_FAMILIAR]), source: fraction(EFFECT::ENERGIZED_FAMILIAR_GAIN.0, EFFECT::ENERGIZED_FAMILIAR_GAIN.1), transform: |value| value },
51        high_voltage: bool { provenance: "High Voltage enables charge generation from Missiles.", gate: params.talent_picked_any(&[TALENT::HIGH_VOLTAGE]), source: true, transform: |value| value },
52        infused_splinters: bool { provenance: "Infused Splinters enables Salvo generation.", gate: params.talent_picked_any(&[TALENT::INFUSED_SPLINTERS]), source: true, transform: |value| value },
53        mana_bomb: bool { provenance: "Either Mana Bomb binding enables Arcane Surge scaling.", gate: params.talent_picked_any(&[TALENT::MANA_BOMB, TALENT::MANA_BOMB_DRIVER]), source: true, transform: |value| value },
54        mana_adept_refund_per_charge: f64 { provenance: "Mana Adept effect refund fraction per Arcane Charge.", gate: params.talent_picked_any(&[TALENT::MANA_ADEPT]), source: fraction(EFFECT::MANA_ADEPT_BONUS.0, EFFECT::MANA_ADEPT_BONUS.1), transform: |value| value },
55        orb_barrage: bool { provenance: "Orb Barrage enables its Salvo-stack proc.", gate: params.talent_picked_any(&[TALENT::ORB_BARRAGE]), source: true, transform: |value| value },
56        orb_mastery_damage_fraction: f64 { provenance: "Orb Mastery effect damage fraction.", gate: params.talent_picked(TALENT::ORB_MASTERY_DRIVER), source: fraction(EFFECT::ORB_MASTERY_DAMAGE.0, EFFECT::ORB_MASTERY_DAMAGE.1), transform: |value| value },
57        orb_mastery_extra_orbs: u32 { provenance: "Orb Mastery effect extra-orb count.", gate: params.talent_picked(TALENT::ORB_MASTERY_DRIVER), source: params.game_data.effect_base_points(EFFECT::ORB_MASTERY_COUNT), transform: |value: f64| wowlab_types::numeric::f64_to_u32_saturating_round(value.max(0.0)) },
58        overpowered_missiles: bool { provenance: "Either Overpowered Missiles binding enables doubled ticks.", gate: params.talent_picked_any(&[TALENT::OVERPOWERED_MISSILES, TALENT::OVERPOWERED_MISSILES_DRIVER]), source: true, transform: |value| value },
59        arcane_charge_barrage_fraction: f64 { provenance: "Arcane Charge Barrage effect fraction per charge.", gate: true, source: fraction(EFFECT::ARCANE_CHARGE_BARRAGE.0, EFFECT::ARCANE_CHARGE_BARRAGE.1), transform: |value| value },
60        arcane_charge_blast_fraction: f64 { provenance: "Arcane Charge aura e1 Blast damage fraction.", gate: true, source: fraction(EFFECT::ARCANE_CHARGE_BLAST.0, EFFECT::ARCANE_CHARGE_BLAST.1), transform: |value| value },
61        arcane_salvo_fraction_per_stack: f64 { provenance: "Arcane Salvo aura e1 fraction per stack.", gate: true, source: fraction(EFFECT::ARCANE_SALVO_DAMAGE.0, EFFECT::ARCANE_SALVO_DAMAGE.1), transform: |value| value },
62        intuition_damage_mult: f64 { provenance: "Intuition aura e1 damage fraction converted to a multiplier.", gate: true, source: fraction(EFFECT::INTUITION_DAMAGE.0, EFFECT::INTUITION_DAMAGE.1), transform: |value| 1.0 + value },
63        spellfrost_teachings: bool { provenance: "Spellfrost Teachings enables Arcane Orb cooldown reduction.", gate: params.talent_picked_any(&[TALENT::SPELLFROST_TEACHINGS]), source: true, transform: |value| value },
64        splintering_orbs: bool { provenance: "Splintering Orbs enables extra orb splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_ORBS]), source: true, transform: |value| value },
65        splintering_sorcery: bool { provenance: "Splintering Sorcery enables cast splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_SORCERY]), source: true, transform: |value| value },
66        splinterstorm: bool { provenance: "Splinterstorm enables its splinter triggers.", gate: params.talent_picked_any(&[TALENT::SPLINTERSTORM]), source: true, transform: |value| value },
67        touch_of_the_archmage: bool { provenance: "Touch of the Archmage enables its Touch rider.", gate: params.talent_picked_any(&[TALENT::TOUCH_OF_THE_ARCHMAGE]), source: true, transform: |value| value },
68    }
69    accessor pub(super) fn arcane_config(ctx);
70    register pub(super) fn register_arcane(built, params) -> ();
71    prepare {
72        let fraction = |spell_id, effect| params.game_data.base_points(SpellIdx::from_raw(spell_id), effect) / HUNDRED;
73        let selected_fraction = |spell_id, effect| gated(params.talent_picked_any(&[spell_id]), || fraction(spell_id, effect));
74        let selected_value = |spell_id, effect| gated(params.talent_picked_any(&[spell_id]), || params.game_data.base_points(SpellIdx::from_raw(spell_id), effect));
75        let mid2_extra_waves = gated(params.set_bonus_auras.contains(&SET_BONUS::MID2_ARCANE_2PC), || params.game_data.base_points(SpellIdx::from_raw(EFFECT::MID2_ARCANE_2PC_EXTRA_WAVES.0), EFFECT::MID2_ARCANE_2PC_EXTRA_WAVES.1));
76        let clearcasting_rate = fraction(EFFECT::CLEARCASTING_PROC.0, EFFECT::CLEARCASTING_PROC.1) + selected_fraction(EFFECT::ILLUMINATED_THOUGHTS_CHANCE.0, EFFECT::ILLUMINATED_THOUGHTS_CHANCE.1) + CLEARCASTING_UNDOCUMENTED_BONUS;
77    }
78    auras {}
79    finish |cfg| {
80        for (local, source) in [
81            (AURA_ARCANE_CHARGE, AURA::ARCANE_CHARGE),
82            (AURA_ARCANE_SALVO, AURA::ARCANE_SALVO),
83            (AURA_INTUITION, AURA::INTUITION),
84        ] {
85            built.patch_aura(local, |aura| {
86                for effect in &mut aura.effects {
87                    if matches!(
88                        effect,
89                        Some(BuffEffect::SpellDamageFromEffect {
90                            source_spell_id,
91                            ..
92                        }) if *source_spell_id == source
93                    ) {
94                        *effect = None;
95                    }
96                }
97            });
98        }
99        built.set_spec_config(cfg);
100        built.set_spec_runtime(ArcaneRuntime::default());
101        built.set_aura_effect(AURA_ENLIGHTENED, BuffEffect::DynamicDamageMult(enlightened_damage_mult));
102        built.set_aura_effect(AURA_ENLIGHTENED, BuffEffect::DynamicRegenPercent(enlightened_regen_percent));
103    }
104}
105
106fn enlightened_damage_mult(
107    program: &CombatProgram,
108    instance: &CombatInstance,
109    _target: Option<wowlab_types::sim::EnemyIdx>,
110    _now: wowlab_types::sim::SimTime,
111) -> f64 {
112    let fraction = program
113        .spec_config::<ArcaneConfig>()
114        .map_or(0.0, |cfg| cfg.enlightened_damage_fraction);
115    let mana_pct =
116        wowlab_engine_combat::CombatView::new(program, instance).primary_resource_fraction();
117
118    enlightened_multiplier(mana_pct, fraction)
119}
120
121fn enlightened_regen_percent(program: &CombatProgram, instance: &CombatInstance) -> f64 {
122    let fraction = program
123        .spec_config::<ArcaneConfig>()
124        .map_or(0.0, |cfg| cfg.enlightened_regen_fraction);
125    let mana_pct =
126        wowlab_engine_combat::CombatView::new(program, instance).primary_resource_fraction();
127
128    (1.0 - mana_pct.clamp(0.0, 1.0)) * fraction * HUNDRED
129}
130
131fn enlightened_multiplier(mana_pct: f64, damage_fraction: f64) -> f64 {
132    1.0 + mana_pct.clamp(0.0, 1.0) * damage_fraction
133}
134
135const fn arcane_max_mana_multiplier(savant_fraction: f64, familiar_fraction: f64) -> f64 {
136    (1.0 + savant_fraction) * (1.0 + familiar_fraction)
137}
138
139pub(super) fn apply_arcane_max_mana(built: &mut CombatSystemDraft, params: &HandlerParams<'_>) {
140    let familiar_fraction = gated(params.talent_picked_any(&[TALENT::ARCANE_FAMILIAR]), || {
141        params
142            .game_data
143            .effect_base_points(EFFECT::ARCANE_FAMILIAR_BONUS)
144            / HUNDRED
145    });
146    let savant_fraction = params
147        .game_data
148        .resolved_effect_points(EFFECT::SAVANT_BASE, built.base_stats().stats.mastery)
149        / HUNDRED;
150    let multiplier = arcane_max_mana_multiplier(savant_fraction, familiar_fraction);
151
152    if multiplier <= 1.0 {
153        return;
154    }
155
156    let old_max = built.base_stats().resource_max;
157
158    built.patch_base_stats(|base| {
159        base.resource_max *= multiplier;
160        base.resource_start *= multiplier;
161        base.base_regen *= multiplier;
162    });
163    let max_delta = built.base_stats().resource_max - old_max;
164
165    built.patch_spells(|spell| {
166        spell.cost.resource_cost += spell.cost.resource_cost_pct.resolve(0.0, max_delta);
167    });
168}
169
170#[cfg(test)]
171mod tests {
172    use googletest::prelude::*;
173
174    use super::*;
175
176    #[gtest]
177    fn enlightened_scales_damage_from_live_mana_fraction() -> Result<()> {
178        verify_that!(enlightened_multiplier(0.75, 0.20), near(1.15, f64::EPSILON))
179    }
180
181    #[gtest]
182    fn savant_and_arcane_familiar_multiply_the_base_mana_pool() -> Result<()> {
183        let multiplier = arcane_max_mana_multiplier(33.13 * 0.0132, 0.10);
184        let expected = (1.0 + 33.13 * 0.0132) * 1.10;
185
186        verify_that!(multiplier, near(expected, 1e-12))
187    }
188}