Skip to main content

sf_api/simulate/
damage.rs

1use 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    // Armor reduction
50    damage *= 1.0 - calculate_damage_reduction(attacker, target);
51    // Class multiplier
52    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
93/// Calculates the amount of damage a hit with the given base damage range
94/// does, when scaled up by the rounds and affected by crit's
95pub(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
111/// Calculates the amount of base damage a swoop attack does
112pub(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
125/// Calculates the raw base damage a fighter does (unaffected by the enemy).
126/// This is mainly just the scaled weapon damage, but may be unarmed damage,
127/// if not weapon is given, or unarmed dmg is higher
128fn 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    // Mage negates enemy armor
180    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        // TODO: Is this right?
206        (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}