poke_engine/genx/
damage_calc.rs

1use super::abilities::Abilities;
2use super::state::{PokemonVolatileStatus, Terrain, Weather};
3use crate::choices::{Choice, MoveCategory};
4use crate::choices::{Choices, MOVES};
5use crate::state::{
6    Pokemon, PokemonBoostableStat, PokemonIndex, PokemonStatus, PokemonType, Side, SideReference,
7    State,
8};
9
10#[rustfmt::skip]
11#[cfg(any(feature = "gen9",feature = "gen8",feature = "gen7",feature = "gen6"))]
12const TYPE_MATCHUP_DAMAGE_MULTIPICATION: [[f32; 19]; 19] = [
13/*         0    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18  */
14/*  0 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 1.0, 1.0, 0.5, 1.0, 1.0],
15/*  1 */ [1.0, 0.5, 0.5, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 0.5, 1.0, 2.0, 1.0, 1.0],
16/*  2 */ [1.0, 2.0, 0.5, 1.0, 0.5, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0],
17/*  3 */ [1.0, 1.0, 2.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0],
18/*  4 */ [1.0, 0.5, 2.0, 1.0, 0.5, 1.0, 1.0, 0.5, 2.0, 0.5, 1.0, 0.5, 2.0, 1.0, 0.5, 1.0, 0.5, 1.0, 1.0],
19/*  5 */ [1.0, 0.5, 0.5, 1.0, 2.0, 0.5, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0],
20/*  6 */ [2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 0.5, 0.5, 0.5, 2.0, 0.0, 1.0, 2.0, 2.0, 0.5, 1.0],
21/*  7 */ [1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 0.0, 2.0, 1.0],
22/*  8 */ [1.0, 2.0, 1.0, 2.0, 0.5, 1.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.5, 2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0],
23/*  9 */ [1.0, 1.0, 1.0, 0.5, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0],
24/* 10 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 0.0, 0.5, 1.0, 1.0],
25/* 11 */ [1.0, 0.5, 1.0, 1.0, 2.0, 1.0, 0.5, 0.5, 1.0, 0.5, 2.0, 1.0, 1.0, 0.5, 1.0, 2.0, 0.5, 0.5, 1.0],
26/* 12 */ [1.0, 2.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 0.5, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0],
27/* 13 */ [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0, 1.0],
28/* 14 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 0.0, 1.0],
29/* 15 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 0.5, 1.0],
30/* 16 */ [1.0, 0.5, 0.5, 0.5, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 0.5, 2.0, 1.0],
31/* 17 */ [1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 0.5, 1.0, 1.0],
32/* 18 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
33];
34
35#[rustfmt::skip]
36#[cfg(any(feature = "gen5",feature = "gen4", feature = "gen3"))]
37const TYPE_MATCHUP_DAMAGE_MULTIPICATION: [[f32; 19]; 19] = [
38/*         0    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18  */
39/*  0 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 1.0, 1.0, 0.5, 1.0, 1.0],
40/*  1 */ [1.0, 0.5, 0.5, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 0.5, 1.0, 2.0, 1.0, 1.0],
41/*  2 */ [1.0, 2.0, 0.5, 1.0, 0.5, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0],
42/*  3 */ [1.0, 1.0, 2.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0],
43/*  4 */ [1.0, 0.5, 2.0, 1.0, 0.5, 1.0, 1.0, 0.5, 2.0, 0.5, 1.0, 0.5, 2.0, 1.0, 0.5, 1.0, 0.5, 1.0, 1.0],
44/*  5 */ [1.0, 0.5, 0.5, 1.0, 2.0, 0.5, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0],
45/*  6 */ [2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 0.5, 0.5, 0.5, 2.0, 0.0, 1.0, 2.0, 2.0, 0.5, 1.0],
46/*  7 */ [1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 0.0, 2.0, 1.0],
47/*  8 */ [1.0, 2.0, 1.0, 2.0, 0.5, 1.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.5, 2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0],
48/*  9 */ [1.0, 1.0, 1.0, 0.5, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0],
49/* 10 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 0.0, 0.5, 1.0, 1.0],
50/* 11 */ [1.0, 0.5, 1.0, 1.0, 2.0, 1.0, 0.5, 0.5, 1.0, 0.5, 2.0, 1.0, 1.0, 0.5, 1.0, 2.0, 0.5, 0.5, 1.0],
51/* 12 */ [1.0, 2.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 0.5, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0],
52/* 13 */ [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 0.5, 0.5, 1.0, 1.0],
53/* 14 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 0.0, 1.0],
54/* 15 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 0.5, 0.5, 0.5, 1.0],
55/* 16 */ [1.0, 0.5, 0.5, 0.5, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 0.5, 2.0, 1.0],
56/* 17 */ [1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 0.5, 1.0, 1.0],
57/* 18 */ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
58];
59
60#[cfg(any(feature = "gen3", feature = "gen4", feature = "gen5"))]
61pub const CRIT_MULTIPLIER: f32 = 2.0;
62
63#[cfg(any(feature = "gen6", feature = "gen7", feature = "gen8", feature = "gen9"))]
64pub const CRIT_MULTIPLIER: f32 = 1.5;
65
66#[allow(dead_code)]
67pub enum DamageRolls {
68    Average,
69    Min,
70    Max,
71}
72
73fn type_enum_to_type_matchup_int(type_enum: &PokemonType) -> usize {
74    match type_enum {
75        PokemonType::NORMAL => 0,
76        PokemonType::FIRE => 1,
77        PokemonType::WATER => 2,
78        PokemonType::ELECTRIC => 3,
79        PokemonType::GRASS => 4,
80        PokemonType::ICE => 5,
81        PokemonType::FIGHTING => 6,
82        PokemonType::POISON => 7,
83        PokemonType::GROUND => 8,
84        PokemonType::FLYING => 9,
85        PokemonType::PSYCHIC => 10,
86        PokemonType::BUG => 11,
87        PokemonType::ROCK => 12,
88        PokemonType::GHOST => 13,
89        PokemonType::DRAGON => 14,
90        PokemonType::DARK => 15,
91        PokemonType::STEEL => 16,
92        PokemonType::FAIRY => 17,
93        PokemonType::TYPELESS => 18,
94        PokemonType::STELLAR => 18, // Stellar is typeless for type effectiveness
95    }
96}
97
98pub fn type_effectiveness_modifier(attacking_type: &PokemonType, defender: &Pokemon) -> f32 {
99    #[cfg(not(feature = "terastallization"))]
100    let defending_types = defender.types;
101    #[cfg(feature = "terastallization")]
102    let defending_types = if defender.terastallized {
103        (defender.tera_type, PokemonType::TYPELESS)
104    } else {
105        defender.types
106    };
107    _type_effectiveness_modifier(attacking_type, &defending_types)
108}
109
110fn _type_effectiveness_modifier(
111    attacking_type: &PokemonType,
112    defending_types: &(PokemonType, PokemonType),
113) -> f32 {
114    let mut modifier = 1.0;
115    let attacking_type_index = type_enum_to_type_matchup_int(attacking_type);
116    modifier = modifier
117        * TYPE_MATCHUP_DAMAGE_MULTIPICATION[attacking_type_index]
118            [type_enum_to_type_matchup_int(&defending_types.0)];
119    modifier = modifier
120        * TYPE_MATCHUP_DAMAGE_MULTIPICATION[attacking_type_index]
121            [type_enum_to_type_matchup_int(&defending_types.1)];
122    modifier
123}
124
125fn weather_modifier(attacking_move_type: &PokemonType, weather: &Weather) -> f32 {
126    match weather {
127        Weather::SUN => match attacking_move_type {
128            PokemonType::FIRE => 1.5,
129            PokemonType::WATER => 0.5,
130            _ => 1.0,
131        },
132        Weather::RAIN => match attacking_move_type {
133            PokemonType::WATER => 1.5,
134            PokemonType::FIRE => 0.5,
135            _ => 1.0,
136        },
137        Weather::HARSHSUN => match attacking_move_type {
138            PokemonType::FIRE => 1.5,
139            PokemonType::WATER => 0.0,
140            _ => 1.0,
141        },
142        Weather::HEAVYRAIN => match attacking_move_type {
143            PokemonType::WATER => 1.5,
144            PokemonType::FIRE => 0.0,
145            _ => 1.0,
146        },
147        _ => 1.0,
148    }
149}
150
151fn stab_modifier(attacking_move_type: &PokemonType, active_pkmn: &Pokemon) -> f32 {
152    if attacking_move_type == &PokemonType::TYPELESS {
153        return 1.0;
154    }
155
156    let active_types = active_pkmn.types;
157    let move_has_basic_stab =
158        attacking_move_type == &active_types.0 || attacking_move_type == &active_types.1;
159    if active_pkmn.terastallized {
160        if &active_pkmn.tera_type == attacking_move_type && move_has_basic_stab {
161            return 2.0;
162        } else if &active_pkmn.tera_type == attacking_move_type || move_has_basic_stab {
163            return 1.5;
164        }
165    } else if move_has_basic_stab {
166        return 1.5;
167    }
168    1.0
169}
170
171fn burn_modifier(
172    attacking_move_category: &MoveCategory,
173    attacking_pokemon_status: &PokemonStatus,
174) -> f32 {
175    if attacking_pokemon_status == &PokemonStatus::BURN
176        && attacking_move_category == &MoveCategory::Physical
177    {
178        return 0.5;
179    }
180
181    1.0
182}
183
184fn terrain_modifier(
185    terrain: &Terrain,
186    attacker: &Pokemon,
187    defender: &Pokemon,
188    choice: &Choice,
189) -> f32 {
190    #[cfg(any(feature = "gen9", feature = "gen8"))]
191    let terrain_boost = 1.3;
192
193    #[cfg(not(any(feature = "gen9", feature = "gen8")))]
194    let terrain_boost = 1.5;
195
196    match terrain {
197        Terrain::ELECTRICTERRAIN => {
198            if choice.move_type == PokemonType::ELECTRIC && attacker.is_grounded() {
199                terrain_boost
200            } else {
201                1.0
202            }
203        }
204        Terrain::GRASSYTERRAIN => {
205            if choice.move_type == PokemonType::GRASS && attacker.is_grounded() {
206                terrain_boost
207            } else if choice.move_id == Choices::EARTHQUAKE {
208                0.5
209            } else {
210                1.0
211            }
212        }
213        Terrain::MISTYTERRAIN => {
214            if choice.move_type == PokemonType::DRAGON && defender.is_grounded() {
215                0.5
216            } else {
217                1.0
218            }
219        }
220        Terrain::PSYCHICTERRAIN => {
221            if choice.move_type == PokemonType::PSYCHIC && attacker.is_grounded() {
222                terrain_boost
223            } else {
224                1.0
225            }
226        }
227        Terrain::NONE => 1.0,
228    }
229}
230
231fn volatile_status_modifier(choice: &Choice, attacking_side: &Side, defending_side: &Side) -> f32 {
232    let mut modifier = 1.0;
233    for vs in attacking_side.volatile_statuses.iter() {
234        match vs {
235            PokemonVolatileStatus::FLASHFIRE if choice.move_type == PokemonType::FIRE => {
236                modifier *= 1.5;
237            }
238            PokemonVolatileStatus::SLOWSTART if choice.category == MoveCategory::Physical => {
239                modifier *= 0.5;
240            }
241            PokemonVolatileStatus::CHARGE if choice.move_type == PokemonType::ELECTRIC => {
242                modifier *= 2.0;
243            }
244            PokemonVolatileStatus::PROTOSYNTHESISATK | PokemonVolatileStatus::QUARKDRIVEATK
245                if choice.category == MoveCategory::Physical =>
246            {
247                modifier *= 1.3;
248            }
249            PokemonVolatileStatus::PROTOSYNTHESISSPA | PokemonVolatileStatus::QUARKDRIVESPA
250                if choice.category == MoveCategory::Special =>
251            {
252                modifier *= 1.3;
253            }
254            _ => {}
255        }
256    }
257
258    for vs in defending_side.volatile_statuses.iter() {
259        match vs {
260            PokemonVolatileStatus::MAGNETRISE
261                if choice.move_type == PokemonType::GROUND
262                    && choice.move_id != Choices::THOUSANDARROWS =>
263            {
264                return 0.0;
265            }
266            PokemonVolatileStatus::TARSHOT if choice.move_type == PokemonType::FIRE => {
267                modifier *= 2.0;
268            }
269            PokemonVolatileStatus::PHANTOMFORCE
270            | PokemonVolatileStatus::SHADOWFORCE
271            | PokemonVolatileStatus::BOUNCE
272            | PokemonVolatileStatus::DIG
273            | PokemonVolatileStatus::DIVE
274            | PokemonVolatileStatus::FLY => {
275                return 0.0;
276            }
277            PokemonVolatileStatus::GLAIVERUSH => {
278                modifier *= 2.0;
279            }
280            PokemonVolatileStatus::PROTOSYNTHESISDEF | PokemonVolatileStatus::QUARKDRIVEDEF
281                if choice.category == MoveCategory::Physical =>
282            {
283                modifier /= 1.3;
284            }
285            PokemonVolatileStatus::PROTOSYNTHESISSPD | PokemonVolatileStatus::QUARKDRIVESPD
286                if choice.category == MoveCategory::Special =>
287            {
288                modifier /= 1.3;
289            }
290            _ => {}
291        }
292    }
293
294    modifier
295}
296
297fn get_defending_types(
298    side: &Side,
299    defending_pkmn: &Pokemon,
300    attacking_pkmn: &Pokemon,
301    attacking_choice: &Choice,
302) -> (PokemonType, PokemonType) {
303    if defending_pkmn.terastallized && !(defending_pkmn.tera_type == PokemonType::STELLAR) {
304        return (defending_pkmn.tera_type, PokemonType::TYPELESS);
305    }
306    let mut defender_types = defending_pkmn.types;
307    if side
308        .volatile_statuses
309        .contains(&PokemonVolatileStatus::ROOST)
310    {
311        if defender_types.0 == PokemonType::FLYING {
312            defender_types = (PokemonType::TYPELESS, defender_types.1);
313        }
314        if defender_types.1 == PokemonType::FLYING {
315            defender_types = (defender_types.0, PokemonType::TYPELESS);
316        }
317    }
318    if (attacking_pkmn.ability == Abilities::SCRAPPY
319        || attacking_pkmn.ability == Abilities::MINDSEYE)
320        && (attacking_choice.move_type == PokemonType::NORMAL
321            || attacking_choice.move_type == PokemonType::FIGHTING)
322    {
323        if defender_types.0 == PokemonType::GHOST {
324            defender_types = (PokemonType::TYPELESS, defender_types.1);
325        }
326        if defender_types.1 == PokemonType::GHOST {
327            defender_types = (defender_types.0, PokemonType::TYPELESS);
328        }
329    }
330    defender_types
331}
332
333fn get_attacking_and_defending_stats(
334    attacker: &Pokemon,
335    defender: &Pokemon,
336    attacking_side: &Side,
337    defending_side: &Side,
338    state: &State,
339    choice: &Choice,
340) -> (i16, i16, i16, i16) {
341    let mut should_calc_attacker_boost = true;
342    let mut should_calc_defender_boost = true;
343    let defending_stat;
344    let (
345        attacking_final_stat,
346        mut defending_final_stat,
347        mut crit_attacking_stat,
348        mut crit_defending_stat,
349    );
350
351    match choice.category {
352        MoveCategory::Physical => {
353            if attacking_side.attack_boost > 0 {
354                crit_attacking_stat =
355                    attacking_side.calculate_boosted_stat(PokemonBoostableStat::Attack);
356            } else {
357                crit_attacking_stat = attacker.attack;
358            }
359            if defending_side.defense_boost <= 0 {
360                crit_defending_stat =
361                    defending_side.calculate_boosted_stat(PokemonBoostableStat::Defense);
362            } else {
363                crit_defending_stat = defender.defense;
364            }
365
366            // Unaware checks
367            if defender.ability == Abilities::UNAWARE && attacker.ability != Abilities::MOLDBREAKER
368            {
369                should_calc_attacker_boost = false;
370            }
371            if attacker.ability == Abilities::UNAWARE && defender.ability != Abilities::MOLDBREAKER
372            {
373                should_calc_defender_boost = false;
374            }
375
376            // Get the attacking stat
377
378            // checks for moves that change which stat is used for the attacking_stat
379            if choice.move_id == Choices::FOULPLAY {
380                if should_calc_attacker_boost {
381                    attacking_final_stat =
382                        defending_side.calculate_boosted_stat(PokemonBoostableStat::Attack);
383                } else {
384                    attacking_final_stat = defender.attack;
385                }
386                crit_attacking_stat = defending_side.get_active_immutable().attack;
387            } else if choice.move_id == Choices::BODYPRESS {
388                if should_calc_attacker_boost {
389                    attacking_final_stat =
390                        attacking_side.calculate_boosted_stat(PokemonBoostableStat::Defense);
391                } else {
392                    attacking_final_stat = attacker.defense;
393                }
394                crit_attacking_stat = attacking_side.get_active_immutable().defense;
395            } else if should_calc_attacker_boost {
396                attacking_final_stat =
397                    attacking_side.calculate_boosted_stat(PokemonBoostableStat::Attack);
398            } else {
399                attacking_final_stat = attacker.attack;
400            }
401
402            // Get the defending stat
403            defending_stat = PokemonBoostableStat::Defense;
404            if should_calc_defender_boost {
405                defending_final_stat =
406                    defending_side.calculate_boosted_stat(PokemonBoostableStat::Defense);
407            } else {
408                defending_final_stat = defender.defense;
409            }
410        }
411        MoveCategory::Special => {
412            if attacking_side.special_attack_boost > 0 {
413                crit_attacking_stat =
414                    attacking_side.calculate_boosted_stat(PokemonBoostableStat::SpecialAttack);
415            } else {
416                crit_attacking_stat = attacker.special_attack;
417            }
418            if defending_side.special_defense_boost <= 0 {
419                crit_defending_stat =
420                    defending_side.calculate_boosted_stat(PokemonBoostableStat::SpecialDefense);
421            } else {
422                crit_defending_stat = defender.special_defense;
423            }
424
425            // Unaware checks
426            if defender.ability == Abilities::UNAWARE && attacker.ability != Abilities::MOLDBREAKER
427            {
428                should_calc_attacker_boost = false;
429            }
430            if attacker.ability == Abilities::UNAWARE && defender.ability != Abilities::MOLDBREAKER
431            {
432                should_calc_defender_boost = false;
433            }
434
435            // Get the attacking stat
436            if should_calc_attacker_boost {
437                attacking_final_stat =
438                    attacking_side.calculate_boosted_stat(PokemonBoostableStat::SpecialAttack);
439            } else {
440                attacking_final_stat = attacker.special_attack;
441            }
442
443            // Get the defending stat
444            // check for moves that change which stat is used for the defending_stat
445            if choice.move_id == Choices::PSYSHOCK
446                || choice.move_id == Choices::SECRETSWORD
447                || choice.move_id == Choices::PSYSTRIKE
448            {
449                if defending_side.defense_boost <= 0 {
450                    crit_defending_stat =
451                        defending_side.calculate_boosted_stat(PokemonBoostableStat::Defense);
452                } else {
453                    crit_defending_stat = defender.defense;
454                }
455
456                defending_stat = PokemonBoostableStat::Defense;
457                if should_calc_defender_boost {
458                    defending_final_stat =
459                        defending_side.calculate_boosted_stat(PokemonBoostableStat::Defense);
460                } else {
461                    defending_final_stat = defender.defense;
462                }
463            } else {
464                defending_stat = PokemonBoostableStat::SpecialDefense;
465                if should_calc_defender_boost {
466                    defending_final_stat =
467                        defending_side.calculate_boosted_stat(PokemonBoostableStat::SpecialDefense);
468                } else {
469                    defending_final_stat = defender.special_defense;
470                }
471            }
472        }
473        _ => panic!("Can only calculate damage for physical or special moves"),
474    }
475
476    #[cfg(any(
477        feature = "gen4",
478        feature = "gen5",
479        feature = "gen6",
480        feature = "gen7",
481        feature = "gen8",
482        feature = "gen9"
483    ))]
484    if state.weather_is_active(&Weather::SNOW)
485        && defender.has_type(&PokemonType::ICE)
486        && defending_stat == PokemonBoostableStat::Defense
487    {
488        defending_final_stat = (defending_final_stat as f32 * 1.5) as i16;
489    } else if state.weather_is_active(&Weather::SAND)
490        && defender.has_type(&PokemonType::ROCK)
491        && defending_stat == PokemonBoostableStat::SpecialDefense
492    {
493        defending_final_stat = (defending_final_stat as f32 * 1.5) as i16;
494    }
495
496    (
497        attacking_final_stat,
498        defending_final_stat,
499        crit_attacking_stat,
500        crit_defending_stat,
501    )
502}
503
504fn common_pkmn_damage_calc(
505    attacking_side: &Side,
506    attacker: &Pokemon,
507    attacking_stat: i16,
508    defending_side: &Side,
509    defender: &Pokemon,
510    defending_stat: i16,
511    weather: &Weather,
512    terrain: &Terrain,
513    choice: &Choice,
514) -> f32 {
515    let mut damage: f32;
516    damage = 2.0 * attacker.level as f32;
517    damage = damage.floor() / 5.0;
518    damage = damage.floor() + 2.0;
519    damage = damage.floor() * choice.base_power;
520    damage = damage * attacking_stat as f32 / defending_stat as f32;
521    damage = damage.floor() / 50.0;
522    damage = damage.floor() + 2.0;
523
524    let defender_types = get_defending_types(&defending_side, defender, attacker, choice);
525
526    let mut damage_modifier = 1.0;
527
528    if defender.terastallized && choice.move_type == PokemonType::STELLAR {
529        damage_modifier *= 2.0;
530    } else {
531        damage_modifier *= _type_effectiveness_modifier(&choice.move_type, &defender_types);
532    }
533
534    if attacker.ability != Abilities::CLOUDNINE
535        && attacker.ability != Abilities::AIRLOCK
536        && defender.ability != Abilities::CLOUDNINE
537        && defender.ability != Abilities::AIRLOCK
538    {
539        damage_modifier *= weather_modifier(&choice.move_type, weather);
540    }
541
542    damage_modifier *= stab_modifier(&choice.move_type, &attacker);
543    damage_modifier *= burn_modifier(&choice.category, &attacker.status);
544    damage_modifier *= volatile_status_modifier(&choice, attacking_side, defending_side);
545    damage_modifier *= terrain_modifier(terrain, attacker, defender, &choice);
546
547    damage * damage_modifier
548}
549
550// This is a basic damage calculation function that assumes special effects/modifiers
551// are reflected in the `Choice` struct
552//
553// i.e. if an ability would multiply a move's base-power by 1.3x, that should already
554// be reflected in the `Choice`
555pub fn calculate_damage(
556    state: &State,
557    attacking_side: &SideReference,
558    choice: &Choice,
559    _damage_rolls: DamageRolls,
560) -> Option<(i16, i16)> {
561    if choice.category == MoveCategory::Status || choice.category == MoveCategory::Switch {
562        return None;
563    } else if choice.base_power == 0.0 {
564        return Some((0, 0));
565    }
566    let (attacking_side, defending_side) = state.get_both_sides_immutable(attacking_side);
567    let attacker = attacking_side.get_active_immutable();
568    let defender = defending_side.get_active_immutable();
569    let (attacking_stat, defending_stat, crit_attacking_stat, crit_defending_stat) =
570        get_attacking_and_defending_stats(
571            attacker,
572            defender,
573            attacking_side,
574            defending_side,
575            state,
576            &choice,
577        );
578
579    let mut damage = common_pkmn_damage_calc(
580        attacking_side,
581        attacker,
582        attacking_stat,
583        defending_side,
584        defender,
585        defending_stat,
586        &state.weather.weather_type,
587        &state.terrain.terrain_type,
588        choice,
589    );
590    if attacker.ability != Abilities::INFILTRATOR {
591        if defending_side.side_conditions.aurora_veil > 0 {
592            damage *= 0.5
593        } else if defending_side.side_conditions.reflect > 0
594            && choice.category == MoveCategory::Physical
595        {
596            damage *= 0.5
597        } else if defending_side.side_conditions.light_screen > 0
598            && choice.category == MoveCategory::Special
599        {
600            damage *= 0.5
601        }
602    }
603
604    let mut crit_damage = common_pkmn_damage_calc(
605        attacking_side,
606        attacker,
607        crit_attacking_stat,
608        defending_side,
609        defender,
610        crit_defending_stat,
611        &state.weather.weather_type,
612        &state.terrain.terrain_type,
613        choice,
614    );
615    crit_damage *= CRIT_MULTIPLIER;
616
617    match _damage_rolls {
618        DamageRolls::Average => {
619            damage = damage.floor() * 0.925;
620            crit_damage = crit_damage.floor() * 0.925;
621        }
622        DamageRolls::Min => {
623            damage = damage.floor() * 0.85;
624            crit_damage = crit_damage.floor() * 0.85;
625        }
626        DamageRolls::Max => {
627            damage = damage.floor();
628            crit_damage = crit_damage.floor();
629        }
630    }
631
632    Some((damage as i16, crit_damage as i16))
633}
634
635pub fn calculate_futuresight_damage(
636    attacking_side: &Side,
637    defending_side: &Side,
638    attacking_side_pokemon_index: &PokemonIndex,
639) -> i16 {
640    let attacking_stat = attacking_side.pokemon[attacking_side_pokemon_index].special_attack;
641    let defending_stat = defending_side.get_active_immutable().special_defense;
642    let attacker = attacking_side.get_active_immutable();
643    let mut damage = common_pkmn_damage_calc(
644        attacking_side,
645        attacker,
646        attacking_stat,
647        defending_side,
648        defending_side.get_active_immutable(),
649        defending_stat,
650        &Weather::NONE,
651        &Terrain::NONE,
652        MOVES.get(&Choices::FUTURESIGHT).unwrap(),
653    );
654    if attacker.ability != Abilities::INFILTRATOR {
655        if defending_side.side_conditions.light_screen > 0 {
656            damage *= 0.5
657        }
658    }
659
660    (damage * 0.925) as i16
661}
662
663#[cfg(test)]
664mod tests {
665    use std::collections::HashSet;
666    use std::iter::FromIterator;
667
668    use super::super::state::{PokemonVolatileStatus, Weather};
669    use super::*;
670    use crate::state::{PokemonStatus, PokemonType, SideReference, State};
671
672    #[test]
673    fn test_basic_damaging_move() {
674        let state = State::default();
675        let mut choice = Choice {
676            ..Default::default()
677        };
678        choice.move_id = Choices::TACKLE;
679        choice.move_type = PokemonType::TYPELESS;
680        choice.base_power = 40.0;
681        choice.category = MoveCategory::Physical;
682
683        let dmg = calculate_damage(
684            &state,
685            &SideReference::SideOne,
686            &choice,
687            DamageRolls::Average,
688        );
689
690        // level 100 tackle with 100 base stats across the board (attacker & defender)
691        assert_eq!(32, dmg.unwrap().0);
692    }
693
694    #[test]
695    fn test_basic_non_damaging_move() {
696        let state = State::default();
697        let mut choice = Choice {
698            ..Default::default()
699        };
700        choice.move_id = Choices::PROTECT;
701        choice.category = MoveCategory::Status;
702
703        let dmg = calculate_damage(
704            &state,
705            &SideReference::SideOne,
706            &choice,
707            DamageRolls::Average,
708        );
709
710        assert_eq!(None, dmg);
711    }
712
713    #[test]
714    fn test_move_with_zero_base_power() {
715        let state = State::default();
716        let mut choice = Choice {
717            ..Default::default()
718        };
719        choice.move_id = Choices::TACKLE;
720        choice.move_type = PokemonType::TYPELESS;
721        choice.base_power = 0.0;
722        choice.category = MoveCategory::Physical;
723
724        let dmg = calculate_damage(
725            &state,
726            &SideReference::SideOne,
727            &choice,
728            DamageRolls::Average,
729        );
730
731        assert_eq!(0, dmg.unwrap().0);
732    }
733
734    #[test]
735    fn test_boosted_damaging_move() {
736        let mut state = State::default();
737        let mut choice = Choice {
738            ..Default::default()
739        };
740        state.side_one.attack_boost = 1;
741        choice.move_id = Choices::TACKLE;
742        choice.move_type = PokemonType::TYPELESS;
743        choice.base_power = 40.0;
744        choice.category = MoveCategory::Physical;
745
746        let dmg = calculate_damage(
747            &state,
748            &SideReference::SideOne,
749            &choice,
750            DamageRolls::Average,
751        );
752
753        assert_eq!(48, dmg.unwrap().0);
754    }
755
756    #[test]
757    fn test_unaware_does_not_get_damaged_by_boosted_stats() {
758        let mut state = State::default();
759        let mut choice = Choice {
760            ..Default::default()
761        };
762        state.side_one.attack_boost = 1;
763        state.side_two.get_active().ability = Abilities::UNAWARE;
764        choice.move_id = Choices::TACKLE;
765        choice.move_type = PokemonType::TYPELESS;
766        choice.base_power = 40.0;
767        choice.category = MoveCategory::Physical;
768
769        let dmg = calculate_damage(
770            &state,
771            &SideReference::SideOne,
772            &choice,
773            DamageRolls::Average,
774        );
775
776        assert_eq!(32, dmg.unwrap().0);
777    }
778
779    #[test]
780    fn test_unaware_does_get_damaged_by_boosted_stats_if_attacker_has_moldbreaker() {
781        let mut state = State::default();
782        let mut choice = Choice {
783            ..Default::default()
784        };
785        state.side_one.attack_boost = 1;
786        state.side_two.get_active().ability = Abilities::UNAWARE;
787        state.side_one.get_active().ability = Abilities::MOLDBREAKER;
788        choice.move_id = Choices::TACKLE;
789        choice.move_type = PokemonType::TYPELESS;
790        choice.base_power = 40.0;
791        choice.category = MoveCategory::Physical;
792
793        let dmg = calculate_damage(
794            &state,
795            &SideReference::SideOne,
796            &choice,
797            DamageRolls::Average,
798        );
799
800        assert_eq!(48, dmg.unwrap().0);
801    }
802
803    #[test]
804    fn test_basic_super_effective_move() {
805        let mut state = State::default();
806        let mut choice = Choice {
807            ..Default::default()
808        };
809
810        state.side_two.get_active().types = (PokemonType::FIRE, PokemonType::TYPELESS);
811        choice.move_id = Choices::WATERGUN;
812        choice.move_type = PokemonType::WATER;
813        choice.base_power = 40.0;
814        choice.category = MoveCategory::Special;
815        let dmg = calculate_damage(
816            &state,
817            &SideReference::SideOne,
818            &choice,
819            DamageRolls::Average,
820        );
821
822        assert_eq!(64, dmg.unwrap().0);
823    }
824
825    #[test]
826    fn test_basic_not_very_effective_move() {
827        let mut state = State::default();
828        let mut choice = Choice {
829            ..Default::default()
830        };
831
832        state.side_two.get_active().types = (PokemonType::WATER, PokemonType::TYPELESS);
833        choice.move_id = Choices::WATERGUN;
834        choice.move_type = PokemonType::WATER;
835        choice.base_power = 40.0;
836        choice.category = MoveCategory::Special;
837        let dmg = calculate_damage(
838            &state,
839            &SideReference::SideOne,
840            &choice,
841            DamageRolls::Average,
842        );
843
844        assert_eq!(15, dmg.unwrap().0);
845    }
846
847    macro_rules! weather_tests {
848        ($($name:ident: $value:expr,)*) => {
849            $(
850                #[test]
851                fn $name() {
852                    let (weather_type, move_type, expected_damage_amount) = $value;
853                    let mut state = State::default();
854                    let mut choice = Choice {
855                        ..Default::default()
856                    };
857                    state.weather.weather_type = weather_type;
858
859                    choice.move_type = move_type;
860                    choice.base_power = 40.0;
861                    choice.category = MoveCategory::Special;
862                    let dmg = calculate_damage(&state, &SideReference::SideOne, &choice, DamageRolls::Average);
863
864                    assert_eq!(expected_damage_amount, dmg.unwrap().0);
865                }
866             )*
867        }
868    }
869    weather_tests! {
870        test_rain_boosting_water: (Weather::RAIN, PokemonType::WATER, 48),
871        test_rain_not_boosting_normal: (Weather::RAIN, PokemonType::NORMAL, 48),
872        test_sun_boosting_fire: (Weather::SUN, PokemonType::FIRE, 48),
873        test_sun_reducing_water: (Weather::SUN, PokemonType::WATER, 15),
874        test_sun_not_boosting_normal: (Weather::SUN, PokemonType::NORMAL, 48),
875        test_heavy_rain_makes_fire_do_zero: (Weather::HEAVYRAIN, PokemonType::FIRE, 0),
876        test_heavy_rain_boost_water: (Weather::HEAVYRAIN, PokemonType::WATER, 48),
877        test_harsh_sun_makes_water_do_zero: (Weather::HARSHSUN, PokemonType::WATER, 0),
878        test_harsh_sun_boosting_fire: (Weather::HARSHSUN, PokemonType::FIRE, 48),
879    }
880
881    macro_rules! stab_tests {
882        ($($name:ident: $value:expr,)*) => {
883            $(
884                #[test]
885                fn $name() {
886                    let (attacker_types, attacking_move_type, expected_damage_amount) = $value;
887                    let mut state = State::default();
888                    let mut choice = Choice {
889                        ..Default::default()
890                    };
891                    state.side_one.get_active().types = attacker_types;
892
893                    choice.move_type = attacking_move_type;
894                    choice.base_power = 40.0;
895                    choice.category = MoveCategory::Special;
896                    let dmg = calculate_damage(&state, &SideReference::SideOne, &choice, DamageRolls::Average);
897
898                    assert_eq!(expected_damage_amount, dmg.unwrap().0);
899                }
900             )*
901        }
902    }
903    stab_tests! {
904        test_basic_stab: ((PokemonType::WATER, PokemonType::FIRE), PokemonType::WATER, 48),
905        test_basic_without_stab: ((PokemonType::WATER, PokemonType::FIRE), PokemonType::NORMAL, 32),
906    }
907
908    macro_rules! burn_tests {
909        ($($name:ident: $value:expr,)*) => {
910            $(
911                #[test]
912                fn $name() {
913                    let (attacking_move_category, expected_damage_amount) = $value;
914                    let mut state = State::default();
915                    let mut choice = Choice {
916                        ..Default::default()
917                    };
918                    state.side_one.get_active().status = PokemonStatus::BURN;
919
920                    choice.category = attacking_move_category;
921                    choice.move_type = PokemonType::TYPELESS;
922                    choice.base_power = 40.0;
923                    let dmg = calculate_damage(&state, &SideReference::SideOne, &choice, DamageRolls::Average);
924
925                    assert_eq!(expected_damage_amount, dmg.unwrap().0);
926                }
927             )*
928        }
929    }
930    burn_tests! {
931        test_physical_move_when_burned_reduces: (MoveCategory::Physical, 15),
932        test_special_move_when_burned_does_not_reduce: (MoveCategory::Special, 32),
933    }
934
935    macro_rules! screens_tests {
936        ($($name:ident: $value:expr,)*) => {
937            $(
938                #[test]
939                fn $name() {
940                    let (reflect_count, lightscreen_count, auroraveil_count, move_category, expected_damage_amount) = $value;
941                    let mut state = State::default();
942                    let mut choice = Choice {
943                        ..Default::default()
944                    };
945                    state.side_two.side_conditions.reflect = reflect_count;
946                    state.side_two.side_conditions.light_screen = lightscreen_count;
947                    state.side_two.side_conditions.aurora_veil = auroraveil_count;
948
949                    choice.category = move_category;
950                    choice.base_power = 40.0;
951                    choice.move_type = PokemonType::TYPELESS;
952                    let dmg = calculate_damage(&state, &SideReference::SideOne, &choice, DamageRolls::Average);
953
954                    assert_eq!(expected_damage_amount, dmg.unwrap().0);
955                }
956             )*
957        }
958    }
959    screens_tests! {
960        test_reflect_reduces_physical_damage_by_half: (1, 0, 0, MoveCategory::Physical, 15),
961        test_lightscreen_reduces_special_damage_by_half: (0, 1, 0, MoveCategory::Special, 15),
962        test_auroraveil_reduces_physical_damage_by_half: (0, 0, 1, MoveCategory::Physical, 15),
963        test_auroraveil_reduces_special_damage_by_half: (0, 0, 1, MoveCategory::Special, 15),
964        test_reflect_does_not_reduce_special_damage: (1, 0, 0, MoveCategory::Special, 32),
965        test_light_screen_does_not_reduce_physical_damage: (0, 1, 0, MoveCategory::Physical, 32),
966        test_auroraveil_does_not_stack_with_reflect: (1, 1, 1, MoveCategory::Physical, 15),
967        test_auroraveil_does_not_stack_with_lightscreen: (1, 1, 1, MoveCategory::Special, 15),
968    }
969
970    macro_rules! volatile_status_tests{
971        ($($name:ident: $value:expr,)*) => {
972            $(
973                #[test]
974                fn $name() {
975                    let (attacking_volatile_status, defending_volatile_status, move_type, move_name, expected_damage_amount) = $value;
976                    let mut state = State::default();
977                    let mut choice = Choice {
978                        ..Default::default()
979                    };
980                    state.side_one.volatile_statuses = HashSet::from_iter(attacking_volatile_status);
981                    state.side_two.volatile_statuses = HashSet::from_iter(defending_volatile_status);
982
983                    choice.move_id = move_name;
984                    choice.category = MoveCategory::Physical;
985                    choice.move_type = move_type;
986                    choice.base_power = 40.0;
987                    let dmg = calculate_damage(&state, &SideReference::SideOne, &choice, DamageRolls::Average);
988
989                    assert_eq!(expected_damage_amount, dmg.unwrap().0);
990                }
991             )*
992        }
993    }
994    volatile_status_tests! {
995        test_flashfire_boosts_fire_move: (
996            vec![PokemonVolatileStatus::FLASHFIRE],
997            vec![],
998            PokemonType::FIRE,
999            Choices::NONE,
1000            48
1001        ),
1002        test_flashfire_does_not_boost_normal_move: (
1003            vec![PokemonVolatileStatus::FLASHFIRE],
1004            vec![],
1005            PokemonType::TYPELESS,
1006            Choices::NONE,
1007            32
1008        ),
1009        test_magnetrise_makes_pkmn_immune_to_ground_move: (
1010            vec![],
1011            vec![PokemonVolatileStatus::MAGNETRISE],
1012            PokemonType::GROUND,
1013            Choices::NONE,
1014            0
1015        ),
1016        test_thousandarrows_can_hit_magnetrise_pokemon: (
1017            vec![],
1018            vec![PokemonVolatileStatus::MAGNETRISE],
1019            PokemonType::GROUND,
1020            Choices::THOUSANDARROWS,
1021            32
1022        ),
1023        test_tarshot_boosts_fire_move: (
1024            vec![],
1025            vec![PokemonVolatileStatus::TARSHOT],
1026            PokemonType::FIRE,
1027            Choices::NONE,
1028            64
1029        ),
1030        test_slowstart_halves_move: (
1031            vec![PokemonVolatileStatus::SLOWSTART],
1032            vec![],
1033            PokemonType::NORMAL,
1034            Choices::NONE,
1035            24
1036        ),
1037        test_tarshot_and_flashfire_together: (
1038            vec![PokemonVolatileStatus::FLASHFIRE],
1039            vec![PokemonVolatileStatus::TARSHOT],
1040            PokemonType::FIRE,
1041            Choices::NONE,
1042            97
1043        ),
1044        test_glaiverush_doubles_damage_against: (
1045            vec![],
1046            vec![PokemonVolatileStatus::GLAIVERUSH],
1047            PokemonType::NORMAL,
1048            Choices::NONE,
1049            97
1050        ),
1051        test_phantomforce_on_defender_causes_0_damage: (
1052            vec![],
1053            vec![PokemonVolatileStatus::PHANTOMFORCE],
1054            PokemonType::NORMAL,
1055            Choices::NONE,
1056            0
1057        ),
1058    }
1059}