From abeb86837abf4cba2464c0089eb7ae9283855c69 Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 03:26:14 +0800 Subject: [PATCH 01/22] Fix Instruct Missing Checks for Focus Punch, Beak Blast, Shell Trap, and Sky Drop (#9152) --- src/battle_script_commands.c | 16 +++++++++ test/battle/move_effect/beak_blast.c | 42 ++++++++--------------- test/battle/move_effect/instruct.c | 51 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 9d4f679208..8819c8afad 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1136,6 +1136,19 @@ static inline bool32 IsBattlerUsingBeakBlast(u32 battler) return !HasBattlerActedThisTurn(battler); } +static inline bool32 IsInstructBannedChargingMove(u32 battler) +{ + enum BattleMoveEffects moveEffect; + + if (gChosenActionByBattler[battler] != B_ACTION_USE_MOVE || HasBattlerActedThisTurn(battler)) + return FALSE; + + moveEffect = GetMoveEffect(gChosenMoveByBattler[battler]); + return moveEffect == EFFECT_FOCUS_PUNCH + || moveEffect == EFFECT_BEAK_BLAST + || moveEffect == EFFECT_SHELL_TRAP; +} + static void Cmd_attackcanceler(void) { CMD_ARGS(); @@ -17379,6 +17392,9 @@ void BS_TryInstruct(void) u16 move = gLastPrintedMoves[gBattlerTarget]; if (move == MOVE_NONE || move == MOVE_UNAVAILABLE || MoveHasAdditionalEffectSelf(move, MOVE_EFFECT_RECHARGE) || IsMoveInstructBanned(move) + || IsInstructBannedChargingMove(gBattlerTarget) + || gBattleMons[gBattlerTarget].volatiles.bideTurns != 0 + || gBattleMons[gBattlerTarget].volatiles.semiInvulnerable == STATE_SKY_DROP || gBattleMoveEffects[GetMoveEffect(move)].twoTurnEffect || (GetActiveGimmick(gBattlerTarget) == GIMMICK_DYNAMAX) || IsZMove(move) diff --git a/test/battle/move_effect/beak_blast.c b/test/battle/move_effect/beak_blast.c index 4cbea8a596..5a9d43df71 100644 --- a/test/battle/move_effect/beak_blast.c +++ b/test/battle/move_effect/beak_blast.c @@ -144,6 +144,7 @@ SINGLE_BATTLE_TEST("Beak Blast doesn't burn fire types") { GIVEN { ASSUME(gSpeciesInfo[SPECIES_ARCANINE].types[0] == TYPE_FIRE || gSpeciesInfo[SPECIES_ARCANINE].types[1] == TYPE_FIRE); + ASSUME(MoveMakesContact(MOVE_SCRATCH)); PLAYER(SPECIES_ARCANINE); OPPONENT(SPECIES_WOBBUFFET); } WHEN { @@ -159,6 +160,7 @@ SINGLE_BATTLE_TEST("Beak Blast doesn't burn after being used") { GIVEN { ASSUME(GetMovePriority(MOVE_COUNTER) < GetMovePriority(MOVE_BEAK_BLAST)); + ASSUME(MoveMakesContact(MOVE_COUNTER)); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { @@ -169,44 +171,29 @@ SINGLE_BATTLE_TEST("Beak Blast doesn't burn after being used") } } -DOUBLE_BATTLE_TEST("Beak Blast doesn't burn if the target is protected") +DOUBLE_BATTLE_TEST("Beak Blast doesn't burn if the target is protected by Mat Block") { - u32 move; - - PARAMETRIZE { move = MOVE_SPIKY_SHIELD; } - PARAMETRIZE { move = MOVE_BANEFUL_BUNKER; } - PARAMETRIZE { move = MOVE_BURNING_BULWARK; } - PARAMETRIZE { move = MOVE_SILK_TRAP; } - GIVEN { - ASSUME(GetMoveEffect(move) == EFFECT_PROTECT); - ASSUME(GetMoveEffect(MOVE_INSTRUCT) == EFFECT_INSTRUCT); - ASSUME(GetMovePriority(MOVE_BEAK_BLAST) > GetMovePriority(MOVE_TRICK_ROOM)); + ASSUME(GetMoveEffect(MOVE_MAT_BLOCK) == EFFECT_MAT_BLOCK); + ASSUME(GetMoveProtectMethod(MOVE_MAT_BLOCK) == PROTECT_MAT_BLOCK); + ASSUME(MoveMakesContact(MOVE_POUND)); PLAYER(SPECIES_WOBBUFFET) { Speed(1); } PLAYER(SPECIES_WYNAUT) { Speed(2); } OPPONENT(SPECIES_WOBBUFFET) { Speed(5); } OPPONENT(SPECIES_WYNAUT) { Speed(10); } } WHEN { - TURN { MOVE(opponentLeft, move); } - TURN { MOVE(opponentRight, MOVE_INSTRUCT, target: opponentLeft, WITH_RNG(RNG_PROTECT_FAIL, 0)); - MOVE(opponentLeft, MOVE_BEAK_BLAST, target: playerLeft); - MOVE(playerRight, MOVE_TRICK_ROOM); - MOVE(playerLeft, MOVE_POUND, target: opponentLeft); } + TURN { + MOVE(opponentRight, MOVE_MAT_BLOCK); + MOVE(opponentLeft, MOVE_BEAK_BLAST, target: playerLeft); + MOVE(playerLeft, MOVE_POUND, target: opponentLeft); + } } SCENE { ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_BEAK_BLAST_SETUP, opponentLeft); - ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, opponentRight); - ANIMATION(ANIM_TYPE_MOVE, move, opponentLeft); - NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_POUND, playerLeft); - if (move == MOVE_SPIKY_SHIELD) { - HP_BAR(playerLeft); - } else if (move == MOVE_BANEFUL_BUNKER) { - STATUS_ICON(playerLeft, STATUS1_POISON); - } else if (move == MOVE_BURNING_BULWARK) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MAT_BLOCK, opponentRight); + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POUND, playerLeft); STATUS_ICON(playerLeft, STATUS1_BURN); - } else if (move == MOVE_SILK_TRAP) { - ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); } - NOT STATUS_ICON(playerLeft, STATUS1_BURN); } } @@ -216,6 +203,7 @@ DOUBLE_BATTLE_TEST("Beak Blast doesn't burn if the target is protected by Quick ASSUME(GetMoveEffect(MOVE_QUICK_GUARD) == EFFECT_PROTECT); ASSUME(GetMoveProtectMethod(MOVE_QUICK_GUARD) == PROTECT_QUICK_GUARD); ASSUME(GetMovePriority(MOVE_QUICK_ATTACK) > 0); + ASSUME(MoveMakesContact(MOVE_QUICK_ATTACK)); PLAYER(SPECIES_WOBBUFFET) { Speed(1); } PLAYER(SPECIES_WYNAUT) { Speed(2); } OPPONENT(SPECIES_WOBBUFFET) { Speed(5); } diff --git a/test/battle/move_effect/instruct.c b/test/battle/move_effect/instruct.c index b0bd715ddd..9bb4eab9de 100644 --- a/test/battle/move_effect/instruct.c +++ b/test/battle/move_effect/instruct.c @@ -52,6 +52,57 @@ DOUBLE_BATTLE_TEST("Instruct fails if move is banned by Instruct") } } +TO_DO_BATTLE_TEST("Instruct fails if target is in the middle of Bide"); + +DOUBLE_BATTLE_TEST("Instruct fails if target is preparing Focus Punch, Beak Blast or Shell Trap") +{ + u32 move, Anim; + PARAMETRIZE { move = MOVE_FOCUS_PUNCH; Anim = B_ANIM_FOCUS_PUNCH_SETUP; } + PARAMETRIZE { move = MOVE_BEAK_BLAST; Anim = B_ANIM_BEAK_BLAST_SETUP; } + PARAMETRIZE { move = MOVE_SHELL_TRAP; Anim = B_ANIM_SHELL_TRAP_SETUP; } + + GIVEN { + ASSUME(GetMoveEffect(MOVE_FOCUS_PUNCH) == EFFECT_FOCUS_PUNCH); + ASSUME(GetMoveEffect(MOVE_BEAK_BLAST) == EFFECT_BEAK_BLAST); + ASSUME(GetMoveEffect(MOVE_SHELL_TRAP) == EFFECT_SHELL_TRAP); + PLAYER(SPECIES_WOBBUFFET) { Speed(4); Moves(MOVE_INSTRUCT, MOVE_CELEBRATE); } + PLAYER(SPECIES_WOBBUFFET) { Speed(3); Moves(MOVE_POUND, move); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + } WHEN { + TURN { MOVE(playerRight, MOVE_POUND, target: opponentLeft); } + TURN { + if (move == MOVE_SHELL_TRAP) + MOVE(playerRight, move); + else + MOVE(playerRight, move, target: opponentLeft); + MOVE(playerLeft, MOVE_INSTRUCT, target: playerRight); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POUND, playerRight); + ANIMATION(ANIM_TYPE_GENERAL, Anim, playerRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, playerLeft); + } +} + +DOUBLE_BATTLE_TEST("Instruct fails if target is picked up by Sky Drop even if one of the battlers has No Guard") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_SKY_DROP) == EFFECT_SKY_DROP); + PLAYER(SPECIES_WOBBUFFET) { Speed(3); Moves(MOVE_INSTRUCT, MOVE_CELEBRATE); } + PLAYER(SPECIES_MACHAMP) { Speed(2); Ability(ABILITY_NO_GUARD); Moves(MOVE_SCRATCH, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(4); Moves(MOVE_SKY_DROP, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_SCRATCH, target: opponentLeft); } + TURN { MOVE(opponentLeft, MOVE_SKY_DROP, target: playerRight); MOVE(playerLeft, MOVE_INSTRUCT, target: playerRight); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SKY_DROP, opponentLeft); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, playerLeft); + } +} + DOUBLE_BATTLE_TEST("Instruct-called move targets the target of the move picked on its last use") { GIVEN { From 20a986519d0b46404e9eaf9e65f3f2c92c0a9631 Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 17:00:42 +0800 Subject: [PATCH 02/22] Fix Aqua Ring reuse failure check and add Aqua Ring/Ingrain tests (#9174) --- data/battle_scripts_1.s | 1 + test/battle/move_effect/aqua_ring.c | 61 ++++++++++++- test/battle/move_effect/ingrain.c | 128 +++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 25262692d7..7abeade1da 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -2302,6 +2302,7 @@ BattleScript_EffectMagicRoom:: BattleScript_EffectAquaRing:: attackcanceler + jumpifvolatile BS_ATTACKER, VOLATILE_AQUA_RING, BattleScript_ButItFailed setvolatile BS_ATTACKER, VOLATILE_AQUA_RING attackanimation waitanimation diff --git a/test/battle/move_effect/aqua_ring.c b/test/battle/move_effect/aqua_ring.c index d137a35276..e800f76c6c 100644 --- a/test/battle/move_effect/aqua_ring.c +++ b/test/battle/move_effect/aqua_ring.c @@ -1,6 +1,27 @@ #include "global.h" #include "test/battle.h" +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_AQUA_RING) == EFFECT_AQUA_RING); +} + +SINGLE_BATTLE_TEST("Aqua Ring fails if already active") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_AQUA_RING); } + TURN { MOVE(player, MOVE_AQUA_RING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + MESSAGE("Wobbuffet surrounded itself with a veil of water!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + MESSAGE("But it failed!"); + } +} + SINGLE_BATTLE_TEST("Aqua Ring recovers 1/16th HP at end of turn") { GIVEN { @@ -15,9 +36,30 @@ SINGLE_BATTLE_TEST("Aqua Ring recovers 1/16th HP at end of turn") } } +SINGLE_BATTLE_TEST("Aqua Ring restores 30% more HP when holding Big Root") +{ + u32 item; + u16 expectedHp; + PARAMETRIZE { item = ITEM_NONE; expectedHp = 58; } + PARAMETRIZE { item = ITEM_BIG_ROOT; expectedHp = 60; } + + GIVEN { + ASSUME(gItemsInfo[ITEM_BIG_ROOT].holdEffect == HOLD_EFFECT_BIG_ROOT); + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_AQUA_RING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + } THEN { + EXPECT_EQ(player->hp, expectedHp); + } +} + SINGLE_BATTLE_TEST("Aqua Ring can be used under Heal Block but will not heal the user") { GIVEN { + ASSUME(GetMoveEffect(MOVE_HEAL_BLOCK) == EFFECT_HEAL_BLOCK); PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); Speed(50); } OPPONENT(SPECIES_WOBBUFFET) { Speed(100); } } WHEN { @@ -29,4 +71,21 @@ SINGLE_BATTLE_TEST("Aqua Ring can be used under Heal Block but will not heal the } } -TO_DO_BATTLE_TEST("Baton Pass passes Aqua Ring's effect"); +SINGLE_BATTLE_TEST("Aqua Ring's effect is passed by Baton Pass") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_BATON_PASS) == EFFECT_BATON_PASS); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT) { HP(50); MaxHP(128); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_AQUA_RING); } + TURN { MOVE(player, MOVE_BATON_PASS); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BATON_PASS, player); + SEND_IN_MESSAGE("Wynaut"); + } THEN { + EXPECT(player->hp == 58); + } +} diff --git a/test/battle/move_effect/ingrain.c b/test/battle/move_effect/ingrain.c index 19213f10d9..2d08a67744 100644 --- a/test/battle/move_effect/ingrain.c +++ b/test/battle/move_effect/ingrain.c @@ -1,4 +1,130 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Ingrain (Move Effect) test titles") +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_INGRAIN) == EFFECT_INGRAIN); +} + +SINGLE_BATTLE_TEST("Ingrain fails if already rooted") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + TURN { MOVE(player, MOVE_INGRAIN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + MESSAGE("Wobbuffet planted its roots!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + MESSAGE("But it failed!"); + } +} + +SINGLE_BATTLE_TEST("Ingrain restores 1/16th HP at the end of turn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + } THEN { + EXPECT_EQ(player->hp, 58); + } +} + +SINGLE_BATTLE_TEST("Ingrain restores 30% more HP when holding Big Root") +{ + u32 item; + u16 expectedHp; + PARAMETRIZE { item = ITEM_NONE; expectedHp = 58; } + PARAMETRIZE { item = ITEM_BIG_ROOT; expectedHp = 60; } + + GIVEN { + ASSUME(gItemsInfo[ITEM_BIG_ROOT].holdEffect == HOLD_EFFECT_BIG_ROOT); + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + } THEN { + EXPECT_EQ(player->hp, expectedHp); + } +} + +SINGLE_BATTLE_TEST("Ingrain can be used under Heal Block but will not heal the user") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_HEAL_BLOCK) == EFFECT_HEAL_BLOCK); + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); Speed(50); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(100); } + } WHEN { + TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_INGRAIN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + } THEN { + EXPECT_EQ(player->hp, 50); + } +} + +SINGLE_BATTLE_TEST("Ingrain prevents regular switching out") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + } THEN { + u32 battler = GetBattlerAtPosition(B_POSITION_PLAYER_LEFT); + EXPECT_EQ(CanBattlerEscape(battler), FALSE); + } +} + +SINGLE_BATTLE_TEST("Ingrain does not prevent switching out with Flip Turn") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_FLIP_TURN) == EFFECT_HIT_ESCAPE); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_INGRAIN, MOVE_FLIP_TURN); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + TURN { MOVE(player, MOVE_FLIP_TURN); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLIP_TURN, player); + HP_BAR(opponent); + SEND_IN_MESSAGE("Wynaut"); + } THEN { + EXPECT_EQ(player->species, SPECIES_WYNAUT); + } +} + +SINGLE_BATTLE_TEST("Ingrain's effect is passed by Baton Pass") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_BATON_PASS) == EFFECT_BATON_PASS); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_INGRAIN, MOVE_BATON_PASS); } + PLAYER(SPECIES_WYNAUT) { HP(50); MaxHP(128); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_INGRAIN); } + TURN { MOVE(player, MOVE_BATON_PASS); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_INGRAIN, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BATON_PASS, player); + SEND_IN_MESSAGE("Wynaut"); + } THEN { + EXPECT_EQ(player->species, SPECIES_WYNAUT); + EXPECT_EQ(player->hp, 58); + } +} + +TO_DO_BATTLE_TEST("Red Card and forced switch moves (Roar/Whirlwind) cannot force out a rooted Pokémon"); From 57c51c0702b50f6f6d53441f87f7a0069187beaf Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 18:59:09 +0800 Subject: [PATCH 03/22] Fix Transform fail conditions with gen-specific checks (#9070) --- include/config/battle.h | 4 + include/constants/generational_changes.h | 6 +- src/battle_script_commands.c | 9 +- src/battle_util.c | 2 +- test/battle/move_effect/transform.c | 132 ++++++++++++++++++++++- 5 files changed, 146 insertions(+), 7 deletions(-) diff --git a/include/config/battle.h b/include/config/battle.h index 965174d579..242e4bea68 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -115,6 +115,10 @@ // Additionally, in gen8+ the Healing Wish's effect will be stored until the user switches into a statused or hurt mon. #define B_DEFOG_EFFECT_CLEARING GEN_LATEST // In Gen5+, Defog does not lower Evasion of target behind Subsitute. In Gen6+, Defog clears Spikes, Toxic Spikes, Stealth Rock and Sticky Web from both sides. In Gen8+, Defog also clears active Terrain. #define B_STOCKPILE_RAISES_DEFS GEN_LATEST // In Gen4+, Stockpile also raises Defense and Sp. Defense stats. Once Spit Up / Swallow is used, these stat changes are lost. +#define B_TRANSFORM_SEMI_INV_FAIL GEN_LATEST // In Gen2+, Transform fails if the target is semi-invulnerable. +#define B_TRANSFORM_TARGET_FAIL GEN_LATEST // In Gen2+, Transform fails if the target is already transformed. +#define B_TRANSFORM_USER_FAIL GEN_LATEST // In Gen5+, Transform fails if the user is already transformed. +#define B_TRANSFORM_SUBSTITUTE_FAIL GEN_LATEST // In Gen5+, Transform fails if the target is behind a Substitute. #define B_TRANSFORM_SHINY GEN_LATEST // In Gen4+, Transform will copy the shiny state of the opponent instead of maintaining its own shiny state. #define B_TRANSFORM_FORM_CHANGES GEN_LATEST // In Gen5+, Transformed Pokemon cannot change forms. #define B_WIDE_GUARD GEN_LATEST // In Gen5 only, Wide Guard has a chance to fail if used consecutively. diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index 0a5331c342..84b9da76b1 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -106,8 +106,12 @@ F(B_HEALING_WISH_SWITCH, healingWishSwitch, (u32, GEN_COUNT - 1)) \ F(B_DEFOG_EFFECT_CLEARING, defogEffectClearing, (u32, GEN_COUNT - 1)) \ F(B_STOCKPILE_RAISES_DEFS, stockpileRaisesDefs, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ + F(B_TRANSFORM_SEMI_INV_FAIL, transformSemiInvFail, (u32, GEN_COUNT - 1)) \ + F(B_TRANSFORM_TARGET_FAIL, transformTargetFail, (u32, GEN_COUNT - 1)) \ + F(B_TRANSFORM_USER_FAIL, transformUserFail, (u32, GEN_COUNT - 1)) \ + F(B_TRANSFORM_SUBSTITUTE_FAIL, transformSubstituteFail, (u32, GEN_COUNT - 1)) \ F(B_TRANSFORM_SHINY, transformShiny, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ - F(B_TRANSFORM_FORM_CHANGES, transformFormChanges, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ + F(B_TRANSFORM_FORM_CHANGES, transformFormChanges, (u32, GEN_COUNT - 1)) \ F(B_WIDE_GUARD, wideGuard, (u32, GEN_COUNT - 1)) \ F(B_QUICK_GUARD, quickGuard, (u32, GEN_COUNT - 1)) \ F(B_IMPRISON, imprison, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 8819c8afad..0627013f5a 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -11278,10 +11278,11 @@ static void Cmd_transformdataexecution(void) gChosenMove = MOVE_UNAVAILABLE; gBattlescriptCurrInstr = cmd->nextInstr; - if (gBattleMons[gBattlerTarget].volatiles.transformed - || DoesSubstituteBlockMove(gBattlerAttacker, gBattlerTarget, gCurrentMove) - || gBattleStruct->illusion[gBattlerTarget].state == ILLUSION_ON - || IsSemiInvulnerable(gBattlerTarget, EXCLUDE_COMMANDER)) + if ((GetConfig(B_TRANSFORM_SEMI_INV_FAIL) >= GEN_2 && IsSemiInvulnerable(gBattlerTarget, EXCLUDE_COMMANDER)) + || (GetConfig(B_TRANSFORM_TARGET_FAIL) >= GEN_2 && gBattleMons[gBattlerTarget].volatiles.transformed) + || (GetConfig(B_TRANSFORM_USER_FAIL) >= GEN_5 && gBattleMons[gBattlerAttacker].volatiles.transformed) + || (GetConfig(B_TRANSFORM_SUBSTITUTE_FAIL) >= GEN_5 && DoesSubstituteBlockMove(gBattlerAttacker, gBattlerTarget, gCurrentMove)) + || gBattleStruct->illusion[gBattlerTarget].state == ILLUSION_ON) { gBattleStruct->moveResultFlags[gBattlerTarget] |= MOVE_RESULT_FAILED; gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_TRANSFORM_FAILED; diff --git a/src/battle_util.c b/src/battle_util.c index e352173577..f0b4925229 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -9342,7 +9342,7 @@ static bool32 CanBattlerFormChange(u32 battler, enum FormChanges method) { // Can't change form if transformed. if (gBattleMons[battler].volatiles.transformed - && B_TRANSFORM_FORM_CHANGES >= GEN_5) + && GetConfig(B_TRANSFORM_FORM_CHANGES) >= GEN_5) return FALSE; switch (method) diff --git a/test/battle/move_effect/transform.c b/test/battle/move_effect/transform.c index de98de70a6..d82728c184 100644 --- a/test/battle/move_effect/transform.c +++ b/test/battle/move_effect/transform.c @@ -1,7 +1,135 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Transform (Move Effect) test titles") +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_TRANSFORM) == EFFECT_TRANSFORM); +} + +SINGLE_BATTLE_TEST("Transform fails on semi-invulnerable target in Gen2+") +{ + u32 genConfig; + bool32 expectFail; + + PARAMETRIZE { genConfig = GEN_1; expectFail = FALSE; } + PARAMETRIZE { genConfig = GEN_2; expectFail = TRUE; } + + GIVEN { + WITH_CONFIG(B_TRANSFORM_SEMI_INV_FAIL, genConfig); + PLAYER(SPECIES_WOBBUFFET) { Speed(50); Moves(MOVE_DIG); } + OPPONENT(SPECIES_DITTO) { Speed(10); Moves(MOVE_TRANSFORM); } + } WHEN { + TURN { MOVE(player, MOVE_DIG); MOVE(opponent, MOVE_TRANSFORM); } + } SCENE { + if (expectFail) + MESSAGE("But it failed!"); + else + MESSAGE("The opposing Ditto transformed into Wobbuffet!"); + } +} + +SINGLE_BATTLE_TEST("Transform fails on transformed target in Gen2+") +{ + u32 genConfig; + bool32 expectFail; + + PARAMETRIZE { genConfig = GEN_1; expectFail = FALSE; } + PARAMETRIZE { genConfig = GEN_2; expectFail = TRUE; } + + GIVEN { + WITH_CONFIG(B_TRANSFORM_TARGET_FAIL, genConfig); + PLAYER(SPECIES_DITTO) { Speed(50); Moves(MOVE_TRANSFORM, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(10); Moves(MOVE_TRANSFORM, MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(player, MOVE_TRANSFORM); MOVE(opponent, MOVE_CELEBRATE); } + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_TRANSFORM); } + } SCENE { + MESSAGE("Ditto transformed into Wobbuffet!"); + if (expectFail) + MESSAGE("But it failed!"); + else + MESSAGE("The opposing Wobbuffet transformed into Wobbuffet!"); + } +} + +SINGLE_BATTLE_TEST("Transform fails when the user is already transformed in Gen5+") +{ + u32 genConfig; + bool32 expectFail; + + PARAMETRIZE { genConfig = GEN_4; expectFail = FALSE; } + PARAMETRIZE { genConfig = GEN_5; expectFail = TRUE; } + + GIVEN { + WITH_CONFIG(B_TRANSFORM_USER_FAIL, genConfig); + PLAYER(SPECIES_WOBBUFFET) { Speed(50); Moves(MOVE_TRANSFORM, MOVE_CELEBRATE); } + OPPONENT(SPECIES_DITTO) { Speed(10); Moves(MOVE_TRANSFORM, MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_TRANSFORM); } + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_TRANSFORM); } + } SCENE { + MESSAGE("The opposing Ditto transformed into Wobbuffet!"); + if (expectFail) + MESSAGE("But it failed!"); + else + MESSAGE("The opposing Ditto transformed into Wobbuffet!"); + } +} + +SINGLE_BATTLE_TEST("Transform fails on target behind substitute in Gen5+") +{ + u32 genConfig; + bool32 expectFail; + + PARAMETRIZE { genConfig = GEN_4; expectFail = FALSE; } + PARAMETRIZE { genConfig = GEN_5; expectFail = TRUE; } + + GIVEN { + WITH_CONFIG(B_TRANSFORM_SUBSTITUTE_FAIL, genConfig); + PLAYER(SPECIES_WOBBUFFET) { Speed(50); Moves(MOVE_SUBSTITUTE); } + OPPONENT(SPECIES_DITTO) { Speed(10); Moves(MOVE_TRANSFORM); } + } WHEN { + TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_TRANSFORM); } + } SCENE { + if (expectFail) + MESSAGE("But it failed!"); + else + MESSAGE("The opposing Ditto transformed into Wobbuffet!"); + } +} + +SINGLE_BATTLE_TEST("Transformed Pokemon cannot change forms in Gen5+") +{ + u32 genConfig; + bool32 expectFormChange; + + PARAMETRIZE { genConfig = GEN_4; expectFormChange = TRUE; } + PARAMETRIZE { genConfig = GEN_5; expectFormChange = FALSE; } + + GIVEN { + WITH_CONFIG(B_TRANSFORM_FORM_CHANGES, genConfig); + PLAYER(SPECIES_AEGISLASH) { Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + OPPONENT(SPECIES_DITTO) { Moves(MOVE_TACKLE, MOVE_TRANSFORM); } + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_TRANSFORM); } + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_TACKLE); } + } SCENE { + if (expectFormChange) { + ABILITY_POPUP(opponent, ABILITY_STANCE_CHANGE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_FORM_CHANGE, opponent); + } else { + NONE_OF { + ABILITY_POPUP(opponent, ABILITY_STANCE_CHANGE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_FORM_CHANGE, opponent); + } + } + } THEN { + if (expectFormChange) + EXPECT_EQ(opponent->species, SPECIES_AEGISLASH_BLADE); + else + EXPECT_EQ(opponent->species, SPECIES_AEGISLASH); + } +} SINGLE_BATTLE_TEST("(TERA) Transform does not copy the target's Tera Type, and if the user is Terastallized it keeps its own Tera Type") { @@ -42,3 +170,5 @@ SINGLE_BATTLE_TEST("Transform returns the user to normal at the end of the battl EXPECT_EQ(GetMonData(&gPlayerParty[0], MON_DATA_SPECIES), SPECIES_DITTO); } } + +TO_DO_BATTLE_TEST("TODO: Write Transform (Move Effect) test titles") From acfd2f4f8cf786a8520eeac0471f8a106ee13440 Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 19:05:41 +0800 Subject: [PATCH 04/22] Fix Taunt to not block Me First in Gen 5+ (#9069) --- include/config/battle.h | 1 + include/constants/generational_changes.h | 1 + src/battle_util.c | 17 ++++++++++--- test/battle/move_effect/me_first.c | 31 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/include/config/battle.h b/include/config/battle.h index 242e4bea68..16b4ac9b64 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -124,6 +124,7 @@ #define B_WIDE_GUARD GEN_LATEST // In Gen5 only, Wide Guard has a chance to fail if used consecutively. #define B_QUICK_GUARD GEN_LATEST // In Gen5 only, Quick Guard has a chance to fail if used consecutively. #define B_IMPRISON GEN_LATEST // In Gen5+, Imprison doesn't fail if opposing pokemon don't have any moves the user knows. +#define B_TAUNT_ME_FIRST GEN_LATEST // In Gen5+, Taunt does not block Me First. #define B_ALLY_SWITCH_FAIL_CHANCE GEN_LATEST // In Gen9+, using Ally Switch consecutively decreases the chance of success for each consecutive use. #define B_SKETCH_BANS GEN_LATEST // In Gen9+, Sketch is unable to copy more moves than in previous generations. #define B_KNOCK_OFF_REMOVAL GEN_LATEST // In Gen5+, Knock Off removes the foe's item instead of rendering it unusable. diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index 84b9da76b1..8a8a31e830 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -115,6 +115,7 @@ F(B_WIDE_GUARD, wideGuard, (u32, GEN_COUNT - 1)) \ F(B_QUICK_GUARD, quickGuard, (u32, GEN_COUNT - 1)) \ F(B_IMPRISON, imprison, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ + F(B_TAUNT_ME_FIRST, tauntMeFirst, (u32, GEN_COUNT - 1)) \ F(B_ALLY_SWITCH_FAIL_CHANCE, allySwitchFailChance, (u32, GEN_COUNT - 1)) \ F(B_SKETCH_BANS, sketchBans, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ F(B_KNOCK_OFF_REMOVAL, knockOffRemoval, (u32, GEN_COUNT - 1)) /* TODO: use in tests */ \ diff --git a/src/battle_util.c b/src/battle_util.c index f0b4925229..8535da5493 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -1474,7 +1474,10 @@ u32 TrySetCantSelectMoveBattleScript(u32 battler) } } - if (GetActiveGimmick(battler) != GIMMICK_Z_MOVE && gDisableStructs[battler].tauntTimer != 0 && IsBattleMoveStatus(move)) + if (GetActiveGimmick(battler) != GIMMICK_Z_MOVE + && gDisableStructs[battler].tauntTimer != 0 + && IsBattleMoveStatus(move) + && (GetConfig(B_TAUNT_ME_FIRST) < GEN_5 || moveEffect != EFFECT_ME_FIRST)) { if ((GetActiveGimmick(battler) == GIMMICK_DYNAMAX)) gCurrentMove = MOVE_MAX_GUARD; @@ -1709,7 +1712,10 @@ u32 CheckMoveLimitations(u32 battler, u8 unusableMoves, u16 check) else if (check & MOVE_LIMITATION_TORMENTED && move == gLastMoves[battler] && gBattleMons[battler].volatiles.torment == TRUE) unusableMoves |= 1u << i; // Taunt - else if (check & MOVE_LIMITATION_TAUNT && gDisableStructs[battler].tauntTimer && IsBattleMoveStatus(move)) + else if (check & MOVE_LIMITATION_TAUNT + && gDisableStructs[battler].tauntTimer + && IsBattleMoveStatus(move) + && (GetConfig(B_TAUNT_ME_FIRST) < GEN_5 || moveEffect != EFFECT_ME_FIRST)) unusableMoves |= 1u << i; // Imprison else if (check & MOVE_LIMITATION_IMPRISON && GetImprisonedMovesCount(battler, move)) @@ -2249,7 +2255,12 @@ static enum MoveCanceler CancelerVolatileBlocked(struct BattleContext *ctx) static enum MoveCanceler CancelerTaunted(struct BattleContext *ctx) { - if (GetActiveGimmick(ctx->battlerAtk) != GIMMICK_Z_MOVE && gDisableStructs[ctx->battlerAtk].tauntTimer && IsBattleMoveStatus(ctx->currentMove)) + enum BattleMoveEffects moveEffect = GetMoveEffect(ctx->currentMove); + + if (GetActiveGimmick(ctx->battlerAtk) != GIMMICK_Z_MOVE + && gDisableStructs[ctx->battlerAtk].tauntTimer + && IsBattleMoveStatus(ctx->currentMove) + && (GetConfig(B_TAUNT_ME_FIRST) < GEN_5 || moveEffect != EFFECT_ME_FIRST)) { gProtectStructs[ctx->battlerAtk].unableToUseMove = TRUE; CancelMultiTurnMoves(ctx->battlerAtk, SKY_DROP_ATTACKCANCELER_CHECK); diff --git a/test/battle/move_effect/me_first.c b/test/battle/move_effect/me_first.c index 8ccaa324ef..48f8d179be 100644 --- a/test/battle/move_effect/me_first.c +++ b/test/battle/move_effect/me_first.c @@ -82,6 +82,37 @@ SINGLE_BATTLE_TEST("Me First can be selected if users holds Assault Vest") } } +SINGLE_BATTLE_TEST("Me First can be selected under Taunt in Gen5+") +{ + u32 gen = 0; + + PARAMETRIZE { gen = GEN_4; } + PARAMETRIZE { gen = GEN_5; } + + GIVEN { + WITH_CONFIG(B_TAUNT_ME_FIRST, gen); + PLAYER(SPECIES_WOBBUFFET) { Speed(100); Moves(MOVE_ME_FIRST, MOVE_TACKLE); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(50); Moves(MOVE_TAUNT, MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); MOVE(opponent, MOVE_TAUNT); } + if (gen >= GEN_5) { + TURN { MOVE(player, MOVE_ME_FIRST); MOVE(opponent, MOVE_TACKLE); } + } else { + TURN { + MOVE(player, MOVE_ME_FIRST, allowed: FALSE); + MOVE(player, MOVE_TACKLE); + MOVE(opponent, MOVE_TACKLE); + } + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TAUNT, opponent); + if (gen >= GEN_5) + ANIMATION(ANIM_TYPE_MOVE, MOVE_ME_FIRST, player); + else + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + } +} + SINGLE_BATTLE_TEST("Me First deducts power points from itself, not the copied move") { ASSUME(GetMovePP(MOVE_ME_FIRST) == 20); From 164e0c7ebc240e2f9d74b584f56840cb1dedcb86 Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 20:56:38 +0800 Subject: [PATCH 05/22] Fix Present heal miss-flag handling and enforce Telepathy blocking (#9170) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- src/battle_script_commands.c | 6 ++- test/battle/move_effect/present.c | 61 ++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 0627013f5a..fbf4486689 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -12133,13 +12133,17 @@ static void Cmd_presentdamagecalculation(void) { gBattlescriptCurrInstr = BattleScript_HitFromCritCalc; } + else if (gBattlerTarget == BATTLE_PARTNER(gBattlerAttacker) && GetBattlerAbility(gBattlerTarget) == ABILITY_TELEPATHY) + { + gBattlescriptCurrInstr = BattleScript_MoveMissedPause; + } else if (gBattleMons[gBattlerTarget].maxHP == gBattleMons[gBattlerTarget].hp) { gBattlescriptCurrInstr = BattleScript_AlreadyAtFullHp; } else { - gBattleStruct->moveResultFlags[gBattlerTarget] &= ~MOVE_RESULT_DOESNT_AFFECT_FOE; + gBattleStruct->moveResultFlags[gBattlerTarget] &= ~(MOVE_RESULT_MISSED | MOVE_RESULT_DOESNT_AFFECT_FOE); gBattlescriptCurrInstr = BattleScript_PresentHealTarget; } } diff --git a/test/battle/move_effect/present.c b/test/battle/move_effect/present.c index bd21f13de4..224e85bf8f 100644 --- a/test/battle/move_effect/present.c +++ b/test/battle/move_effect/present.c @@ -1,4 +1,63 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Present (Move Effect) test titles") +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_PRESENT) == EFFECT_PRESENT); +} + +SINGLE_BATTLE_TEST("Present healing through Wonder Guard is still considered to have affected the target") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_MIRROR_MOVE) == EFFECT_MIRROR_MOVE); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SHEDINJA) { Ability(ABILITY_WONDER_GUARD); HP(1); MaxHP(100); } + } WHEN { + TURN { MOVE(player, MOVE_PRESENT, WITH_RNG(RNG_PRESENT, 254)); } + TURN { MOVE(opponent, MOVE_MIRROR_MOVE, WITH_RNG(RNG_PRESENT, 1)); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_PRESENT, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_MIRROR_MOVE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_PRESENT, opponent); + HP_BAR(player); + } +} + +DOUBLE_BATTLE_TEST("Present healing is blocked by Telepathy on an ally target") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_TELEPATHY); HP(50); MaxHP(100); } + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_PRESENT, target: playerRight, WITH_RNG(RNG_PRESENT, 254)); } + } SCENE { + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_PRESENT, playerLeft); + HP_BAR(playerRight); + } + } THEN { + EXPECT_EQ(playerRight->hp, 50); + } +} + +SINGLE_BATTLE_TEST("Present with Parental Bond hits twice when damaging, but only once when healing") +{ + GIVEN { + ASSUME(GetSpeciesAbility(SPECIES_KANGASKHAN_MEGA, 0) == ABILITY_PARENTAL_BOND); + PLAYER(SPECIES_KANGASKHAN) { Item(ITEM_KANGASKHANITE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_PRESENT, gimmick: GIMMICK_MEGA, WITH_RNG(RNG_PRESENT, 1)); } + TURN { MOVE(player, MOVE_PRESENT, WITH_RNG(RNG_PRESENT, 254)); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_PRESENT, player); + HP_BAR(opponent); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_PRESENT, player); + HP_BAR(opponent); + NOT HP_BAR(opponent); + } +} From 1e7208dfca3483667b3643be1b437d5ccc06d7de Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 10 Feb 2026 21:38:16 +0800 Subject: [PATCH 06/22] Fix Commander cleanup after Volt Switch switch-in (#9141) --- src/battle_main.c | 3 +++ test/battle/ability/commander.c | 33 +++++++++++++++++++++++++++++++++ test/battle/ai/ai_switching.c | 9 +++++---- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/battle_main.c b/src/battle_main.c index c10b3cafe1..462331ae7c 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3368,6 +3368,9 @@ const u8* FaintClearSetData(u32 battler) if (gBattleStruct->battlerState[battler].commanderSpecies != SPECIES_NONE) { u32 partner = BATTLE_PARTNER(battler); + // Clear commander state immediately so a replacement doesn't inherit it. + gBattleStruct->battlerState[battler].commanderSpecies = SPECIES_NONE; + gBattleMons[partner].volatiles.semiInvulnerable = STATE_NONE; if (IsBattlerAlive(partner)) { BtlController_EmitSpriteInvisibility(partner, B_COMM_TO_CONTROLLER, FALSE); diff --git a/test/battle/ability/commander.c b/test/battle/ability/commander.c index e33c4e1521..93a8021877 100644 --- a/test/battle/ability/commander.c +++ b/test/battle/ability/commander.c @@ -380,6 +380,7 @@ DOUBLE_BATTLE_TEST("Commander Tatsugiri does not attack if Dondozo faints the sa DOUBLE_BATTLE_TEST("Commander Tatsugiri does not get hit by Dragon Darts when a commanded Dondozo faints") { GIVEN { + KNOWN_FAILING; ASSUME(GetMoveEffect(MOVE_DRAGON_DARTS) == EFFECT_DRAGON_DARTS); PLAYER(SPECIES_WOBBUFFET); PLAYER(SPECIES_DONDOZO) { HP(1); } @@ -474,3 +475,35 @@ DOUBLE_BATTLE_TEST("Commander will not activate if partner Dondozo is about to s NOT ABILITY_POPUP(playerRight, ABILITY_COMMANDER); } } + +DOUBLE_BATTLE_TEST("Commander clears when Dondozo is replaced and Tatsugiri can be hit") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_VOLT_SWITCH) == EFFECT_HIT_ESCAPE); + PLAYER(SPECIES_DONDOZO) { HP(1); Speed(1); } + PLAYER(SPECIES_TATSUGIRI) { Ability(ABILITY_COMMANDER); MaxHP(400); HP(400); Speed(2); } + PLAYER(SPECIES_SEADRA) { Speed(3); } + OPPONENT(SPECIES_VENUSAUR) { Speed(5); } + OPPONENT(SPECIES_LUXRAY) { Speed(6); } + OPPONENT(SPECIES_BUTTERFREE) { Speed(4); } + } WHEN { + TURN { + MOVE(opponentLeft, MOVE_SEED_BOMB, target: playerRight); + MOVE(opponentRight, MOVE_VOLT_SWITCH, target: playerLeft); + SEND_OUT(opponentRight, 2); + SEND_OUT(playerLeft, 2); + } + TURN { + MOVE(opponentRight, MOVE_BUG_BUZZ, target: playerRight); + } + } SCENE { + ABILITY_POPUP(playerRight, ABILITY_COMMANDER); + MESSAGE("Tatsugiri was swallowed by Dondozo and became Dondozo's commander!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_VOLT_SWITCH, opponentRight); + MESSAGE("Dondozo fainted!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEED_BOMB, opponentLeft); + HP_BAR(playerRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BUG_BUZZ, opponentRight); + HP_BAR(playerRight); + } +} diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index 7d7cf7561f..7e0e923e71 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -1742,9 +1742,10 @@ AI_DOUBLE_BATTLE_TEST("AI will not choose to switch out Dondozo with Commander T PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_CELEBRATE); } PLAYER(SPECIES_ZIGZAGOON) { Moves (MOVE_CELEBRATE); } } WHEN { - TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_PERISH_SONG); } - TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); } - TURN { SWITCH(playerLeft, 2); SWITCH(playerRight, 3); } - TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); EXPECT_MOVE(opponentLeft, MOVE_WATER_GUN); } + // Commander Tatsugiri cannot act while swallowed, so skip its turn explicitly. + TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_PERISH_SONG); SKIP_TURN(opponentRight); } + TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); SKIP_TURN(opponentRight); } + TURN { SWITCH(playerLeft, 2); SWITCH(playerRight, 3); SKIP_TURN(opponentRight); } + TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); EXPECT_MOVE(opponentLeft, MOVE_WATER_GUN); SKIP_TURN(opponentRight); } } } From ce15e5486de9e57f677dad7da929963e5c41a13d Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:13:27 +0100 Subject: [PATCH 07/22] Fixes Gulp Missile crash on targets that can be statused (#9179) --- data/battle_scripts_1.s | 6 ++---- src/battle_script_commands.c | 8 ++++++++ test/battle/ability/gulp_missile.c | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 7abeade1da..043a7fbd24 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -5321,9 +5321,7 @@ BattleScript_GulpMissileNoDmgGorging: handleformchange BS_TARGET, 0 playanimation BS_TARGET, B_ANIM_FORM_CHANGE waitanimation - swapattackerwithtarget - seteffectprimary BS_ATTACKER, BS_TARGET, MOVE_EFFECT_PARALYSIS - swapattackerwithtarget + seteffectprimary BS_TARGET, BS_ATTACKER, MOVE_EFFECT_PARALYSIS return BattleScript_GulpMissileNoSecondEffectGorging: handleformchange BS_TARGET, 0 @@ -5353,7 +5351,7 @@ BattleScript_GulpMissileNoDmgGulping: printfromtable gStatDownStringIds waitmessage B_WAIT_TIME_LONG BattleScript_GulpMissileGulpingEnd: - swapattackerwithtarget @ restore the battlers, just in case + swapattackerwithtarget return BattleScript_GulpMissileNoSecondEffectGulping: handleformchange BS_TARGET, 0 diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index fbf4486689..77464b1751 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -3108,7 +3108,9 @@ void SetMoveEffect(u32 battler, u32 effectBattler, enum MoveEffect moveEffect, c case MOVE_EFFECT_TOXIC: case MOVE_EFFECT_FROSTBITE: if (IsSafeguardProtected(gBattlerAttacker, gEffectBattler, GetBattlerAbility(gBattlerAttacker)) && !primary) + { gBattlescriptCurrInstr = battleScript; + } else if (CanSetNonVolatileStatus( gBattlerAttacker, gEffectBattler, @@ -3116,7 +3118,13 @@ void SetMoveEffect(u32 battler, u32 effectBattler, enum MoveEffect moveEffect, c battlerAbility, moveEffect, CHECK_TRIGGER)) + { SetNonVolatileStatus(gEffectBattler, moveEffect, battleScript, TRIGGER_ON_MOVE); + } + else + { + gBattlescriptCurrInstr = battleScript; + } break; case MOVE_EFFECT_CONFUSION: if (!CanBeConfused(gEffectBattler) diff --git a/test/battle/ability/gulp_missile.c b/test/battle/ability/gulp_missile.c index 5a3266ff17..89c6d904db 100644 --- a/test/battle/ability/gulp_missile.c +++ b/test/battle/ability/gulp_missile.c @@ -201,3 +201,30 @@ SINGLE_BATTLE_TEST("Gulp Missile triggered by explosion doesn't freeze the game" TURN { MOVE(opponent, MOVE_SURF); MOVE(player, MOVE_EXPLOSION); } } } + +SINGLE_BATTLE_TEST("(Gulp Missile) Cramorant in Gorging damages an electric type without paralysing") +{ + GIVEN { + PLAYER(SPECIES_CRAMORANT) { HP(120); MaxHP(250); Ability(ABILITY_GULP_MISSILE); } + OPPONENT(SPECIES_EELEKTROSS); + } WHEN { + TURN { MOVE(player, MOVE_SURF); MOVE(opponent, MOVE_SCRATCH); } + TURN { MOVE(player, MOVE_SCRATCH); MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SURF, player); + HP_BAR(opponent); + ABILITY_POPUP(player, ABILITY_GULP_MISSILE); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + ABILITY_POPUP(player, ABILITY_GULP_MISSILE); + HP_BAR(opponent); + NONE_OF { + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent); + STATUS_ICON(opponent, paralysis: TRUE); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + } +} From 312ddddc178f20efbafc593efb9f6c0c7497581b Mon Sep 17 00:00:00 2001 From: GGbond Date: Fri, 13 Feb 2026 07:00:35 +0800 Subject: [PATCH 08/22] Fix multi battle switch checks for Eject items (#9190) --- src/battle_script_commands.c | 4 ++-- src/battle_util.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 77464b1751..732ce1c5de 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -5643,7 +5643,7 @@ static inline bool32 CanEjectButtonTrigger(u32 battlerAtk, u32 battlerDef, enum && battlerAtk != battlerDef && IsBattlerTurnDamaged(battlerDef) && IsBattlerAlive(battlerDef) - && CountUsablePartyMons(battlerDef) > 0 + && CanBattlerSwitch(battlerDef) && !(moveEffect == EFFECT_HIT_SWITCH_TARGET && CanBattlerSwitch(battlerAtk))) return TRUE; @@ -5655,7 +5655,7 @@ static inline bool32 CanEjectPackTrigger(u32 battlerAtk, u32 battlerDef, enum Ba if (gDisableStructs[battlerDef].tryEjectPack && GetBattlerHoldEffect(battlerDef) == HOLD_EFFECT_EJECT_PACK && IsBattlerAlive(battlerDef) - && CountUsablePartyMons(battlerDef) > 0 + && CanBattlerSwitch(battlerDef) && !gProtectStructs[battlerDef].disableEjectPack && !(moveEffect == EFFECT_HIT_SWITCH_TARGET && CanBattlerSwitch(battlerAtk)) && !(moveEffect == EFFECT_PARTING_SHOT && CanBattlerSwitch(battlerAtk))) diff --git a/src/battle_util.c b/src/battle_util.c index 8535da5493..9760173f68 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -10691,7 +10691,7 @@ bool32 TrySwitchInEjectPack(enum EjectPackTiming timing) if (gDisableStructs[i].tryEjectPack && GetBattlerHoldEffect(i) == HOLD_EFFECT_EJECT_PACK && IsBattlerAlive(i) - && CountUsablePartyMons(i) > 0) + && CanBattlerSwitch(i)) { ejectPackBattlers |= 1u << i; numEjectPackBattlers++; From a3ab5bf6932cfda5432944a3e96cbddb0ac17ae8 Mon Sep 17 00:00:00 2001 From: GGbond Date: Sat, 14 Feb 2026 07:59:08 +0800 Subject: [PATCH 09/22] =?UTF-8?q?Fix=20incorrect=20player=20berry=20animat?= =?UTF-8?q?ion=20on=20opponent=E2=80=99s=20low-HP=20heal=20at=20battle=20s?= =?UTF-8?q?tart=20(#9198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/battle_main.c | 1 + test/battle/ability/dazzling.c | 4 ++-- test/battle/hold_effect/restore_hp.c | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/battle_main.c b/src/battle_main.c index 462331ae7c..708448f1b7 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3890,6 +3890,7 @@ static void TryDoEventsBeforeFirstTurn(void) while (gBattleStruct->switchInBattlerCounter < gBattlersCount) // From fastest to slowest { u32 battler = gBattlerByTurnOrder[gBattleStruct->switchInBattlerCounter++]; + gBattlerAttacker = battler; if (ItemBattleEffects(battler, 0, GetBattlerHoldEffect(battler), IsOnSwitchInFirstTurnActivation)) return; } diff --git a/test/battle/ability/dazzling.c b/test/battle/ability/dazzling.c index f54f024452..26279fa360 100644 --- a/test/battle/ability/dazzling.c +++ b/test/battle/ability/dazzling.c @@ -163,8 +163,8 @@ SINGLE_BATTLE_TEST("Dazzling, Queenly Majesty and Armor Tail do not block Teatim GIVEN { ASSUME(GetMoveEffect(MOVE_TEATIME) == EFFECT_TEATIME); ASSUME(GetItemHoldEffect(ITEM_ORAN_BERRY) == HOLD_EFFECT_RESTORE_HP); - PLAYER(SPECIES_MURKROW) { Ability(ABILITY_PRANKSTER); Item(ITEM_ORAN_BERRY); HP(1); MaxHP(100); } - OPPONENT(species) { Ability(ability); Item(ITEM_ORAN_BERRY); HP(1); MaxHP(100); } + PLAYER(SPECIES_MURKROW) { Ability(ABILITY_PRANKSTER); Item(ITEM_ORAN_BERRY); HP(60); MaxHP(100); } + OPPONENT(species) { Ability(ability); Item(ITEM_ORAN_BERRY); HP(60); MaxHP(100); } } WHEN { TURN { MOVE(player, MOVE_TEATIME); } } SCENE { diff --git a/test/battle/hold_effect/restore_hp.c b/test/battle/hold_effect/restore_hp.c index 2441e92449..9304296c1b 100644 --- a/test/battle/hold_effect/restore_hp.c +++ b/test/battle/hold_effect/restore_hp.c @@ -82,3 +82,16 @@ SINGLE_BATTLE_TEST("Sitrus Berry restores HP immediately after Leech Seed damage HP_BAR(player); } } + +SINGLE_BATTLE_TEST("Healing berry animates on the correct battler at battle start") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { HP(1); MaxHP(400); Item(ITEM_ORAN_BERRY); } + } WHEN { + TURN { } + } SCENE { + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); + } +} From 757cbc2e7d736c38459c69a371fb1beb3068d197 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:25:19 +0000 Subject: [PATCH 10/22] Fixes Protective Pads preventing Poison Touch activation (#9222) --- src/battle_util.c | 6 ++--- test/battle/ability/fluffy.c | 22 +++++++++++++++++++ test/battle/ability/pickpocket.c | 29 +++++++++++++++++++++++++ test/battle/ability/poison_touch.c | 35 ++++++++++++++++++++++++++++++ test/battle/ability/tough_claws.c | 24 ++++++++++++++++++++ test/battle/ability/unseen_fist.c | 31 ++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 3 deletions(-) diff --git a/src/battle_util.c b/src/battle_util.c index 9760173f68..dd08a155ef 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5418,7 +5418,7 @@ u32 AbilityBattleEffects(enum AbilityEffect caseID, u32 battler, enum Ability ab if (IsBattlerAlive(gBattlerTarget) && !gProtectStructs[gBattlerAttacker].confusionSelfDmg && CanBePoisoned(gBattlerAttacker, gBattlerTarget, gLastUsedAbility, GetBattlerAbility(gBattlerTarget)) - && !CanBattlerAvoidContactEffects(gBattlerAttacker, gBattlerTarget, GetBattlerAbility(gBattlerAttacker), GetBattlerHoldEffect(gBattlerAttacker), move) + && IsMoveMakingContact(gBattlerAttacker, gBattlerTarget, GetBattlerAbility(gBattlerAttacker), GetBattlerHoldEffect(gBattlerAttacker), move) && IsBattlerTurnDamaged(gBattlerTarget) // Need to actually hit the target && RandomPercentage(RNG_POISON_TOUCH, 30)) { @@ -8383,12 +8383,12 @@ static inline uq4_12_t GetDefenderAbilitiesModifier(struct DamageContext *ctx) } break; case ABILITY_FLUFFY: - if (ctx->moveType == TYPE_FIRE && !IsMoveMakingContact(ctx->battlerAtk, ctx->battlerDef, ABILITY_NONE, ctx->holdEffectAtk, ctx->move)) + if (ctx->moveType == TYPE_FIRE && !IsMoveMakingContact(ctx->battlerAtk, ctx->battlerDef, ctx->abilityAtk, ctx->holdEffectAtk, ctx->move)) { modifier = UQ_4_12(2.0); recordAbility = TRUE; } - if (ctx->moveType != TYPE_FIRE && IsMoveMakingContact(ctx->battlerAtk, ctx->battlerDef, ABILITY_NONE, ctx->holdEffectAtk, ctx->move)) + if (ctx->moveType != TYPE_FIRE && IsMoveMakingContact(ctx->battlerAtk, ctx->battlerDef, ctx->abilityAtk, ctx->holdEffectAtk, ctx->move)) { modifier = UQ_4_12(0.5); recordAbility = TRUE; diff --git a/test/battle/ability/fluffy.c b/test/battle/ability/fluffy.c index 68afbd8993..964e7d9b06 100644 --- a/test/battle/ability/fluffy.c +++ b/test/battle/ability/fluffy.c @@ -70,6 +70,7 @@ SINGLE_BATTLE_TEST("Fluffy halves damage taken from moves that make direct conta PARAMETRIZE { ability = ABILITY_KLUTZ; } PARAMETRIZE { ability = ABILITY_FLUFFY; } GIVEN { + ASSUME(MoveMakesContact(MOVE_THUNDER_PUNCH)); PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_PROTECTIVE_PADS); } OPPONENT(SPECIES_STUFFUL) { Ability(ability); } } WHEN { @@ -88,6 +89,8 @@ SINGLE_BATTLE_TEST("Fluffy does not halve damage taken from moves that make dire PARAMETRIZE { ability = ABILITY_KLUTZ; } PARAMETRIZE { ability = ABILITY_FLUFFY; } GIVEN { + ASSUME(MoveMakesContact(MOVE_THUNDER_PUNCH)); + ASSUME(IsPunchingMove(MOVE_THUNDER_PUNCH)); PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_PUNCHING_GLOVE); } OPPONENT(SPECIES_STUFFUL) { Ability(ability); } } WHEN { @@ -99,3 +102,22 @@ SINGLE_BATTLE_TEST("Fluffy does not halve damage taken from moves that make dire EXPECT_EQ(results[0].damage, results[1].damage); } } + +SINGLE_BATTLE_TEST("Fluffy does not halve damage taken from moves that make direct contact but are ignored by Long Reach", s16 damage) +{ + enum Ability ability; + PARAMETRIZE { ability = ABILITY_KLUTZ; } + PARAMETRIZE { ability = ABILITY_FLUFFY; } + GIVEN { + ASSUME(MoveMakesContact(MOVE_THUNDER_PUNCH)); + PLAYER(SPECIES_ROWLET) { Ability(ABILITY_LONG_REACH); } + OPPONENT(SPECIES_STUFFUL) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_THUNDER_PUNCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_PUNCH, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_EQ(results[0].damage, results[1].damage); + } +} diff --git a/test/battle/ability/pickpocket.c b/test/battle/ability/pickpocket.c index e6b92a6e8e..559a8e2a21 100644 --- a/test/battle/ability/pickpocket.c +++ b/test/battle/ability/pickpocket.c @@ -310,3 +310,32 @@ SINGLE_BATTLE_TEST("Pickpocket does not prevent King's Rock or Razor Fang flinch EXPECT(player->item == ITEM_NONE); } } + +SINGLE_BATTLE_TEST("Pickpocket activates when user has Protective Pads, but not with Punching Glove or Long Reach") +{ + u32 item, ability; + + PARAMETRIZE { item = ITEM_PROTECTIVE_PADS; ability = ABILITY_OVERGROW; } + PARAMETRIZE { item = ITEM_PUNCHING_GLOVE; ability = ABILITY_OVERGROW; } + PARAMETRIZE { item = ITEM_NONE; ability = ABILITY_LONG_REACH; } + + GIVEN { + ASSUME(MoveMakesContact(MOVE_MACH_PUNCH)); + ASSUME(IsPunchingMove(MOVE_MACH_PUNCH)); + ASSUME(GetItemHoldEffect(ITEM_PROTECTIVE_PADS) == HOLD_EFFECT_PROTECTIVE_PADS); + ASSUME(GetItemHoldEffect(ITEM_PUNCHING_GLOVE) == HOLD_EFFECT_PUNCHING_GLOVE); + ASSUME(GetItemHoldEffect(ITEM_FOCUS_SASH) == HOLD_EFFECT_FOCUS_SASH); + PLAYER(SPECIES_DECIDUEYE) { Ability(ability); Item(item); } + OPPONENT(SPECIES_SNEASEL) { Ability(ABILITY_PICKPOCKET); Item(ITEM_FOCUS_SASH); } + } WHEN { + TURN { MOVE(player, MOVE_MACH_PUNCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MACH_PUNCH, player); + + if (item == ITEM_PROTECTIVE_PADS) { + ABILITY_POPUP(opponent, ABILITY_PICKPOCKET); + } else { + NOT ABILITY_POPUP(opponent, ABILITY_PICKPOCKET); + } + } +} diff --git a/test/battle/ability/poison_touch.c b/test/battle/ability/poison_touch.c index e3775d7427..b0c8c73329 100644 --- a/test/battle/ability/poison_touch.c +++ b/test/battle/ability/poison_touch.c @@ -75,3 +75,38 @@ SINGLE_BATTLE_TEST("Poison Touch applies between multi-hit move hits") STATUS_ICON(opponent, poison: TRUE); } } + +SINGLE_BATTLE_TEST("Poison Touch activates when user has Protective Pads, but not with Punching Glove") +{ + u32 item; + + PARAMETRIZE { item = ITEM_PROTECTIVE_PADS; } + PARAMETRIZE { item = ITEM_PUNCHING_GLOVE; } + + GIVEN { + ASSUME(MoveMakesContact(MOVE_MACH_PUNCH)); + ASSUME(IsPunchingMove(MOVE_MACH_PUNCH)); + ASSUME(GetItemHoldEffect(ITEM_PROTECTIVE_PADS) == HOLD_EFFECT_PROTECTIVE_PADS); + ASSUME(GetItemHoldEffect(ITEM_PUNCHING_GLOVE) == HOLD_EFFECT_PUNCHING_GLOVE); + PLAYER(SPECIES_GRIMER) { Ability(ABILITY_POISON_TOUCH); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_MACH_PUNCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MACH_PUNCH, player); + + if (item != ITEM_PUNCHING_GLOVE) { + ABILITY_POPUP(player, ABILITY_POISON_TOUCH); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + MESSAGE("The opposing Wobbuffet was poisoned by Grimer's Poison Touch!"); + STATUS_ICON(opponent, poison: TRUE); + } else { + NONE_OF { + ABILITY_POPUP(player, ABILITY_POISON_TOUCH); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + MESSAGE("The opposing Wobbuffet was poisoned by Grimer's Poison Touch!"); + STATUS_ICON(opponent, poison: TRUE); + } + } + } +} diff --git a/test/battle/ability/tough_claws.c b/test/battle/ability/tough_claws.c index 4e6f4ecf8b..f2dd887fe4 100644 --- a/test/battle/ability/tough_claws.c +++ b/test/battle/ability/tough_claws.c @@ -2,3 +2,27 @@ #include "test/battle.h" TO_DO_BATTLE_TEST("TODO: Write Tough Claws (Ability) test titles") + +SINGLE_BATTLE_TEST("Tough Claws boosts contact moves when user has Protective Pads, but not with Punching Glove", s16 damage) +{ + u32 item; + + PARAMETRIZE { item = ITEM_PROTECTIVE_PADS; } + PARAMETRIZE { item = ITEM_PUNCHING_GLOVE; } + + GIVEN { + ASSUME(MoveMakesContact(MOVE_MACH_PUNCH)); + ASSUME(IsPunchingMove(MOVE_MACH_PUNCH)); + ASSUME(GetItemHoldEffect(ITEM_PROTECTIVE_PADS) == HOLD_EFFECT_PROTECTIVE_PADS); + ASSUME(GetItemHoldEffect(ITEM_PUNCHING_GLOVE) == HOLD_EFFECT_PUNCHING_GLOVE); + PLAYER(SPECIES_BARBARACLE) { Ability(ABILITY_TOUGH_CLAWS); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_MACH_PUNCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MACH_PUNCH, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[1].damage, UQ_4_12(1.18), results[0].damage); // 1.3 / 1.1 ~= 1.18 + } +} diff --git a/test/battle/ability/unseen_fist.c b/test/battle/ability/unseen_fist.c index 046ef7d2fb..da5880c042 100644 --- a/test/battle/ability/unseen_fist.c +++ b/test/battle/ability/unseen_fist.c @@ -1,4 +1,35 @@ #include "global.h" #include "test/battle.h" +ASSUMPTIONS +{ + ASSUME(MoveMakesContact(MOVE_SCRATCH)); + ASSUME(GetMoveEffect(MOVE_PROTECT) == EFFECT_PROTECT); +} + TO_DO_BATTLE_TEST("TODO: Write Unseen Fist (Ability) test titles") + +SINGLE_BATTLE_TEST("Unseen Fist ignores Protect when user has Protective Pads, but not with Punching Glove", s16 damage) +{ + u32 item; + + PARAMETRIZE { item = ITEM_PROTECTIVE_PADS; } + PARAMETRIZE { item = ITEM_PUNCHING_GLOVE; } + + GIVEN { + ASSUME(MoveMakesContact(MOVE_MACH_PUNCH)); + ASSUME(IsPunchingMove(MOVE_MACH_PUNCH)); + ASSUME(GetItemHoldEffect(ITEM_PROTECTIVE_PADS) == HOLD_EFFECT_PROTECTIVE_PADS); + ASSUME(GetItemHoldEffect(ITEM_PUNCHING_GLOVE) == HOLD_EFFECT_PUNCHING_GLOVE); + PLAYER(SPECIES_URSHIFU) { Ability(ABILITY_UNSEEN_FIST); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_MACH_PUNCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, opponent); + if (item != ITEM_PUNCHING_GLOVE) + ANIMATION(ANIM_TYPE_MOVE, MOVE_MACH_PUNCH, player); + else + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_MACH_PUNCH, player); + } +} From a3d9aa7ae23b39783684be0db6e22c631e339991 Mon Sep 17 00:00:00 2001 From: grintoul <166724814+grintoul1@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:57:53 +0000 Subject: [PATCH 11/22] Fix tera icon palettes (#9208) --- src/battle_controller_player_partner.c | 8 +----- src/battle_controller_recorded_partner.c | 33 +++++++++++++++++++----- src/battle_controllers.c | 8 +++--- src/battle_gfx_sfx_util.c | 3 +-- src/reshow_battle_screen.c | 4 +-- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/battle_controller_player_partner.c b/src/battle_controller_player_partner.c index 37af6497df..738b6e4c38 100644 --- a/src/battle_controller_player_partner.c +++ b/src/battle_controller_player_partner.c @@ -206,13 +206,7 @@ static void PlayerPartnerHandleDrawTrainerPic(u32 battler) enum DifficultyLevel difficulty = GetBattlePartnerDifficultyLevel(gPartnerTrainerId); - if (IsMultibattleTest()) - { - trainerPicId = TRAINER_BACK_PIC_STEVEN; - xPos = 90; - yPos = (8 - gTrainerBacksprites[trainerPicId].coordinates.size) * 4 + 80; - } - else if (gPartnerTrainerId > TRAINER_PARTNER(PARTNER_NONE)) + if (gPartnerTrainerId > TRAINER_PARTNER(PARTNER_NONE)) { trainerPicId = gBattlePartners[difficulty][gPartnerTrainerId - TRAINER_PARTNER(PARTNER_NONE)].trainerBackPic; xPos = 90; diff --git a/src/battle_controller_recorded_partner.c b/src/battle_controller_recorded_partner.c index 4a59002020..dddf32982b 100644 --- a/src/battle_controller_recorded_partner.c +++ b/src/battle_controller_recorded_partner.c @@ -203,11 +203,32 @@ static void RecordedPartnerHandleDrawTrainerPic(u32 battler) s16 xPos, yPos; u32 trainerPicId; - trainerPicId = TRAINER_BACK_PIC_STEVEN; - xPos = 90; - yPos = (8 - gTrainerBacksprites[trainerPicId].coordinates.size) * 4 + 80; + enum DifficultyLevel difficulty = GetBattlePartnerDifficultyLevel(gPartnerTrainerId); - isFrontPic = FALSE; + if (gPartnerTrainerId > TRAINER_PARTNER(PARTNER_NONE)) + { + trainerPicId = gBattlePartners[difficulty][gPartnerTrainerId - TRAINER_PARTNER(PARTNER_NONE)].trainerBackPic; + xPos = 90; + yPos = (8 - gTrainerBacksprites[trainerPicId].coordinates.size) * 4 + 80; + } + else if (IsAiVsAiBattle()) + { + trainerPicId = GetTrainerPicFromId(gPartnerTrainerId); + xPos = 60; + yPos = 80; + } + else + { + trainerPicId = GetFrontierTrainerFrontSpriteId(gPartnerTrainerId); + xPos = 32; + yPos = 80; + } + + // Use back pic only if the partner Steven or is custom. + if (gPartnerTrainerId > TRAINER_PARTNER(PARTNER_NONE)) + isFrontPic = FALSE; + else + isFrontPic = TRUE; BtlController_HandleDrawTrainerPic(battler, trainerPicId, isFrontPic, xPos, yPos, -1); } @@ -246,9 +267,9 @@ static void RecordedPartnerHandleIntroTrainerBallThrow(u32 battler) enum DifficultyLevel difficulty = GetBattlePartnerDifficultyLevel(gPartnerTrainerId); if (gPartnerTrainerId > TRAINER_PARTNER(PARTNER_NONE)) - trainerPal = gTrainerBacksprites[gBattlePartners[difficulty][gPartnerTrainerId - TRAINER_PARTNER(PARTNER_NONE)].trainerPic].palette.data; + trainerPal = gTrainerBacksprites[gBattlePartners[difficulty][gPartnerTrainerId - TRAINER_PARTNER(PARTNER_NONE)].trainerBackPic].palette.data; else if (IsAiVsAiBattle()) - trainerPal = gTrainerSprites[GetTrainerPicFromId(gPartnerTrainerId)].palette.data; + trainerPal = gTrainerSprites[GetTrainerBackPicFromId(gPartnerTrainerId)].palette.data; else trainerPal = gTrainerSprites[GetFrontierTrainerFrontSpriteId(gPartnerTrainerId)].palette.data; // 2 vs 2 multi battle in Battle Frontier, load front sprite and pal. diff --git a/src/battle_controllers.c b/src/battle_controllers.c index dd8abcf213..819fc10f30 100644 --- a/src/battle_controllers.c +++ b/src/battle_controllers.c @@ -2397,8 +2397,7 @@ void BtlController_HandleDrawTrainerPic(u32 battler, u32 trainerPicId, bool32 is if ((gBattleTypeFlags & BATTLE_TYPE_SAFARI) && GetBattlerPosition(battler) == B_POSITION_PLAYER_LEFT) gBattlerSpriteIds[battler] = gBattleStruct->trainerSlideSpriteIds[battler]; - // Aiming for palette slots 8 and 9 for Player and PlayerPartner to prevent Trainer Slides causing mons to change colour - gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = (8 + battler/2); + gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = battler; } gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].x2 = DISPLAY_WIDTH; gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].sSpeedX = -2; @@ -2423,8 +2422,7 @@ void BtlController_HandleTrainerSlide(u32 battler, u32 trainerPicId) 30); if ((gBattleTypeFlags & BATTLE_TYPE_SAFARI) && GetBattlerPosition(battler) == B_POSITION_PLAYER_LEFT) gBattlerSpriteIds[battler] = gBattleStruct->trainerSlideSpriteIds[battler]; - // Aiming for palette slots 8 and 9 for Player and PlayerPartner to prevent Trainer Slides causing mons to change colour - gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = (8 + battler/2); + gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = battler; gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].x2 = -96; gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].sSpeedX = 2; } @@ -2772,7 +2770,7 @@ void BtlController_HandleIntroTrainerBallThrow(u32 battler, u16 tagTrainerPal, c paletteNum = AllocSpritePalette(tagTrainerPal); LoadPalette(trainerPal, OBJ_PLTT_ID(paletteNum), PLTT_SIZE_4BPP); - gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = (8 + battler/2); + gSprites[gBattleStruct->trainerSlideSpriteIds[battler]].oam.paletteNum = paletteNum; } else { diff --git a/src/battle_gfx_sfx_util.c b/src/battle_gfx_sfx_util.c index 44f75a78d0..74587b3648 100644 --- a/src/battle_gfx_sfx_util.c +++ b/src/battle_gfx_sfx_util.c @@ -700,9 +700,8 @@ void DecompressTrainerBackPic(u16 backPicId, u8 battler) { u8 position = GetBattlerPosition(battler); CopyTrainerBackspriteFramesToDest(backPicId, gMonSpritesGfxPtr->spritesGfx[position]); - // Aiming for palette slots 8 and 9 for Player and PlayerPartner to prevent Trainer Slides causing mons to change colour LoadPalette(gTrainerBacksprites[backPicId].palette.data, - OBJ_PLTT_ID(8 + battler/2), PLTT_SIZE_4BPP); + OBJ_PLTT_ID(battler), PLTT_SIZE_4BPP); } void FreeTrainerFrontPicPalette(u16 frontPicId) diff --git a/src/reshow_battle_screen.c b/src/reshow_battle_screen.c index d56f910dda..fa8e1037fd 100644 --- a/src/reshow_battle_screen.c +++ b/src/reshow_battle_screen.c @@ -324,7 +324,7 @@ void CreateBattlerSprite(u32 battler) gBattlerSpriteIds[battler] = CreateSprite(&gMultiuseSpriteTemplate, 0x50, (8 - gTrainerBacksprites[gSaveBlock2Ptr->playerGender].coordinates.size) * 4 + 80, GetBattlerSpriteSubpriority(0)); - gSprites[gBattlerSpriteIds[battler]].oam.paletteNum = (8 + battler / 2); + gSprites[gBattlerSpriteIds[battler]].oam.paletteNum = battler; gSprites[gBattlerSpriteIds[battler]].callback = SpriteCallbackDummy; gSprites[gBattlerSpriteIds[battler]].data[0] = battler; } @@ -334,7 +334,7 @@ void CreateBattlerSprite(u32 battler) gBattlerSpriteIds[battler] = CreateSprite(&gMultiuseSpriteTemplate, 0x50, (8 - gTrainerBacksprites[TRAINER_BACK_PIC_WALLY].coordinates.size) * 4 + 80, GetBattlerSpriteSubpriority(0)); - gSprites[gBattlerSpriteIds[battler]].oam.paletteNum = (8 + battler / 2); + gSprites[gBattlerSpriteIds[battler]].oam.paletteNum = battler; gSprites[gBattlerSpriteIds[battler]].callback = SpriteCallbackDummy; gSprites[gBattlerSpriteIds[battler]].data[0] = battler; } From 9119a6cc53f8abeae9c14756b9e24ddf091c3afc Mon Sep 17 00:00:00 2001 From: GGbond Date: Tue, 17 Feb 2026 00:16:28 +0800 Subject: [PATCH 12/22] Fix AI semi-invulnerable move handling and simplify switching logic (#9180) --- src/battle_ai_switch_items.c | 34 +++++++++++++++++++++------------- src/battle_ai_util.c | 2 +- test/battle/ai/ai_switching.c | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index f5d105d04c..ad1023ac15 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -480,7 +480,6 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) u32 opposingBattler = GetOppositeBattler(battler); u32 incomingMove = GetIncomingMove(battler, opposingBattler, gAiLogicData); enum Type incomingType = CheckDynamicMoveType(GetBattlerMon(opposingBattler), incomingMove, opposingBattler, MON_IN_BATTLE); - bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); s32 i, j; if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) @@ -530,42 +529,42 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_FLASH_FIRE; } - if (incomingType == TYPE_WATER || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_WATER)) + if (incomingType == TYPE_WATER) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WATER_ABSORB; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_DRY_SKIN; if (GetConfig(B_REDIRECT_ABILITY_IMMUNITY) >= GEN_5) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_STORM_DRAIN; } - if (incomingType == TYPE_ELECTRIC || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_ELECTRIC)) + if (incomingType == TYPE_ELECTRIC) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_VOLT_ABSORB; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_MOTOR_DRIVE; if (GetConfig(B_REDIRECT_ABILITY_IMMUNITY) >= GEN_5) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LIGHTNING_ROD; } - if (incomingType == TYPE_GRASS || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GRASS)) + if (incomingType == TYPE_GRASS) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SAP_SIPPER; } - if (incomingType == TYPE_GROUND || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GROUND)) + if (incomingType == TYPE_GROUND) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_EARTH_EATER; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LEVITATE; } - if (IsSoundMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsSoundMove(incomingMove))) + if (IsSoundMove(incomingMove)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SOUNDPROOF; } - if (IsBallisticMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsBallisticMove(incomingMove))) + if (IsBallisticMove(incomingMove)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_BULLETPROOF; } - if (IsWindMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsWindMove(incomingMove))) + if (IsWindMove(incomingMove)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WIND_RIDER; } - if (IsPowderMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsPowderMove(incomingMove))) + if (IsPowderMove(incomingMove)) { if (GetConfig(B_POWDER_OVERCOAT) >= GEN_6) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_OVERCOAT; @@ -617,14 +616,23 @@ static bool32 ShouldSwitchIfOpponentChargingOrInvulnerable(u32 battler) { u32 opposingBattler = GetOppositeBattler(battler); u32 incomingMove = GetIncomingMove(battler, opposingBattler, gAiLogicData); - - bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); + enum BattleMoveEffects effect = GetMoveEffect(incomingMove); if (IsDoubleBattle() || !(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; + // Two-turn attacks that charge without entering semi-invulnerable state (e.g. Solar Beam). + // First turn of Fly/Dive/Bounce/Sky Drop: move is selected this turn but user is not yet semi-invulnerable. + // Opponent is already semi-invulnerable. + if (!(IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove) + || ((effect == EFFECT_SEMI_INVULNERABLE || effect == EFFECT_SKY_DROP) && !IsSemiInvulnerable(opposingBattler, CHECK_ALL)) + || IsSemiInvulnerable(opposingBattler, CHECK_ALL))) + { + return FALSE; + } + // In a world with a unified ShouldSwitch function, also want to check whether we already win 1v1 and if we do don't switch; not worth doubling the HasBadOdds computation for now - if (isOpposingBattlerChargingOrInvulnerable && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_FREE_TURN, GetSwitchChance(SHOULD_SWITCH_FREE_TURN))) + if (gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_FREE_TURN, GetSwitchChance(SHOULD_SWITCH_FREE_TURN))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); return FALSE; @@ -654,7 +662,7 @@ static bool32 ShouldSwitchIfTrapperInParty(u32 battler) for (i = firstId; i < lastId; i++) { if (IsAceMon(battler, i)) - return FALSE; + continue; monAbility = GetMonAbility(&party[i]); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 83a93bb60c..8e6f0b2c36 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -54,7 +54,7 @@ static bool32 AI_IsDoubleSpreadMove(u32 battlerAtk, u32 move) if (moveTargetType == MOVE_TARGET_BOTH && battlerAtk == BATTLE_PARTNER(battlerDef)) continue; - if (IsBattlerAlive(battlerDef) && !IsSemiInvulnerable(battlerDef, move)) + if (IsBattlerAlive(battlerDef) && (!IsSemiInvulnerable(battlerDef, CHECK_ALL) || BreaksThroughSemiInvulnerablity(battlerDef, move))) numOfTargets++; } diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index 7e0e923e71..af67f4d051 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -1030,6 +1030,21 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if player's m } } +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out on turn 1 if it predicts a semi-invulnerable move and it has a good switchin") +{ + PASSES_RANDOMLY(PREDICT_MOVE_CHANCE, 100, RNG_AI_PREDICT_MOVE); + PASSES_RANDOMLY(SHOULD_SWITCH_FREE_TURN_PERCENTAGE, 100, RNG_AI_SWITCH_FREE_TURN); + GIVEN { + ASSUME(GetMoveType(MOVE_DIVE) == TYPE_WATER); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + PLAYER(SPECIES_LUVDISC) { Level(1); Moves(MOVE_DIVE); } + OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_SCRATCH); } + OPPONENT(SPECIES_PIKACHU) { Moves(MOVE_THUNDERBOLT); } + } WHEN { + TURN { MOVE(player, MOVE_DIVE); EXPECT_SWITCH(opponent, 1); } + } +} + AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if it has an absorber but current mon has SE move 33% of the time") { PASSES_RANDOMLY(33, 100, RNG_AI_SWITCH_ABSORBING_STAY_IN); From d01442299ac3d539d4829b3631ecd7f5ace086c1 Mon Sep 17 00:00:00 2001 From: psf <77138753+pkmnsnfrn@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:58:57 -0800 Subject: [PATCH 13/22] Running from trainer battles properly handles whiteouts (#9228) --- src/battle_setup.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/battle_setup.c b/src/battle_setup.c index 9b8a669255..59cb31a515 100644 --- a/src/battle_setup.c +++ b/src/battle_setup.c @@ -1321,6 +1321,13 @@ static void CB2_EndTrainerBattle(void) DowngradeBadPoison(); SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic); } + else if (DidPlayerForfeitNormalTrainerBattle()) + { + if (FlagGet(B_FLAG_NO_WHITEOUT) || CurrentBattlePyramidLocation() != PYRAMID_LOCATION_NONE || InTrainerHillChallenge()) + SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic); + else + SetMainCallback2(CB2_WhiteOut); + } else if (IsPlayerDefeated(gBattleOutcome) == TRUE) { if (CurrentBattlePyramidLocation() != PYRAMID_LOCATION_NONE || InTrainerHillChallenge() || (!NoAliveMonsForPlayer()) || FlagGet(B_FLAG_NO_WHITEOUT)) @@ -1328,10 +1335,6 @@ static void CB2_EndTrainerBattle(void) else SetMainCallback2(CB2_WhiteOut); } - else if (DidPlayerForfeitNormalTrainerBattle()) - { - SetMainCallback2(CB2_WhiteOut); - } else { SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic); From fa52f33ffafc7154fd3fdb07dcfb563021d26d52 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:04:56 -0800 Subject: [PATCH 14/22] add LogicalLlama as a contributor for bug (#9229) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ CREDITS.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8ae6daf62c..627b52827a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -642,6 +642,15 @@ "contributions": [ "code" ] + }, + { + "login": "LogicalLlama", + "name": "LogicalLlama", + "avatar_url": "https://avatars.githubusercontent.com/u/248230900?v=4", + "profile": "https://github.com/LogicalLlama", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/CREDITS.md b/CREDITS.md index 7d9e48d70e..c3d7df099a 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -92,6 +92,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d SabataLunar
SabataLunar

🎨 PacFire
PacFire

🎨 ChrispyChris27
ChrispyChris27

💻 + LogicalLlama
LogicalLlama

🐛 From 3fca2dbb6dc187793100b567d31f85bef16a88c3 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Tue, 17 Feb 2026 10:50:02 +0100 Subject: [PATCH 15/22] Show ability num instead of ability id whn picking ability with debug givemon (#9225) --- src/debug.c | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/debug.c b/src/debug.c index dd1b853ddc..f84429d435 100644 --- a/src/debug.c +++ b/src/debug.c @@ -2425,10 +2425,11 @@ static void DebugAction_Give_Pokemon_SelectShiny(u8 taskId) } } -static void Debug_Display_Ability(enum Ability abilityId, u32 digit, u8 windowId)//(u32 natureId, u32 digit, u8 windowId) +static void Debug_Display_Ability(u32 abilityNum, u32 digit, u8 windowId)//(u32 natureId, u32 digit, u8 windowId) { + enum Ability abilityId = GetAbilityBySpecies(sDebugMonData->species, abilityNum); StringCopy(gStringVar2, gText_DigitIndicator[digit]); - ConvertIntToDecimalStringN(gStringVar3, abilityId, STR_CONV_MODE_LEADING_ZEROS, 2); + ConvertIntToDecimalStringN(gStringVar3, abilityNum, STR_CONV_MODE_LEFT_ALIGN, 2); StringCopyPadded(gStringVar3, gStringVar3, CHAR_SPACE, 15); u8 *end = StringCopy(gStringVar1, gAbilitiesInfo[abilityId].name); WrapFontIdToFit(gStringVar1, end, DEBUG_MENU_FONT, WindowWidthPx(windowId)); @@ -2464,8 +2465,7 @@ static void DebugAction_Give_Pokemon_SelectNature(u8 taskId) gTasks[taskId].tInput = 0; gTasks[taskId].tDigit = 0; - enum Ability abilityId = GetAbilityBySpecies(sDebugMonData->species, 0); - Debug_Display_Ability(abilityId, gTasks[taskId].tDigit, gTasks[taskId].tSubWindowId); + Debug_Display_Ability(0, gTasks[taskId].tDigit, gTasks[taskId].tSubWindowId); gTasks[taskId].func = DebugAction_Give_Pokemon_SelectAbility; } @@ -2489,8 +2489,7 @@ static void Debug_Display_TeraType(u32 typeId, u32 digit, u8 windowId) static void DebugAction_Give_Pokemon_SelectAbility(u8 taskId) { - u8 abilityCount = NUM_ABILITY_SLOTS - 1; //-1 for proper iteration - u8 i = 0; + s32 abilityNum = -1; if (JOY_NEW(DPAD_ANY)) { @@ -2498,28 +2497,31 @@ static void DebugAction_Give_Pokemon_SelectAbility(u8 taskId) if (JOY_NEW(DPAD_UP)) { - gTasks[taskId].tInput += sPowersOfTen[gTasks[taskId].tDigit]; - if (gTasks[taskId].tInput > abilityCount) - gTasks[taskId].tInput = abilityCount; + abilityNum = gTasks[taskId].tInput + 1; + while (GetSpeciesAbility(sDebugMonData->species, abilityNum) == ABILITY_NONE && abilityNum < NUM_ABILITY_SLOTS) + { + abilityNum++; + } } if (JOY_NEW(DPAD_DOWN)) { - gTasks[taskId].tInput -= sPowersOfTen[gTasks[taskId].tDigit]; - if (gTasks[taskId].tInput < 0) - gTasks[taskId].tInput = 0; + abilityNum = gTasks[taskId].tInput - 1; + while (GetSpeciesAbility(sDebugMonData->species, abilityNum) == ABILITY_NONE && abilityNum >= 0) + { + abilityNum--; + } } - while (GetAbilityBySpecies(sDebugMonData->species, gTasks[taskId].tInput - i) == ABILITY_NONE && gTasks[taskId].tInput - i < NUM_ABILITY_SLOTS) + if (abilityNum >= 0 && abilityNum < NUM_ABILITY_SLOTS) { - i++; + gTasks[taskId].tInput = abilityNum; + Debug_Display_Ability(abilityNum, gTasks[taskId].tDigit, gTasks[taskId].tSubWindowId); } - enum Ability abilityId = GetAbilityBySpecies(sDebugMonData->species, gTasks[taskId].tInput - i); - Debug_Display_Ability(abilityId, gTasks[taskId].tDigit, gTasks[taskId].tSubWindowId); } if (JOY_NEW(A_BUTTON)) { - sDebugMonData->abilityNum = gTasks[taskId].tInput - i; + sDebugMonData->abilityNum = gTasks[taskId].tInput; gTasks[taskId].tInput = 0; gTasks[taskId].tDigit = 0; From bc6bbb1bc5a89ae1f0a5b7f87dad2be52cb4c42b Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Tue, 17 Feb 2026 13:54:36 +0100 Subject: [PATCH 16/22] Make sure grass effect palette ignore fog when time blended (#9235) --- include/field_effect_helpers.h | 2 ++ src/field_effect.c | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/include/field_effect_helpers.h b/include/field_effect_helpers.h index 65dce3dfda..7f772c0c6c 100644 --- a/include/field_effect_helpers.h +++ b/include/field_effect_helpers.h @@ -43,4 +43,6 @@ void UpdateSparkleFieldEffect(struct Sprite *sprite); void SetSpriteInvisible(u8 spriteId); void ShowWarpArrowSprite(u8 spriteId, u8 direction, s16 x, s16 y); +u32 FldEff_TallGrass(void); + #endif //GUARD_FIELD_EFFECT_HELPERS_H diff --git a/src/field_effect.c b/src/field_effect.c index 9127e6b0ea..14194c8ac2 100644 --- a/src/field_effect.c +++ b/src/field_effect.c @@ -803,14 +803,22 @@ void FieldEffectScript_LoadTiles(u8 **script) (*script) += 4; } +static bool32 ShouldFieldEffectBeFogBlended(u8 *script) +{ + u32 ptr = FieldEffectScript_ReadWord(&script); + if (ptr == (u32)FldEff_TallGrass) + return FALSE; + return TRUE; +} + void FieldEffectScript_LoadFadedPalette(u8 **script) { struct SpritePalette *palette = (struct SpritePalette *)FieldEffectScript_ReadWord(script); u32 paletteSlot = LoadSpritePalette(palette); (*script) += 4; SetPaletteColorMapType(paletteSlot + 16, T1_READ_8(*script)); - UpdateSpritePaletteWithWeather(paletteSlot, TRUE); (*script)++; + UpdateSpritePaletteWithWeather(paletteSlot, ShouldFieldEffectBeFogBlended(*script)); } void FieldEffectScript_LoadPalette(u8 **script) From 353d011c722f6ecd3bbd79abd1e6cdce48a7c680 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:32:29 +0000 Subject: [PATCH 17/22] Prevents `seteffectprimary` and `seteffectsecondary` from softlocking (#9236) --- src/battle_script_commands.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 732ce1c5de..de23842fac 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -4267,7 +4267,8 @@ static void Cmd_seteffectprimary(void) u32 battler = GetBattlerForBattleScript(cmd->battler); u32 effectBattler = GetBattlerForBattleScript(cmd->effectBattler); - SetMoveEffect(battler, effectBattler, gBattleScripting.moveEffect, cmd->nextInstr, EFFECT_PRIMARY); + gBattlescriptCurrInstr = cmd->nextInstr; + SetMoveEffect(battler, effectBattler, gBattleScripting.moveEffect, gBattlescriptCurrInstr, EFFECT_PRIMARY); } static void Cmd_seteffectsecondary(void) @@ -4276,7 +4277,8 @@ static void Cmd_seteffectsecondary(void) u32 battler = GetBattlerForBattleScript(cmd->battler); u32 effectBattler = GetBattlerForBattleScript(cmd->effectBattler); - SetMoveEffect(battler, effectBattler, gBattleScripting.moveEffect, cmd->nextInstr, EFFECT_PRIMARY); + gBattlescriptCurrInstr = cmd->nextInstr; + SetMoveEffect(battler, effectBattler, gBattleScripting.moveEffect, gBattlescriptCurrInstr, NO_FLAGS); } static void Cmd_clearvolatile(void) From 07fa2e2751b745b929fbeb7319e61491111ed3fc Mon Sep 17 00:00:00 2001 From: Kildemal <206095739+izrofid@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:29:02 +0530 Subject: [PATCH 18/22] fix(bttl-anim): remove unused battle selector to silence warning (#9218) --- data/battle_anim_scripts.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/battle_anim_scripts.s b/data/battle_anim_scripts.s index 78f45c1c61..2f3de93af4 100644 --- a/data/battle_anim_scripts.s +++ b/data/battle_anim_scripts.s @@ -25680,7 +25680,7 @@ SnoreEffect: playsewithpan SE_M_SNORE, SOUND_PAN_ATTACKER createvisualtask AnimTask_ScaleMonAndRestore, 5, -7, -7, 7, ANIM_ATTACKER, 1 createvisualtask AnimTask_ShakeMon2, 2, ANIM_TARGET, 4, 0, 7, 1 - shake_mon_or_platform velocity=6, shake_timer=1, shake_duration=14, type=0, battler_selector=0 + shake_mon_or_platform velocity=6, shake_timer=1, shake_duration=14, type=0 createsprite gSnoreZSpriteTemplate, ANIM_ATTACKER, 2, 0, 0, -42, -38, 24, 0, 0 createsprite gSnoreZSpriteTemplate, ANIM_ATTACKER, 2, 0, 0, 0, -42, 24, 0, 0 createsprite gSnoreZSpriteTemplate, ANIM_ATTACKER, 2, 0, 0, 42, -38, 24, 0, 0 From 56f22adc158ca840eba154152fccb456e7083bd6 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Tue, 17 Feb 2026 15:08:00 -0300 Subject: [PATCH 19/22] Added Weight battle tests (#9202) --- test/battle/ability/heavy_metal.c | 2 +- test/battle/ability/light_metal.c | 28 ++++++++++++++++++- test/battle/hold_effect/float_stone.c | 30 ++++++++++++++++++++- test/battle/move_effect/autotomize.c | 39 ++++++++++++++++++++++++--- test/battle/move_effect/low_kick.c | 27 ++++++++++++++++++- test/battle/move_effect/sky_drop.c | 2 +- 6 files changed, 120 insertions(+), 8 deletions(-) diff --git a/test/battle/ability/heavy_metal.c b/test/battle/ability/heavy_metal.c index baaa039b19..2257b5d068 100644 --- a/test/battle/ability/heavy_metal.c +++ b/test/battle/ability/heavy_metal.c @@ -1,4 +1,4 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Heavy Metal (Ability) test titles") +// Tests for Heavy Metal are handled in test/battle/ability/light_metal.c diff --git a/test/battle/ability/light_metal.c b/test/battle/ability/light_metal.c index 8ad4a6a4b5..d09204c25d 100644 --- a/test/battle/ability/light_metal.c +++ b/test/battle/ability/light_metal.c @@ -1,4 +1,30 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Light Metal (Ability) test titles") +SINGLE_BATTLE_TEST("Light Metal and Heavy Metal affect the power of Low Kick", s16 damage) +{ + enum Ability ability; + PARAMETRIZE { ability = ABILITY_LIGHT_METAL; } // 10.0 - 24.9 kg (40 power) + PARAMETRIZE { ability = ABILITY_STALWART; } // 25.0 - 49.9 kg (60 power) + PARAMETRIZE { ability = ABILITY_HEAVY_METAL; } // 50.0 - 99.9 kg (80 power) + + GIVEN { + ASSUME(GetMoveEffect(MOVE_SOAK) == EFFECT_SOAK); + ASSUME(GetMoveArgType(MOVE_SOAK) == TYPE_WATER); + ASSUME(GetSpeciesWeight(SPECIES_DURALUDON) == 400); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_DURALUDON) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_SOAK); } // To remove super-effectiveness, as it was messing with calculations. + TURN { MOVE(player, MOVE_LOW_KICK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } THEN { + // Calc 20 power increase, with the first iteration being 40 power + if (i != 0) + EXPECT_MUL_EQ(results[0].damage, Q_4_12((i * 0.5) + 1), results[i].damage); + } +} + +TO_DO_BATTLE_TEST("Light Metal and Heavy Metal don't affect Heavy Ball's multiplier") diff --git a/test/battle/hold_effect/float_stone.c b/test/battle/hold_effect/float_stone.c index f677c41369..88221a7122 100644 --- a/test/battle/hold_effect/float_stone.c +++ b/test/battle/hold_effect/float_stone.c @@ -1,4 +1,32 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Float Stone (Hold Effect) test titles") +ASSUMPTIONS +{ + ASSUME(GetItemHoldEffect(ITEM_FLOAT_STONE) == HOLD_EFFECT_FLOAT_STONE); +} + +SINGLE_BATTLE_TEST("Float Stone halves the holder's weight", s16 damage) +{ + u32 item; + PARAMETRIZE { item = ITEM_FLOAT_STONE; } // 10.0 - 24.9 kg (40 power) + PARAMETRIZE { item = ITEM_NONE; } // 25.0 - 49.9 kg (60 power) + + GIVEN { + ASSUME(GetMoveEffect(MOVE_SOAK) == EFFECT_SOAK); + ASSUME(GetMoveArgType(MOVE_SOAK) == TYPE_WATER); + ASSUME(GetSpeciesWeight(SPECIES_DURALUDON) == 400); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_DURALUDON) { Ability(ABILITY_STALWART); Item(item); } + } WHEN { + TURN { MOVE(player, MOVE_SOAK); } // To remove super-effectiveness, as it was messing with calculations. + TURN { MOVE(player, MOVE_LOW_KICK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} + +TO_DO_BATTLE_TEST("Float Stone doesn't affect Heavy Ball's multiplier") diff --git a/test/battle/move_effect/autotomize.c b/test/battle/move_effect/autotomize.c index 79a71ecbc7..e749ea1cbc 100644 --- a/test/battle/move_effect/autotomize.c +++ b/test/battle/move_effect/autotomize.c @@ -1,12 +1,45 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Autotomize increases Speed by 2 stages"); -TO_DO_BATTLE_TEST("Autotomize decreases weight by 100kg (220 lbs.)"); -TO_DO_BATTLE_TEST("Autotomize can be used multiple times to decrease weight each time"); +TO_DO_BATTLE_TEST("Autotomize increases Speed by 2 stages") + +SINGLE_BATTLE_TEST("Autotomize decreases weight by 100kg (220 lbs.) each time it's used") +{ + s16 damage[3]; + + GIVEN { + ASSUME(GetSpeciesWeight(SPECIES_METANG) == 2025); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_METANG); + } WHEN { + TURN { MOVE(player, MOVE_LOW_KICK); } + TURN { MOVE(opponent, MOVE_AUTOTOMIZE); MOVE(player, MOVE_LOW_KICK); } + TURN { MOVE(opponent, MOVE_AUTOTOMIZE); MOVE(player, MOVE_LOW_KICK); } + } SCENE { + // 200.0 kg or more (120 power) + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &damage[0]); + + // 100.0 - 199.9 kg (100 power) + ANIMATION(ANIM_TYPE_MOVE, MOVE_AUTOTOMIZE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &damage[1]); + + // 0.1 - 9.9 kg (20 power) + ANIMATION(ANIM_TYPE_MOVE, MOVE_AUTOTOMIZE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &damage[2]); + } THEN { + EXPECT_MUL_EQ(damage[2], Q_4_12(6.0), damage[0]); + EXPECT_MUL_EQ(damage[2], Q_4_12(5.0), damage[0]); + } +} + + TO_DO_BATTLE_TEST("Autotomize cannot decrease weight below 0.1kg (0.2 lbs)"); TO_DO_BATTLE_TEST("Autotomize's weight reduction cannot be Baton Passed"); TO_DO_BATTLE_TEST("Autotomize's weight reduction cannot be removed by Haze"); TO_DO_BATTLE_TEST("Autotomize's weight reduction is reset upon form change (Gen6+)"); TO_DO_BATTLE_TEST("Autotomize's weight reduction is reset upon switch"); TO_DO_BATTLE_TEST("Autotomize's weight reduction is reset upon fainting"); +TO_DO_BATTLE_TEST("Autotomize doesn't affect Heavy Ball's multiplier") diff --git a/test/battle/move_effect/low_kick.c b/test/battle/move_effect/low_kick.c index c68b152e2d..d88996aba6 100644 --- a/test/battle/move_effect/low_kick.c +++ b/test/battle/move_effect/low_kick.c @@ -1,4 +1,29 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Low Kick (Move Effect) test titles") +SINGLE_BATTLE_TEST("Low Kick's damage varies based on the target's weight", s16 damage) +{ + u32 species, weight; + + PARAMETRIZE { species = SPECIES_CUBONE; weight = 65; } // 0.1 - 9.9 kg (20 power) + PARAMETRIZE { species = SPECIES_SANDSHREW; weight = 120; } // 10.0 - 24.9 kg (40 power) + PARAMETRIZE { species = SPECIES_MAROWAK; weight = 450; } // 25.0 - 49.9 kg (60 power) + PARAMETRIZE { species = SPECIES_SANDACONDA; weight = 655; } // 50.0 - 99.9 kg (80 power) + PARAMETRIZE { species = SPECIES_DONPHAN; weight = 1200; } // 100.0 - 199.9 kg (100 power) + PARAMETRIZE { species = SPECIES_HIPPOWDON; weight = 3000; } // 200.0 kg or more (120 power) + + GIVEN { + ASSUME(GetSpeciesWeight(species) == weight); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(species) { Defense(170); } // Cubone's Defense, the lowest one in hopes of avoid distorting the results. + } WHEN { + TURN { MOVE(player, MOVE_LOW_KICK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LOW_KICK, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } THEN { + // Since Low Kick increases by 20 each tier, multiply by tier number to compare with the first tier. + if (i != 0) + EXPECT_MUL_EQ(results[0].damage, Q_4_12(i + 1), results[i].damage); + } +} diff --git a/test/battle/move_effect/sky_drop.c b/test/battle/move_effect/sky_drop.c index b3bc6739af..29cb431bb4 100644 --- a/test/battle/move_effect/sky_drop.c +++ b/test/battle/move_effect/sky_drop.c @@ -68,7 +68,7 @@ DOUBLE_BATTLE_TEST("Sky Drop is cancelled if Gravity activated") } } -SINGLE_BATTLE_TEST("Sky Drop fails on heavy targets") +SINGLE_BATTLE_TEST("Sky Drop fails on targets heavier or equal than 200kg") { GIVEN { ASSUME(gSpeciesInfo[SPECIES_METAGROSS].weight >= 2000); From 33b89f227dccee0ad26869a4240fd5cc915ba789 Mon Sep 17 00:00:00 2001 From: GGbond Date: Wed, 18 Feb 2026 22:39:55 +0800 Subject: [PATCH 20/22] =?UTF-8?q?Fix=20AI=20Sheer=20Force=20checks=20to=20?= =?UTF-8?q?allow=20Order=20Up=E2=80=99s=20Commander=20stat=20boost=20(#925?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/battle_ai_main.c | 5 ++++- src/battle_ai_util.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 68cf333b57..bf6768f71c 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -5694,8 +5694,11 @@ static s32 AI_CalcAdditionalEffectScore(u32 battlerAtk, u32 battlerDef, u32 move u32 i; u32 additionalEffectCount = GetMoveAdditionalEffectCount(move); - if (IsSheerForceAffected(move, aiData->abilities[battlerAtk])) + if (IsSheerForceAffected(move, aiData->abilities[battlerAtk]) + && !(GetMoveEffect(move) == EFFECT_ORDER_UP && gBattleStruct->battlerState[battlerAtk].commanderSpecies != SPECIES_NONE)) + { return score; + } // check move additional effects that are likely to happen for (i = 0; i < additionalEffectCount; i++) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 8e6f0b2c36..1b55026ac8 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1034,8 +1034,11 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s3 enum Ability abilityDef = gAiLogicData->abilities[battlerDef]; enum Ability abilityAtk = gAiLogicData->abilities[battlerAtk]; - if (IsSheerForceAffected(move, abilityAtk)) + if (IsSheerForceAffected(move, abilityAtk) + && !(GetMoveEffect(move) == EFFECT_ORDER_UP && gBattleStruct->battlerState[battlerAtk].commanderSpecies != SPECIES_NONE)) + { return FALSE; + } switch (GetMoveEffect(move)) { From 3494d6b064532f238c917cb67781a32e2c069ca0 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:40:30 +0000 Subject: [PATCH 21/22] Fixes Throat Chop timer being reset with every use of the move (#9246) --- src/battle_script_commands.c | 7 ++++-- .../move_effect_secondary/throat_chop.c | 22 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index de23842fac..225c411095 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -3514,8 +3514,11 @@ void SetMoveEffect(u32 battler, u32 effectBattler, enum MoveEffect moveEffect, c } break; case MOVE_EFFECT_THROAT_CHOP: - gDisableStructs[gEffectBattler].throatChopTimer = 2; - gBattlescriptCurrInstr = battleScript; + if (gDisableStructs[gEffectBattler].throatChopTimer == 0) + { + gDisableStructs[gEffectBattler].throatChopTimer = 2; + gBattlescriptCurrInstr = battleScript; + } break; case MOVE_EFFECT_INCINERATE: if (((gBattleMons[gEffectBattler].item >= FIRST_BERRY_INDEX && gBattleMons[gEffectBattler].item <= LAST_BERRY_INDEX) diff --git a/test/battle/move_effect_secondary/throat_chop.c b/test/battle/move_effect_secondary/throat_chop.c index 3d6438a4ba..27eb27c304 100644 --- a/test/battle/move_effect_secondary/throat_chop.c +++ b/test/battle/move_effect_secondary/throat_chop.c @@ -27,8 +27,9 @@ SINGLE_BATTLE_TEST("Throat Chop prevents the usage of sound moves") SINGLE_BATTLE_TEST("Throat Chop prevents sound base moves for 2 turns") { GIVEN { + ASSUME(IsSoundMove(MOVE_HYPER_VOICE)); PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HYPER_VOICE, MOVE_ALLURING_VOICE, MOVE_OVERDRIVE, MOVE_ROUND); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HYPER_VOICE); } } WHEN { TURN { MOVE(opponent, MOVE_HYPER_VOICE); MOVE(player, MOVE_THROAT_CHOP); } TURN { FORCED_MOVE(opponent); } @@ -45,3 +46,22 @@ SINGLE_BATTLE_TEST("Throat Chop prevents sound base moves for 2 turns") ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPER_VOICE, opponent); } } + +SINGLE_BATTLE_TEST("Throat Chop usage when target is already prevented from using sound moves doesn't reset timer") +{ + GIVEN { + ASSUME(IsSoundMove(MOVE_HYPER_VOICE)); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HYPER_VOICE); } + } WHEN { + TURN { MOVE(opponent, MOVE_HYPER_VOICE); MOVE(player, MOVE_THROAT_CHOP); } + TURN { FORCED_MOVE(opponent); MOVE(player, MOVE_THROAT_CHOP); } + TURN { MOVE(opponent, MOVE_HYPER_VOICE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPER_VOICE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THROAT_CHOP, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STRUGGLE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THROAT_CHOP, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPER_VOICE, opponent); + } +} From eb68d746e2a821c0e2cbabca7b7e5a758f9b4545 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Thu, 19 Feb 2026 14:21:36 +0100 Subject: [PATCH 22/22] Fix batle dome streak thresholds (#9257) --- src/frontier_util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontier_util.c b/src/frontier_util.c index e821f5ba2e..72ef530b6e 100644 --- a/src/frontier_util.c +++ b/src/frontier_util.c @@ -138,7 +138,7 @@ const struct FrontierBrain gFrontierBrainInfo[NUM_FRONTIER_FACILITIES] = COMPOUND_STRING("My DOME ACE title isn't just for show!") //Gold }, .battledBit = {1 << 2, 1 << 3}, - .streakAppearances = {1, 2, 5, 0}, + .streakAppearances = {4, 9, 5, 0}, }, [FRONTIER_FACILITY_PALACE] = {