From fbc640d6922a2ddc30131e7eba4f467ada7a814c Mon Sep 17 00:00:00 2001 From: moostoet <70690976+moostoet@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:18:51 +0100 Subject: [PATCH] Refactor Beat Up handling for Gen 3/4 defaults, fix crit check, and expand test coverage (#8307) --- asm/macros/battle_script.inc | 4 +- data/battle_scripts_1.s | 31 +- include/battle.h | 2 +- include/constants/battle_script_commands.h | 7 +- include/constants/generational_changes.h | 1 + include/generational_changes.h | 1 + src/battle_ai_util.c | 2 +- src/battle_script_commands.c | 49 +-- src/battle_util.c | 40 ++- src/data/battle_move_effects.h | 2 +- test/battle/move_effect/beat_up.c | 396 +++++++++++++++++++-- 11 files changed, 428 insertions(+), 107 deletions(-) diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index 0825fe013b..588360d226 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1804,8 +1804,8 @@ .4byte \failInstr .endm - .macro jumpifcriticalhit failInstr:req - callnative BS_JumpIfCriticalHit + .macro jumpifnotcriticalhit failInstr:req + callnative BS_JumpIfNotCriticalHit .4byte \failInstr .endm diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 93e928c880..a859d32cdf 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -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 diff --git a/include/battle.h b/include/battle.h index fb9fcbc695..1c7b824fd1 100755 --- a/include/battle.h +++ b/include/battle.h @@ -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; diff --git a/include/constants/battle_script_commands.h b/include/constants/battle_script_commands.h index 2cadf215af..81dafd04df 100644 --- a/include/constants/battle_script_commands.h +++ b/include/constants/battle_script_commands.h @@ -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) diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index 52c7401afe..9e4682c127 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -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, diff --git a/include/generational_changes.h b/include/generational_changes.h index dc612e9d64..c85956c816 100644 --- a/include/generational_changes.h +++ b/include/generational_changes.h @@ -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, diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 904d8770e9..0a9bd476c4 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -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; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 6fc03dcf26..e246c48fbb 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -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; diff --git a/src/battle_util.c b/src/battle_util.c index ae522ad593..5296798db4 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -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; } diff --git a/src/data/battle_move_effects.h b/src/data/battle_move_effects.h index 51bbdba0eb..88d4075570 100644 --- a/src/data/battle_move_effects.h +++ b/src/data/battle_move_effects.h @@ -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, }, diff --git a/test/battle/move_effect/beat_up.c b/test/battle/move_effect/beat_up.c index e584ccc0f9..1abc7bb958 100644 --- a/test/battle/move_effect/beat_up.c +++ b/test/battle/move_effect/beat_up.c @@ -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); + } +}