Skip to main content

wowlab_engine_content/hooks/windwalker_monk/
config.rs

1use wowlab_types::constants::HUNDRED;
2
3use wowlab_engine_combat::BuffEffect;
4use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
5use wowlab_types::sim::SpellIdx;
6
7use crate::generated::specs::windwalker_monk::{
8    AURA_COMBO_STRIKES, EFFECT, REPORTED_SPELL, SPELL, SPELL_FISTS_OF_FURY, SPELL_RISING_SUN_KICK,
9    TALENT,
10};
11/// Two-hand adjustment for Dual Threat.
12const DUAL_THREAT_TWO_HAND_MULT: f64 = 3.6 / 2.6 * 2.0;
13
14static SUNFIRE_SPIRAL_SPELLS: [u32; 2] =
15    [SPELL::RISING_SUN_KICK, REPORTED_SPELL::GLORY_OF_THE_DAWN];
16#[derive(Clone, Copy, Debug, Default)]
17pub(super) struct WindwalkerRuntime {
18    pub(super) dual_threat_ready: bool,
19    pub(super) last_combo_spell: u32,
20}
21
22crate::hooks::define_spec_config! {
23pub(super) struct WindwalkerConfig {
24    dual_threat_chance: f64 { provenance: "Dual Threat 451823 e1 replacement chance.", gate: params.talent_picked(TALENT::DUAL_THREAT), source: base(EFFECT::DUAL_THREAT_CHANCE), transform: |value| value / HUNDRED },
25    dual_threat_damage_mult: f64 { provenance: "`SimC` Dual Threat two-hand adjustment; dual-wield uses 1.0.", gate: params.talent_picked(TALENT::DUAL_THREAT), source: params.game_data.off_hand().is_none(), transform: |two_hand| if two_hand { DUAL_THREAT_TWO_HAND_MULT } else { 1.0 }, fallback: 1.0 },
26    hit_combo: bool { provenance: "Hit Combo 196740 gates stacking the 196741 buff.", gate: params.talent_picked(TALENT::HIT_COMBO), source: true, transform: |value| value },
27    teachings: bool { provenance: "Teachings of the Monastery 116645 selection.", gate: params.talent_picked(TALENT::TEACHINGS_OF_THE_MONASTERY), source: true, transform: |value| value },
28    totm_reset_chance: f64 { provenance: "Teachings 116645 e1 Rising Sun Kick reset chance.", gate: true, source: base(EFFECT::TOTM_RESET), transform: |value| value / HUNDRED },
29    combo_breaker_chance: f64 { provenance: "Combo Breaker 137384 e1 proc chance.", gate: params.talent_picked(TALENT::COMBO_BREAKER), source: base(EFFECT::COMBO_BREAKER_CHANCE), transform: |value| value / HUNDRED },
30    glory_of_the_dawn: bool { provenance: "Glory of the Dawn 392958 selection.", gate: params.talent_picked(TALENT::GLORY_OF_THE_DAWN), source: true, transform: |value| value },
31    momentum_boost: bool { provenance: "Momentum Boost 451294 selection.", gate: params.talent_picked(TALENT::MOMENTUM_BOOST), source: true, transform: |value| value },
32    sequenced_strikes_chance: f64 { provenance: "Sequenced Strikes 451515 e1 proc chance.", gate: params.talent_picked(TALENT::SEQUENCED_STRIKES), source: base(EFFECT::SEQUENCED_CHANCE), transform: |value| value / HUNDRED },
33    flurry_strikes: bool { provenance: "Flurry Strikes 450615 Shado-Pan keystone.", gate: flurry_strikes, source: true, transform: |value| value },
34    flurry_charges_per_swing: i32 { provenance: "Flurry Strikes 450615 e2 two-hander charges per swing.", gate: true, source: base(EFFECT::FLURRY_CHARGES_2H), transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
35    flurry_crit_charge_mult: i32 { provenance: "One Versus Many 450988 e1 crit charge bonus.", gate: params.talent_picked(TALENT::ONE_VERSUS_MANY), source: 1 + wowlab_types::numeric::f64_to_i32_saturating_trunc(base(EFFECT::OVM_CHARGE_BONUS)), transform: |value| value, fallback: 1 },
36    flurry_coef: f64 { provenance: "Flurry Strike 450617 e1 AP coefficient.", gate: true, source: ap(EFFECT::FLURRY_DMG), transform: |value| value },
37    shado_coef: f64 { provenance: "Shado Over the Battlefield 451250 e1 AP coefficient.", gate: true, source: ap(EFFECT::SHADO_DMG), transform: |value| value },
38    wisdom_of_the_wall: bool { provenance: "Wisdom of the Wall 1272821 requires Flurry Strikes.", gate: flurry_strikes && params.talent_picked(TALENT::WISDOM_OF_THE_WALL), source: true, transform: |value| value },
39    wotw_count: i32 { provenance: "Wisdom of the Wall 1272821 e1 strike count.", gate: true, source: base(EFFECT::WOTW_COUNT), transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
40    stand_ready: bool { provenance: "Stand Ready 1262603 requires Flurry Strikes.", gate: flurry_strikes && params.talent_picked(TALENT::STAND_READY), source: true, transform: |value| value },
41    stand_ready_count: i32 { provenance: "Stand Ready 1262603 e1 strike count.", gate: true, source: base(EFFECT::STAND_READY_COUNT), transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
42    stand_ready_mult: f64 { provenance: "Stand Ready 1262603 e2 effectiveness.", gate: true, source: base(EFFECT::STAND_READY_MULT), transform: |value| value / HUNDRED },
43    jadefire_stomp: bool { provenance: "Jadefire Stomp 457974 selection; its damage rides Fists of Fury's final tick.", gate: params.talent_picked(TALENT::JADEFIRE_STOMP), source: true, transform: |value| value },
44    zenith_stomp: bool { provenance: "Zenith Stomp 1272694 selection.", gate: params.talent_picked(TALENT::ZENITH_STOMP), source: true, transform: |value| value },
45    combat_wisdom: bool { provenance: "Combat Wisdom 121817 selection.", gate: params.talent_picked(TALENT::COMBAT_WISDOM), source: true, transform: |value| value },
46    initial_chi: f64 { provenance: "Combat Wisdom 121817 e1 initial chi.", gate: true, source: base(EFFECT::COMBAT_WISDOM_CHI), transform: |value| value },
47}
48accessor pub(super) fn windwalker_config(ctx);
49register pub(super) fn register_windwalker(built, params) -> Result<WindwalkerConfig, wowlab_engine_ports::EngineError>;
50prepare {
51    let data = &params.game_data;
52    let base = |e: (u32, u8)| data.base_points(SpellIdx::from_raw(e.0), e.1);
53    let ap = |effect| data.effect_ap_coefficient(effect);
54
55    let flurry_strikes = params.talent_picked(TALENT::FLURRY_STRIKES);
56}
57auras {
58    { provenance: "Sunfire Spiral Combo Strikes damage multiplier.", gate: params.talent_picked(TALENT::SUNFIRE_SPIRAL), aura: AURA_COMBO_STRIKES, effect: BuffEffect::DamageMultSpells(1.0 + base(EFFECT::SUNFIRE_SPIRAL_PCT) / HUNDRED, &SUNFIRE_SPIRAL_SPELLS) },
59}
60wiring {
61    { provenance: "Dance of Chi-Ji uses its DBC RPPM driver and proc-category recovery.", gate: params.talent_picked(TALENT::DANCE_OF_CHI_JI), apply: {
62        let _ = built.register_system_rppm_from_data(TALENT::DANCE_OF_CHI_JI)?;
63    } },
64    { provenance: "Dual Threat conditionally replaces landed player swings before white damage.", gate: params.talent_picked(TALENT::DUAL_THREAT), apply: {
65        built.patch_auto_attacks(|attacks| for attack in attacks.iter_mut().filter(|attack| !attack.is_pet) {
66            attack.replacement_hook = Some(super::hooks::dual_threat_replacement);
67        });
68    } },
69    { provenance: "Brawler's Intensity Rising Sun Kick cooldown reduction.", gate: params.talent_picked(TALENT::BRAWLERS_INTENSITY), apply: {
70        let cdr_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(-base(EFFECT::BRAWLERS_RSK_CDR));
71        built.patch_spell(SPELL_RISING_SUN_KICK, |spell| {
72            spell.cooldown.cooldown_duration_ms = spell.cooldown.cooldown_duration_ms.saturating_sub(cdr_ms);
73        });
74    } },
75}
76finish |cfg| {
77    // Fists of Fury's parent deals no damage; only its ticks may use the DBC damage.
78    built.patch_spell(SPELL_FISTS_OF_FURY, |spell| {
79        spell.damage = wowlab_engine_combat::RuntimeDamageDef::None;
80        spell.channel.tick_damage = wowlab_engine_combat::RuntimeDamageDef::None;
81    });
82
83    built.state.set_spec_config(cfg);
84    built.state.set_spec_runtime(WindwalkerRuntime::default());
85    Ok(cfg)
86}
87}
88
89#[cfg(test)]
90mod tests {
91    use googletest::prelude::*;
92    use wowlab_engine_combat::AuraOps as _;
93    use wowlab_engine_combat::HookCtx;
94    use wowlab_engine_domain::dbc::{AuraSubtypeKind, ModifierOperation, ModifierPropertyKind};
95    use wowlab_engine_ports::{CombatStats, ConsumableFlags};
96    use wowlab_engine_telemetry::{TelemetrySink};
97use wowlab_engine_gamedata::{AuraProps, ResolvedGameData, SpellProps};
98    use wowlab_types::{
99        combat::ResourceType,
100        game::RaceId,
101        sim::{SimTime, SpellIdx},
102    };
103
104    use super::*;
105    use crate::generated::specs::windwalker_monk::AURA;
106
107    const UNUSUAL_FLAT_COST: f64 = -37.0;
108    const BASE_COST: f64 = 100.0;
109
110    #[gtest]
111    fn zenith_cost_uses_one_dbc_effect_and_the_production_affected_spell_mask() -> Result<()> {
112        let encounter = wowlab_engine_ports::test_support::introspection_fixture(60.0).or_fail()?;
113        let environment = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
114        let zenith = SpellIdx::from_raw(AURA::ZENITH);
115        let flying_serpent_kick = SpellIdx::from_raw(SPELL::FLYING_SERPENT_KICK);
116        let tiger_palm = SpellIdx::from_raw(SPELL::TIGER_PALM);
117        let mut data = ResolvedGameData::builder();
118
119        data.set_armor_constant(environment.armor_constant());
120        data.set_creature_armor(environment.creature_armor());
121        data.set_armor_constant_mod(environment.armor_constant_mod());
122        data.insert_aura_props(
123            zenith,
124            AuraProps {
125                duration_ms: 15_000,
126                ..AuraProps::default()
127            },
128        );
129        data.insert_spell_props(zenith, SpellProps::default());
130        data.insert_effect_aura(
131            wowlab_types::sim::EffectRef::new(zenith, 7),
132            AuraSubtypeKind::AddFlatModifier as i32,
133        );
134        data.insert_effect_misc_value_0(
135            wowlab_types::sim::EffectRef::new(zenith, 7),
136            ModifierPropertyKind::ResourceCost1 as i32,
137        );
138        data.insert_base_points(zenith, 7, UNUSUAL_FLAT_COST);
139        data.insert_effect_class_mask(zenith, 7, [1, 0, 0, 0]);
140        data.insert_spell_class_flags(zenith, 1, [0, 0, 0, 0]);
141        data.insert_spell_class_flags(flying_serpent_kick, 1, [1, 0, 0, 0]);
142        data.insert_spell_class_flags(tiger_palm, 1, [2, 0, 0, 0]);
143
144        for spell in [flying_serpent_kick, tiger_palm] {
145            data.insert_spell_props(
146                spell,
147                SpellProps {
148                    secondary_cost_entry: 1,
149                    ..SpellProps::default()
150                },
151            );
152        }
153
154        let data = data.build();
155        let stats = CombatStats::default();
156        let rotation = crate::test_support::wait_rotation();
157        let params =
158            crate::test_support::handler_params(crate::test_support::HandlerParamsFixture {
159                game_data: data.clone(),
160                rotation: &rotation,
161                stats: &stats,
162                talent_selections: &[],
163                encounter: &encounter,
164                consumables: ConsumableFlags::default(),
165                race: RaceId::Human,
166            })
167            .or_fail()?;
168        let mut built = crate::test_combat_builder(CombatStats::default())
169            .game_data(data.clone())
170            .secondary_resource("chi", BASE_COST)
171            .secondary_resource_type_id(ResourceType::Chi.into())
172            .aura(
173                "whirling_dragon_punch",
174                AURA::WHIRLING_DRAGON_PUNCH,
175                |aura| Ok(aura.duration_ms(1_000)),
176            )
177            .aura("blackout_kick", AURA::BLACKOUT_KICK, |aura| {
178                Ok(aura.duration_ms(1_000))
179            })
180            .aura("zenith", AURA::ZENITH, |aura| {
181                aura.apply_base_from_data(&data, AURA::ZENITH)
182            })
183            .spell("flying_serpent_kick", SPELL::FLYING_SERPENT_KICK, |spell| {
184                Ok(spell.secondary_cost(BASE_COST))
185            })
186            .spell("tiger_palm", SPELL::TIGER_PALM, |spell| {
187                Ok(spell.secondary_cost(BASE_COST))
188            })
189            .build(crate::test_support::wait_rotation())
190            .or_fail()?;
191
192        register_windwalker(&mut built, &params)?;
193
194        let local = built.state.aura_local(AURA::ZENITH).or_fail()?;
195        let effects: Vec<_> = built
196            .aura(local)
197            .or_fail()?
198            .effects
199            .iter()
200            .flatten()
201            .collect();
202
203        verify_that!(effects.len(), eq(1))?;
204        verify_true!(matches!(
205            effects[0],
206            BuffEffect::SpellCostFromEffect {
207                value: UNUSUAL_FLAT_COST,
208                source_spell_id: AURA::ZENITH,
209                effect_index: 7,
210                cost_entry: 1,
211                operation: ModifierOperation::Flat,
212            }
213        ))?;
214        let mut sink = TelemetrySink::default();
215        let mut rng = || 0.5;
216
217        HookCtx::new(
218            wowlab_engine_combat::HookCtxServices {
219                state: &mut built.state,
220                buf: &mut built.buffer,
221                sink: &mut sink,
222                rng: &mut rng,
223            },
224            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
225        )
226        .apply_aura(local.raw());
227
228        let flying_serpent_kick_cost = built
229            .buffer
230            .spell(flying_serpent_kick)
231            .or_fail()?
232            .secondary_cost;
233        let tiger_palm_cost = built.buffer.spell(tiger_palm).or_fail()?.secondary_cost;
234
235        verify_that!(
236            flying_serpent_kick_cost,
237            near(BASE_COST + UNUSUAL_FLAT_COST, f64::EPSILON)
238        )?;
239
240        verify_that!(tiger_palm_cost, near(BASE_COST, f64::EPSILON))
241    }
242}