Skip to main content

wowlab_engine_content/hooks/arms_warrior/
config.rs

1use wowlab_engine_combat::{BuffEffect, PassiveDriverEffect, PeriodicDriver};
2use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
3use wowlab_types::{
4    constants::{HUNDRED, MS_PER_SECOND},
5    sim::SpellIdx,
6};
7
8use crate::generated::specs::arms_warrior::{
9    AURA_BATTLE_STANCE, EFFECT, SET_BONUS, SPELL, SPELL_OVERPOWER, TALENT,
10};
11use crate::hooks::gated_or;
12use crate::hooks::shared::warrior::{
13    EXECUTE_BASE_PCT, FROTHING_CHANCE, IMPROVED_ATTACK_CHARGES, PRD_C_15,
14};
15
16const BASE_RAGE_PER_SWING_SECOND: f64 = 1.75;
17
18/// Execute `SpellPower` 163201: cost 200, optional cost 200.
19const EXECUTE_MAX_RAGE: f64 = 40.0;
20
21static MID2_2PC_DAMAGE_SPELLS: [u32; 2] = [SPELL::MORTAL_STRIKE, SPELL::EXECUTE_DAMAGE];
22static MID2_4PC_DAMAGE_SPELLS: [u32; 1] = [SPELL::OVERPOWER];
23
24crate::hooks::define_spec_config! {
25/// Talent-gated Arms hook parameters.
26pub(crate) struct ArmsConfig {
27    crit_swing_extra_rage: f64 { provenance: "Seasoned Soldier 279423 e1 extra rage on crit swings.", gate: true, source: crit_swing_extra_rage, transform: |value| value },
28    execute_threshold: f64 { provenance: "Massacre 281001 e2 execute threshold, otherwise the 20% baseline.", gate: true, source: gated_or(params.talent_picked(TALENT::MASSACRE), EXECUTE_BASE_PCT / HUNDRED, || base(EFFECT::MASSACRE_PCT) / HUNDRED), transform: |value| value },
29    execute_coef: f64 { provenance: "Execute damage 260798 e1 AP coefficient.", gate: true, source: ap(EFFECT::EXECUTE_DMG), transform: |value| value },
30    execute_refund_pct: f64 { provenance: "Execute 163201 e2 plus rank-scaled Critical Thinking 389306 e2.", gate: params.talent_picked(TALENT::IMPROVED_EXECUTE), source: base(EFFECT::EXECUTE_REFUND) / HUNDRED + base(EFFECT::CRITICAL_THINKING_REFUND) * f64::from(params.talent_ranks(TALENT::CRITICAL_THINKING)) / HUNDRED, transform: |value| value },
31    deep_wounds: bool { provenance: "Deep Wounds 1261060 applies the shared execute-impact `DoT`.", gate: params.talent_picked(TALENT::DEEP_WOUNDS), source: true, transform: |value| value },
32    mortal_wounds: bool { provenance: "Mortal Wounds 1261062 applies Deep Wounds from qualifying crits.", gate: params.talent_picked(TALENT::MORTAL_WOUNDS), source: true, transform: |value| value },
33    fatality_chance: f64 { provenance: "Fatality 383703 mark chance.", gate: params.talent_picked(TALENT::FATALITY), source: base(EFFECT::FATALITY_CHANCE), transform: |value| value / HUNDRED },
34    fatality_coef: f64 { provenance: "Fatality burst 383706 e1 AP coefficient.", gate: true, source: ap(EFFECT::FATALITY_DMG), transform: |value| value },
35    dreadnaught_coef: f64 { provenance: "Dreadnaught wave 315961 e1 AP coefficient.", gate: params.talent_picked(TALENT::DREADNAUGHT), source: ap(EFFECT::DREADNAUGHT_DMG), transform: |value| value },
36    tactician_chance: f64 { provenance: "Tactician 184783 e1 reset chance.", gate: params.talent_picked(TALENT::TACTICIAN), source: base(EFFECT::TACTICIAN_CHANCE), transform: |value| value / HUNDRED },
37    anger_management_div: f64 { provenance: "Anger Management 152278 e1 rage per second of CDR.", gate: params.talent_picked(TALENT::ANGER_MANAGEMENT), source: base(EFFECT::ANGER_MANAGEMENT_DIV), transform: |value| value },
38    martial_prowess: bool { provenance: "Martial Prowess selection gates Mortal Strike buff stacks.", gate: params.talent_picked(TALENT::MARTIAL_PROWESS), source: true, transform: |value| value },
39    executioners_precision: bool { provenance: "Executioner's Precision selection gates its Mortal Strike stacks.", gate: params.talent_picked(TALENT::EXECUTIONERS_PRECISION), source: true, transform: |value| value },
40    master_of_warfare: bool { provenance: "Master of Warfare 1269314 gates qualifying impact rolls.", gate: params.talent_picked(TALENT::MASTER_OF_WARFARE), source: true, transform: |value| value },
41    prd_c: f64 { provenance: "PRD constant for the 15% nominal proc rate.", gate: true, source: PRD_C_15, transform: |value| value },
42    overpower_coef: f64 { provenance: "Overpower AP coefficient from its manifest effect binding.", gate: true, source: ap(EFFECT::OVERPOWER_DMG), transform: |value| value },
43    slayers_dominance: bool { provenance: "Slayer's Dominance 444767 selection.", gate: params.talent_picked(TALENT::SLAYERS_DOMINANCE), source: true, transform: |value| value },
44    slayers_strike_coef: f64 { provenance: "Slayer's Strike AP coefficient from its manifest effect binding.", gate: true, source: ap(EFFECT::SLAYERS_STRIKE_DMG), transform: |value| value },
45    imminent_demise_every: i32 { provenance: "Imminent Demise 444769 e1 strike interval, gated by Sudden Death.", gate: imminent_demise, source: base(EFFECT::IMMINENT_DEMISE_EVERY), transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
46    reap_the_storm: bool { provenance: "Reap the Storm requires its talent and active Imminent Demise.", gate: params.talent_picked(TALENT::REAP_THE_STORM) && imminent_demise, source: true, transform: |value| value },
47    reap_coef: f64 { provenance: "Reap the Storm AP coefficient from its manifest effect binding.", gate: true, source: ap(EFFECT::REAP_DMG), transform: |value| value },
48    unhinged: bool { provenance: "Unhinged 386628 makes odd Bladestorm ticks deal Mortal Strike.", gate: params.talent_picked(TALENT::UNHINGED), source: true, transform: |value| value },
49    overwhelming_blades: bool { provenance: "Overwhelming Blades 444772 stacks Overwhelmed from Bladestorm.", gate: params.talent_picked(TALENT::OVERWHELMING_BLADES), source: true, transform: |value| value },
50    op_finish_pct: f64 { provenance: "Overpowering Finish 400205 e1 damage fraction.", gate: params.talent_picked(TALENT::OVERPOWERING_FINISH), source: base(EFFECT::OVERPOWERING_FINISH_PCT), transform: |value| value / HUNDRED },
51    op_finish_below: f64 { provenance: "Overpowering Finish 400205 e2 health threshold.", gate: true, source: base(EFFECT::OVERPOWERING_FINISH_BELOW), transform: |value| value / HUNDRED },
52    frothing_chance: f64 { provenance: "Frothing Berserker `SpellAuraOptions` proc chance.", gate: params.talent_picked(TALENT::FROTHING_BERSERKER), source: FROTHING_CHANCE, transform: |value| value },
53    frothing_refund_pct: f64 { provenance: "Frothing Berserker 392792 e1 rage-refund fraction.", gate: true, source: base(EFFECT::FROTHING_REFUND), transform: |value| value / HUNDRED },
54    uo_cdr_ms_per_stack: u32 { provenance: "Unrelenting Onslaught 444780 e1 seconds converted to milliseconds.", gate: params.talent_picked(TALENT::UNRELENTING_ONSLAUGHT), source: base(EFFECT::UNRELENTING_CDR_S), transform: |value| wowlab_types::numeric::f64_to_u32_saturating_trunc(value * MS_PER_SECOND) },
55    uo_overwhelmed_per_stack: i32 { provenance: "Unrelenting Onslaught 444780 e2 Overwhelmed stacks.", gate: true, source: base(EFFECT::UNRELENTING_OVERWHELMED), transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
56    fierce_followthrough: bool { provenance: "Fierce Followthrough selection gates its next-Mortal-Strike proc.", gate: params.talent_picked(TALENT::FIERCE_FOLLOWTHROUGH), source: true, transform: |value| value },
57    mid2_2pc: bool { provenance: "MID2 Arms two-piece enables Concussive Slam.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_2PC), source: true, transform: |value| value },
58    mid2_4pc: bool { provenance: "MID2 Arms four-piece enables Winding Up.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_4PC), source: true, transform: |value| value },
59    mid2_concussive_slam_coef: f64 { provenance: "Concussive Slam 1300660 e1 AP coefficient.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_2PC), source: ap(EFFECT::MID2_CONCUSSIVE_SLAM_DAMAGE), transform: |value| value },
60    mid2_concussive_slam_reduced_targets: usize { provenance: "Concussive Slam 1300660 e2 reduced-AoE threshold.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_2PC), source: base(EFFECT::MID2_CONCUSSIVE_SLAM_REDUCED_TARGETS), transform: wowlab_types::numeric::f64_to_usize_saturating_trunc },
61    execute_max_rage: f64 { provenance: "Execute maximum rage consumption from `SpellPower` 163201.", gate: true, source: EXECUTE_MAX_RAGE, transform: |value| value },
62    bladestorm_duration_ms: u32 { provenance: "Bladestorm's resolved aura lifetime drives its finite mechanic callbacks.", gate: bladestorm_driver, source: data.require_aura_duration_ms(SpellIdx::from_raw(SPELL::BLADESTORM))?, transform: |value| value },
63    bladestorm_period_ms: u32 { provenance: "Bladestorm 227847 e1 provides its mechanic callback cadence and Imminent Demise extension quantum.", gate: bladestorm_driver, source: data.effect_lookup(EFFECT::BLADESTORM_PERIOD).period(), transform: wowlab_types::numeric::f64_to_u32_saturating_round },
64    unhinged_coef: f64 { provenance: "Unhinged's Mortal Strike callbacks use Mortal Strike 12294 e1's AP coefficient.", gate: params.talent_picked(TALENT::UNHINGED), source: ap(EFFECT::MORTAL_STRIKE_DMG), transform: |value| value },
65}
66accessor pub(super) fn arms_config(ctx);
67register pub(super) fn register_arms(built, params) -> Result<ArmsConfig, wowlab_engine_ports::EngineError>;
68prepare {
69    let data = &params.game_data;
70    let base = |effect| data.effect_base_points(effect);
71    let ap = |effect| data.effect_ap_coefficient(effect);
72    let swing_s = built
73        .first_auto_attack()
74        .map_or(0.0, |aa| f64::from(aa.swing_ms) / MS_PER_SECOND);
75    let crit_swing_extra_rage =
76        swing_s * BASE_RAGE_PER_SWING_SECOND * (base(EFFECT::SEASONED_SOLDIER_CRIT) / HUNDRED);
77
78    let imminent_demise =
79        params.talent_picked(TALENT::IMMINENT_DEMISE) && params.talent_picked(TALENT::SUDDEN_DEATH);
80    let bladestorm_driver = params.talent_picked(TALENT::OVERWHELMING_BLADES)
81        || params.talent_picked(TALENT::UNHINGED);
82
83}
84auras {}
85wiring {
86    { provenance: "MID2 Arms two-piece increases Mortal Strike and Execute damage.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_2PC), apply: {
87        built.register_passive_driver_effect(PassiveDriverEffect::new(
88            SET_BONUS::MID2_ARMS_2PC,
89            BuffEffect::DamageMultSpells(1.0 + base(EFFECT::MID2_MORTAL_STRIKE_EXECUTE_DAMAGE) / HUNDRED, &MID2_2PC_DAMAGE_SPELLS),
90        ));
91    } },
92    { provenance: "MID2 Arms four-piece increases Overpower damage.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_ARMS_4PC), apply: {
93        built.register_passive_driver_effect(PassiveDriverEffect::new(
94            SET_BONUS::MID2_ARMS_4PC,
95            BuffEffect::DamageMultSpells(1.0 + base(EFFECT::MID2_OVERPOWER_DAMAGE) / HUNDRED, &MID2_4PC_DAMAGE_SPELLS),
96        ));
97    } },
98    { provenance: "Sudden Death uses its DBC RPPM driver and proc-category recovery.", gate: params.talent_picked(TALENT::SUDDEN_DEATH), apply: {
99        built.bind_system_rppm(wowlab_engine_combat::generated_identities::system_rppm::SUDDEN_DEATH, TALENT::SUDDEN_DEATH)?;
100    } },
101    { provenance: "Improved Overpower charge and initialized cooldown adjustment.", gate: params.talent_picked(TALENT::IMPROVED_OVERPOWER), apply: {
102        built.patch_spell(SPELL_OVERPOWER, |spell| {
103            spell.cooldown.max_charges = IMPROVED_ATTACK_CHARGES;
104            if spell.cooldown.recharge_ms == 0 { spell.cooldown.recharge_ms = spell.cooldown.cooldown_duration_ms; }
105        });
106    } },
107    { provenance: "Battle Stance precombat aura.", gate: params.talent_picked(TALENT::BATTLE_STANCE), apply: {
108        built.push_precombat_aura(AURA_BATTLE_STANCE);
109    } },
110}
111finish |cfg| {
112    let period = std::num::NonZeroU32::new(cfg.bladestorm_period_ms);
113    let duration = std::num::NonZeroU32::new(cfg.bladestorm_duration_ms);
114
115    match (period, duration) {
116        (Some(period), Some(duration)) => {
117            if cfg.overwhelming_blades {
118                built.register_periodic_driver(
119                    PeriodicDriver::new(
120                        TALENT::OVERWHELMING_BLADES,
121                        period.get(),
122                        super::hooks::overwhelming_blades_tick,
123                    )
124                    .triggered_for(duration.get()),
125                );
126            }
127            if cfg.unhinged {
128                let Some(unhinged_period) = std::num::NonZeroU32::new(period.get().saturating_mul(2)) else {
129                    return Err(wowlab_engine_ports::EngineError::spec_construction(
130                        "Arms Unhinged periodic driver interval overflowed",
131                    ));
132                };
133                built.register_periodic_driver(
134                    PeriodicDriver::new(
135                        TALENT::UNHINGED,
136                        unhinged_period.get(),
137                        super::hooks::unhinged_tick,
138                    )
139                    .triggered_for(duration.get()),
140                );
141            }
142        }
143        _ if bladestorm_driver && !params.game_data.is_empty() => {
144            return Err(wowlab_engine_ports::EngineError::spec_construction(
145                "Arms Bladestorm periodic driver resolved a zero duration or interval",
146            ));
147        }
148        _ => {}
149    }
150    built.set_spec_config(cfg);
151    Ok(cfg)
152}
153}