From 77442987888fffbdbdf9c7ff5f760cd9bdef0ff0 Mon Sep 17 00:00:00 2001 From: Philipp AUER Date: Thu, 5 Dec 2024 06:35:56 -0500 Subject: [PATCH] Fix fixed point damage calculation off-by-1s (#5775) Co-authored-by: sbird --- include/fpmath.h | 3 +- include/gba/defines.h | 2 + src/battle_dynamax.c | 6 +- src/battle_util.c | 145 ++++++------------------------- test/battle/ability/transistor.c | 22 ++--- test/battle/damage_formula.c | 131 ++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 137 deletions(-) diff --git a/include/fpmath.h b/include/fpmath.h index 6e3edd64e0..69265652e6 100644 --- a/include/fpmath.h +++ b/include/fpmath.h @@ -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)) diff --git a/include/gba/defines.h b/include/gba/defines.h index c54dac8c13..0bf7110810 100644 --- a/include/gba/defines.h +++ b/include/gba/defines.h @@ -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 diff --git a/src/battle_dynamax.c b/src/battle_dynamax.c index 1523bcbd6e..98ee89af9b 100644 --- a/src/battle_dynamax.c +++ b/src/battle_dynamax.c @@ -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); diff --git a/src/battle_util.c b/src/battle_util.c index 4b764776e2..c00598eb14 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -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); diff --git a/test/battle/ability/transistor.c b/test/battle/ability/transistor.c index f02743ab8c..8dd1a1bdb3 100644 --- a/test/battle/ability/transistor.c +++ b/test/battle/ability/transistor.c @@ -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 } } diff --git a/test/battle/damage_formula.c b/test/battle/damage_formula.c index 473f631b11..049bbf4051 100644 --- a/test/battle/damage_formula.c +++ b/test/battle/damage_formula.c @@ -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); + } +}