Skip to content

Commit

Permalink
[battle, damage] refactor damage formula to match gen5+
Browse files Browse the repository at this point in the history
  • Loading branch information
sbird committed Aug 4, 2023
1 parent f94efa9 commit 384ce49
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 313 deletions.
7 changes: 0 additions & 7 deletions include/battle_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,12 @@ void BufferStatChange(u8 battlerId, u8 statId, u8 stringId);
bool32 BlocksPrankster(u16 move, u8 battlerPrankster, u8 battlerDef, bool32 checkTarget);
u16 GetUsedHeldItem(u8 battler);
bool32 IsBattlerWeatherAffected(u8 battlerId, u32 weatherFlags);
u32 ApplyWeatherDamageMultiplier(u8 battlerAtk, u16 move, u8 moveType, u32 dmg, u16 holdEffectAtk, u16 holdEffectDef);
u32 GetBattlerMoveTargetType(u8 battlerId, u16 move);
bool32 CanTargetBattler(u8 battlerAtk, u8 battlerDef, u16 move);
bool8 IsMoveAffectedByParentalBond(u16 move, u8 battlerId);
void CopyMonLevelAndBaseStatsToBattleMon(u32 battler, struct Pokemon *mon);
void CopyMonAbilityAndTypesToBattleMon(u32 battler, struct Pokemon *mon);
void RecalcBattlerStats(u32 battler, struct Pokemon *mon);
void MulModifier(u16 *modifier, u16 val);
bool32 IsAlly(u32 battlerAtk, u32 battlerDef);

// Ability checks
Expand Down Expand Up @@ -245,9 +243,4 @@ u8 GetBattlerGender(u8 battlerId);
bool8 AreBattlersOfOppositeGender(u8 battler1, u8 battler2);
u32 CalcSecondaryEffectChance(u8 battlerId, u8 secondaryEffectChance);

static inline u32 ApplyModifier(uq4_12_t modifier, u32 val)
{
return UQ_4_12_TO_INT((modifier * val) + UQ_4_12_ROUND);
}

#endif // GUARD_BATTLE_UTIL_H
20 changes: 20 additions & 0 deletions include/fpmath.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,30 @@ static inline uq4_12_t uq4_12_multiply(uq4_12_t a, uq4_12_t b)
return (product + UQ_4_12_ROUND) >> UQ_4_12_SHIFT;
}

static inline uq4_12_t uq4_12_multiply_half_down(uq4_12_t a, uq4_12_t b)
{
u32 product = (u32) a * b;
return (product + UQ_4_12_ROUND - 1) >> UQ_4_12_SHIFT;
}

static inline uq4_12_t uq4_12_divide(uq4_12_t dividend, uq4_12_t divisor)
{
if (divisor == UQ_4_12(0.0)) return UQ_4_12(0);
return (dividend << UQ_4_12_SHIFT) / divisor;
}

// Multiplies value by the UQ_4_12 number modifier.
// Returns an integer, rounded to nearest (rounding down on n.5)
static inline u32 uq4_12_multiply_by_int_half_down(uq4_12_t modifier, u32 value)
{
return UQ_4_12_TO_INT((modifier * value) + UQ_4_12_ROUND - 1);
}

// Multiplies value by the UQ_4_12 number modifier.
// Returns an integer, rounded to nearest (rounding up on n.5)
static inline u32 uq4_12_multiply_by_int_half_up(uq4_12_t modifier, u32 value)
{
return UQ_4_12_TO_INT((modifier * value) + UQ_4_12_ROUND);
}

#endif // FPMATH_H_
2 changes: 1 addition & 1 deletion src/battle_script_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -1414,7 +1414,7 @@ static void Cmd_attackcanceler(void)
return;
}

// Z-moves and Max Moves bypass protection, but deal reduced damage (factored in CalcFinalDmg)
// Z-moves and Max Moves bypass protection, but deal reduced damage (factored in AccumulateOtherModifiers)
if (gBattleStruct->zmove.active && IS_BATTLER_PROTECTED(gBattlerTarget))
{
BattleScriptPush(cmd->nextInstr);
Expand Down
699 changes: 396 additions & 303 deletions src/battle_util.c

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/ability_contrary.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ SINGLE_BATTLE_TEST("Contrary raises Attack when Intimidated", s16 damage)
HP_BAR(player, captureDamage: &results[i].damage);
}
FINALLY {
EXPECT_MUL_EQ(results[1].damage, Q_4_12(2.125), results[0].damage);
EXPECT_MUL_EQ(results[1].damage, Q_4_12(2.25), results[0].damage);
}
}

