Skip to main content

wowlab_engine_content/hooks/windwalker_monk/
config.rs

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