From a0780a6f91bb7e97cbdf4667fd84971a1ddc8b69 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Wed, 19 Nov 2025 22:13:28 +0100 Subject: [PATCH 01/12] Make MON_DATA_NICKNAME10 return a 10 character string (#8291) --- src/pokemon.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pokemon.c b/src/pokemon.c index 3f70f6feca..2e9d766185 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -2434,7 +2434,7 @@ u32 GetBoxMonData3(struct BoxPokemon *boxMon, s32 field, u8 *data) data[retVal++] = substruct0->nickname12; } } - else if (POKEMON_NAME_LENGTH >= 11) + else if (field != MON_DATA_NICKNAME10 && POKEMON_NAME_LENGTH >= 11) { if (substruct0->nickname11 == 0) { From 867b45a6f3d160ad68de1fc36a75d276e15bf285 Mon Sep 17 00:00:00 2001 From: cawtds <38510667+cawtds@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:41:37 +0100 Subject: [PATCH 02/12] Fix error when compiling with P_FUSION_FORMS disabled (#8298) --- src/party_menu.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/party_menu.c b/src/party_menu.c index ce18d0a0db..40815c9044 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -6300,6 +6300,7 @@ static void DeleteInvalidFusionMoves(struct Pokemon *mon, u32 species) } } +#if P_FUSION_FORMS static void SwapFusionMonMoves(struct Pokemon *mon, const u16 moveTable[][2], u32 mode) { u32 oldMoveIndex, newMoveIndex; @@ -6328,6 +6329,8 @@ static void SwapFusionMonMoves(struct Pokemon *mon, const u16 moveTable[][2], u3 } } +#endif //P_FUSION_FORMS + static void Task_TryItemUseFusionChange(u8 taskId) { struct Pokemon *mon = &gPlayerParty[gTasks[taskId].firstFusionSlot]; @@ -6421,6 +6424,7 @@ static void Task_TryItemUseFusionChange(u8 taskId) { if (gTasks[taskId].fusionType == FUSE_MON) { +#if P_FUSION_FORMS #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_WHITE) @@ -6431,11 +6435,13 @@ static void Task_TryItemUseFusionChange(u8 taskId) SwapFusionMonMoves(mon, gKyuremBlackSwapMoveTable, FUSE_MON); #endif //P_FAMILY_ZEKROM #endif //P_FAMILY_KYUREM +#endif //P_FUSION_FORMS if (gTasks[taskId].moveToLearn != 0) FormChangeTeachMove(taskId, gTasks[taskId].moveToLearn, gTasks[taskId].firstFusionSlot); } else //(gTasks[taskId].fusionType == UNFUSE_MON) { +#if P_FUSION_FORMS #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_WHITE) @@ -6446,6 +6452,7 @@ static void Task_TryItemUseFusionChange(u8 taskId) SwapFusionMonMoves(mon, gKyuremBlackSwapMoveTable, UNFUSE_MON); #endif //P_FAMILY_ZEKROM #endif //P_FAMILY_KYUREM +#endif //P_FUSION_FORMS if ( gTasks[taskId].tExtraMoveHandling == FORGET_EXTRA_MOVES) { DeleteInvalidFusionMoves(mon, gTasks[taskId].fusionResult); From e5e99317d05ea35f605f3b23b23c48615cd67d11 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Thu, 20 Nov 2025 10:01:02 -0300 Subject: [PATCH 03/12] Slightly increase headless test speed by modifying animations (#8299) --- src/battle_anim_throw.c | 4 ++-- src/battle_controllers.c | 14 ++++++++++++-- src/pokeball.c | 19 ++++++++++++------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/battle_anim_throw.c b/src/battle_anim_throw.c index 54f16003be..5860a57630 100644 --- a/src/battle_anim_throw.c +++ b/src/battle_anim_throw.c @@ -14,6 +14,7 @@ #include "sound.h" #include "sprite.h" #include "task.h" +#include "test_runner.h" #include "trig.h" #include "util.h" #include "data.h" @@ -2435,7 +2436,7 @@ void TryShinyAnimation(u8 battler, struct Pokemon *mon) if (illusionMon != NULL) mon = illusionMon; - if (IsBattlerSpriteVisible(battler) && IsValidForBattle(mon)) + if (IsBattlerSpriteVisible(battler) && IsValidForBattle(mon) && !gTestRunnerHeadless) { if (isShiny) { @@ -2768,4 +2769,3 @@ static void CB_CriticalCaptureThrownBallMovement(struct Sprite *sprite) sprite->callback = SpriteCB_Ball_Bounce_Step; } } - diff --git a/src/battle_controllers.c b/src/battle_controllers.c index f2880d31af..79828d065d 100644 --- a/src/battle_controllers.c +++ b/src/battle_controllers.c @@ -2578,7 +2578,7 @@ void BtlController_HandleStatusAnimation(u32 battler) void BtlController_HandleHitAnimation(u32 battler) { - if (gSprites[gBattlerSpriteIds[battler]].invisible == TRUE) + if (gSprites[gBattlerSpriteIds[battler]].invisible == TRUE || gTestRunnerHeadless) { BtlController_Complete(battler); } @@ -2593,6 +2593,11 @@ void BtlController_HandleHitAnimation(u32 battler) void BtlController_HandlePlaySE(u32 battler) { + if (gTestRunnerHeadless) + { + BtlController_Complete(battler); + return; + } s32 pan = IsOnPlayerSide(battler) ? SOUND_PAN_ATTACKER : SOUND_PAN_TARGET; PlaySE12WithPanning(gBattleResources->bufferA[battler][1] | (gBattleResources->bufferA[battler][2] << 8), pan); @@ -2601,6 +2606,11 @@ void BtlController_HandlePlaySE(u32 battler) void BtlController_HandlePlayFanfareOrBGM(u32 battler) { + if (gTestRunnerHeadless) + { + BtlController_Complete(battler); + return; + } if (gBattleResources->bufferA[battler][3]) { BattleStopLowHpSound(); @@ -2867,7 +2877,7 @@ void AnimateMonAfterPokeBallFail(u32 battler) { if (B_ANIMATE_MON_AFTER_FAILED_POKEBALL == FALSE) return; - + LaunchKOAnimation(battler, ReturnAnimIdForBattler(TRUE, battler), TRUE); TryShinyAnimation(gBattlerTarget, GetBattlerMon(gBattlerTarget)); } diff --git a/src/pokeball.c b/src/pokeball.c index 5b32aa7e1d..2b36072e60 100644 --- a/src/pokeball.c +++ b/src/pokeball.c @@ -11,6 +11,7 @@ #include "sprite.h" #include "task.h" #include "trig.h" +#include "test_runner.h" #include "util.h" #include "data.h" #include "item.h" @@ -597,8 +598,8 @@ static void Task_DoPokeballSendOutAnim(u8 taskId) { u32 throwCaseId, ballId, battler, ballSpriteId; bool32 notSendOut = FALSE; - u32 throwXoffset = (B_ENEMY_THROW_BALLS >= GEN_6) ? 24 : 0; - s32 throwYoffset = (B_ENEMY_THROW_BALLS >= GEN_6) ? -16 : 24; + u32 throwXoffset = (B_ENEMY_THROW_BALLS >= GEN_6 && !gTestRunnerHeadless) ? 24 : 0; + s32 throwYoffset = (B_ENEMY_THROW_BALLS >= GEN_6 && !gTestRunnerHeadless) ? -16 : 24; if (gTasks[taskId].tFrames == 0) { @@ -675,7 +676,7 @@ static inline void DoPokeballSendOutSoundEffect(u32 battler) static inline void *GetOpponentMonSendOutCallback(void) { - return (B_ENEMY_THROW_BALLS >= GEN_6) ? SpriteCB_MonSendOut_1 : SpriteCB_OpponentMonSendOut; + return (B_ENEMY_THROW_BALLS >= GEN_6 && !gTestRunnerHeadless) ? SpriteCB_MonSendOut_1 : SpriteCB_OpponentMonSendOut; } // This sequence of functions is very similar to those that get run when @@ -1205,7 +1206,7 @@ static void SpriteCB_MonSendOut_2(struct Sprite *sprite) u32 r7; bool32 rightPosition = (IsBattlerPlayer(sprite->sBattler)) ? B_POSITION_PLAYER_RIGHT : B_POSITION_OPPONENT_RIGHT; - if (HIBYTE(sprite->data[7]) >= 35 && HIBYTE(sprite->data[7]) < 80) + if (HIBYTE(sprite->data[7]) >= 35 && HIBYTE(sprite->data[7]) < 80 && !gTestRunnerHeadless) { s16 r4; @@ -1246,7 +1247,8 @@ static void SpriteCB_MonSendOut_2(struct Sprite *sprite) sprite->data[0] = 0; if (IsDoubleBattle() && gBattleSpritesDataPtr->animationData->introAnimActive - && sprite->sBattler == GetBattlerAtPosition(rightPosition)) + && sprite->sBattler == GetBattlerAtPosition(rightPosition) + && !gTestRunnerHeadless) sprite->callback = SpriteCB_ReleaseMon2FromBall; else sprite->callback = SpriteCB_ReleaseMonFromBall; @@ -1269,12 +1271,15 @@ static void SpriteCB_ReleaseMon2FromBall(struct Sprite *sprite) static void SpriteCB_OpponentMonSendOut(struct Sprite *sprite) { + if (gTestRunnerHeadless) + sprite->data[0] = 15; sprite->data[0]++; if (sprite->data[0] > 15) { sprite->data[0] = 0; if (IsDoubleBattle() && gBattleSpritesDataPtr->animationData->introAnimActive - && sprite->sBattler == GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT)) + && sprite->sBattler == GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT) + && !gTestRunnerHeadless) sprite->callback = SpriteCB_ReleaseMon2FromBall; else sprite->callback = SpriteCB_ReleaseMonFromBall; @@ -1534,7 +1539,7 @@ void StartHealthboxSlideIn(u8 battler) healthboxSprite->y2 = -healthboxSprite->y2; } gSprites[healthboxSprite->data[5]].callback(&gSprites[healthboxSprite->data[5]]); - if (GetBattlerPosition(battler) == B_POSITION_PLAYER_RIGHT) + if (GetBattlerPosition(battler) == B_POSITION_PLAYER_RIGHT && !gTestRunnerHeadless) healthboxSprite->callback = SpriteCB_HealthboxSlideInDelayed; } From 6095e2e85a2053b1e9255b3c2ceea4fb61e19cfc Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:05:54 +0100 Subject: [PATCH 04/12] Fix compile on gcc11 (#8300) --- src/item_use.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/item_use.c b/src/item_use.c index b97ac1202b..460eb8a883 100644 --- a/src/item_use.c +++ b/src/item_use.c @@ -1254,8 +1254,7 @@ bool32 CannotUseItemsInBattle(u16 itemId, struct Pokemon *mon) switch (battleUsage) { case EFFECT_ITEM_INCREASE_STAT: - u32 ability = GetBattlerAbility(gBattlerInMenuId); - if (CompareStat(gBattlerInMenuId, GetItemEffect(itemId)[1], MAX_STAT_STAGE, CMP_EQUAL, ability)) + if (CompareStat(gBattlerInMenuId, GetItemEffect(itemId)[1], MAX_STAT_STAGE, CMP_EQUAL, GetBattlerAbility(gBattlerInMenuId))) cannotUse = TRUE; break; case EFFECT_ITEM_SET_FOCUS_ENERGY: From cd6d293ad15b3990c07b3af4baffbf3f4a7ebe3e Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Thu, 20 Nov 2025 14:19:48 +0100 Subject: [PATCH 05/12] Fix some move animations leaking VRAM and freeing already freed tags (#7977) Co-authored-by: Hedara --- data/battle_anim_scripts.s | 11 +-- include/global.h | 2 + src/battle_script_commands.c | 9 +++ src/sprite.c | 10 +++ test/battle/move_animations/all_anims.c | 93 ++++++++++++++++--------- 5 files changed, 85 insertions(+), 40 deletions(-) diff --git a/data/battle_anim_scripts.s b/data/battle_anim_scripts.s index 158de50992..b546558daa 100644 --- a/data/battle_anim_scripts.s +++ b/data/battle_anim_scripts.s @@ -11932,7 +11932,6 @@ gBattleAnimMove_PrismaticLaser:: loadspritegfx ANIM_TAG_CIRCLE_OF_LIGHT @charge animation loadspritegfx ANIM_TAG_TEAL_ALERT @straight lines loadspritegfx ANIM_TAG_GREEN_SPIKE @needle arm animation - loadspritegfx ANIM_TAG_NEEDLE @sting monbg ANIM_ATTACKER setalpha 14, 8 createvisualtask AnimTask_BlendBattleAnimPal, 10, F_PAL_BG, 1, 0, 16, RGB_BLACK @@ -11961,6 +11960,7 @@ gBattleAnimMove_PrismaticLaser:: unloadspritegfx ANIM_TAG_GREEN_SPIKE unloadspritegfx ANIM_TAG_ICE_CHUNK unloadspritegfx ANIM_TAG_CIRCLE_OF_LIGHT + loadspritegfx ANIM_TAG_NEEDLE @sting delay 30 createvisualtask AnimTask_HorizontalShake, 5, (MAX_BATTLERS_COUNT + 1), 10, 0x32 createvisualtask AnimTask_HorizontalShake, 5, MAX_BATTLERS_COUNT, 10, 0x32 @@ -15059,6 +15059,7 @@ gBattleAnimMove_Poltergeist:: waitbgfadein clearmonbg 0x3 blendoff + unloadspritegfx ANIM_TAG_ITEM_BAG end @Credits to Skeli @@ -32845,7 +32846,6 @@ gBattleAnimMove_SavageSpinOut:: blendoff waitforvisualfinish unloadspritegfx ANIM_TAG_STRING - unloadspritegfx ANIM_TAG_CIRCLE_OF_LIGHT loadspritegfx ANIM_TAG_COCOON loadspritegfx ANIM_TAG_IMPACT @hit delay 1 @@ -33315,7 +33315,6 @@ FinishInfernoOverdrive: delay 16 createvisualtask AnimTask_ShakeMon2, 2, ANIM_TARGET, 8, 0, 16, 1 playsewithpan SE_M_MEGA_KICK2, SOUND_PAN_TARGET - unloadspritegfx ANIM_TAG_CIRCLE_OF_LIGHT createvisualtask AnimTask_ShakeMon, 5, ANIM_TARGET, 0, 2, 79, 1 call InfernoOverdriveExplosion delay 6 @@ -34495,7 +34494,6 @@ gBattleAnimMove_BlackHoleEclipse:: unloadspritegfx ANIM_TAG_VERTICAL_HEX @red unloadspritegfx ANIM_TAG_SHADOW_BALL unloadspritegfx ANIM_TAG_BLACK_BALL_2 - unloadspritegfx ANIM_TAG_FOCUS_ENERGY loadspritegfx ANIM_TAG_EXPLOSION_2 call BlackHoleEclipseExplosion createvisualtask AnimTask_BlendBattleAnimPal, 10, (F_PAL_BG | F_PAL_BATTLERS_2), 1, 0, 16, RGB_WHITE @ bg to white pal @@ -36727,8 +36725,6 @@ gBattleAnimMove_ClangorousSoulblaze:: playsewithpan SE_SHINY, SOUND_PAN_ATTACKER createsprite gClangorousSoulRedRingTemplate, ANIM_ATTACKER, 3, 0x0, 0x0, 0x0, 0x0 waitforvisualfinish - unloadspritegfx ANIM_TAG_HORSESHOE_SIDE_FIST - unloadspritegfx ANIM_TAG_SPARKLE_2 @stars loadspritegfx ANIM_TAG_ROUND_SHADOW @ fly playsewithpan SE_M_FLY, SOUND_PAN_ATTACKER createsprite gClangoorousSoulblazeWhiteFlySpriteTemplate, ANIM_ATTACKER, 2, 0x0, 0x0, 0xd, 0x150 @@ -37206,8 +37202,6 @@ SearingSunrazeSmashImpact: loadspritegfx ANIM_TAG_CROSS_IMPACT @x delay 0 unloadspritegfx ANIM_TAG_METEOR @superpower - unloadspritegfx ANIM_TAG_DRAGON_ASCENT @dragon ascent 1 - unloadspritegfx ANIM_TAG_DRAGON_ASCENT_FOE @dragon ascent 2 createsprite gSearingSunrazeSmashCrossImpactSpriteTemplate, ANIM_TARGET, 2, 0x0, 0x0, 0x1, 0x24 playsewithpan SE_M_LEER, SOUND_PAN_TARGET visible ANIM_ATTACKER @@ -37769,7 +37763,6 @@ gBattleAnimMove_SoulStealing7StarStrike:: call SoulStealingSevenStarStrikeBlueParalysis waitforvisualfinish visible ANIM_ATTACKER - unloadspritegfx ANIM_TAG_ROUND_SHADOW loadspritegfx ANIM_TAG_SPARKLE_4 @ detect loadspritegfx ANIM_TAG_EXPLOSION @ explosion playsewithpan SE_M_DETECT, SOUND_PAN_ATTACKER diff --git a/include/global.h b/include/global.h index ff0b6f79e1..a3dacfb3d4 100644 --- a/include/global.h +++ b/include/global.h @@ -1180,6 +1180,8 @@ struct MapPosition #if T_SHOULD_RUN_MOVE_ANIM extern bool32 gLoadFail; +extern bool32 gCountAllocs; +extern s32 gSpriteAllocs; #endif // T_SHOULD_RUN_MOVE_ANIM #endif // GUARD_GLOBAL_H diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index be70970fa7..efea210440 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -2228,6 +2228,10 @@ static void Cmd_attackanimation(void) gBattleMons[gBattlerAttacker].friendship, &gDisableStructs[gBattlerAttacker], multihit); +#if T_SHOULD_RUN_MOVE_ANIM + gCountAllocs = TRUE; + gSpriteAllocs = 0; +#endif gBattleScripting.animTurn++; gBattleScripting.animTargetsHit++; MarkBattlerForControllerExec(gBattlerAttacker); @@ -2246,7 +2250,12 @@ static void Cmd_waitanimation(void) CMD_ARGS(); if (gBattleControllerExecFlags == 0 && gBattleStruct->battlerKOAnimsRunning == 0) + { +#if T_SHOULD_RUN_MOVE_ANIM + gCountAllocs = FALSE; +#endif gBattlescriptCurrInstr = cmd->nextInstr; + } } static void DoublesHPBarReduction(void) diff --git a/src/sprite.c b/src/sprite.c index 1b89a50be1..2df5e2760a 100644 --- a/src/sprite.c +++ b/src/sprite.c @@ -28,6 +28,8 @@ #if T_SHOULD_RUN_MOVE_ANIM EWRAM_DATA bool32 gLoadFail = FALSE; +EWRAM_DATA bool32 gCountAllocs = FALSE; +EWRAM_DATA s32 gSpriteAllocs = 0; #endif // T_SHOULD_RUN_MOVE_ANIM struct SpriteCopyRequest @@ -1501,6 +1503,10 @@ void LoadSpriteSheets(const struct SpriteSheet *sheets) void FreeSpriteTilesByTag(u16 tag) { +#if T_SHOULD_RUN_MOVE_ANIM + if (gCountAllocs) + gSpriteAllocs--; +#endif u8 index = IndexOfSpriteTileTag(tag); if (index != 0xFF) { @@ -1566,6 +1572,10 @@ u16 GetSpriteTileTagByTileStart(u16 start) void AllocSpriteTileRange(u16 tag, u16 start, u16 count) { +#if T_SHOULD_RUN_MOVE_ANIM + if (gCountAllocs) + gSpriteAllocs++; +#endif u8 freeIndex = IndexOfSpriteTileTag(TAG_NONE); sSpriteTileRangeTags[freeIndex] = tag; SET_SPRITE_TILE_RANGE(freeIndex, start, count); diff --git a/test/battle/move_animations/all_anims.c b/test/battle/move_animations/all_anims.c index acf5dcbb56..e69eb59750 100644 --- a/test/battle/move_animations/all_anims.c +++ b/test/battle/move_animations/all_anims.c @@ -409,9 +409,10 @@ SINGLE_BATTLE_TEST("Move Animations don't leak when used - Singles (player to op SceneSingles(move, player); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -447,9 +448,10 @@ SINGLE_BATTLE_TEST("Move Animations don't leak when used - Singles (opponent to SceneSingles(move, opponent); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -505,9 +507,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (playerLeft t DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -564,9 +567,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (opponentLeft DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -623,9 +627,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (playerLeft t DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -682,9 +687,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (opponentRigh DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -741,9 +747,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (playerRight DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -800,9 +807,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (opponentLeft DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -859,9 +867,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (playerRight DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -918,9 +927,10 @@ DOUBLE_BATTLE_TEST("Move Animations don't leak when used - Doubles (opponentRigh DoublesScene(move, attacker); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1205,9 +1215,10 @@ SINGLE_BATTLE_TEST("Move Animations occur before their stat change animations - SceneSingles(move, player); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1273,9 +1284,10 @@ SINGLE_BATTLE_TEST("Z-Moves don't leak when used - Singles (player to opponent)" ANIMATION(ANIM_TYPE_MOVE, zmove, player); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1302,9 +1314,10 @@ SINGLE_BATTLE_TEST("Z-Moves don't leak when used - Singles (opponent to player)" ANIMATION(ANIM_TYPE_MOVE, zmove, opponent); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1333,9 +1346,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (playerLeft to oppone ANIMATION(ANIM_TYPE_MOVE, zmove, playerLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1364,9 +1378,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (playerLeft to oppone ANIMATION(ANIM_TYPE_MOVE, zmove, playerLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1395,9 +1410,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (playerRight to oppon ANIMATION(ANIM_TYPE_MOVE, zmove, playerRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1426,9 +1442,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (playerRight to oppon ANIMATION(ANIM_TYPE_MOVE, zmove, playerRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1457,9 +1474,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (opponentLeft to play ANIMATION(ANIM_TYPE_MOVE, zmove, opponentLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1488,9 +1506,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (opponentLeft to play ANIMATION(ANIM_TYPE_MOVE, zmove, opponentLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1519,9 +1538,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (opponentRight to pla ANIMATION(ANIM_TYPE_MOVE, zmove, opponentRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1550,9 +1570,10 @@ DOUBLE_BATTLE_TEST("Z-Moves don't leak when used - Doubles (opponentRight to pla ANIMATION(ANIM_TYPE_MOVE, zmove, opponentRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1595,9 +1616,10 @@ SINGLE_BATTLE_TEST("Tera Blast doesn't leak when used - Singles (player to oppon ANIMATION(ANIM_TYPE_MOVE, move, player); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1617,9 +1639,10 @@ SINGLE_BATTLE_TEST("Tera Blast doesn't leak when used - Singles (opponent to pla ANIMATION(ANIM_TYPE_MOVE, move, opponent); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1641,9 +1664,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (playerLeft to o ANIMATION(ANIM_TYPE_MOVE, move, playerLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1665,9 +1689,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (playerLeft to o ANIMATION(ANIM_TYPE_MOVE, move, playerLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1689,9 +1714,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (playerRight to ANIMATION(ANIM_TYPE_MOVE, move, playerRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1713,9 +1739,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (playerRight to ANIMATION(ANIM_TYPE_MOVE, move, playerRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1737,9 +1764,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (opponentLeft to ANIMATION(ANIM_TYPE_MOVE, move, opponentLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1761,9 +1789,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (opponentLeft to ANIMATION(ANIM_TYPE_MOVE, move, opponentLeft); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1785,9 +1814,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (opponentRight t ANIMATION(ANIM_TYPE_MOVE, move, opponentRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } @@ -1809,9 +1839,10 @@ DOUBLE_BATTLE_TEST("Tera Blast doesn't leak when used - Doubles (opponentRight t ANIMATION(ANIM_TYPE_MOVE, move, opponentRight); } THEN { FORCE_MOVE_ANIM(FALSE); - if (gLoadFail) + if (gLoadFail || gSpriteAllocs != 0) DebugPrintf("Move failed: %S (%u)", GetMoveName(move), move); EXPECT_EQ(gLoadFail, FALSE); + EXPECT_EQ(gSpriteAllocs, 0); } } From e41de2544a694d690da9870ee6ab5b11c5a79e3c Mon Sep 17 00:00:00 2001 From: Jamie Foster Date: Thu, 20 Nov 2025 16:38:55 +0100 Subject: [PATCH 06/12] Fix ohko moves ai tests --- test/battle/ai/can_use_all_moves.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/battle/ai/can_use_all_moves.c b/test/battle/ai/can_use_all_moves.c index fb6f7a343a..efae2b7544 100644 --- a/test/battle/ai/can_use_all_moves.c +++ b/test/battle/ai/can_use_all_moves.c @@ -9,7 +9,6 @@ AI_DOUBLE_BATTLE_TEST("AI uses Final Gambit") { - KNOWN_FAILING; GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); PLAYER(SPECIES_WOBBUFFET); @@ -20,13 +19,12 @@ AI_DOUBLE_BATTLE_TEST("AI uses Final Gambit") OPPONENT(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { - TURN { EXPECT_MOVE(opponentLeft, MOVE_FINAL_GAMBIT); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_FINAL_GAMBIT); SEND_OUT(playerLeft, 2); } } } AI_DOUBLE_BATTLE_TEST("AI uses Guillotine") { - KNOWN_FAILING; GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); PLAYER(SPECIES_WOBBUFFET); @@ -37,13 +35,12 @@ AI_DOUBLE_BATTLE_TEST("AI uses Guillotine") OPPONENT(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { - TURN { EXPECT_MOVE(opponentLeft, MOVE_GUILLOTINE); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_GUILLOTINE); SEND_OUT(playerLeft, 2); } } } AI_DOUBLE_BATTLE_TEST("AI uses Sheer Cold") { - KNOWN_FAILING; GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); PLAYER(SPECIES_WOBBUFFET); @@ -54,7 +51,7 @@ AI_DOUBLE_BATTLE_TEST("AI uses Sheer Cold") OPPONENT(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { - TURN { EXPECT_MOVE(opponentLeft, MOVE_SHEER_COLD); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_SHEER_COLD); SEND_OUT(playerLeft, 2); } } } From acdfa39f76c17ffa3be79218174d43af174e10d9 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Thu, 20 Nov 2025 17:19:06 +0100 Subject: [PATCH 07/12] Make switchout abilities trigger after a pokemon has returned to its ball (#8304) --- data/battle_scripts_1.s | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 26ee741f71..bacb8e7386 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -309,10 +309,10 @@ BattleScript_MoveSwitch: waitmessage B_WAIT_TIME_SHORT BattleScript_MoveSwitchOpenPartyScreen:: openpartyscreen BS_ATTACKER, BattleScript_MoveSwitchEnd - switchoutabilities BS_ATTACKER waitstate - switchhandleorder BS_ATTACKER, 2 returntoball BS_ATTACKER, FALSE + switchoutabilities BS_ATTACKER + switchhandleorder BS_ATTACKER, 2 getswitchedmondata BS_ATTACKER switchindataupdate BS_ATTACKER hpthresholds BS_ATTACKER @@ -4099,10 +4099,10 @@ BattleScript_EffectBatonPass:: attackanimation waitanimation openpartyscreen BS_ATTACKER, BattleScript_ButItFailed - switchoutabilities BS_ATTACKER waitstate - switchhandleorder BS_ATTACKER, 2 returntoball BS_ATTACKER, FALSE + switchoutabilities BS_ATTACKER + switchhandleorder BS_ATTACKER, 2 getswitchedmondata BS_ATTACKER switchindataupdate BS_ATTACKER hpthresholds BS_ATTACKER @@ -5418,11 +5418,11 @@ BattleScript_ActionSwitch:: end2 BattleScript_DoSwitchOut:: - switchoutabilities BS_ATTACKER undodynamax BS_ATTACKER waitstate returnatktoball waitstate + switchoutabilities BS_ATTACKER drawpartystatussummary BS_ATTACKER switchhandleorder BS_ATTACKER, 1 getswitchedmondata BS_ATTACKER @@ -5724,9 +5724,9 @@ BattleScript_RoarSuccessRet: attackanimation waitanimation BattleScript_RoarSuccessRet_Ret: - switchoutabilities BS_TARGET returntoball BS_TARGET, FALSE waitstate + switchoutabilities BS_TARGET return BattleScript_WeaknessPolicy:: @@ -7219,10 +7219,10 @@ BattleScript_EmergencyExit:: playanimation BS_SCRIPTING, B_ANIM_SLIDE_OFFSCREEN waitanimation openpartyscreen BS_SCRIPTING, BattleScript_EmergencyExitRet - switchoutabilities BS_SCRIPTING waitstate - switchhandleorder BS_SCRIPTING, 2 returntoball BS_SCRIPTING, FALSE + switchoutabilities BS_SCRIPTING + switchhandleorder BS_SCRIPTING, 2 getswitchedmondata BS_SCRIPTING switchindataupdate BS_SCRIPTING hpthresholds BS_SCRIPTING @@ -7252,10 +7252,10 @@ BattleScript_EmergencyExitEnd2:: playanimation BS_ATTACKER, B_ANIM_SLIDE_OFFSCREEN waitanimation openpartyscreen BS_ATTACKER, BattleScript_EmergencyExitRetEnd2 - switchoutabilities BS_ATTACKER waitstate - switchhandleorder BS_ATTACKER, 2 returntoball BS_ATTACKER, FALSE + switchoutabilities BS_ATTACKER + switchhandleorder BS_ATTACKER, 2 getswitchedmondata BS_ATTACKER switchindataupdate BS_ATTACKER hpthresholds BS_ATTACKER @@ -9191,12 +9191,12 @@ BattleScript_EjectButtonActivates:: undodynamax BS_SCRIPTING makeinvisible BS_SCRIPTING openpartyscreen BS_SCRIPTING, BattleScript_EjectButtonEnd + waitstate + returntoball BS_SCRIPTING, FALSE copybyte sSAVED_BATTLER, sBATTLER switchoutabilities BS_SCRIPTING copybyte sBATTLER, sSAVED_BATTLER - waitstate switchhandleorder BS_SCRIPTING, 0x2 - returntoball BS_SCRIPTING, FALSE getswitchedmondata BS_SCRIPTING switchindataupdate BS_SCRIPTING hpthresholds BS_SCRIPTING From 2f5dfa99f49639148f706bacdb3a9e3897bea4b5 Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Thu, 20 Nov 2025 22:14:34 +0100 Subject: [PATCH 08/12] Make `gTestRunnerHeadless` into a constant outside of tests (#8306) Co-authored-by: Hedara --- include/test_runner.h | 4 ++++ src/test_runner_stub.c | 2 ++ 2 files changed, 6 insertions(+) diff --git a/include/test_runner.h b/include/test_runner.h index 9e0d96ff5b..b7aa69a076 100644 --- a/include/test_runner.h +++ b/include/test_runner.h @@ -2,7 +2,11 @@ #define GUARD_TEST_RUNNER_H extern const bool8 gTestRunnerEnabled; +#if TESTING extern const bool8 gTestRunnerHeadless; +#else +#define gTestRunnerHeadless FALSE +#endif extern const bool8 gTestRunnerSkipIsFail; #if TESTING diff --git a/src/test_runner_stub.c b/src/test_runner_stub.c index 9a9452ed21..20aabe3d9a 100644 --- a/src/test_runner_stub.c +++ b/src/test_runner_stub.c @@ -7,5 +7,7 @@ const bool8 gTestRunnerEnabled = FALSE; // The Makefile patches gTestRunnerHeadless as part of make test. // This allows us to open the ROM in an mgba with a UI and see the // animations and messages play, which helps when debugging a test. +#if TESTING const bool8 gTestRunnerHeadless = FALSE; +#endif const bool8 gTestRunnerSkipIsFail = FALSE; From 32b48977477ee16f5ff5b7e5c8af629e40324a0e Mon Sep 17 00:00:00 2001 From: moostoet <70690976+moostoet@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:30:29 +0100 Subject: [PATCH 09/12] Fix Shed Shell allowing fleeing/teleporting and Smoke Ball failing to guarantee escape (#8286) --- src/battle_main.c | 2 +- src/battle_script_commands.c | 2 +- src/battle_util.c | 6 +-- test/battle/hold_effect/shed_shell.c | 67 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 test/battle/hold_effect/shed_shell.c diff --git a/src/battle_main.c b/src/battle_main.c index 5620b584b6..3c992bc87f 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4329,7 +4329,7 @@ static void HandleTurnActionSelectionState(void) case B_ACTION_SWITCH: gBattleStruct->battlerPartyIndexes[battler] = gBattlerPartyIndexes[battler]; if (gBattleTypeFlags & BATTLE_TYPE_ARENA - || !CanBattlerEscape(battler)) + || (!CanBattlerEscape(battler) && GetBattlerHoldEffect(battler, TRUE) != HOLD_EFFECT_SHED_SHELL)) { BtlController_EmitChoosePokemon(battler, B_COMM_TO_CONTROLLER, PARTY_ACTION_CANT_SWITCH, PARTY_SIZE, ABILITY_NONE, 0, gBattleStruct->battlerPartyOrders[battler]); } diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index efea210440..d8c8e32e91 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -7433,7 +7433,7 @@ static void Cmd_jumpifcantswitch(void) CMD_ARGS(u8 battler:7, u8 ignoreEscapePrevention:1, const u8 *jumpInstr); u32 battler = GetBattlerForBattleScript(cmd->battler); - if (!cmd->ignoreEscapePrevention && !CanBattlerEscape(battler)) + if (!cmd->ignoreEscapePrevention && !CanBattlerEscape(battler) && GetBattlerHoldEffect(battler, TRUE) != HOLD_EFFECT_SHED_SHELL) { gBattlescriptCurrInstr = cmd->jumpInstr; } diff --git a/src/battle_util.c b/src/battle_util.c index f98b83d9a6..696efdea4d 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -733,7 +733,9 @@ void HandleAction_Run(void) } else { - if (!CanBattlerEscape(gBattlerAttacker)) + if (GetBattlerHoldEffect(gBattlerAttacker, TRUE) != HOLD_EFFECT_CAN_ALWAYS_RUN + && GetBattlerAbility(gBattlerAttacker) != ABILITY_RUN_AWAY + && !CanBattlerEscape(gBattlerAttacker)) { gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_ATTACKER_CANT_ESCAPE; gBattlescriptCurrInstr = BattleScript_PrintFailedToRunString; @@ -5448,8 +5450,6 @@ bool32 CanBattlerEscape(u32 battler) // no ability check { if (gBattleStruct->battlerState[battler].commanderSpecies != SPECIES_NONE) return FALSE; - else if (GetBattlerHoldEffect(battler, TRUE) == HOLD_EFFECT_SHED_SHELL) - return TRUE; else if (B_GHOSTS_ESCAPE >= GEN_6 && IS_BATTLER_OF_TYPE(battler, TYPE_GHOST)) return TRUE; else if (gBattleMons[battler].volatiles.escapePrevention) diff --git a/test/battle/hold_effect/shed_shell.c b/test/battle/hold_effect/shed_shell.c new file mode 100644 index 0000000000..93c3fb0e53 --- /dev/null +++ b/test/battle/hold_effect/shed_shell.c @@ -0,0 +1,67 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gItemsInfo[ITEM_SHED_SHELL].holdEffect == HOLD_EFFECT_SHED_SHELL); +}; + +SINGLE_BATTLE_TEST("Shed Shell allows switching out even when trapped by Mean Look") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SHED_SHELL); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_GASTLY); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_MEAN_LOOK); } + TURN { SWITCH(player, 1); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MEAN_LOOK, opponent); + SWITCH_OUT_MESSAGE("Wobbuffet"); + SEND_IN_MESSAGE("Wynaut"); + } +} + +SINGLE_BATTLE_TEST("Shed Shell allows switching out even when trapped by Shadow Tag") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SHED_SHELL); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET) { Ability(ABILITY_SHADOW_TAG); } + } WHEN { + TURN { SWITCH(player, 1); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + SWITCH_OUT_MESSAGE("Wobbuffet"); + SEND_IN_MESSAGE("Wynaut"); + } +} + +SINGLE_BATTLE_TEST("Shed Shell allows switching out even when trapped by Arena Trap") +{ + GIVEN { + PLAYER(SPECIES_DIGLETT) { Item(ITEM_SHED_SHELL); } // Grounded + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_DIGLETT) { Ability(ABILITY_ARENA_TRAP); } + } WHEN { + TURN { SWITCH(player, 1); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + SWITCH_OUT_MESSAGE("Diglett"); + SEND_IN_MESSAGE("Wynaut"); + } +} + +SINGLE_BATTLE_TEST("Shed Shell does not allow Teleport when trapped") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_TELEPORT) == EFFECT_TELEPORT); + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SHED_SHELL); Moves(MOVE_TELEPORT, MOVE_SPLASH, MOVE_CELEBRATE); } + OPPONENT(SPECIES_GASTLY); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_MEAN_LOOK); } + TURN { MOVE(player, MOVE_TELEPORT); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MEAN_LOOK, opponent); + MESSAGE("Wobbuffet used Teleport!"); + MESSAGE("But it failed!"); + } +} From 7ec0a1db9a3cfb2575acb37d6a93a9b48b53b5ee Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 21 Nov 2025 11:14:25 +0100 Subject: [PATCH 10/12] Fix bug where defiant/competitive would pass their stat change to the next target (#8312) --- data/battle_scripts_1.s | 4 +++ test/battle/gimmick/dynamax.c | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index bacb8e7386..638f81bf7b 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -9339,8 +9339,10 @@ BattleScript_EffectMaxMove:: BattleScript_EffectRaiseStatAllies:: savetarget copybyte gBattlerTarget, gBattlerAttacker + copybyte sSAVED_STAT_CHANGER, sSTATCHANGER BattleScript_RaiseSideStatsLoop: jumpifabsent BS_TARGET, BattleScript_RaiseSideStatsIncrement + copybyte sSTATCHANGER, sSAVED_STAT_CHANGER statbuffchange BS_TARGET, STAT_CHANGE_ALLOW_PTR, BattleScript_RaiseSideStatsIncrement jumpifbyte CMP_EQUAL, cMULTISTRING_CHOOSER, B_MSG_STAT_WONT_INCREASE, BattleScript_RaiseSideStatsIncrement printfromtable gStatUpStringIds @@ -9355,8 +9357,10 @@ BattleScript_RaiseSideStatsEnd: BattleScript_EffectLowerStatFoes:: savetarget copybyte sBATTLER, gBattlerTarget + copybyte sSAVED_STAT_CHANGER, sSTATCHANGER BattleScript_LowerSideStatsLoop: jumpifabsent BS_TARGET, BattleScript_LowerSideStatsIncrement + copybyte sSTATCHANGER, sSAVED_STAT_CHANGER statbuffchange BS_TARGET, STAT_CHANGE_ALLOW_PTR, BattleScript_LowerSideStatsIncrement jumpifbyte CMP_EQUAL, cMULTISTRING_CHOOSER, B_MSG_STAT_WONT_DECREASE, BattleScript_LowerSideStatsIncrement printfromtable gStatDownStringIds diff --git a/test/battle/gimmick/dynamax.c b/test/battle/gimmick/dynamax.c index bb8814fa42..0329ee903c 100644 --- a/test/battle/gimmick/dynamax.c +++ b/test/battle/gimmick/dynamax.c @@ -1671,5 +1671,71 @@ SINGLE_BATTLE_TEST("Dynamax: Destiny Bond fails if a dynamaxed battler is presen } } +DOUBLE_BATTLE_TEST("Dynamax stat lowering moves don't make stat-changing abilities apply to partner") +{ + u32 move, stat, ability; + move = 0; stat = 0; ability = 0; + u32 abilityList[] = {ABILITY_COMPETITIVE, ABILITY_DEFIANT, ABILITY_CONTRARY, ABILITY_SIMPLE}; + for (u32 j = 0; j < 4; j++) + { + PARAMETRIZE { move = MOVE_SCRATCH; stat = STAT_SPEED; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_FURY_CUTTER; stat = STAT_SPATK; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_LICK; stat = STAT_DEF; ability = abilityList[j]; ;} + PARAMETRIZE { move = MOVE_DRAGON_CLAW; stat = STAT_ATK; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_CRUNCH; stat = STAT_SPDEF; ability = abilityList[j]; } + } + GIVEN { + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_STRIKE, MOVE_EFFECT_LOWER_SPEED_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_FLUTTERBY, MOVE_EFFECT_LOWER_SP_ATK_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_PHANTASM, MOVE_EFFECT_LOWER_DEFENSE_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_WYRMWIND, MOVE_EFFECT_LOWER_ATTACK_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_DARKNESS, MOVE_EFFECT_LOWER_SP_DEF_SIDE)); + PLAYER(SPECIES_WOBBUFFET) { } + PLAYER(SPECIES_WOBBUFFET) { } + OPPONENT(SPECIES_WOBBUFFET) { Ability(ability); } + OPPONENT(SPECIES_WOBBUFFET) { Ability(ABILITY_SHADOW_TAG); } + } WHEN { + TURN { MOVE(playerLeft, move, target: opponentLeft, gimmick: GIMMICK_DYNAMAX);} + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + } THEN { + EXPECT_EQ(opponentRight->statStages[stat], DEFAULT_STAT_STAGE - 1); + } +} + +DOUBLE_BATTLE_TEST("Dynamax stat raising moves don't make stat-changing abilities apply to partner") +{ + u32 move, stat, ability; + move = 0; stat = 0; ability = 0; + u32 abilityList[] = {ABILITY_CONTRARY, ABILITY_SIMPLE}; + for (u32 j = 0; j < 2; j++) + { + PARAMETRIZE { move = MOVE_PECK; stat = STAT_SPEED; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_POISON_JAB; stat = STAT_SPATK; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_BULLET_PUNCH; stat = STAT_DEF; ability = abilityList[j]; ;} + PARAMETRIZE { move = MOVE_DOUBLE_KICK; stat = STAT_ATK; ability = abilityList[j]; } + PARAMETRIZE { move = MOVE_MUD_SLAP; stat = STAT_SPDEF; ability = abilityList[j]; } + } + GIVEN { + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_STRIKE, MOVE_EFFECT_LOWER_SPEED_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_FLUTTERBY, MOVE_EFFECT_LOWER_SP_ATK_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_PHANTASM, MOVE_EFFECT_LOWER_DEFENSE_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_WYRMWIND, MOVE_EFFECT_LOWER_ATTACK_SIDE)); + ASSUME(MoveHasAdditionalEffect(MOVE_MAX_DARKNESS, MOVE_EFFECT_LOWER_SP_DEF_SIDE)); + PLAYER(SPECIES_WOBBUFFET) { Ability(ability); } + PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_SHADOW_TAG); } + OPPONENT(SPECIES_WOBBUFFET) {} + OPPONENT(SPECIES_WOBBUFFET) {} + } WHEN { + TURN { MOVE(playerLeft, move, target: opponentLeft, gimmick: GIMMICK_DYNAMAX);} + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + } THEN { + EXPECT_EQ(playerRight->statStages[stat], DEFAULT_STAT_STAGE + 1); + } +} + TO_DO_BATTLE_TEST("Dynamax: Contrary inverts stat-lowering Max Moves, without showing a message") TO_DO_BATTLE_TEST("Dynamax: Contrary inverts stat-increasing Max Moves, without showing a message") From 7229305ff97c9fd71782dac7b039e8efa6dee282 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 21 Nov 2025 15:15:01 +0100 Subject: [PATCH 11/12] Fix max move message against semi invulnerable target (#8313) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- data/battle_scripts_1.s | 4 +++- src/battle_script_commands.c | 17 ++++++++++++++++- test/battle/gimmick/dynamax.c | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 638f81bf7b..640d57bc6e 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -9333,8 +9333,10 @@ BattleScript_TargetAbilityStatRaiseRet_End: @@@ MAX MOVES @@@ BattleScript_EffectMaxMove:: attackcanceler + attackstring + ppreduce accuracycheck BattleScript_ButItFailed, NO_ACC_CALC_CHECK_LOCK_ON - goto BattleScript_HitFromAtkString + goto BattleScript_HitFromCritCalc BattleScript_EffectRaiseStatAllies:: savetarget diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index d8c8e32e91..820023793d 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1370,8 +1370,23 @@ static void AccuracyCheck(bool32 recalcDragonDarts, const u8 *nextInstr, const u if (gBattleMons[gBattlerTarget].volatiles.lockOn && gDisableStructs[gBattlerTarget].battlerWithSureHit == gBattlerAttacker) gBattlescriptCurrInstr = nextInstr; else if (IsSemiInvulnerable(gBattlerTarget, CHECK_ALL)) + { + if (gBattlerTarget != BATTLE_PARTNER(gBattlerAttacker)) + { + gBattleStruct->moveResultFlags[gBattlerTarget] |= MOVE_RESULT_MISSED; + gBattleStruct->missStringId[gBattlerTarget] = gBattleCommunication[MISS_TYPE] = B_MSG_AVOIDED_ATK; + } gBattlescriptCurrInstr = failInstr; - else if (!JumpIfMoveAffectedByProtect(gCurrentMove, gBattlerTarget, TRUE, failInstr)) + } + else if (IsBattlerProtected(gBattlerAttacker, gBattlerTarget, gCurrentMove)) + { + gBattleStruct->moveResultFlags[gBattlerTarget] |= MOVE_RESULT_MISSED; + gBattleStruct->missStringId[gBattlerTarget] = gBattleCommunication[MISS_TYPE] = B_MSG_PROTECTED; + gLastLandedMoves[gBattlerTarget] = 0; + gLastHitByType[gBattlerTarget] = 0; + gBattlescriptCurrInstr = failInstr; + } + else gBattlescriptCurrInstr = nextInstr; if (GetActiveGimmick(gBattlerAttacker) == GIMMICK_DYNAMAX) { diff --git a/test/battle/gimmick/dynamax.c b/test/battle/gimmick/dynamax.c index 0329ee903c..5cf79e2949 100644 --- a/test/battle/gimmick/dynamax.c +++ b/test/battle/gimmick/dynamax.c @@ -1671,6 +1671,20 @@ SINGLE_BATTLE_TEST("Dynamax: Destiny Bond fails if a dynamaxed battler is presen } } +SINGLE_BATTLE_TEST("Dynamax: max move against semi-invulnerable target prints the correct message") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) {Speed(1);}; + OPPONENT(SPECIES_WOBBUFFET) {Speed(2);}; + } WHEN { + TURN { MOVE(player, MOVE_SCRATCH, gimmick: GIMMICK_DYNAMAX); MOVE(opponent, MOVE_FLY); } + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_DYNAMAX_GROWTH, player); + MESSAGE("Wobbuffet used Max Strike!"); + MESSAGE("The opposing Wobbuffet avoided the attack!"); + } +} + DOUBLE_BATTLE_TEST("Dynamax stat lowering moves don't make stat-changing abilities apply to partner") { u32 move, stat, ability; From 65a5d1e7d85668afa207253564d900634bec6105 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:58:02 +0000 Subject: [PATCH 12/12] Fixes Neutralizing Gas displaying message when exiting with multiple users (#8318) --- asm/macros/battle_script.inc | 4 +- data/battle_scripts_1.s | 3 +- src/battle_main.c | 9 +- src/battle_script_commands.c | 116 +++++++++++++++---------- src/battle_util.c | 22 ++--- test/battle/ability/neutralizing_gas.c | 43 +++++++++ 6 files changed, 129 insertions(+), 68 deletions(-) diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index 754a5221f6..517ab09e45 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1924,8 +1924,8 @@ 1: .endm - .macro jumpifabilitycantbesuppressed battler:req, jumpInstr:req - callnative BS_JumpIfAbilityCantBeSuppressed + .macro jumpifabilitycantbereactivated battler:req, jumpInstr:req + callnative BS_JumpIfAbilityCantBeReactivated .byte \battler .4byte \jumpInstr .endm diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 640d57bc6e..8a93eb9436 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -9296,8 +9296,7 @@ BattleScript_NeutralizingGasExits:: setbyte gBattlerAttacker, 0 BattleScript_NeutralizingGasExitsLoop: copyarraywithindex gBattlerTarget, gBattlerByTurnOrder, gBattlerAttacker, 1 - jumpifabilitycantbesuppressed BS_TARGET, BattleScript_NeutralizingGasExitsLoopIncrement - jumpifability BS_TARGET, ABILITY_IMPOSTER, BattleScript_NeutralizingGasExitsLoopIncrement @ Imposter only activates when first entering the field + jumpifabilitycantbereactivated BS_TARGET, BattleScript_NeutralizingGasExitsLoopIncrement saveattacker switchinabilities BS_TARGET restoreattacker diff --git a/src/battle_main.c b/src/battle_main.c index 3c992bc87f..c8c8480007 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3861,8 +3861,13 @@ static void TryDoEventsBeforeFirstTurn(void) gBattleStruct->eventsBeforeFirstTurnState++; break; case FIRST_TURN_EVENTS_NEUTRALIZING_GAS: - if (AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, 0, 0, 0, 0) != 0) - return; + while (gBattleStruct->switchInBattlerCounter < gBattlersCount) // From fastest to slowest + { + i = gBattlerByTurnOrder[gBattleStruct->switchInBattlerCounter++]; + if (AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, i, 0, 0, 0) != 0) + return; + } + gBattleStruct->switchInBattlerCounter = 0; gBattleStruct->eventsBeforeFirstTurnState++; break; case FIRST_TURN_EVENTS_SWITCH_IN_ABILITIES: diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 820023793d..c45c3cc9a5 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -4244,14 +4244,17 @@ static void Cmd_tryfaintmon(void) } else { - if (gBattleMons[battler].ability == ABILITY_NEUTRALIZING_GAS + if (gDisableStructs[battler].neutralizingGas && !(gAbsentBattlerFlags & (1u << battler)) && !IsBattlerAlive(battler)) { - gBattleMons[battler].ability = ABILITY_NONE; - BattleScriptPush(gBattlescriptCurrInstr); - gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; - return; + gDisableStructs[battler].neutralizingGas = FALSE; + if (!IsNeutralizingGasOnField()) + { + BattleScriptPush(gBattlescriptCurrInstr); + gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; + return; + } } if (cmd->battler == BS_ATTACKER) { @@ -13424,44 +13427,46 @@ static void Cmd_switchoutabilities(void) CMD_ARGS(u8 battler); u32 battler = GetBattlerForBattleScript(cmd->battler); - if (gBattleMons[battler].ability == ABILITY_NEUTRALIZING_GAS) + if (gDisableStructs[battler].neutralizingGas) { - gBattleMons[battler].ability = ABILITY_NONE; - BattleScriptPush(gBattlescriptCurrInstr); - gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; + gDisableStructs[battler].neutralizingGas = FALSE; + if (!IsNeutralizingGasOnField()) + { + BattleScriptPush(gBattlescriptCurrInstr); + gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; + return; + } } - else + + switch (GetBattlerAbility(battler)) { - switch (GetBattlerAbility(battler)) - { - case ABILITY_NATURAL_CURE: - if (gBattleMons[battler].status1 & STATUS1_SLEEP) - TryDeactivateSleepClause(GetBattlerSide(battler), gBattlerPartyIndexes[battler]); + case ABILITY_NATURAL_CURE: + if (gBattleMons[battler].status1 & STATUS1_SLEEP) + TryDeactivateSleepClause(GetBattlerSide(battler), gBattlerPartyIndexes[battler]); - gBattleMons[battler].status1 = 0; - BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, - 1u << gBattleStruct->battlerPartyIndexes[battler], - sizeof(gBattleMons[battler].status1), - &gBattleMons[battler].status1); - MarkBattlerForControllerExec(battler); - break; - case ABILITY_REGENERATOR: - { - u32 regenerate = GetNonDynamaxMaxHP(battler) / 3; - regenerate += gBattleMons[battler].hp; - if (regenerate > gBattleMons[battler].maxHP) - regenerate = gBattleMons[battler].maxHP; - BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_HP_BATTLE, - 1u << gBattleStruct->battlerPartyIndexes[battler], - sizeof(regenerate), - ®enerate); - MarkBattlerForControllerExec(battler); - break; - } - } - - gBattlescriptCurrInstr = cmd->nextInstr; + gBattleMons[battler].status1 = 0; + BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_STATUS_BATTLE, + 1u << gBattleStruct->battlerPartyIndexes[battler], + sizeof(gBattleMons[battler].status1), + &gBattleMons[battler].status1); + MarkBattlerForControllerExec(battler); + break; + case ABILITY_REGENERATOR: + { + u32 regenerate = GetNonDynamaxMaxHP(battler) / 3; + regenerate += gBattleMons[battler].hp; + if (regenerate > gBattleMons[battler].maxHP) + regenerate = gBattleMons[battler].maxHP; + BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_HP_BATTLE, + 1u << gBattleStruct->battlerPartyIndexes[battler], + sizeof(regenerate), + ®enerate); + MarkBattlerForControllerExec(battler); + break; } + } + + gBattlescriptCurrInstr = cmd->nextInstr; } static void Cmd_jumpifhasnohp(void) @@ -16797,15 +16802,27 @@ void BS_TryBoosterEnergy(void) gBattlescriptCurrInstr = cmd->nextInstr; } -void BS_JumpIfAbilityCantBeSuppressed(void) +void BS_JumpIfAbilityCantBeReactivated(void) { NATIVE_ARGS(u8 battler, const u8 *jumpInstr); u32 battler = GetBattlerForBattleScript(cmd->battler); + u32 ability = gBattleMons[battler].ability; - if (gAbilitiesInfo[gBattleMons[battler].ability].cantBeSuppressed) + switch (ability) + { + case ABILITY_IMPOSTER: + case ABILITY_NEUTRALIZING_GAS: + case ABILITY_AIR_LOCK: + case ABILITY_CLOUD_NINE: gBattlescriptCurrInstr = cmd->jumpInstr; - else - gBattlescriptCurrInstr = cmd->nextInstr; + break; + default: + if (gAbilitiesInfo[ability].cantBeSuppressed) + gBattlescriptCurrInstr = cmd->jumpInstr; + else + gBattlescriptCurrInstr = cmd->nextInstr; + break; + } } void BS_TryActivateAbilityShield(void) @@ -18245,13 +18262,16 @@ void BS_TryEndNeutralizingGas(void) if (gSpecialStatuses[gBattlerTarget].neutralizingGasRemoved) { gSpecialStatuses[gBattlerTarget].neutralizingGasRemoved = FALSE; - BattleScriptPush(cmd->nextInstr); - gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; - } - else - { - gBattlescriptCurrInstr = cmd->nextInstr; + gDisableStructs[gBattlerTarget].neutralizingGas = FALSE; + if (!IsNeutralizingGasOnField()) + { + BattleScriptPush(cmd->nextInstr); + gBattlescriptCurrInstr = BattleScript_NeutralizingGasExits; + return; + } } + + gBattlescriptCurrInstr = cmd->nextInstr; } void BS_GetRototillerTargets(void) diff --git a/src/battle_util.c b/src/battle_util.c index 696efdea4d..8266c22f76 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5170,19 +5170,13 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITYEFFECT_NEUTRALIZINGGAS: // Prints message only. separate from ABILITYEFFECT_ON_SWITCHIN bc activates before entry hazards - for (i = 0; i < gBattlersCount; i++) + if (gBattleMons[battler].ability == ABILITY_NEUTRALIZING_GAS && !gDisableStructs[battler].neutralizingGas) { - if (gBattleMons[i].ability == ABILITY_NEUTRALIZING_GAS && !gDisableStructs[i].neutralizingGas) - { - gDisableStructs[i].neutralizingGas = TRUE; - gBattlerAbility = i; - gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_SWITCHIN_NEUTRALIZING_GAS; - BattleScriptPushCursorAndCallback(BattleScript_SwitchInAbilityMsg); - effect++; - } - - if (effect != 0) - break; + gDisableStructs[battler].neutralizingGas = TRUE; + gBattlerAbility = battler; + gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_SWITCHIN_NEUTRALIZING_GAS; + BattleScriptPushCursorAndCallback(BattleScript_SwitchInAbilityMsg); + effect++; } break; case ABILITYEFFECT_ON_WEATHER: // For ability effects that activate when the battle weather changes. @@ -5295,7 +5289,7 @@ bool32 IsNeutralizingGasOnField(void) for (i = 0; i < gBattlersCount; i++) { - if (IsBattlerAlive(i) && gBattleMons[i].ability == ABILITY_NEUTRALIZING_GAS && !gBattleMons[i].volatiles.gastroAcid) + if (gDisableStructs[i].neutralizingGas && !gBattleMons[i].volatiles.gastroAcid) return TRUE; } @@ -5371,7 +5365,7 @@ u32 GetBattlerAbilityInternal(u32 battler, u32 ignoreMoldBreaker, u32 noAbilityS if (!hasAbilityShield && IsNeutralizingGasOnField() - && gBattleMons[battler].ability != ABILITY_NEUTRALIZING_GAS) + && !gDisableStructs[battler].neutralizingGas) return ABILITY_NONE; if (CanBreakThroughAbility(gBattlerAttacker, battler, gBattleMons[gBattlerAttacker].ability, hasAbilityShield, ignoreMoldBreaker)) diff --git a/test/battle/ability/neutralizing_gas.c b/test/battle/ability/neutralizing_gas.c index f83e928c4a..426f4c969f 100644 --- a/test/battle/ability/neutralizing_gas.c +++ b/test/battle/ability/neutralizing_gas.c @@ -310,3 +310,46 @@ SINGLE_BATTLE_TEST("Neutralizing Gas exiting the field does not activate Imposte NOT ABILITY_POPUP(player, ABILITY_IMPOSTER); } } + +SINGLE_BATTLE_TEST("Neutralizing Gas exiting the field does not activate Air Lock/Cloud Nine but their effects are kept") +{ + u32 species, ability; + + PARAMETRIZE { species = SPECIES_GOLDUCK; ability = ABILITY_CLOUD_NINE; } + PARAMETRIZE { species = SPECIES_RAYQUAZA; ability = ABILITY_AIR_LOCK; } + + GIVEN { + ASSUME(GetMoveEffect(MOVE_RAIN_DANCE) == EFFECT_RAIN_DANCE); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(species) { Ability(ability); } + OPPONENT(SPECIES_WEEZING) { Ability(ABILITY_NEUTRALIZING_GAS); } + OPPONENT(SPECIES_LUDICOLO) { Ability(ABILITY_RAIN_DISH); } + } WHEN { + TURN { SWITCH(player, 1); SWITCH(opponent, 1); } + TURN { MOVE(player, MOVE_RAIN_DANCE); } + } SCENE { + NOT ABILITY_POPUP(player, ABILITY_AIR_LOCK); + MESSAGE("The effects of the neutralizing gas wore off!"); + NOT ABILITY_POPUP(player, ABILITY_AIR_LOCK); + ANIMATION(ANIM_TYPE_MOVE, MOVE_RAIN_DANCE, player); + NOT ABILITY_POPUP(opponent, ABILITY_RAIN_DISH); + } +} + +SINGLE_BATTLE_TEST("Neutralizing Gas only displays exiting message for the last user leaving the field") +{ + GIVEN { + PLAYER(SPECIES_WEEZING) { Ability(ABILITY_NEUTRALIZING_GAS); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WEEZING) { Ability(ABILITY_NEUTRALIZING_GAS); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { SWITCH(player, 1); SWITCH(opponent, 1); } + } SCENE { + ABILITY_POPUP(player, ABILITY_NEUTRALIZING_GAS); + ABILITY_POPUP(opponent, ABILITY_NEUTRALIZING_GAS); + SEND_IN_MESSAGE("Wobbuffet"); + MESSAGE("The effects of the neutralizing gas wore off!"); + NOT MESSAGE("The effects of the neutralizing gas wore off!"); + } +}