sf_api/simulate/
damage.rs1use fastrand::Rng;
2
3use crate::{
4 gamestate::character::Class,
5 simulate::{Weapon, fighter::Fighter},
6};
7
8#[derive(Debug, Clone, Copy, Default)]
9pub struct DamageRange {
10 pub min: f64,
11 pub max: f64,
12}
13
14impl std::ops::Mul<f64> for DamageRange {
15 type Output = DamageRange;
16
17 fn mul(self, rhs: f64) -> DamageRange {
18 DamageRange {
19 min: self.min * rhs,
20 max: self.max * rhs,
21 }
22 }
23}
24
25impl std::ops::MulAssign<f64> for DamageRange {
26 fn mul_assign(&mut self, rhs: f64) {
27 self.min *= rhs;
28 self.max *= rhs;
29 }
30}
31
32pub(crate) fn calculate_damage(
33 attacker: &Fighter,
34 target: &Fighter,
35 is_secondary: bool,
36) -> DamageRange {
37 let weapon = match is_secondary {
38 true => &attacker.second_weapon,
39 false => &attacker.first_weapon,
40 }
41 .as_ref();
42 let mut damage = get_base_damge(weapon, attacker, is_secondary);
43
44 damage *= 1.0 + attacker.portal_dmg_bonus / 100.0;
45
46 apply_attributes_bonus(attacker, target, &mut damage);
47 apply_rune_bonus(weapon, target, &mut damage);
48
49 damage *= 1.0 - calculate_damage_reduction(attacker, target);
51 damage *= calculate_damage_multiplier(attacker, target);
53
54 damage
55}
56
57fn apply_rune_bonus(
58 weapon: Option<&Weapon>,
59 target: &Fighter,
60 damage: &mut DamageRange,
61) {
62 let Some(weapon) = weapon else {
63 return;
64 };
65 let Some(element) = weapon.rune_type else {
66 return;
67 };
68
69 let enemy_rune_resistence = 75.min(target.resistances[element]);
70
71 let mut rune_bonus = f64::from(weapon.rune_value) / 100.0;
72 rune_bonus *= (100.0 - f64::from(enemy_rune_resistence)) / 100.0;
73 rune_bonus += 1.0;
74
75 *damage *= rune_bonus;
76}
77
78fn apply_attributes_bonus(
79 attacker: &Fighter,
80 target: &Fighter,
81 damage: &mut DamageRange,
82) {
83 let main_attribute = attacker.class.main_attribute();
84 let mut attribute = attacker.attributes[main_attribute] / 2;
85 attribute = attribute.max(
86 attacker.attributes[main_attribute]
87 .saturating_sub(target.attributes[main_attribute] / 2),
88 );
89 let attribute_bonus = 1.0 + f64::from(attribute) / 10.0;
90 *damage *= attribute_bonus;
91}
92
93pub(crate) fn calculate_hit_damage(
96 damage: &DamageRange,
97 round: u32,
98 crit_chance: f64,
99 crit_multiplier: f64,
100 rng: &mut Rng,
101) -> f64 {
102 let base_damage = rng.f64() * (1.0 + damage.max - damage.min) + damage.min;
103 let mut dmg = base_damage * (1.0 + (f64::from(round) - 1.0) * (1.0 / 6.0));
104
105 if rng.f64() < crit_chance {
106 dmg *= crit_multiplier;
107 }
108 dmg
109}
110
111pub(crate) fn calculate_swoop_damage(
113 attacker: &Fighter,
114 target: &Fighter,
115) -> f64 {
116 let dmg_multiplier = calculate_damage_multiplier(attacker, target);
117 let base_dmg_multiplier = Class::Druid.damage_multiplier();
118 let class_specific_dmg_multiplier = dmg_multiplier / base_dmg_multiplier;
119
120 (dmg_multiplier / class_specific_dmg_multiplier + 0.8)
121 * class_specific_dmg_multiplier
122 / dmg_multiplier
123}
124
125fn get_base_damge(
129 weapon: Option<&Weapon>,
130 attacker: &Fighter,
131 is_secondary: bool,
132) -> DamageRange {
133 let hand_damage = get_hand_damage(attacker, is_secondary);
134
135 let Some(weapon) = weapon else {
136 return hand_damage;
137 };
138
139 if weapon.damage.min < hand_damage.min
140 && weapon.damage.max < hand_damage.max
141 {
142 return hand_damage;
143 }
144
145 weapon.damage
146}
147
148fn get_hand_damage(attacker: &Fighter, is_secondary: bool) -> DamageRange {
149 if attacker.level <= 10 {
150 return DamageRange { min: 1.0, max: 2.0 };
151 }
152
153 let mut multiplier = 0.7;
154 if attacker.class == Class::Assassin {
155 multiplier = if is_secondary { 1.25 } else { 0.875 };
156 }
157
158 let weapon_multi = attacker.class.weapon_multiplier();
159 let damage: f64 =
160 multiplier * (f64::from(attacker.level) - 9.0) * weapon_multi;
161 let min = 1.0f64.max((damage * 2.0 / 3.0).ceil());
162 let max = 2.0f64.max((damage * 4.0 / 3.0).round());
163
164 DamageRange { min, max }
165}
166
167pub fn calculate_fire_ball_damage(attacker: &Fighter, target: &Fighter) -> f64 {
168 if target.class == Class::Mage {
169 return 0.0;
170 }
171
172 let multiplier = target.class.health_multiplier(target.is_companion);
173
174 let dmg = (multiplier * 0.05 * attacker.max_health).ceil();
175 (target.max_health / 3.0).ceil().min(dmg)
176}
177
178pub fn calculate_damage_reduction(attacker: &Fighter, target: &Fighter) -> f64 {
179 if attacker.class == Class::Mage {
181 return 0.0;
182 }
183
184 if target.armor == 0 {
185 return 0.0;
186 }
187
188 let max_dmg_reduction = target.class.max_armor_reduction();
189
190 let damage_reduction = target.class.armor_multiplier()
191 * f64::from(target.armor)
192 / f64::from(attacker.level)
193 / 100.0;
194
195 damage_reduction.min(f64::from(max_dmg_reduction) / 100.0)
196}
197
198pub fn calculate_damage_multiplier(
199 attacker: &Fighter,
200 target: &Fighter,
201) -> f64 {
202 let base_multi = attacker.class.damage_multiplier();
203 match (attacker.class, target.class) {
204 (Class::Mage, Class::Paladin) |
205 (Class::Paladin, Class::Mage) => base_multi * 1.5,
207 (Class::Druid, Class::Mage) => base_multi * 4.0 / 3.0,
208 (Class::Druid, Class::DemonHunter) => base_multi * 1.15,
209 (Class::Bard, Class::PlagueDoctor) => base_multi * 1.05,
210 (Class::Necromancer, Class::DemonHunter) => base_multi + 0.1,
211 (Class::PlagueDoctor, Class::DemonHunter) => base_multi * 1.06,
212 (_, _) => base_multi,
213 }
214}