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}