Skip to main content

wowlab_engine_content/hooks/frost_mage/
config.rs

1use wowlab_engine_combat::{ImpactProc, PeriodicDriver};
2use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
3use wowlab_types::constants::HUNDRED;
4#[cfg(test)]
5use wowlab_types::sim::SpellIdx;
6
7use crate::generated::specs::frost_mage::{EFFECT, SET_BONUS, TALENT};
8
9use super::hooks::{
10    HAND_OF_FROST_STACK_CHANCE_SCALE, HEART_OF_ICE_EXTRA_CONSUME, ICE_LANCE_BASE_CONSUME,
11    POLISHED_FOCUS_EXTRA_CONSUME, frostbite_impact, frostbolt_impact_filter, icicles_hook,
12};
13
14#[derive(Clone, Copy, Debug, Default)]
15pub(super) struct FrostRuntime {
16    pub(super) augury_attempts: u32,
17    pub(super) augury_icd_until_ms: u32,
18    pub(super) rof_ticks_fired: u32,
19}
20
21crate::hooks::define_spec_config! {
22    pub(super) struct FrostConfig {
23        augury_abounds: bool { provenance: "Augury Abounds enables accumulated splinter rolls.", gate: params.talent_picked_any(&[TALENT::AUGURY_ABOUNDS]), source: true, transform: |value| value },
24        brain_freeze_chance: f64 { provenance: "Brain Freeze 190447 e1 resolved proc chance.", gate: params.talent_picked(TALENT::BRAIN_FREEZE), source: params.game_data.effect_base_points(EFFECT::BRAIN_FREEZE_CHANCE), transform: |value| value / HUNDRED },
25        deep_shatter_crit_factor: f64 { provenance: "Deep Shatter effect bonus as a fraction.", gate: params.talent_picked(TALENT::DEEP_SHATTER), source: params.game_data.effect_base_points(EFFECT::DEEP_SHATTER_BONUS), transform: |value| value / HUNDRED },
26        everlasting_frost: bool { provenance: "Everlasting Frost enables its Ice Lance rider.", gate: params.talent_picked_any(&[TALENT::EVERLASTING_FROST]), source: true, transform: |value| value },
27        flash_freeze_fof_chance: f64 { provenance: "Flash Freeze 379993 e1 resolved Glacial Spike Fingers of Frost chance.", gate: params.talent_picked(TALENT::FLASH_FREEZE), source: params.game_data.effect_base_points(EFFECT::FLASH_FREEZE_FOF_CHANCE), transform: |value| value / HUNDRED },
28        force_of_will: bool { provenance: "Force of Will enables its splinter rider.", gate: params.talent_picked_any(&[TALENT::FORCE_OF_WILL]), source: true, transform: |value| value },
29        fingers_of_frost_chance: f64 { provenance: "Fingers of Frost 112965 e1 resolved proc chance.", gate: params.talent_picked(TALENT::FINGERS_OF_FROST), source: params.game_data.effect_base_points(EFFECT::FINGERS_OF_FROST_CHANCE), transform: |value| value / HUNDRED },
30        glacial_assault: bool { provenance: "Glacial Assault enables its impact rider.", gate: params.talent_picked_any(&[TALENT::GLACIAL_ASSAULT]), source: true, transform: |value| value },
31        glaciate: bool { provenance: "Glaciate enables its damage conversion.", gate: params.talent_picked_any(&[TALENT::GLACIATE]), source: true, transform: |value| value },
32        hand_of_frost: bool { provenance: "Hand of Frost rank one enables accumulated rolls.", gate: params.talent_picked_any(&[TALENT::HAND_OF_FROST_1]), source: true, transform: |value| value },
33        hand_of_frost_chance_per_stack: f64 { provenance: "Hand of Frost effect chance scaled per stack.", gate: true, source: params.game_data.effect_base_points(EFFECT::HAND_OF_FROST_CHANCE), transform: |value| value * HAND_OF_FROST_STACK_CHANCE_SCALE / HUNDRED },
34        improved_flurry: bool { provenance: "Improved Flurry enables its freezing application.", gate: params.talent_picked_any(&[TALENT::IMPROVED_FLURRY]), source: true, transform: |value| value },
35        infused_splinters: bool { provenance: "Infused Splinters enables freezing from splinters.", gate: params.talent_picked_any(&[TALENT::INFUSED_SPLINTERS]), source: true, transform: |value| value },
36        icicles_period_ms: u32 { provenance: "Icicles passive periodic stack cadence.", gate: true, source: params.game_data.effect_lookup(EFFECT::ICICLES_PERIOD).period(), transform: wowlab_types::numeric::f64_to_u32_saturating_round },
37        lance_consume: i32 { provenance: "Shatter base Ice Lance consumption plus Heart of Ice and Polished Focus.", gate: true, source: ICE_LANCE_BASE_CONSUME + i32::from(params.talent_picked_any(&[TALENT::HEART_OF_ICE])) * HEART_OF_ICE_EXTRA_CONSUME + i32::from(params.talent_picked_any(&[TALENT::POLISHED_FOCUS])) * POLISHED_FOCUS_EXTRA_CONSUME, transform: |value| value },
38        permafrost_lances: bool { provenance: "Permafrost Lances enables its lance rider.", gate: params.talent_picked_any(&[TALENT::PERMAFROST_LANCES]), source: true, transform: |value| value },
39        signature_spell: bool { provenance: "Signature Spell enables its splinter trigger.", gate: params.talent_picked_any(&[TALENT::SIGNATURE_SPELL]), source: true, transform: |value| value },
40        set2_fof_chance: f64 { provenance: "MID1 Frost 2pc Fingers of Frost chance.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID1_FROST_2PC), source: params.game_data.effect_base_points(EFFECT::MID1_FROST_2PC_FOF_CHANCE), transform: |value| value / HUNDRED },
41        set4_fof_shatter_mult: f64 { provenance: "MID1 Frost 4pc Fingers of Frost Shatter multiplier.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID1_FROST_4PC), source: params.game_data.effect_base_points(EFFECT::MID1_FROST_4PC_FOF_SHATTER), transform: |value| value / HUNDRED },
42        spellfrost_teachings: bool { provenance: "Spellfrost Teachings enables Arcane Orb CDR.", gate: params.talent_picked_any(&[TALENT::SPELLFROST_TEACHINGS]), source: true, transform: |value| value },
43        splintering_orbs: bool { provenance: "Splintering Orbs enables additional splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_ORBS]), source: true, transform: |value| value },
44        splintering_sorcery: bool { provenance: "Splintering Sorcery enables cast splinters.", gate: params.talent_picked_any(&[TALENT::SPLINTERING_SORCERY]), source: true, transform: |value| value },
45        splinterstorm: bool { provenance: "Splinterstorm enables its splinter triggers.", gate: params.talent_picked_any(&[TALENT::SPLINTERSTORM]), source: true, transform: |value| value },
46        thermal_void: bool { provenance: "Either Thermal Void binding enables extension behavior.", gate: params.talent_picked_any(&[TALENT::THERMAL_VOID, TALENT::THERMAL_VOID_TRAIT]), source: true, transform: |value| value },
47        wintertide: bool { provenance: "Either Wintertide binding enables its lance rider.", gate: params.talent_picked_any(&[TALENT::WINTERTIDE, TALENT::WINTERTIDE_TRAIT]), source: true, transform: |value| value },
48    }
49    accessor pub(super) fn frost_config(ctx);
50    register pub(super) fn register_frost(built, params) -> Result<(), wowlab_engine_ports::EngineError>;
51    prepare {}
52    auras {}
53    wiring {
54        { provenance: "Frostbite critical Frostbolt impact proc.", gate: params.talent_picked_any(&[TALENT::FROSTBITE]), apply: {
55            built.register_impact_proc(ImpactProc::new(frostbite_impact).with_spell_filter(frostbolt_impact_filter).skip_periodic().crit_only());
56        } },
57    }
58    finish |cfg| {
59        if let Some(icicles_period) = std::num::NonZeroU32::new(cfg.icicles_period_ms) {
60            built.register_periodic_driver(
61                PeriodicDriver::new(TALENT::ICICLES, icicles_period.get(), icicles_hook).hasted(),
62            );
63        } else if !params.game_data.is_empty() {
64            return Err(wowlab_engine_ports::EngineError::spec_construction(
65                "Icicles periodic driver resolved a zero-millisecond interval",
66            ));
67        }
68        built.state.set_spec_config(cfg);
69        built.state.set_spec_runtime(FrostRuntime::default());
70        Ok(())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use googletest::prelude::*;
77    use wowlab_engine_ports::{CombatStats, ConsumableFlags, TalentSelection};
78use wowlab_engine_gamedata::{ResolvedGameData};
79    use wowlab_types::game::RaceId;
80
81    use super::*;
82
83    const UNUSUAL_FINGERS_CHANCE_PERCENT: f64 = 37.0;
84    const UNUSUAL_BRAIN_FREEZE_CHANCE_PERCENT: f64 = 61.0;
85    const UNUSUAL_FLASH_FREEZE_CHANCE_PERCENT: f64 = 73.0;
86
87    fn selection(spell_id: u32) -> TalentSelection {
88        TalentSelection {
89            spell_id,
90            override_spell_id: 0,
91            replaces_spell_id: 0,
92            ranks: 1,
93            precombat_aura: false,
94            hero_tree: None,
95            effect_overrides: Vec::new(),
96        }
97    }
98
99    fn registered_config(
100        fingers_selected: bool,
101        brain_freeze_selected: bool,
102        flash_freeze_selected: bool,
103    ) -> Result<FrostConfig> {
104        let encounter = wowlab_engine_ports::test_support::introspection_fixture(60.0).or_fail()?;
105        let environment = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
106        let mut data = ResolvedGameData::builder();
107
108        data.set_armor_constant(environment.armor_constant());
109        data.set_creature_armor(environment.creature_armor());
110        data.set_armor_constant_mod(environment.armor_constant_mod());
111        data.insert_base_points(
112            SpellIdx::from_raw(EFFECT::FINGERS_OF_FROST_CHANCE.0),
113            EFFECT::FINGERS_OF_FROST_CHANCE.1,
114            UNUSUAL_FINGERS_CHANCE_PERCENT,
115        );
116        data.insert_base_points(
117            SpellIdx::from_raw(EFFECT::BRAIN_FREEZE_CHANCE.0),
118            EFFECT::BRAIN_FREEZE_CHANCE.1,
119            UNUSUAL_BRAIN_FREEZE_CHANCE_PERCENT,
120        );
121        data.insert_base_points(
122            SpellIdx::from_raw(EFFECT::FLASH_FREEZE_FOF_CHANCE.0),
123            EFFECT::FLASH_FREEZE_FOF_CHANCE.1,
124            UNUSUAL_FLASH_FREEZE_CHANCE_PERCENT,
125        );
126        data.insert_period(
127            SpellIdx::from_raw(EFFECT::ICICLES_PERIOD.0),
128            EFFECT::ICICLES_PERIOD.1,
129            1_000.0,
130        );
131        let data = data.build();
132        let stats = CombatStats::default();
133        let selected = [
134            fingers_selected.then(|| selection(TALENT::FINGERS_OF_FROST)),
135            brain_freeze_selected.then(|| selection(TALENT::BRAIN_FREEZE)),
136            flash_freeze_selected.then(|| selection(TALENT::FLASH_FREEZE)),
137        ]
138        .into_iter()
139        .flatten()
140        .collect::<Vec<_>>();
141        let rotation = crate::test_support::wait_rotation();
142        let params =
143            crate::test_support::handler_params(crate::test_support::HandlerParamsFixture {
144                game_data: data.clone(),
145                rotation: &rotation,
146                stats: &stats,
147                talent_selections: &selected,
148                encounter: &encounter,
149                consumables: ConsumableFlags::default(),
150                race: RaceId::Human,
151            })
152            .or_fail()?;
153        let mut built = crate::test_combat_builder(CombatStats::default())
154            .game_data(data)
155            .build(crate::test_support::wait_rotation())
156            .or_fail()?;
157
158        register_frost(&mut built, &params)?;
159
160        built.state.spec_config::<FrostConfig>().copied().or_fail()
161    }
162
163    #[gtest]
164    fn proc_chances_use_unusual_resolved_effect_values() -> Result<()> {
165        let config = registered_config(true, true, true)?;
166
167        verify_that!(
168            config.fingers_of_frost_chance,
169            near(UNUSUAL_FINGERS_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
170        )?;
171
172        verify_that!(
173            config.brain_freeze_chance,
174            near(UNUSUAL_BRAIN_FREEZE_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
175        )?;
176
177        verify_that!(
178            config.flash_freeze_fof_chance,
179            near(UNUSUAL_FLASH_FREEZE_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
180        )
181    }
182
183    #[gtest]
184    fn proc_chances_are_independently_talent_gated() -> Result<()> {
185        let fingers_only = registered_config(true, false, false)?;
186        let brain_freeze_only = registered_config(false, true, false)?;
187        let flash_freeze_only = registered_config(false, false, true)?;
188
189        verify_that!(
190            fingers_only.fingers_of_frost_chance,
191            near(UNUSUAL_FINGERS_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
192        )?;
193        verify_that!(fingers_only.brain_freeze_chance, eq(0.0))?;
194        verify_that!(fingers_only.flash_freeze_fof_chance, eq(0.0))?;
195        verify_that!(brain_freeze_only.fingers_of_frost_chance, eq(0.0))?;
196        verify_that!(brain_freeze_only.flash_freeze_fof_chance, eq(0.0))?;
197        verify_that!(flash_freeze_only.fingers_of_frost_chance, eq(0.0))?;
198        verify_that!(flash_freeze_only.brain_freeze_chance, eq(0.0))?;
199
200        verify_that!(
201            brain_freeze_only.brain_freeze_chance,
202            near(UNUSUAL_BRAIN_FREEZE_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
203        )?;
204
205        verify_that!(
206            flash_freeze_only.flash_freeze_fof_chance,
207            near(UNUSUAL_FLASH_FREEZE_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
208        )
209    }
210}