From 755bec128b809a2763b754f6d123f5af0780e44a Mon Sep 17 00:00:00 2001 From: moostoet <70690976+moostoet@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:56:17 +0100 Subject: [PATCH] Fix incorrect "last to move" check for Analytic, Snatch, and Magic Coat (#8536) --- include/battle_util.h | 1 + src/battle_script_commands.c | 6 +-- src/battle_util.c | 22 +++++++++- test/battle/ability/analytic.c | 52 ++++++++++++++++++++++- test/battle/move_effect/magic_coat.c | 36 ++++++++++++++++ test/battle/move_effect/protect.c | 36 ++++++++++++++++ test/battle/move_effect/snatch.c | 62 +++++++++++++++++++++++++++- 7 files changed, 209 insertions(+), 6 deletions(-) diff --git a/include/battle_util.h b/include/battle_util.h index 3e6ae5917b..8ea72866e5 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -238,6 +238,7 @@ void MarkBattlerForControllerExec(u32 battler); void MarkBattlerReceivedLinkData(u32 battler); const u8 *CancelMultiTurnMoves(u32 battler, enum SkyDropState skyDropState); bool32 WasUnableToUseMove(u32 battler); +bool32 IsLastMonToMove(u32 battler); bool32 ShouldDefiantCompetitiveActivate(u32 battler, enum Ability ability); void PrepareStringBattle(enum StringID stringId, u32 battler); void ResetSentPokesToOpponentValue(void); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index dda8e64c49..1ab165ce8f 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -9711,7 +9711,7 @@ static void Cmd_setprotectlike(void) TryResetProtectUseCounter(gBattlerAttacker); - if (gCurrentTurnActionNumber == (gBattlersCount - 1)) + if (IsLastMonToMove(gBattlerAttacker)) notLastTurn = FALSE; if ((sProtectSuccessRates[gDisableStructs[gBattlerAttacker].protectUses] >= RandomUniform(RNG_PROTECT_FAIL, 0, USHRT_MAX) && notLastTurn) @@ -13136,7 +13136,7 @@ static void Cmd_trysetmagiccoat(void) { CMD_ARGS(const u8 *failInstr); - if (gCurrentTurnActionNumber == gBattlersCount - 1) // moves last turn + if (IsLastMonToMove(gBattlerAttacker)) // fails if moving last { gBattlescriptCurrInstr = cmd->failInstr; } @@ -13152,7 +13152,7 @@ static void Cmd_trysetsnatch(void) { CMD_ARGS(const u8 *failInstr); - if (gCurrentTurnActionNumber == gBattlersCount - 1) // moves last turn + if (IsLastMonToMove(gBattlerAttacker)) // fails if moving last { gBattlescriptCurrInstr = cmd->failInstr; } diff --git a/src/battle_util.c b/src/battle_util.c index f4393e2aaa..da74fe3b7f 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -1216,6 +1216,26 @@ bool32 WasUnableToUseMove(u32 battler) return FALSE; } +// Returns TRUE if no other battler after this one in turn order will use a move +bool32 IsLastMonToMove(u32 battler) +{ + u32 i; + u32 battlerTurnOrderNum = GetBattlerTurnOrderNum(battler); + + if (battlerTurnOrderNum >= gBattlersCount - 1) + return TRUE; + + for (i = battlerTurnOrderNum + 1; i < gBattlersCount; i++) + { + u32 otherBattler = gBattlerByTurnOrder[i]; + if (!IsBattlerAlive(otherBattler)) + continue; + if (gActionsByTurnOrder[i] == B_ACTION_USE_MOVE) + return FALSE; + } + return TRUE; +} + bool32 ShouldDefiantCompetitiveActivate(u32 battler, enum Ability ability) { u32 side = GetBattlerSide(battler); @@ -7358,7 +7378,7 @@ static inline u32 CalcMoveBasePowerAfterModifiers(struct DamageContext *ctx) modifier = uq4_12_multiply(modifier, UQ_4_12(0.75)); break; case ABILITY_ANALYTIC: - if (GetBattlerTurnOrderNum(battlerAtk) == gBattlersCount - 1 && move != MOVE_FUTURE_SIGHT && move != MOVE_DOOM_DESIRE) + if (IsLastMonToMove(battlerAtk) && move != MOVE_FUTURE_SIGHT && move != MOVE_DOOM_DESIRE) modifier = uq4_12_multiply(modifier, UQ_4_12(1.3)); break; case ABILITY_TOUGH_CLAWS: diff --git a/test/battle/ability/analytic.c b/test/battle/ability/analytic.c index 5e50c24d59..da2b8e28a6 100644 --- a/test/battle/ability/analytic.c +++ b/test/battle/ability/analytic.c @@ -20,9 +20,59 @@ SINGLE_BATTLE_TEST("Analytic increases the power of moves by 30% if it's the las } } +DOUBLE_BATTLE_TEST("Analytic activates correctly with empty slot after fainting", s16 damage) +{ + u32 speed; + + PARAMETRIZE { speed = 50; } + PARAMETRIZE { speed = 5; } + + GIVEN { + PLAYER(SPECIES_MAGNEMITE) { Ability(ABILITY_ANALYTIC); Speed(speed); } + PLAYER(SPECIES_WYNAUT) { HP(1); Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(20); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerRight); } + TURN { + MOVE(opponentRight, MOVE_CELEBRATE); + MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); + } + } SCENE { + HP_BAR(opponentLeft, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.3), results[1].damage); + } +} + +DOUBLE_BATTLE_TEST("Analytic does not activate when not moving last with empty slot", s16 damage) +{ + u32 speed; + + PARAMETRIZE { speed = 50; } + PARAMETRIZE { speed = 15; } + + GIVEN { + PLAYER(SPECIES_MAGNEMITE) { Ability(ABILITY_ANALYTIC); Speed(speed); } + PLAYER(SPECIES_WYNAUT) { HP(1); Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(20); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerRight); } + TURN { + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_CELEBRATE); + MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); + } + } SCENE { + HP_BAR(opponentLeft, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_EQ(results[0].damage, results[1].damage); + } +} + TO_DO_BATTLE_TEST("Analytic takes into account modifications to speeed an priority (Gen 5-8)"); //Eg. Paralysis, Power Weight, Stall TO_DO_BATTLE_TEST("Analytic does not take into account modifications to speeed an priority (Gen 8)"); //Eg. Paralysis, Power Weight, Stall -TO_DO_BATTLE_TEST("Analytic takes into account the turn order of what fainted Pokémon would've moved"); // Triple Battles needed to test //TO_DO_BATTLE_TEST("If the Pokémon with Analytic is targeting a Pokémon in a flank position that chooses to switch with its ally in the middle, its move's power will always be normal when it attacks the Pokémon that is shifted into the flank position"); diff --git a/test/battle/move_effect/magic_coat.c b/test/battle/move_effect/magic_coat.c index ca7b78883e..05e53b5e12 100644 --- a/test/battle/move_effect/magic_coat.c +++ b/test/battle/move_effect/magic_coat.c @@ -33,6 +33,42 @@ SINGLE_BATTLE_TEST("Magic Coat prints the correct message when bouncing back a m } } +SINGLE_BATTLE_TEST("Magic Coat fails if user moves last") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_MAGIC_COAT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_MAGIC_COAT, player); + } +} + +DOUBLE_BATTLE_TEST("Magic Coat fails when the only slower battler is a fainted ally") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + PLAYER(SPECIES_WYNAUT) { HP(1); Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(20); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerRight); } + TURN { + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_CELEBRATE); + MOVE(playerLeft, MOVE_MAGIC_COAT); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponentLeft); + MESSAGE("Wynaut fainted!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_MAGIC_COAT, playerLeft); + } +} + DOUBLE_BATTLE_TEST("Magic Coat reflects hazards regardless of the user's position") { struct BattlePokemon *coatUser = NULL; diff --git a/test/battle/move_effect/protect.c b/test/battle/move_effect/protect.c index e029c6c849..05b238fe0e 100644 --- a/test/battle/move_effect/protect.c +++ b/test/battle/move_effect/protect.c @@ -356,6 +356,42 @@ SINGLE_BATTLE_TEST("Protect: Multi-hit moves don't hit a protected target and fa } } +SINGLE_BATTLE_TEST("Protect fails if user moves last") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_PROTECT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, player); + } +} + +DOUBLE_BATTLE_TEST("Protect fails when the only slower battler is a fainted ally") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + PLAYER(SPECIES_WYNAUT) { HP(1); Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(20); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerRight); } + TURN { + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_CELEBRATE); + MOVE(playerLeft, MOVE_PROTECT); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponentLeft); + MESSAGE("Wynaut fainted!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, playerLeft); + } +} + DOUBLE_BATTLE_TEST("Protect: Wide Guard protects self and ally from multi-target moves") { u16 move = MOVE_NONE; diff --git a/test/battle/move_effect/snatch.c b/test/battle/move_effect/snatch.c index f8e2891837..42fb79b2a0 100644 --- a/test/battle/move_effect/snatch.c +++ b/test/battle/move_effect/snatch.c @@ -1,4 +1,64 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Snatch (Move Effect) test titles") +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_SNATCH) == EFFECT_SNATCH); + ASSUME(MoveCanBeSnatched(MOVE_SWORDS_DANCE)); +} + +SINGLE_BATTLE_TEST("Snatch steals stat-boosting moves from the opponent") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(10); } + OPPONENT(SPECIES_WYNAUT) { Speed(5); } + } WHEN { + TURN { MOVE(player, MOVE_SNATCH); MOVE(opponent, MOVE_SWORDS_DANCE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SNATCH, player); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE); + } +} + +SINGLE_BATTLE_TEST("Snatch fails if user moves last") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_SNATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_SNATCH, player); + } +} + +DOUBLE_BATTLE_TEST("Snatch fails when the only slower battler is a fainted ally") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } + PLAYER(SPECIES_WYNAUT) { HP(1); Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(20); } + OPPONENT(SPECIES_WYNAUT) { Speed(10); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerRight); } + TURN { + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_CELEBRATE); + MOVE(playerLeft, MOVE_SNATCH); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponentLeft); + MESSAGE("Wynaut fainted!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_SNATCH, playerLeft); + } +} + +TO_DO_BATTLE_TEST("Snatch does not steal moves that cannot be snatched"); +TO_DO_BATTLE_TEST("Snatch can steal healing moves");