Expand Down
66 changes: 66 additions & 0 deletions test/ability_fluffy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include "global.h"
#include "test_battle.h"

ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TACKLE].makesContact);
ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
ASSUME(gBattleMoves[MOVE_TACKLE].makesContact);
ASSUME(gBattleMoves[MOVE_FIRE_PUNCH].makesContact);
ASSUME(gBattleMoves[MOVE_FIRE_PUNCH].type == TYPE_FIRE);
ASSUME(P_GEN_7_POKEMON == TRUE);
}

SINGLE_BATTLE_TEST("Fluffy halves damage taken from moves that make direct contact", s16 damage)
{
u32 ability;
PARAMETRIZE { ability = ABILITY_KLUTZ; }
PARAMETRIZE { ability = ABILITY_FLUFFY; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_STUFFUL) { Ability(ability); }
} WHEN {
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
MESSAGE("Wobbuffet used Tackle!");
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(0.5), results[1].damage);
}
}

SINGLE_BATTLE_TEST("Fluffy doubles damage taken from fire type moves", s16 damage)
{
u32 ability;
PARAMETRIZE { ability = ABILITY_KLUTZ; }
PARAMETRIZE { ability = ABILITY_FLUFFY; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_STUFFUL) { Ability(ability); }
} WHEN {
TURN { MOVE(player, MOVE_EMBER); }
} SCENE {
MESSAGE("Wobbuffet used Ember!");
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage);
}
}

SINGLE_BATTLE_TEST("Fluffy does not alter damage of fire-type moves that make direct contact", s16 damage)
{
u32 ability;
PARAMETRIZE { ability = ABILITY_KLUTZ; }
PARAMETRIZE { ability = ABILITY_FLUFFY; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_STUFFUL) { Ability(ability); }
} WHEN {
TURN { MOVE(player, MOVE_FIRE_PUNCH); }
} SCENE {
MESSAGE("Wobbuffet used Fire Punch!");
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_EQ(results[0].damage, results[1].damage);
}
}
78 changes: 78 additions & 0 deletions test/damage_formula.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#include "global.h"
#include "test_battle.h"

// From https://bulbapedia.bulbagarden.net/wiki/Damage#Example

SINGLE_BATTLE_TEST("Damage calculation matches Gen5+")
{
s16 dmg;
s16 expectedDamage;
PARAMETRIZE { expectedDamage = 196; }
PARAMETRIZE { expectedDamage = 192; }
PARAMETRIZE { expectedDamage = 192; }
PARAMETRIZE { expectedDamage = 192; }
PARAMETRIZE { expectedDamage = 184; }
PARAMETRIZE { expectedDamage = 184; }
PARAMETRIZE { expectedDamage = 184; }
PARAMETRIZE { expectedDamage = 180; }
PARAMETRIZE { expectedDamage = 180; }
PARAMETRIZE { expectedDamage = 180; }
PARAMETRIZE { expectedDamage = 172; }
PARAMETRIZE { expectedDamage = 172; }
PARAMETRIZE { expectedDamage = 172; }
PARAMETRIZE { expectedDamage = 168; }
PARAMETRIZE { expectedDamage = 168; }
PARAMETRIZE { expectedDamage = 168; }
GIVEN {
PLAYER(SPECIES_GLACEON) { Level(75); Attack(123); }
OPPONENT(SPECIES_GARCHOMP) { Defense(163); }
} WHEN {
TURN {
MOVE(player, MOVE_ICE_FANG, WITH_RNG(RNG_DAMAGE_MODIFIER, i));
}
}
SCENE{
MESSAGE("Glaceon used Ice Fang!");
HP_BAR(opponent, captureDamage: &dmg);
}
THEN{
EXPECT_EQ(expectedDamage, dmg);
}
}

