Fix fixed point damage calculation off-by-1s (#5775)

Co-authored-by: sbird <sbird@no.tld>
This commit is contained in:
Philipp AUER 2024-12-05 06:35:56 -05:00 committed by GitHub
parent a92350fb53
commit 7744298788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 172 additions and 137 deletions

View file

@ -12,7 +12,8 @@ typedef u32 uq4_12_t;
// Converts a number to Q4.12 fixed-point format
#define Q_4_12(n) ((q4_12_t)((n) * 4096))
#define UQ_4_12(n) ((uq4_12_t)((n) * 4096))
#define UQ_4_12(n) ((uq4_12_t)((n) * 4096 + 0.5))
#define UQ_4_12_FLOORED(n) ((uq4_12_t)((n) * 4096))
// Converts a number to Q24.8 fixed-point format
#define Q_24_8(n) ((s32)((n) << 8))

View file

@ -13,6 +13,8 @@
#define COMMON_DATA __attribute__((section("common_data")))
#define UNUSED __attribute__((unused))
#define ARM_FUNC __attribute__((target("arm")))
#if MODERN
#define NOINLINE __attribute__((noinline))
#else

View file

@ -151,7 +151,7 @@ u16 GetNonDynamaxHP(u32 battler)
return gBattleMons[battler].hp;
else
{
u16 mult = UQ_4_12(1.0/1.5); // placeholder
u16 mult = UQ_4_12_FLOORED(1.0/1.5); // placeholder
u16 hp = UQ_4_12_TO_INT((gBattleMons[battler].hp * mult) + UQ_4_12_ROUND);
return hp;
}
@ -164,7 +164,7 @@ u16 GetNonDynamaxMaxHP(u32 battler)
return gBattleMons[battler].maxHP;
else
{
u16 mult = UQ_4_12(1.0/1.5); // placeholder
u16 mult = UQ_4_12_FLOORED(1.0/1.5); // placeholder
u16 maxHP = UQ_4_12_TO_INT((gBattleMons[battler].maxHP * mult) + UQ_4_12_ROUND);
return maxHP;
}
@ -202,7 +202,7 @@ void UndoDynamax(u32 battler)
if (GetActiveGimmick(battler) == GIMMICK_DYNAMAX)
{
struct Pokemon *mon = (side == B_SIDE_PLAYER) ? &gPlayerParty[monId] : &gEnemyParty[monId];
u16 mult = UQ_4_12(1.0/1.5); // placeholder
u16 mult = UQ_4_12_FLOORED(1.0/1.5); // placeholder
gBattleMons[battler].hp = UQ_4_12_TO_INT((GetMonData(mon, MON_DATA_HP) * mult + 1) + UQ_4_12_ROUND); // round up
SetMonData(mon, MON_DATA_HP, &gBattleMons[battler].hp);
CalculateMonStats(mon);

View file

@ -62,6 +62,8 @@ static u32 GetFlingPowerFromItemId(u32 itemId);
static void SetRandomMultiHitCounter();
static u32 GetBattlerItemHoldEffectParam(u32 battler, u32 item);
static bool32 CanBeInfinitelyConfused(u32 battler);
ARM_FUNC NOINLINE static uq4_12_t PercentToUQ4_12(u32 percent);
ARM_FUNC NOINLINE static uq4_12_t PercentToUQ4_12_Floored(u32 percent);
extern const u8 *const gBattlescriptsForRunningByItem[];
extern const u8 *const gBattlescriptsForUsingItem[];
@ -757,113 +759,18 @@ static const u8 sHoldEffectToType[][2] =
{HOLD_EFFECT_FAIRY_POWER, TYPE_FAIRY},
};
// percent in UQ_4_12 format
static const uq4_12_t sPercentToModifier[] =
{
UQ_4_12(0.00), // 0
UQ_4_12(0.01), // 1
UQ_4_12(0.02), // 2
UQ_4_12(0.03), // 3
UQ_4_12(0.04), // 4
UQ_4_12(0.05), // 5
UQ_4_12(0.06), // 6
UQ_4_12(0.07), // 7
UQ_4_12(0.08), // 8
UQ_4_12(0.09), // 9
UQ_4_12(0.10), // 10
UQ_4_12(0.11), // 11
UQ_4_12(0.12), // 12
UQ_4_12(0.13), // 13
UQ_4_12(0.14), // 14
UQ_4_12(0.15), // 15
UQ_4_12(0.16), // 16
UQ_4_12(0.17), // 17
UQ_4_12(0.18), // 18
UQ_4_12(0.19), // 19
UQ_4_12(0.20), // 20
UQ_4_12(0.21), // 21
UQ_4_12(0.22), // 22
UQ_4_12(0.23), // 23
UQ_4_12(0.24), // 24
UQ_4_12(0.25), // 25
UQ_4_12(0.26), // 26
UQ_4_12(0.27), // 27
UQ_4_12(0.28), // 28
UQ_4_12(0.29), // 29
UQ_4_12(0.30), // 30
UQ_4_12(0.31), // 31
UQ_4_12(0.32), // 32
UQ_4_12(0.33), // 33
UQ_4_12(0.34), // 34
UQ_4_12(0.35), // 35
UQ_4_12(0.36), // 36
UQ_4_12(0.37), // 37
UQ_4_12(0.38), // 38
UQ_4_12(0.39), // 39
UQ_4_12(0.40), // 40
UQ_4_12(0.41), // 41
UQ_4_12(0.42), // 42
UQ_4_12(0.43), // 43
UQ_4_12(0.44), // 44
UQ_4_12(0.45), // 45
UQ_4_12(0.46), // 46
UQ_4_12(0.47), // 47
UQ_4_12(0.48), // 48
UQ_4_12(0.49), // 49
UQ_4_12(0.50), // 50
UQ_4_12(0.51), // 51
UQ_4_12(0.52), // 52
UQ_4_12(0.53), // 53
UQ_4_12(0.54), // 54
UQ_4_12(0.55), // 55
UQ_4_12(0.56), // 56
UQ_4_12(0.57), // 57
UQ_4_12(0.58), // 58
UQ_4_12(0.59), // 59
UQ_4_12(0.60), // 60
UQ_4_12(0.61), // 61
UQ_4_12(0.62), // 62
UQ_4_12(0.63), // 63
UQ_4_12(0.64), // 64
UQ_4_12(0.65), // 65
UQ_4_12(0.66), // 66
UQ_4_12(0.67), // 67
UQ_4_12(0.68), // 68
UQ_4_12(0.69), // 69
UQ_4_12(0.70), // 70
UQ_4_12(0.71), // 71
UQ_4_12(0.72), // 72
UQ_4_12(0.73), // 73
UQ_4_12(0.74), // 74
UQ_4_12(0.75), // 75
UQ_4_12(0.76), // 76
UQ_4_12(0.77), // 77
UQ_4_12(0.78), // 78
UQ_4_12(0.79), // 79
UQ_4_12(0.80), // 80
UQ_4_12(0.81), // 81
UQ_4_12(0.82), // 82
UQ_4_12(0.83), // 83
UQ_4_12(0.84), // 84
UQ_4_12(0.85), // 85
UQ_4_12(0.86), // 86
UQ_4_12(0.87), // 87
UQ_4_12(0.88), // 88
UQ_4_12(0.89), // 89
UQ_4_12(0.90), // 90
UQ_4_12(0.91), // 91
UQ_4_12(0.92), // 92
UQ_4_12(0.93), // 93
UQ_4_12(0.94), // 94
UQ_4_12(0.95), // 95
UQ_4_12(0.96), // 96
UQ_4_12(0.97), // 97
UQ_4_12(0.98), // 98
UQ_4_12(0.99), // 99
UQ_4_12(1.00), // 100
};
// code
ARM_FUNC NOINLINE static uq4_12_t PercentToUQ4_12(u32 percent)
{
return (4096 * percent + 50) / 100;
}
ARM_FUNC NOINLINE static uq4_12_t PercentToUQ4_12_Floored(u32 percent)
{
return (4096 * percent) / 100;
}
u8 GetBattlerForBattleScript(u8 caseId)
{
u8 ret = 0;
@ -4136,7 +4043,7 @@ static inline u8 GetBattlerSideFaintCounter(u32 battler)
// Supreme Overlord adds a x0.1 damage boost for each fainted ally.
static inline uq4_12_t GetSupremeOverlordModifier(u32 battler)
{
return UQ_4_12(1.0) + (UQ_4_12(0.1) * gBattleStruct->supremeOverlordCounter[battler]);
return UQ_4_12(1.0) + (PercentToUQ4_12(gBattleStruct->supremeOverlordCounter[battler] * 10));
}
static inline bool32 HadMoreThanHalfHpNowDoesnt(u32 battler)
@ -9302,7 +9209,7 @@ static inline u32 CalcMoveBasePowerAfterModifiers(struct DamageCalculationData *
if (gProtectStructs[battlerAtk].helpingHand)
modifier = uq4_12_multiply(modifier, UQ_4_12(1.5));
if (gSpecialStatuses[battlerAtk].gemBoost)
modifier = uq4_12_multiply(modifier, UQ_4_12(1.0) + sPercentToModifier[gSpecialStatuses[battlerAtk].gemParam]);
modifier = uq4_12_multiply(modifier, uq4_12_add(UQ_4_12(1.0), PercentToUQ4_12(gSpecialStatuses[battlerAtk].gemParam)));
if (gStatuses3[battlerAtk] & STATUS3_CHARGED_UP && moveType == TYPE_ELECTRIC)
modifier = uq4_12_multiply(modifier, UQ_4_12(2.0));
if (gStatuses3[battlerAtk] & STATUS3_ME_FIRST)
@ -9491,18 +9398,18 @@ static inline u32 CalcMoveBasePowerAfterModifiers(struct DamageCalculationData *
if (holdEffectParamAtk > 100)
holdEffectParamAtk = 100;
holdEffectModifier = UQ_4_12(1.0) + sPercentToModifier[holdEffectParamAtk];
holdEffectModifier = uq4_12_add(UQ_4_12(1.0), PercentToUQ4_12(holdEffectParamAtk));
// attacker's hold effect
switch (holdEffectAtk)
{
case HOLD_EFFECT_MUSCLE_BAND:
if (IS_MOVE_PHYSICAL(move))
modifier = uq4_12_multiply(modifier, holdEffectModifier);
modifier = uq4_12_multiply(modifier, uq4_12_add(UQ_4_12(1.0), PercentToUQ4_12_Floored(holdEffectParamAtk)));
break;
case HOLD_EFFECT_WISE_GLASSES:
if (IS_MOVE_SPECIAL(move))
modifier = uq4_12_multiply(modifier, holdEffectModifier);
modifier = uq4_12_multiply(modifier, uq4_12_add(UQ_4_12(1.0), PercentToUQ4_12_Floored(holdEffectParamAtk)));
break;
case HOLD_EFFECT_LUSTROUS_ORB:
if (GET_BASE_SPECIES_ID(gBattleMons[battlerAtk].species) == SPECIES_PALKIA && (moveType == TYPE_WATER || moveType == TYPE_DRAGON))
@ -9768,11 +9675,11 @@ static inline u32 CalcAttackStat(struct DamageCalculationData *damageCalcData, u
break;
case ABILITY_ORICHALCUM_PULSE:
if ((weather & B_WEATHER_SUN) && WEATHER_HAS_EFFECT && IS_MOVE_PHYSICAL(move))
modifier = uq4_12_multiply(modifier, UQ_4_12(1.33));
modifier = uq4_12_multiply(modifier, UQ_4_12(1.3333));
break;
case ABILITY_HADRON_ENGINE:
if (gFieldStatuses & STATUS_FIELD_ELECTRIC_TERRAIN && IS_MOVE_SPECIAL(move))
modifier = uq4_12_multiply(modifier, UQ_4_12(1.33));
modifier = uq4_12_multiply(modifier, UQ_4_12(1.3333));
break;
}
@ -10234,19 +10141,23 @@ static inline uq4_12_t GetDefenderPartnerAbilitiesModifier(u32 battlerPartnerDef
static inline uq4_12_t GetAttackerItemsModifier(u32 battlerAtk, uq4_12_t typeEffectivenessModifier, u32 holdEffectAtk)
{
u32 percentBoost;
u32 metronomeTurns;
uq4_12_t metronomeBoostBase;
switch (holdEffectAtk)
{
case HOLD_EFFECT_METRONOME:
percentBoost = min((gBattleStruct->sameMoveTurns[battlerAtk] * GetBattlerHoldEffectParam(battlerAtk)), 100);
return uq4_12_add(sPercentToModifier[percentBoost], UQ_4_12(1.0));
metronomeBoostBase = PercentToUQ4_12(GetBattlerHoldEffectParam(battlerAtk));
metronomeTurns = min(gBattleStruct->sameMoveTurns[battlerAtk], 5);
// according to bulbapedia this is the "correct" way to calculate the metronome boost
// due to the limited domain of damage numbers it will never really matter whether this is off by one
return uq4_12_add(UQ_4_12(1.0), metronomeBoostBase * metronomeTurns);
break;
case HOLD_EFFECT_EXPERT_BELT:
if (typeEffectivenessModifier >= UQ_4_12(2.0))
return UQ_4_12(1.2);
break;
case HOLD_EFFECT_LIFE_ORB:
return UQ_4_12(1.3);
return UQ_4_12_FLOORED(1.3);
break;
}
return UQ_4_12(1.0);

View file

@ -4,7 +4,7 @@
#include "global.h"
#include "test/battle.h"
SINGLE_BATTLE_TEST("Transistor increases Electric-type move damage", s16 damage)
SINGLE_BATTLE_TEST("Transistor increases Electric-type attack / special attack", s16 damage)
{
u32 move;
u16 ability;
@ -30,20 +30,13 @@ SINGLE_BATTLE_TEST("Transistor increases Electric-type move damage", s16 damage)
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_EQ(results[0].damage, results[1].damage); // Tackle should be unaffected
if (B_TRANSISTOR_BOOST >= GEN_9)
{
EXPECT_MUL_EQ(results[2].damage, Q_4_12(1.3), results[3].damage); // Wild Charge should be affected
EXPECT_MUL_EQ(results[4].damage, Q_4_12(1.3), results[5].damage); // Thunder Shock should be affected
}
else
{
EXPECT_MUL_EQ(results[2].damage, Q_4_12(1.5), results[3].damage); // Wild Charge should be affected
EXPECT_MUL_EQ(results[4].damage, Q_4_12(1.5), results[5].damage); // Thunder Shock should be affected
}
EXPECT_LT(results[2].damage, results[3].damage); // cannot test exact factor because ATK / SPATK introduces inaccuracies
EXPECT_LT(results[4].damage, results[5].damage);
}
}
SINGLE_BATTLE_TEST("Transistor boosts Electric type moves by 1.5 in Gen8 and 1.3 in Gen9+", s16 damage)
SINGLE_BATTLE_TEST("Transistor is blocked by neutralizing gas", s16 damage)
{
u16 ability;
PARAMETRIZE { ability = ABILITY_NEUTRALIZING_GAS; }
@ -58,9 +51,6 @@ SINGLE_BATTLE_TEST("Transistor boosts Electric type moves by 1.5 in Gen8 and 1.3
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
if (B_TRANSISTOR_BOOST >= GEN_9)
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.3), results[1].damage);
else
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
EXPECT_LT(results[0].damage, results[1].damage); // cannot test exact factor because ATK / SPATK introduces inaccuracies
}
}

View file

@ -150,3 +150,134 @@ DOUBLE_BATTLE_TEST("A spread move will do correct damage to the second mon if th
EXPECT_EQ(damage[4], damage[5]);
}
}
SINGLE_BATTLE_TEST("Punching Glove vs Muscle Band Damage calculation")
{
s16 dmgPlayer, dmgOpponent;
s16 expectedDamagePlayer, expectedDamageOpponent;
PARAMETRIZE { expectedDamagePlayer = 204, expectedDamageOpponent = 201; }
PARAMETRIZE { expectedDamagePlayer = 201, expectedDamageOpponent = 198; }
PARAMETRIZE { expectedDamagePlayer = 199, expectedDamageOpponent = 196; }
PARAMETRIZE { expectedDamagePlayer = 196, expectedDamageOpponent = 193; }
PARAMETRIZE { expectedDamagePlayer = 195, expectedDamageOpponent = 192; }
PARAMETRIZE { expectedDamagePlayer = 193, expectedDamageOpponent = 190; }
PARAMETRIZE { expectedDamagePlayer = 190, expectedDamageOpponent = 187; }
PARAMETRIZE { expectedDamagePlayer = 189, expectedDamageOpponent = 186; }
PARAMETRIZE { expectedDamagePlayer = 187, expectedDamageOpponent = 184; }
PARAMETRIZE { expectedDamagePlayer = 184, expectedDamageOpponent = 181; }
PARAMETRIZE { expectedDamagePlayer = 183, expectedDamageOpponent = 180; }
PARAMETRIZE { expectedDamagePlayer = 181, expectedDamageOpponent = 178; }
PARAMETRIZE { expectedDamagePlayer = 178, expectedDamageOpponent = 175; }
PARAMETRIZE { expectedDamagePlayer = 177, expectedDamageOpponent = 174; }
PARAMETRIZE { expectedDamagePlayer = 174, expectedDamageOpponent = 172; }
PARAMETRIZE { expectedDamagePlayer = 172, expectedDamageOpponent = 169; }
GIVEN {
PLAYER(SPECIES_MAKUHITA) { Item(ITEM_PUNCHING_GLOVE); }
OPPONENT(SPECIES_MAKUHITA) { Item(ITEM_MUSCLE_BAND); }
} WHEN {
TURN {
MOVE(player, MOVE_DRAIN_PUNCH, WITH_RNG(RNG_DAMAGE_MODIFIER, i));
MOVE(opponent, MOVE_DRAIN_PUNCH, WITH_RNG(RNG_DAMAGE_MODIFIER, i));
}
}
SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DRAIN_PUNCH, player);
HP_BAR(opponent, captureDamage: &dmgPlayer);
ANIMATION(ANIM_TYPE_MOVE, MOVE_DRAIN_PUNCH, opponent);
HP_BAR(player, captureDamage: &dmgOpponent);
}
THEN {
EXPECT_EQ(expectedDamagePlayer, dmgPlayer);
EXPECT_EQ(expectedDamageOpponent, dmgOpponent);
}
}
SINGLE_BATTLE_TEST("Gem boosted Damage calculation")
{
s16 dmg;
s16 expectedDamage;
PARAMETRIZE { expectedDamage = 240; }
PARAMETRIZE { expectedDamage = 237; }
PARAMETRIZE { expectedDamage = 234; }
PARAMETRIZE { expectedDamage = 232; }
PARAMETRIZE { expectedDamage = 229; }
PARAMETRIZE { expectedDamage = 228; }
PARAMETRIZE { expectedDamage = 225; }
PARAMETRIZE { expectedDamage = 222; }
PARAMETRIZE { expectedDamage = 220; }
PARAMETRIZE { expectedDamage = 217; }
PARAMETRIZE { expectedDamage = 216; }
PARAMETRIZE { expectedDamage = 213; }
PARAMETRIZE { expectedDamage = 210; }
PARAMETRIZE { expectedDamage = 208; }
PARAMETRIZE { expectedDamage = 205; }
PARAMETRIZE { expectedDamage = 204; }
GIVEN {
PLAYER(SPECIES_MAKUHITA) { Item(ITEM_FIGHTING_GEM); }
OPPONENT(SPECIES_MAKUHITA);
} WHEN {
TURN {
MOVE(player, MOVE_DRAIN_PUNCH, WITH_RNG(RNG_DAMAGE_MODIFIER, i));
}
}
SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DRAIN_PUNCH, player);
HP_BAR(opponent, captureDamage: &dmg);
}
THEN {
EXPECT_EQ(expectedDamage, dmg);
}
}
#define NUM_DAMAGE_SPREADS (DMG_ROLL_PERCENT_HI - DMG_ROLL_PERCENT_LO) + 1
static const s16 sThunderShockTransistorSpread[] = { 54, 55, 56, 57, 57, 58, 59, 60, 60, 60, 61, 62, 63, 63, 64, 65 };
static const s16 sThunderShockRegularSpread[] = { 42, 42, 43, 43, 44, 45, 45, 45, 46, 46, 47, 48, 48, 48, 49, 50 };
static const s16 sWildChargeTransistorSpread[] = { 123, 124, 126, 127, 129, 130, 132, 133, 135, 136, 138, 139, 141, 142, 144, 145 };
static const s16 sWildChargeRegularSpread[] = { 94, 96, 96, 98, 99, 100, 101, 102, 103, 105, 105, 107, 108, 109, 110, 111 };
DOUBLE_BATTLE_TEST("Transistor Damage calculation", s16 damage)
{
s16 expectedDamageTransistorSpec = 0, expectedDamageRegularPhys = 0, expectedDamageRegularSpec = 0, expectedDamageTransistorPhys = 0;
s16 damagePlayerLeft, damagePlayerRight, damageOpponentLeft, damageOpponentRight;
for (u32 spread = 0; spread < 16; ++spread) {
PARAMETRIZE { expectedDamageTransistorSpec = sThunderShockTransistorSpread[spread],
expectedDamageRegularSpec = sThunderShockRegularSpread[spread],
expectedDamageTransistorPhys = sWildChargeTransistorSpread[spread],
expectedDamageRegularPhys = sWildChargeRegularSpread[spread];
}
}
GIVEN {
ASSUME(gMovesInfo[MOVE_WILD_CHARGE].type == TYPE_ELECTRIC);
ASSUME(gMovesInfo[MOVE_THUNDER_SHOCK].type == TYPE_ELECTRIC);
ASSUME(gMovesInfo[MOVE_WILD_CHARGE].category == DAMAGE_CATEGORY_PHYSICAL);
ASSUME(gMovesInfo[MOVE_THUNDER_SHOCK].category == DAMAGE_CATEGORY_SPECIAL);
ASSUME(NUM_DAMAGE_SPREADS == 16);
PLAYER(SPECIES_REGIELEKI) { Ability(ABILITY_KLUTZ); }
PLAYER(SPECIES_REGIELEKI) { Ability(ABILITY_TRANSISTOR); }
OPPONENT(SPECIES_REGIELEKI) { Ability(ABILITY_KLUTZ); }
OPPONENT(SPECIES_REGIELEKI) { Ability(ABILITY_TRANSISTOR); }
} WHEN {
TURN {
MOVE(playerLeft, MOVE_THUNDER_SHOCK, target: opponentLeft, WITH_RNG(RNG_DAMAGE_MODIFIER, 15 - i));
MOVE(playerRight, MOVE_THUNDER_SHOCK, target: opponentRight, WITH_RNG(RNG_DAMAGE_MODIFIER, 15 - i));
MOVE(opponentLeft, MOVE_WILD_CHARGE, target: playerLeft, WITH_RNG(RNG_DAMAGE_MODIFIER, 15 - i));
MOVE(opponentRight, MOVE_WILD_CHARGE, target: playerRight, WITH_RNG(RNG_DAMAGE_MODIFIER, 15 - i));
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, playerLeft);
HP_BAR(opponentLeft, captureDamage: &damageOpponentLeft);
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, playerRight);
HP_BAR(opponentRight, captureDamage: &damageOpponentRight);
ANIMATION(ANIM_TYPE_MOVE, MOVE_WILD_CHARGE, opponentLeft);
HP_BAR(playerLeft, captureDamage: &damagePlayerLeft);
ANIMATION(ANIM_TYPE_MOVE, MOVE_WILD_CHARGE, opponentRight);
HP_BAR(playerRight, captureDamage: &damagePlayerRight);
} THEN {
EXPECT_EQ(damageOpponentLeft, expectedDamageRegularSpec);
EXPECT_EQ(damageOpponentRight, expectedDamageTransistorSpec);
EXPECT_EQ(damagePlayerLeft, expectedDamageRegularPhys);
EXPECT_EQ(damagePlayerRight, expectedDamageTransistorPhys);
}
}