Skip to main content

wowlab_engine_combat/systems/damage_pipeline/
weapon.rs

1//! Weapon and dynamic power inputs for outgoing damage.
2
3use wowlab_types::constants::{HUNDRED, MS_PER_SECOND};
4
5use super::dynamic_base_powers;
6#[cfg(feature = "rotation-optimizations")]
7use super::static_damage_buff_projection;
8use crate::state::{BuffTotals, CombatInstance, SwingHand, WeaponApType};
9
10/// Unarmed weapon DPS.
11const UNARMED_WDPS: f64 = 0.5;
12/// Dual-weapon AP composite scale.
13const WEAPON_BOTH_SCALE: f64 = 2.0 / 3.0;
14const HALF: f64 = 2.0;
15
16fn weapon_dps(weapon: &wowlab_types::data::WeaponStats) -> f64 {
17    if weapon.speed_ms == 0 || weapon.max_damage <= 0.0 {
18        return UNARMED_WDPS;
19    }
20
21    (weapon.min_damage + weapon.max_damage) / HALF / (f64::from(weapon.speed_ms) / MS_PER_SECOND)
22}
23
24/// Returns the attack-power composite for a weapon type.
25pub(crate) fn typed_attack_power(
26    program: &crate::state::CombatProgram,
27    state: &CombatInstance,
28    ap_type: WeaponApType,
29) -> f64 {
30    #[cfg(feature = "rotation-optimizations")]
31    {
32        let projection =
33            static_damage_buff_projection(program, state, wowlab_types::sim::ActorId::Player);
34
35        typed_attack_power_from_projection(
36            program,
37            ap_type,
38            projection.attack_power,
39            projection.attack_power_pct,
40        )
41    }
42
43    #[cfg(not(feature = "rotation-optimizations"))]
44    {
45        let view = crate::CombatView::new(program, state);
46        let totals = view.buff_totals();
47
48        typed_attack_power_with(program, state, ap_type, &totals)
49    }
50}
51
52#[cfg(feature = "rotation-optimizations")]
53fn typed_attack_power_from_projection(
54    program: &crate::state::CombatProgram,
55    ap_type: WeaponApType,
56    attack_power: f64,
57    attack_power_pct: f64,
58) -> f64 {
59    let ap_mult = 1.0 + attack_power_pct / HUNDRED;
60    let wdps = match ap_type {
61        WeaponApType::None => return attack_power * ap_mult,
62        WeaponApType::MainHand => weapon_dps(&program.damage.main_hand),
63        WeaponApType::OffHand => program
64            .damage
65            .off_hand
66            .map_or(UNARMED_WDPS, |weapon| weapon_dps(&weapon)),
67        WeaponApType::Both => {
68            let main_hand = weapon_dps(&program.damage.main_hand);
69            let off_hand = program
70                .damage
71                .off_hand
72                .map_or(UNARMED_WDPS, |weapon| weapon_dps(&weapon));
73
74            (main_hand + off_hand / HALF) * WEAPON_BOTH_SCALE
75        }
76    };
77
78    (attack_power + (wdps * wowlab_engine_domain::damage::WEAPON_POWER_COEFFICIENT).trunc())
79        * ap_mult
80}
81
82pub(crate) fn typed_attack_power_with(
83    program: &crate::state::CombatProgram,
84    state: &CombatInstance,
85    ap_type: WeaponApType,
86    totals: &BuffTotals,
87) -> f64 {
88    let (ap, _) = dynamic_base_powers(program, state, totals);
89    let ap_mult = 1.0 + totals.attack_power_pct / HUNDRED;
90    let wdps = match ap_type {
91        WeaponApType::None => return ap * ap_mult,
92        WeaponApType::MainHand => weapon_dps(&program.damage.main_hand),
93        WeaponApType::OffHand => program
94            .damage
95            .off_hand
96            .map_or(UNARMED_WDPS, |weapon| weapon_dps(&weapon)),
97        WeaponApType::Both => {
98            let main_hand = weapon_dps(&program.damage.main_hand);
99            let off_hand = program
100                .damage
101                .off_hand
102                .map_or(UNARMED_WDPS, |weapon| weapon_dps(&weapon));
103
104            (main_hand + off_hand / HALF) * WEAPON_BOTH_SCALE
105        }
106    };
107
108    (ap + (wdps * wowlab_engine_domain::damage::WEAPON_POWER_COEFFICIENT).trunc()) * ap_mult
109}
110
111/// Returns dynamic spell power including active stat buffs.
112pub(crate) fn current_spell_power(
113    program: &crate::state::CombatProgram,
114    state: &CombatInstance,
115) -> f64 {
116    #[cfg(feature = "rotation-optimizations")]
117    {
118        static_damage_buff_projection(program, state, wowlab_types::sim::ActorId::Player)
119            .spell_power
120    }
121
122    #[cfg(not(feature = "rotation-optimizations"))]
123    {
124        let view = crate::CombatView::new(program, state);
125        let totals = view.buff_totals();
126
127        dynamic_base_powers(program, state, &totals).1
128    }
129}
130
131/// Weapon-damage inputs for one damage request.
132#[derive(Clone, Copy, Debug, Default)]
133pub(in crate::systems) struct WeaponDamageInput {
134    pub min: f64,
135    pub max: f64,
136    pub multiplier: f64,
137    pub ap_coefficient: f64,
138    pub is_auto_attack: bool,
139    pub swing_hand: Option<SwingHand>,
140    pub swing_speed_ms: u32,
141    pub miss_chance: f64,
142}
143
144impl WeaponDamageInput {
145    pub(in crate::systems) const fn none() -> Self {
146        Self {
147            min: 0.0,
148            max: 0.0,
149            multiplier: 0.0,
150            ap_coefficient: 0.0,
151            is_auto_attack: false,
152            swing_hand: None,
153            swing_speed_ms: 0,
154            miss_chance: 0.0,
155        }
156    }
157}
158
159pub(super) fn weapon_damage_input(
160    program: &crate::state::CombatProgram,
161    _state: &CombatInstance,
162    hand: WeaponApType,
163    multiplier: f64,
164    normalized: bool,
165) -> WeaponDamageInput {
166    let (weapon, swing_hand) = match hand {
167        WeaponApType::MainHand => (Some(program.damage.main_hand), SwingHand::MainHand),
168        WeaponApType::OffHand => (program.damage.off_hand, SwingHand::OffHand),
169        WeaponApType::Both | WeaponApType::None => (None, SwingHand::MainHand),
170    };
171    let Some(weapon) = weapon.filter(|weapon| weapon.speed_ms > 0 && weapon.max_damage > 0.0)
172    else {
173        return WeaponDamageInput::none();
174    };
175    let hand_multiplier = if swing_hand == SwingHand::OffHand {
176        wowlab_engine_domain::constants::OFF_HAND_DAMAGE_MULT
177    } else {
178        1.0
179    };
180    let multiplier = multiplier * hand_multiplier;
181    let speed_s = if normalized {
182        weapon.normalized_speed_s
183    } else {
184        f64::from(weapon.speed_ms) / MS_PER_SECOND
185    };
186
187    WeaponDamageInput {
188        min: weapon.min_damage,
189        max: weapon.max_damage,
190        multiplier,
191        ap_coefficient: speed_s / wowlab_engine_domain::damage::WEAPON_POWER_COEFFICIENT
192            * multiplier,
193        is_auto_attack: false,
194        swing_hand: Some(swing_hand),
195        swing_speed_ms: weapon.speed_ms,
196        miss_chance: 0.0,
197    }
198}