Skip to main content

wowlab_engine_content/hooks/arcane_mage/
config.rs

1use wowlab_engine_combat::{BuffEffect, BuiltCombatSystem, CombatState, LocalSpellIdx};
2use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
3use wowlab_engine_domain::rotation::DenseBuffer;
4use wowlab_engine_ports::HandlerParams;
5use wowlab_engine_rng::prd_constant_capped;
6use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
7
8use crate::{
9    generated::specs::arcane_mage::{
10        AURA, AURA_ARCANE_CHARGE, AURA_ARCANE_SALVO, AURA_ENLIGHTENED, AURA_INTUITION, EFFECT,
11        TALENT,
12    },
13    hooks::gated,
14};
15
16use super::hooks::{
17    CLEARCASTING_CONSTANT_CAP, CLEARCASTING_UNDOCUMENTED_BONUS, arcane_missiles_ticks,
18};
19
20#[derive(Clone, Copy, Debug, Default)]
21pub(super) struct ArcaneRuntime {
22    pub(super) augury_attempts: u32,
23    pub(super) augury_icd_until_ms: u32,
24    pub(super) clearcasting_attempts: u32,
25    pub(super) touch_of_the_magi_start_damage: f64,
26}
27
28crate::hooks::define_spec_config! {
29    pub(super) struct ArcaneConfig {
30        arcane_missiles_ticks: u32 { provenance: "Arcane Missiles base ticks plus Amplification Waves.", gate: true, source: selected_value(EFFECT::AMPLIFICATION_WAVES.0, EFFECT::AMPLIFICATION_WAVES.1), transform: arcane_missiles_ticks },
31        arcane_familiar: bool { provenance: "Arcane Familiar enables its periodic assault.", gate: params.talent_picked_any(&[TALENT::ARCANE_FAMILIAR]), source: true, transform: |value| value },
32        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 },
33        aegwynns_technique: bool { provenance: "Aegwynn's Technique enables its familiar rider.", gate: params.talent_picked_any(&[TALENT::AEGWYNNS_TECHNIQUE]), source: true, transform: |value| value },
34        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 },
35        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 },
36        augury_abounds: bool { provenance: "Augury Abounds enables accumulated splinter rolls.", gate: params.talent_picked_any(&[TALENT::AUGURY_ABOUNDS]), source: true, transform: |value| value },
37        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 },
38        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) },
39        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 },
40        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 },
41        enlightened_damage_fraction: f64 { provenance: "Enlightened damage effect fraction.", gate: true, source: fraction(EFFECT::ENLIGHTENED_DAMAGE.0, EFFECT::ENLIGHTENED_DAMAGE.1), transform: |value| value },
42        enlightened_regen_fraction: f64 { provenance: "Enlightened regeneration effect fraction.", gate: true, source: fraction(EFFECT::ENLIGHTENED_REGEN.0, EFFECT::ENLIGHTENED_REGEN.1), transform: |value| value },
43        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 },
44        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 },
45        high_voltage: bool { provenance: "High Voltage enables charge generation from Missiles.", gate: params.talent_picked_any(&[TALENT::HIGH_VOLTAGE]), source: true, transform: |value| value },
46        infused_splinters: bool { provenance: "Infused Splinters enables Salvo generation.", gate: params.talent_picked_any(&[TALENT::INFUSED_SPLINTERS]), source: true, transform: |value| value },
47        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 },
48        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 },
49        orb_barrage: bool { provenance: "Orb Barrage enables its Salvo-stack proc.", gate: params.talent_picked_any(&[TALENT::ORB_BARRAGE]), source: true, transform: |value| value },
50        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 },
51        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)) },
52        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 },
53        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 },
54        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 },
55        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 },
56        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 },
57        spellfrost_teachings: bool { provenance: "Spellfrost Teachings enables Arcane Orb cooldown reduction.", gate: params.talent_picked_any(&[TALENT::SPELLFROST_TEACHINGS]), source: true, transform: |value| value },
58        splintering_orbs: bool { provenance: "Splintering Orbs enables extra orb splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_ORBS]), source: true, transform: |value| value },
59        splintering_sorcery: bool { provenance: "Splintering Sorcery enables cast splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_SORCERY]), source: true, transform: |value| value },
60        splinterstorm: bool { provenance: "Splinterstorm enables its splinter triggers.", gate: params.talent_picked_any(&[TALENT::SPLINTERSTORM]), source: true, transform: |value| value },
61        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 },
62    }
63    accessor pub(super) fn arcane_config(ctx);
64    register pub(super) fn register_arcane(built, params) -> ();
65    prepare {
66        let fraction = |spell_id, effect| params.game_data.base_points(SpellIdx::from_raw(spell_id), effect) / HUNDRED;
67        let selected_fraction = |spell_id, effect| gated(params.talent_picked_any(&[spell_id]), || fraction(spell_id, effect));
68        let selected_value = |spell_id, effect| gated(params.talent_picked_any(&[spell_id]), || params.game_data.base_points(SpellIdx::from_raw(spell_id), effect));
69        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;
70    }
71    auras {}
72    finish |cfg| {
73        for (local, source) in [
74            (AURA_ARCANE_CHARGE, AURA::ARCANE_CHARGE),
75            (AURA_ARCANE_SALVO, AURA::ARCANE_SALVO),
76            (AURA_INTUITION, AURA::INTUITION),
77        ] {
78            built.patch_aura(local, |aura| {
79                for effect in &mut aura.effects {
80                    if matches!(
81                        effect,
82                        Some(BuffEffect::SpellDamageFromEffect {
83                            source_spell_id,
84                            ..
85                        }) if *source_spell_id == source
86                    ) {
87                        *effect = None;
88                    }
89                }
90            });
91        }
92        built.state.set_spec_config(cfg);
93        built.state.set_spec_runtime(ArcaneRuntime::default());
94        built.set_aura_effect(AURA_ENLIGHTENED, BuffEffect::DynamicDamageMult(enlightened_damage_mult));
95        built.set_aura_effect(AURA_ENLIGHTENED, BuffEffect::DynamicRegenPercent(enlightened_regen_percent));
96    }
97}
98
99fn enlightened_damage_mult(
100    state: &CombatState,
101    buf: &DenseBuffer,
102    _target: Option<wowlab_types::sim::EnemyIdx>,
103) -> f64 {
104    let fraction = state
105        .spec_config::<ArcaneConfig>()
106        .map_or(0.0, |cfg| cfg.enlightened_damage_fraction);
107    let mana_pct = buf
108        .resource(wowlab_types::combat::ResourceType::Mana)
109        .map_or(1.0, |resource| {
110            if resource.max > 0.0 {
111                resource.current / resource.max
112            } else {
113                1.0
114            }
115        });
116
117    enlightened_multiplier(mana_pct, fraction)
118}
119
120fn enlightened_regen_percent(state: &CombatState, buf: &DenseBuffer) -> f64 {
121    let fraction = state
122        .spec_config::<ArcaneConfig>()
123        .map_or(0.0, |cfg| cfg.enlightened_regen_fraction);
124    let mana_pct = buf
125        .resource(wowlab_types::combat::ResourceType::Mana)
126        .map_or(1.0, |resource| {
127            if resource.max > 0.0 {
128                resource.current / resource.max
129            } else {
130                1.0
131            }
132        });
133
134    (1.0 - mana_pct.clamp(0.0, 1.0)) * fraction * HUNDRED
135}
136
137fn enlightened_multiplier(mana_pct: f64, damage_fraction: f64) -> f64 {
138    1.0 + mana_pct.clamp(0.0, 1.0) * damage_fraction
139}
140
141const fn arcane_max_mana_multiplier(savant_fraction: f64, familiar_fraction: f64) -> f64 {
142    (1.0 + savant_fraction) * (1.0 + familiar_fraction)
143}
144
145pub(super) fn apply_arcane_max_mana(built: &mut BuiltCombatSystem, params: &HandlerParams<'_>) {
146    let familiar_fraction = gated(params.talent_picked_any(&[TALENT::ARCANE_FAMILIAR]), || {
147        params
148            .game_data
149            .effect_base_points(EFFECT::ARCANE_FAMILIAR_BONUS)
150            / HUNDRED
151    });
152    let savant_fraction = params.game_data.resolved_effect_points(
153        EFFECT::SAVANT_BASE,
154        built.state.config.base_stats.stats.mastery,
155    ) / HUNDRED;
156    let multiplier = arcane_max_mana_multiplier(savant_fraction, familiar_fraction);
157
158    if multiplier <= 1.0 {
159        return;
160    }
161
162    let old_max = built.state.config.base_stats.resource_max;
163
164    built.patch_base_stats(|base| {
165        base.resource_max *= multiplier;
166        base.resource_start *= multiplier;
167        base.base_regen *= multiplier;
168    });
169    let max_delta = built.state.config.base_stats.resource_max - old_max;
170    let spell_count = built.spell_count();
171
172    for index in 0..spell_count {
173        let local =
174            LocalSpellIdx::new(u8::try_from(index).expect("spell count validated below 256"));
175
176        built.patch_spell(local, |spell| {
177            spell.cost.resource_cost += spell.cost.resource_cost_pct.resolve(0.0, max_delta);
178        });
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use googletest::prelude::*;
185
186    use super::*;
187
188    #[gtest]
189    fn enlightened_scales_damage_from_live_mana_fraction() -> Result<()> {
190        verify_that!(enlightened_multiplier(0.75, 0.20), near(1.15, f64::EPSILON))
191    }
192
193    #[gtest]
194    fn savant_and_arcane_familiar_multiply_the_base_mana_pool() -> Result<()> {
195        let multiplier = arcane_max_mana_multiplier(33.13 * 0.0132, 0.10);
196        let expected = (1.0 + 33.13 * 0.0132) * 1.10;
197
198        verify_that!(multiplier, near(expected, 1e-12))
199    }
200}