Refactor Beat Up handling for Gen 3/4 defaults, fix crit check, and expand test coverage (#8307)

This commit is contained in:
moostoet 2025-11-25 22:18:51 +01:00 committed by GitHub
parent 152ad88436
commit fbc640d692
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 428 additions and 107 deletions

View File

@ -1804,8 +1804,8 @@
.4byte \failInstr
.endm
.macro jumpifcriticalhit failInstr:req
callnative BS_JumpIfCriticalHit
.macro jumpifnotcriticalhit failInstr:req
callnative BS_JumpIfNotCriticalHit
.4byte \failInstr
.endm

View File

@ -4290,37 +4290,18 @@ BattleScript_DoEffectTeleport::
goto BattleScript_MoveEnd
BattleScript_EffectBeatUp::
jumpifgenconfiglowerthan GEN_CONFIG_BEAT_UP, GEN_5, BattleScript_EffectBeatUpGen3
goto BattleScript_EffectHit
BattleScript_EffectBeatUpGen3:
attackcanceler
accuracycheck BattleScript_PrintMoveMissed, ACC_CURR_MOVE
attackstring
pause B_WAIT_TIME_SHORT
ppreduce
setbyte gBattleCommunication, 0
BattleScript_BeatUpLoop::
movevaluescleanup
trydobeatup BattleScript_BeatUpEnd, BattleScript_ButItFailed
trydobeatup BattleScript_MoveEnd, BattleScript_ButItFailed
printstring STRINGID_PKMNATTACK
critcalc
jumpifcriticalhit BattleScript_BeatUpAttack
manipulatedamage DMG_DOUBLED
BattleScript_BeatUpAttack::
adjustdamage
attackanimation
waitanimation
effectivenesssound
hitanimation BS_TARGET
waitstate
healthbarupdate BS_TARGET
datahpupdate BS_TARGET
critmessage
waitmessage B_WAIT_TIME_LONG
resultmessage
waitmessage B_WAIT_TIME_LONG
tryfaintmon BS_TARGET
moveendto MOVEEND_NEXT_TARGET
goto BattleScript_BeatUpLoop
BattleScript_BeatUpEnd::
end
goto BattleScript_HitFromCritCalc
BattleScript_EffectDefenseCurl::
attackcanceler

View File

@ -739,7 +739,7 @@ struct BattleStruct
u8 appearedInBattle; // Bitfield to track which Pokemon appeared in battle. Used for Burmy's form change
u8 skyDropTargets[MAX_BATTLERS_COUNT]; // For Sky Drop, to account for if multiple Pokemon use Sky Drop in a double battle.
// When using a move which hits multiple opponents which is then bounced by a target, we need to make sure, the move hits both opponents, the one with bounce, and the one without.
u16 beatUpSpecies[PARTY_SIZE];
u16 beatUpSpecies[PARTY_SIZE]; // Species for Gen5+ Beat Up, otherwise party indexes
u8 attackerBeforeBounce:2;
u8 beatUpSlot:3;
u8 pledgeMove:1;

View File

@ -96,10 +96,9 @@ enum CmdVarious
// Cmd_manipulatedamage
#define DMG_CHANGE_SIGN 1
#define DMG_DOUBLED 2
#define DMG_1_8_TARGET_HP 3
#define DMG_FULL_ATTACKER_HP 4
#define DMG_BIG_ROOT 5
#define DMG_1_8_TARGET_HP 2
#define DMG_FULL_ATTACKER_HP 3
#define DMG_BIG_ROOT 4
// Cmd_jumpifcantswitch
#define SWITCH_IGNORE_ESCAPE_PREVENTION (1 << 7)

View File

@ -48,6 +48,7 @@ enum GenConfigTag
GEN_CONFIG_PARALYZE_ELECTRIC,
GEN_CONFIG_BADGE_BOOST,
GEN_CONFIG_LEAF_GUARD_PREVENTS_REST,
GEN_CONFIG_BEAT_UP,
GEN_CONFIG_WIDE_GUARD,
GEN_CONFIG_QUICK_GUARD,
GEN_CONFIG_DEFOG_EFFECT_CLEARING,

View File

@ -51,6 +51,7 @@ static const u8 sGenerationalChanges[GEN_CONFIG_COUNT] =
[GEN_CONFIG_PARALYZE_ELECTRIC] = B_PARALYZE_ELECTRIC,
[GEN_CONFIG_BADGE_BOOST] = B_BADGE_BOOST,
[GEN_CONFIG_LEAF_GUARD_PREVENTS_REST] = B_LEAF_GUARD_PREVENTS_REST,
[GEN_CONFIG_BEAT_UP] = B_BEAT_UP,
[GEN_CONFIG_WIDE_GUARD] = B_WIDE_GUARD,
[GEN_CONFIG_QUICK_GUARD] = B_QUICK_GUARD,
[GEN_CONFIG_DEFOG_EFFECT_CLEARING] = B_DEFOG_EFFECT_CLEARING,

View File

@ -786,7 +786,7 @@ static inline void CalcDynamicMoveDamage(struct DamageContext *ctx, u16 *medianD
median = maximum = minimum = max(0, gBattleMons[ctx->battlerDef].hp - gBattleMons[ctx->battlerAtk].hp);
break;
case EFFECT_BEAT_UP:
if (B_BEAT_UP >= GEN_5)
if (GetGenConfig(GEN_CONFIG_BEAT_UP) >= GEN_5)
{
u32 partyCount = CalculatePartyCount(GetBattlerParty(ctx->battlerAtk));
u32 i;

View File

@ -9870,9 +9870,6 @@ static void Cmd_manipulatedamage(void)
case DMG_CHANGE_SIGN:
gBattleStruct->moveDamage[gBattlerAttacker] *= -1;
break;
case DMG_DOUBLED:
gBattleStruct->moveDamage[gBattlerTarget] *= 2;
break;
case DMG_1_8_TARGET_HP:
gBattleStruct->moveDamage[gBattlerTarget] = GetNonDynamaxMaxHP(gBattlerTarget) / 8;
if (gBattleStruct->moveDamage[gBattlerTarget] == 0)
@ -12592,48 +12589,20 @@ static void Cmd_trysetfutureattack(void)
static void Cmd_trydobeatup(void)
{
CMD_ARGS(const u8 *endInstr, const u8 *failInstr);
struct Pokemon *party = GetBattlerParty(gBattlerAttacker);
if (!IsBattlerAlive(gBattlerTarget))
{
gMultiHitCounter = 0;
gBattlescriptCurrInstr = cmd->endInstr;
}
else if (gBattleStruct->beatUpSlot == 0 && gMultiHitCounter == 0)
{
gBattlescriptCurrInstr = cmd->failInstr;
}
else
{
u8 beforeLoop = gBattleCommunication[0];
for (;gBattleCommunication[0] < PARTY_SIZE; gBattleCommunication[0]++)
{
if (GetMonData(&party[gBattleCommunication[0]], MON_DATA_HP)
&& GetMonData(&party[gBattleCommunication[0]], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE
&& GetMonData(&party[gBattleCommunication[0]], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG
&& !GetMonData(&party[gBattleCommunication[0]], MON_DATA_STATUS))
break;
}
if (gBattleCommunication[0] < PARTY_SIZE)
{
PREPARE_MON_NICK_WITH_PREFIX_BUFFER(gBattleTextBuff1, gBattlerAttacker, gBattleCommunication[0])
gBattlescriptCurrInstr = cmd->nextInstr;
gBattleStruct->moveDamage[gBattlerTarget] = GetSpeciesBaseAttack(GetMonData(&party[gBattleCommunication[0]], MON_DATA_SPECIES));
gBattleStruct->moveDamage[gBattlerTarget] *= GetMovePower(gCurrentMove);
gBattleStruct->moveDamage[gBattlerTarget] *= (GetMonData(&party[gBattleCommunication[0]], MON_DATA_LEVEL) * 2 / 5 + 2);
gBattleStruct->moveDamage[gBattlerTarget] /= GetSpeciesBaseDefense(gBattleMons[gBattlerTarget].species);
gBattleStruct->moveDamage[gBattlerTarget] = (gBattleStruct->moveDamage[gBattlerTarget] / 50) + 2;
if (gProtectStructs[gBattlerAttacker].helpingHand)
gBattleStruct->moveDamage[gBattlerTarget] = gBattleStruct->moveDamage[gBattlerTarget] * 15 / 10;
gBattleCommunication[0]++;
}
else if (beforeLoop != 0)
{
gBattlescriptCurrInstr = cmd->endInstr;
}
else
{
gBattlescriptCurrInstr = cmd->failInstr;
}
PREPARE_MON_NICK_WITH_PREFIX_BUFFER(gBattleTextBuff1, gBattlerAttacker, gBattleStruct->beatUpSpecies[gBattleStruct->beatUpSlot])
gBattlescriptCurrInstr = cmd->nextInstr;
}
}
@ -16447,11 +16416,11 @@ void BS_JumpIfMoveResultFlags(void)
gBattlescriptCurrInstr = cmd->nextInstr;
}
void BS_JumpIfCriticalHit(void)
void BS_JumpIfNotCriticalHit(void)
{
NATIVE_ARGS(const u8 *jumpInstr);
if (gSpecialStatuses[gBattlerTarget].criticalHit)
if (!gSpecialStatuses[gBattlerTarget].criticalHit)
gBattlescriptCurrInstr = cmd->jumpInstr;
else
gBattlescriptCurrInstr = cmd->nextInstr;

View File

@ -270,12 +270,34 @@ bool32 EndOrContinueWeather(void)
return FALSE;
}
// Gen5+
static u32 CalcBeatUpPower(void)
{
u32 species = gBattleStruct->beatUpSpecies[gBattleStruct->beatUpSlot++];
return (GetSpeciesBaseAttack(species) / 10) + 5;
}
static s32 CalcBeatUpDamage(struct DamageContext *ctx)
{
u32 partyIndex = gBattleStruct->beatUpSpecies[gBattleStruct->beatUpSlot++];
struct Pokemon *party = GetBattlerParty(ctx->battlerAtk);
u32 species = GetMonData(&party[partyIndex], MON_DATA_SPECIES);
u32 levelFactor = GetMonData(&party[partyIndex], MON_DATA_LEVEL) * 2 / 5 + 2;
s32 dmg = GetSpeciesBaseAttack(species);
dmg *= GetMovePower(ctx->move);
dmg *= levelFactor;
dmg /= GetSpeciesBaseDefense(gBattleMons[ctx->battlerDef].species);
dmg = (dmg / 50) + 2;
if (gProtectStructs[ctx->battlerAtk].helpingHand)
dmg = dmg * 15 / 10;
if (ctx->isCrit)
dmg *= 2;
return dmg;
}
static bool32 ShouldTeraShellDistortTypeMatchups(u32 move, u32 battlerDef, u32 abilityDef)
{
if (!gSpecialStatuses[battlerDef].distortedTypeMatchups
@ -2480,11 +2502,13 @@ static enum MoveCanceler CancelerMultihitMoves(void)
PREPARE_BYTE_NUMBER_BUFFER(gBattleScripting.multihitString, 3, 0)
}
else if (B_BEAT_UP >= GEN_5 && GetMoveEffect(gCurrentMove) == EFFECT_BEAT_UP)
else if (GetMoveEffect(gCurrentMove) == EFFECT_BEAT_UP)
{
struct Pokemon* party = GetBattlerParty(gBattlerAttacker);
int i;
gBattleStruct->beatUpSlot = 0;
gMultiHitCounter = 0;
memset(gBattleStruct->beatUpSpecies, 0xFF, sizeof(gBattleStruct->beatUpSpecies));
for (i = 0; i < PARTY_SIZE; i++)
{
@ -2494,12 +2518,14 @@ static enum MoveCanceler CancelerMultihitMoves(void)
&& !GetMonData(&party[i], MON_DATA_IS_EGG)
&& !GetMonData(&party[i], MON_DATA_STATUS))
{
gBattleStruct->beatUpSpecies[gBattleStruct->beatUpSlot++] = species;
if (GetGenConfig(GEN_CONFIG_BEAT_UP) >= GEN_5)
gBattleStruct->beatUpSpecies[gMultiHitCounter] = species;
else
gBattleStruct->beatUpSpecies[gMultiHitCounter] = i;
gMultiHitCounter++;
}
}
gBattleStruct->beatUpSlot = 0;
PREPARE_BYTE_NUMBER_BUFFER(gBattleScripting.multihitString, 1, 0)
}
else
@ -8267,7 +8293,7 @@ static inline u32 CalcMoveBasePower(struct DamageContext *ctx)
basePower *= 2;
break;
case EFFECT_BEAT_UP:
if (B_BEAT_UP >= GEN_5)
if (GetGenConfig(GEN_CONFIG_BEAT_UP) >= GEN_5)
basePower = CalcBeatUpPower();
break;
case EFFECT_PSYBLADE:
@ -9508,6 +9534,12 @@ s32 DoFixedDamageMoveCalc(struct DamageContext *ctx)
case EFFECT_FINAL_GAMBIT:
dmg = GetNonDynamaxHP(ctx->battlerAtk);
break;
case EFFECT_BEAT_UP:
if (GetGenConfig(GEN_CONFIG_BEAT_UP) < GEN_5)
dmg = CalcBeatUpDamage(ctx);
else
return INT32_MAX;
break;
default:
return INT32_MAX;
}

View File

@ -791,7 +791,7 @@ const struct BattleMoveEffect gBattleMoveEffects[NUM_BATTLE_MOVE_EFFECTS] =
[EFFECT_BEAT_UP] =
{
.battleScript = (B_BEAT_UP >= GEN_5) ? BattleScript_EffectHit : BattleScript_EffectBeatUp,
.battleScript = BattleScript_EffectBeatUp,
.battleTvScore = 2,
},

View File

@ -1,10 +1,18 @@
#include "global.h"
#include "test/battle.h"
// General
// TODO: Beat Up's strikes have each an independent chance of a critical hit
// Unconfirmed by Bulbapedia
// - Technician interacion
SINGLE_BATTLE_TEST("Beat Up hits the target for each non-fainted, non-statused member in the party")
{
u32 gen;
PARAMETRIZE { gen = GEN_3; }
PARAMETRIZE { gen = GEN_5; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, gen);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
PLAYER(SPECIES_PICHU)
@ -21,37 +29,367 @@ SINGLE_BATTLE_TEST("Beat Up hits the target for each non-fainted, non-statused m
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
MESSAGE("The Pokémon was hit 4 time(s)!");
} THEN {
EXPECT_EQ(gBattleStruct->beatUpSpecies[0], SPECIES_WOBBUFFET);
EXPECT_EQ(gBattleStruct->beatUpSpecies[1], SPECIES_WYNAUT);
EXPECT_EQ(gBattleStruct->beatUpSpecies[2], SPECIES_PICHU);
EXPECT_EQ(gBattleStruct->beatUpSpecies[3], SPECIES_RAICHU);
if (gen == GEN_5) {
EXPECT_EQ(gBattleStruct->beatUpSpecies[0], SPECIES_WOBBUFFET);
EXPECT_EQ(gBattleStruct->beatUpSpecies[1], SPECIES_WYNAUT);
EXPECT_EQ(gBattleStruct->beatUpSpecies[2], SPECIES_PICHU);
EXPECT_EQ(gBattleStruct->beatUpSpecies[3], SPECIES_RAICHU);
}
else {
EXPECT_EQ(gBattleStruct->beatUpSpecies[0], 0);
EXPECT_EQ(gBattleStruct->beatUpSpecies[1], 1);
EXPECT_EQ(gBattleStruct->beatUpSpecies[2], 2);
EXPECT_EQ(gBattleStruct->beatUpSpecies[3], 4);
}
}
}
TO_DO_BATTLE_TEST("Beat Up doesn't consider Comatose as a status")
TO_DO_BATTLE_TEST("Beat Up's strikes have each an independent chance of a critical hit");
SINGLE_BATTLE_TEST("Beat Up doesn't consider Comatose as a status")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { Status1(STATUS1_POISON); }
PLAYER(SPECIES_WYNAUT) { Status1(STATUS1_SLEEP); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
MESSAGE("The Pokémon was hit 2 time(s)!");
}
}
// B_BEAT_UP Gen2-4
TO_DO_BATTLE_TEST("Beat Up lists each party member's name");
TO_DO_BATTLE_TEST("Beat Up's damage is typeless");
TO_DO_BATTLE_TEST("Beat Up's damage doesn't consider STAB");
TO_DO_BATTLE_TEST("Beat Up's last strike-only can trigger King's Rock");
TO_DO_BATTLE_TEST("Beat Up's base power is the same for each strike");
TO_DO_BATTLE_TEST("Beat Up's damage is determined by each striking Pokémon's base attack and level and the target's defense");
TO_DO_BATTLE_TEST("Beat Up ignores stat stage changes"); //eg. Swords Dance
TO_DO_BATTLE_TEST("Beat Up ignores Huge Power");
TO_DO_BATTLE_TEST("Beat Up ignores Choice Band");
SINGLE_BATTLE_TEST("Beat Up doesn't list party member's name (Gen5+)")
{
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_5);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
NONE_OF {
MESSAGE("Wobbuffet's attack!");
MESSAGE("Wynaut's attack!");
}
MESSAGE("The Pokémon was hit 2 time(s)!");
}
}
// B_BEAT_UP Gen5+
TO_DO_BATTLE_TEST("Beat Up doesn't list party member's name");
TO_DO_BATTLE_TEST("Beat Up's damage is Dark-typed");
TO_DO_BATTLE_TEST("Beat Up's damage receives STAB");
TO_DO_BATTLE_TEST("Beat Up's can trigger King's Rock on all strikes");
TO_DO_BATTLE_TEST("Beat Up's base power is determined by each striking Pokémon");
TO_DO_BATTLE_TEST("Beat Up's damage is determined by the user's attack and the target's defense");
TO_DO_BATTLE_TEST("Beat Up's damage considers stat stage changes"); //eg. Swords Dance
TO_DO_BATTLE_TEST("Beat Up's damage considers Huge Power");
TO_DO_BATTLE_TEST("Beat Up's damage considers Choice Band");
SINGLE_BATTLE_TEST("Beat Up's damage is Dark-typed (Gen5+)", s16 damage)
{
bool32 targetIsFairy;
PARAMETRIZE { targetIsFairy = FALSE; }
PARAMETRIZE { targetIsFairy = TRUE; }
// Unconfirmed by Bulbapedia
// - Technician interacion
ASSUME(GetMoveType(MOVE_BEAT_UP) == TYPE_DARK);
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_5);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(targetIsFairy ? SPECIES_SYLVEON : SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (targetIsFairy)
EXPECT_LT(results[i].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Beat Up's base power is determined by each striking Pokémon (Gen5+)")
{
s16 firstHit, secondHit;
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_5);
PLAYER(SPECIES_SHUCKLE);
PLAYER(SPECIES_DEOXYS_ATTACK);
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
OPPONENT(SPECIES_BLISSEY);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &firstHit);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &secondHit);
} THEN {
EXPECT_LT(firstHit, secondHit);
}
}
SINGLE_BATTLE_TEST("Beat Up's damage considers stat stage changes (Gen5+)", s16 damage)
{
bool32 boosted;
PARAMETRIZE { boosted = FALSE; }
PARAMETRIZE { boosted = TRUE; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_5);
PLAYER(SPECIES_UMBREON);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { if (boosted) { MOVE(player, MOVE_SWORDS_DANCE); } else { MOVE(player, MOVE_CELEBRATE); } }
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
if (boosted)
ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (boosted)
EXPECT_GT(results[i].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Beat Up's damage considers Huge Power and Choice Band (Gen5+)", s16 damage)
{
u16 ability;
u16 item;
PARAMETRIZE { ability = ABILITY_THICK_FAT; item = ITEM_NONE; }
PARAMETRIZE { ability = ABILITY_HUGE_POWER; item = ITEM_NONE; }
PARAMETRIZE { ability = ABILITY_THICK_FAT; item = ITEM_CHOICE_BAND; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_5);
PLAYER(SPECIES_AZUMARILL) { Ability(ability); Item(item); Moves(MOVE_BEAT_UP); }
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (i == 1)
EXPECT_GT(results[i].damage, results[0].damage);
if (i == 2)
EXPECT_GT(results[i].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Beat Up lists each party member's name")
{
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { Status1(STATUS1_POISON); }
PLAYER(SPECIES_PIKACHU);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
MESSAGE("Wobbuffet's attack!");
MESSAGE("Wynaut's attack!");
NOT MESSAGE("Wynaut's attack!");
MESSAGE("Pikachu's attack!");
}
}
SINGLE_BATTLE_TEST("Beat Up's damage is typeless", s16 damage)
{
u16 defender = SPECIES_WOBBUFFET;
u16 type1, type2;
PARAMETRIZE { defender = SPECIES_BLISSEY; } // Normal
PARAMETRIZE { defender = SPECIES_MACHAMP; } // Fighting
PARAMETRIZE { defender = SPECIES_TORNADUS; } // Flying
PARAMETRIZE { defender = SPECIES_GRIMER; } // Poison
PARAMETRIZE { defender = SPECIES_SANDSHREW; } // Ground
PARAMETRIZE { defender = SPECIES_NOSEPASS; } // Rock
PARAMETRIZE { defender = SPECIES_CATERPIE; } // Bug
PARAMETRIZE { defender = SPECIES_DUSKULL; } // Ghost
PARAMETRIZE { defender = SPECIES_REGISTEEL; } // Steel
PARAMETRIZE { defender = SPECIES_CHIMCHAR; } // Fire
PARAMETRIZE { defender = SPECIES_WARTORTLE; } // Water
PARAMETRIZE { defender = SPECIES_TANGELA; } // Grass
PARAMETRIZE { defender = SPECIES_PIKACHU; } // Electric
PARAMETRIZE { defender = SPECIES_ABRA; } // Psychic
PARAMETRIZE { defender = SPECIES_SNORUNT; } // Ice
PARAMETRIZE { defender = SPECIES_BAGON; } // Dragon
PARAMETRIZE { defender = SPECIES_UMBREON; } // Dark
PARAMETRIZE { defender = SPECIES_SYLVEON; } // Fairy
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
type1 = GetSpeciesType(defender, 0);
type2 = GetSpeciesType(defender, 1);
ASSUME(type2 == type1 || type2 == TYPE_MYSTERY); // Ensure monotype targets
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(defender);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
NONE_OF {
MESSAGE("It's super effective!");
MESSAGE("It's not very effective...");
MESSAGE("It doesn't affect");
}
} THEN {
EXPECT_GT(results[i].damage, 0);
}
}
SINGLE_BATTLE_TEST("Beat Up's damage doesn't consider STAB")
{
s16 damage;
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
damage = 0;
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT) { HP(0); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
HP_BAR(opponent, captureDamage: &damage);
} THEN {
// Raw damage: baseAtk 33 * basePower 1 * levelFactor ((100 * 2 / 5) + 2 = 42) = 1386
// Divide by baseDef 58 -> 23 (floor); 23/50 + 2 = 2;
u16 expected = 2;
EXPECT_EQ(damage, expected);
}
}
SINGLE_BATTLE_TEST("Beat Up's base power is the same for each strike")
{
s16 firstHit, secondHit;
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
firstHit = 0;
secondHit = 0;
PLAYER(SPECIES_WYNAUT);
PLAYER(SPECIES_WYNAUT);
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &firstHit);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &secondHit);
} THEN {
EXPECT_EQ(firstHit, secondHit);
}
}
SINGLE_BATTLE_TEST("Beat Up's damage is determined by each striking Pokémon's base attack and level and the target's defense")
{
s16 shuckleHit, deoxysHit;
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
shuckleHit = 0;
deoxysHit = 0;
PLAYER(SPECIES_SHUCKLE);
PLAYER(SPECIES_DEOXYS_ATTACK);
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
PLAYER(SPECIES_WYNAUT) { HP(0); }
OPPONENT(SPECIES_BLISSEY);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &shuckleHit);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &deoxysHit);
} THEN {
// Shuckle: baseAtk 10 * basePower 1 * levelFactor 42 = 420; / baseDef 10 -> 42; 42/50 + 2 = 2
u16 shuckleDmg = 2;
// Deoxys-A: baseAtk 180 * basePower 1 * levelFactor 42 = 7560; / baseDef 10 -> 756; 756/50 + 2 = 17
u16 deoxysDmg = 17;
EXPECT_EQ(shuckleHit, shuckleDmg);
EXPECT_EQ(deoxysHit, deoxysDmg);
EXPECT_LT(shuckleHit, deoxysHit);
}
}
SINGLE_BATTLE_TEST("Beat Up ignores stat stage changes", s16 damage)
{
bool32 boosted;
PARAMETRIZE { boosted = FALSE; }
PARAMETRIZE { boosted = TRUE; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { if (boosted) { MOVE(player, MOVE_SWORDS_DANCE); } else { MOVE(player, MOVE_CELEBRATE); } }
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
if (boosted)
ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (boosted)
EXPECT_EQ(results[i].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Beat Up ignores Huge Power", s16 damage)
{
u16 ability;
PARAMETRIZE { ability = ABILITY_THICK_FAT; }
PARAMETRIZE { ability = ABILITY_HUGE_POWER; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
PLAYER(SPECIES_AZUMARILL) { Ability(ability); Moves(MOVE_BEAT_UP); }
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (ability == ABILITY_HUGE_POWER)
EXPECT_EQ(results[i].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Beat Up ignores Choice Band", s16 damage)
{
u16 item;
PARAMETRIZE { item = ITEM_NONE; }
PARAMETRIZE { item = ITEM_CHOICE_BAND; }
GIVEN {
WITH_CONFIG(GEN_CONFIG_BEAT_UP, GEN_3);
PLAYER(SPECIES_URSARING) { Item(item); Moves(MOVE_BEAT_UP); }
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BEAT_UP); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BEAT_UP, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (item == ITEM_CHOICE_BAND)
EXPECT_EQ(results[i].damage, results[0].damage);
}
}