SINGLE_BATTLE_TEST("Damage calculation matches Gen5+ (Muscle Band, crit)")
{
s16 dmg;
s16 expectedDamage;
PARAMETRIZE { expectedDamage = 324; }
PARAMETRIZE { expectedDamage = 316; }
PARAMETRIZE { expectedDamage = 312; }
PARAMETRIZE { expectedDamage = 312; }
PARAMETRIZE { expectedDamage = 304; }
PARAMETRIZE { expectedDamage = 304; }
PARAMETRIZE { expectedDamage = 300; }
PARAMETRIZE { expectedDamage = 300; }
PARAMETRIZE { expectedDamage = 292; }
PARAMETRIZE { expectedDamage = 292; }
PARAMETRIZE { expectedDamage = 288; }
PARAMETRIZE { expectedDamage = 288; }
PARAMETRIZE { expectedDamage = 280; }
PARAMETRIZE { expectedDamage = 276; }
PARAMETRIZE { expectedDamage = 276; }
PARAMETRIZE { expectedDamage = 268; }
GIVEN {
PLAYER(SPECIES_GLACEON) { Level(75); Attack(123); Item(ITEM_MUSCLE_BAND); }
OPPONENT(SPECIES_GARCHOMP) { Defense(163); }
} WHEN {
TURN {
MOVE(player, MOVE_ICE_FANG, WITH_RNG(RNG_DAMAGE_MODIFIER, i), criticalHit: TRUE);
}
}
SCENE{
MESSAGE("Glaceon used Ice Fang!");
HP_BAR(opponent, captureDamage: &dmg);
}
THEN{
EXPECT_EQ(expectedDamage, dmg);
}
}
90 changes: 90 additions & 0 deletions test/status3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "global.h"
#include "test_battle.h"

ASSUMPTIONS {
ASSUME(gBattleMoves[MOVE_MINIMIZE].effect == EFFECT_MINIMIZE);
ASSUME(gBattleMoves[MOVE_STEAMROLLER].minimizeDoubleDamage);
ASSUME(gBattleMoves[MOVE_EARTHQUAKE].damagesUnderground);
ASSUME(gBattleMoves[MOVE_SURF].damagesUnderwater);
ASSUME(gBattleMoves[MOVE_TWISTER].damagesAirborneDoubleDamage);
}

SINGLE_BATTLE_TEST("Minimize causes the target to take double damage from certain moves", s16 damage)
{
bool32 useMinimize;
PARAMETRIZE { useMinimize = FALSE; }
PARAMETRIZE { useMinimize = TRUE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(2); }
} WHEN {
if (useMinimize)
TURN { MOVE(opponent, MOVE_MINIMIZE); MOVE(player, MOVE_STEAMROLLER); }
else
TURN { MOVE(player, MOVE_STEAMROLLER); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage);
}
}

SINGLE_BATTLE_TEST("Being underground causes the target to take double damage from certain moves", s16 damage)
{
bool32 useDig;
PARAMETRIZE { useDig = FALSE; }
PARAMETRIZE { useDig = TRUE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(2); }
} WHEN {
if (useDig)
TURN { MOVE(opponent, MOVE_DIG); MOVE(player, MOVE_EARTHQUAKE); }
else
TURN { MOVE(player, MOVE_EARTHQUAKE); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage);
}
}

SINGLE_BATTLE_TEST("Being underwater causes the target to take double damage from certain moves", s16 damage)
{
bool32 useDive;
PARAMETRIZE { useDive = FALSE; }
PARAMETRIZE { useDive = TRUE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(2); }
} WHEN {
if (useDive)
TURN { MOVE(opponent, MOVE_DIVE); MOVE(player, MOVE_SURF); }
else
TURN { MOVE(player, MOVE_SURF); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage);
}
}

SINGLE_BATTLE_TEST("Being airborne causes the target to take double damage from certain moves", s16 damage)
{
bool32 useDive;
PARAMETRIZE { useDive = FALSE; }
PARAMETRIZE { useDive = TRUE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(2); }
} WHEN {
if (useDive)
TURN { MOVE(opponent, MOVE_FLY); MOVE(player, MOVE_TWISTER); }
else
TURN { MOVE(player, MOVE_TWISTER); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage);
}
}
1 change: 0 additions & 1 deletion test/weather_snow.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ SINGLE_BATTLE_TEST("Snow halves the power of Solar Beam", s16 damage)
SINGLE_BATTLE_TEST("Snow halves the power of Solar Blade", s16 damage)
{
u16 move;
KNOWN_FAILING; // fails bc the bp of solar blade gets rounded up which leads to slightly incorrect calcs down the line
PARAMETRIZE{ move = MOVE_CELEBRATE; }
PARAMETRIZE{ move = MOVE_SNOWSCAPE; }
GIVEN {
Expand Down

0 comments on commit 384ce49

Please sign in to comment.