poke_engine/
damage_calc.rs

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