wowlab_engine_combat/systems/damage_pipeline/
weapon.rs1use 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
10const UNARMED_WDPS: f64 = 0.5;
12const 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
24pub(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
111pub(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#[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}