From 4e6f726efeea46972f0c2ceaa4debf38d8682ab2 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 19 Sep 2025 19:13:29 +0200 Subject: [PATCH 01/25] Fix bug with IF_GENDER evolution condition (#7749) --- src/pokemon.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pokemon.c b/src/pokemon.c index 745c761356..61270d72c3 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -4477,7 +4477,7 @@ bool32 DoesMonMeetAdditionalConditions(struct Pokemon *mon, const struct Evoluti { // Gen 2 case IF_GENDER: - if (gender == GetMonGender(mon)) + if (gender == params[i].arg1) currentCondition = TRUE; break; case IF_MIN_FRIENDSHIP: From f28b3bf3b8dcc606ac2b43bb872cc9ea3d53ba91 Mon Sep 17 00:00:00 2001 From: Phexi Date: Fri, 19 Sep 2025 20:38:20 +0200 Subject: [PATCH 02/25] Fix failing test for B_PREFERRED_ICE_WEATHER = B_ICE_WEATHER_SNOW (#7755) --- test/battle/ability/overcoat.c | 1 + test/battle/ability/slush_rush.c | 1 + test/battle/ai/can_use_all_moves.c | 4 ++++ test/battle/gimmick/dynamax.c | 10 ++++++++++ 4 files changed, 16 insertions(+) diff --git a/test/battle/ability/overcoat.c b/test/battle/ability/overcoat.c index da067a8771..c722d7ac55 100644 --- a/test/battle/ability/overcoat.c +++ b/test/battle/ability/overcoat.c @@ -40,6 +40,7 @@ DOUBLE_BATTLE_TEST("Overcoat blocks damage from sandstorm") DOUBLE_BATTLE_TEST("Overcoat blocks damage from hail") { GIVEN { + ASSUME(GetMoveEffect(MOVE_HAIL) == EFFECT_HAIL); PLAYER(SPECIES_WYNAUT) { Speed(50); Ability(ABILITY_SNOW_CLOAK); } PLAYER(SPECIES_SOLOSIS) { Speed(40); Ability(ABILITY_RUN_AWAY); } OPPONENT(SPECIES_PINECO) { Speed(30); Ability(ABILITY_OVERCOAT); } diff --git a/test/battle/ability/slush_rush.c b/test/battle/ability/slush_rush.c index 8eca46f089..76509db019 100644 --- a/test/battle/ability/slush_rush.c +++ b/test/battle/ability/slush_rush.c @@ -54,6 +54,7 @@ SINGLE_BATTLE_TEST("Slush Rush doesn't prevent non-Ice types from taking damage GIVEN { ASSUME(GetSpeciesType(SPECIES_WOBBUFFET, 0) != TYPE_ICE); ASSUME(GetSpeciesType(SPECIES_WOBBUFFET, 1) != TYPE_ICE); + ASSUME(GetMoveEffect(MOVE_HAIL) == EFFECT_HAIL); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_CETITAN) { Ability(ABILITY_SLUSH_RUSH); } } WHEN { diff --git a/test/battle/ai/can_use_all_moves.c b/test/battle/ai/can_use_all_moves.c index 364ff88a49..fb6f7a343a 100644 --- a/test/battle/ai/can_use_all_moves.c +++ b/test/battle/ai/can_use_all_moves.c @@ -249,7 +249,11 @@ AI_DOUBLE_BATTLE_TEST("AI can use all moves, 201-300") case EFFECT_HEAL_BELL: case EFFECT_SUNNY_DAY: case EFFECT_RAIN_DANCE: + #if B_PREFERRED_ICE_WEATHER == B_ICE_WEATHER_SNOW + case EFFECT_SNOWSCAPE: + #else case EFFECT_HAIL: + #endif case EFFECT_ROLE_PLAY: case EFFECT_REFRESH: diff --git a/test/battle/gimmick/dynamax.c b/test/battle/gimmick/dynamax.c index 194e1f3aac..8dfedad83f 100644 --- a/test/battle/gimmick/dynamax.c +++ b/test/battle/gimmick/dynamax.c @@ -838,7 +838,11 @@ SINGLE_BATTLE_TEST("Dynamax: Max Geyser sets up heavy rain") } } +#if B_PREFERRED_ICE_WEATHER == B_ICE_WEATHER_SNOW +SINGLE_BATTLE_TEST("Dynamax: Max Hailstorm sets up snow") +#else SINGLE_BATTLE_TEST("Dynamax: Max Hailstorm sets up hail") +#endif { GIVEN { ASSUME(MoveHasAdditionalEffect(MOVE_MAX_HAILSTORM, MOVE_EFFECT_HAIL)); @@ -848,9 +852,15 @@ SINGLE_BATTLE_TEST("Dynamax: Max Hailstorm sets up hail") TURN { MOVE(player, MOVE_POWDER_SNOW, gimmick: GIMMICK_DYNAMAX); MOVE(opponent, MOVE_CELEBRATE); } } SCENE { MESSAGE("Wobbuffet used Max Hailstorm!"); +#if B_PREFERRED_ICE_WEATHER == B_ICE_WEATHER_SNOW + MESSAGE("It started to snow!"); + MESSAGE("The opposing Wobbuffet used Celebrate!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_SNOW_CONTINUES); +#else MESSAGE("It started to hail!"); MESSAGE("The opposing Wobbuffet used Celebrate!"); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HAIL_CONTINUES); +#endif } } From 02824009d905319a7938ecac871bb8d79f294ad7 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Sat, 20 Sep 2025 15:38:46 +0200 Subject: [PATCH 03/25] Pokemon storage moving items bugfix (#7763) --- src/pokemon_storage_system.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pokemon_storage_system.c b/src/pokemon_storage_system.c index 2a687b2215..baf083a0dc 100644 --- a/src/pokemon_storage_system.c +++ b/src/pokemon_storage_system.c @@ -10081,7 +10081,7 @@ void UpdateSpeciesSpritePSS(struct BoxPokemon *boxMon) DestroyBoxMonIconAtPosition(sCursorPosition); CreateBoxMonIconAtPos(sCursorPosition); if (sStorage->boxOption == OPTION_MOVE_ITEMS) - SetBoxMonIconObjMode(sCursorPosition, (GetBoxMonData(boxMon, MON_DATA_HELD_ITEM) == ITEM_NONE ? ST_OAM_OBJ_NORMAL : ST_OAM_OBJ_BLEND)); + SetBoxMonIconObjMode(sCursorPosition, (GetBoxMonData(boxMon, MON_DATA_HELD_ITEM) == ITEM_NONE ? ST_OAM_OBJ_BLEND : ST_OAM_OBJ_NORMAL)); } } sJustOpenedBag = FALSE; From abc471e9bdd04bd0393dacdd27a3992ca9f1b59c Mon Sep 17 00:00:00 2001 From: bassforte123 <130828119+bassforte123@users.noreply.github.com> Date: Sun, 21 Sep 2025 07:16:53 -0400 Subject: [PATCH 04/25] Fixed Ball Fetch Ability (#7764) --- include/battle.h | 2 +- src/battle_main.c | 2 +- src/battle_script_commands.c | 7 ++ src/battle_util.c | 7 +- test/battle/ability/ball_fetch.c | 119 ++++++++++++++++++++++++++++++- 5 files changed, 129 insertions(+), 8 deletions(-) diff --git a/include/battle.h b/include/battle.h index 107d7cde27..393da86ad1 100644 --- a/include/battle.h +++ b/include/battle.h @@ -1124,7 +1124,7 @@ extern u8 gHealthboxSpriteIds[MAX_BATTLERS_COUNT]; extern u8 gMultiUsePlayerCursor; extern u8 gNumberOfMovesToChoose; extern bool8 gHasFetchedBall; -extern u8 gLastUsedBall; +extern u16 gLastUsedBall; extern u16 gLastThrownBall; extern u16 gBallToDisplay; extern bool8 gLastUsedBallMenuPresent; diff --git a/src/battle_main.c b/src/battle_main.c index dc2114fbd9..f51b063ba6 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -236,7 +236,7 @@ EWRAM_DATA u16 gBattleTurnCounter = 0; EWRAM_DATA u8 gBattlerAbility = 0; EWRAM_DATA struct QueuedStatBoost gQueuedStatBoosts[MAX_BATTLERS_COUNT] = {0}; EWRAM_DATA bool8 gHasFetchedBall = FALSE; -EWRAM_DATA u8 gLastUsedBall = 0; +EWRAM_DATA u16 gLastUsedBall = 0; EWRAM_DATA u16 gLastThrownBall = 0; EWRAM_DATA u16 gBallToDisplay = 0; EWRAM_DATA bool8 gLastUsedBallMenuPresent = FALSE; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index d74ec3f956..56aeb7fee7 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -13950,6 +13950,13 @@ static void Cmd_handleballthrow(void) BtlController_EmitBallThrowAnim(gBattlerAttacker, B_COMM_TO_CONTROLLER, shakes); MarkBattlerForControllerExec(gBattlerAttacker); + #if TESTING + if (gTestRunnerEnabled) + { + shakes = 0; // Force failure for tests. TODO: make capture RNG flag + } + #endif + if (shakes == maxShakes) // mon caught, copy of the code above { if (IsCriticalCapture()) diff --git a/src/battle_util.c b/src/battle_util.c index d5d848b1bf..f5f40d4496 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -4398,14 +4398,15 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 break; case ABILITY_BALL_FETCH: if (gBattleMons[battler].item == ITEM_NONE - && gBattleResults.catchAttempts[gLastUsedBall - ITEM_ULTRA_BALL] >= 1 + && gBattleResults.catchAttempts[ItemIdToBallId(gLastUsedBall)] >= 1 && !gHasFetchedBall) { + gLastUsedItem = gLastUsedBall; gBattleScripting.battler = battler; - BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_HELDITEM_BATTLE, 0, 2, &gLastUsedBall); + gBattleMons[battler].item = gLastUsedItem; + BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, REQUEST_HELDITEM_BATTLE, 0, 2, &gLastUsedItem); MarkBattlerForControllerExec(battler); gHasFetchedBall = TRUE; - gLastUsedItem = gLastUsedBall; BattleScriptPushCursorAndCallback(BattleScript_BallFetch); effect++; } diff --git a/test/battle/ability/ball_fetch.c b/test/battle/ability/ball_fetch.c index 7411d60a7d..040aa7cb08 100644 --- a/test/battle/ability/ball_fetch.c +++ b/test/battle/ability/ball_fetch.c @@ -1,7 +1,120 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Ball Fetch causes the Pokémon to pick up the last failed Ball at the end of the turn"); -TO_DO_BATTLE_TEST("Ball Fetch doesn't trigger if the Pokémon is already holding an item"); -TO_DO_BATTLE_TEST("Ball Fetch only picks up the first failed ball, once per battle"); // Bestow can help test this +WILD_BATTLE_TEST("Ball Fetch causes the Pokémon to pick up the last failed Ball at the end of the turn") +{ + u32 item = 0; + + PARAMETRIZE { item = ITEM_POKE_BALL; } + PARAMETRIZE { item = ITEM_GREAT_BALL; } + PARAMETRIZE { item = ITEM_ULTRA_BALL; } + PARAMETRIZE { item = ITEM_STRANGE_BALL; } + PARAMETRIZE { item = ITEM_X_ACCURACY; } + + GIVEN { + PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } + OPPONENT(SPECIES_METAGROSS); + } WHEN { + TURN { USE_ITEM(player, item); } + } SCENE { + if (item != ITEM_X_ACCURACY) + ABILITY_POPUP(player, ABILITY_BALL_FETCH); + else + NOT ABILITY_POPUP(player, ABILITY_BALL_FETCH); + } THEN { + if (item != ITEM_X_ACCURACY) + EXPECT_EQ(player->item, item); + else + EXPECT_EQ(player->item, ITEM_NONE); + } +} + +WILD_BATTLE_TEST("Ball Fetch doesn't trigger if the Pokémon is already holding an item") +{ + u32 item = 0; + + PARAMETRIZE { item = ITEM_NONE; } + PARAMETRIZE { item = ITEM_NUGGET; } + + GIVEN { + PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); Item(item); } + OPPONENT(SPECIES_METAGROSS); + } WHEN { + TURN { USE_ITEM(player, ITEM_GREAT_BALL); } + } SCENE { + if (item == ITEM_NONE) + { + MESSAGE("You used Great Ball!"); + ABILITY_POPUP(player, ABILITY_BALL_FETCH); + MESSAGE("Yamper found a Great Ball!"); + } + else + { + NONE_OF + { + ABILITY_POPUP(player, ABILITY_BALL_FETCH); + MESSAGE("Yamper found a Great Ball!"); + } + } + } THEN { + if (item == ITEM_NONE) + EXPECT_EQ(player->item, ITEM_GREAT_BALL); + else + EXPECT_EQ(player->item, item); + } +} + +WILD_BATTLE_TEST("Ball Fetch only picks up the first failed ball, once per battle") +{ + u32 item = 0; + u32 item2 = 0; + + PARAMETRIZE { item = ITEM_GREAT_BALL; item2 = ITEM_X_ACCURACY; } + PARAMETRIZE { item = ITEM_GREAT_BALL; item2 = ITEM_ULTRA_BALL; } + PARAMETRIZE { item = ITEM_GREAT_BALL; item2 = ITEM_FAST_BALL; } + PARAMETRIZE { item = ITEM_GREAT_BALL; item2 = ITEM_STRANGE_BALL; } + + + GIVEN { + PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } + OPPONENT(SPECIES_METAGROSS); + } WHEN { + TURN { USE_ITEM(player, item); } + TURN { MOVE(player, MOVE_BESTOW); } + TURN { USE_ITEM(player, item2); } + } SCENE { + MESSAGE("You used Great Ball!"); + ABILITY_POPUP(player, ABILITY_BALL_FETCH); + MESSAGE("Yamper found a Great Ball!"); + MESSAGE("Yamper used Bestow!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BESTOW, player); + MESSAGE("The wild Metagross received Great Ball from Yamper!"); + NOT ABILITY_POPUP(player, ABILITY_BALL_FETCH); + } THEN { + EXPECT_EQ(player->item, ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Ball Fetch doesn't trigger in Trainer Battles") +{ + u32 item = 0; + + PARAMETRIZE { item = ITEM_POKE_BALL; } + PARAMETRIZE { item = ITEM_GREAT_BALL; } + PARAMETRIZE { item = ITEM_ULTRA_BALL; } + PARAMETRIZE { item = ITEM_STRANGE_BALL; } + PARAMETRIZE { item = ITEM_X_ACCURACY; } + + GIVEN { + PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } + OPPONENT(SPECIES_METAGROSS); + } WHEN { + TURN { USE_ITEM(player, item); } + } SCENE { + NOT ABILITY_POPUP(player, ABILITY_BALL_FETCH); + } THEN { + EXPECT_EQ(player->item, ITEM_NONE); + } +} + TO_DO_BATTLE_TEST("Ball Fetch doesn't trigger in Max Raid Battles"); From 2c16f6ba55e5e2782d4a7f3f87ebafc838b1363f Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:57:11 +0200 Subject: [PATCH 05/25] Fixes Flower Shield affecting semi-invulnerable mons (#7766) --- data/battle_scripts_1.s | 8 ++++-- test/battle/move_effect/flower_shield.c | 36 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 2afd6f26a2..b0b0c61fcd 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -1475,14 +1475,17 @@ BattleScript_EffectFlowerShield:: ppreduce savetarget selectfirstvalidtarget -BattleScript_FlowerShieldIsAnyGrass: +BattleScript_FlowerShieldIsAnyValidTarget: + jumpifvolatile BS_TARGET, VOLATILE_SEMI_INVULNERABLE, BattleScript_FlowerShieldCheckNextTarget jumpiftype BS_TARGET, TYPE_GRASS, BattleScript_FlowerShieldLoopStart - jumpifnexttargetvalid BattleScript_FlowerShieldIsAnyGrass +BattleScript_FlowerShieldCheckNextTarget: + jumpifnexttargetvalid BattleScript_FlowerShieldIsAnyValidTarget goto BattleScript_RestoreTargetButItFailed BattleScript_FlowerShieldLoopStart: selectfirstvalidtarget BattleScript_FlowerShieldLoop: movevaluescleanup + jumpifvolatile BS_TARGET, VOLATILE_SEMI_INVULNERABLE, BattleScript_FlowerShieldMoveTargetEnd jumpiftype BS_TARGET, TYPE_GRASS, BattleScript_FlowerShieldLoop2 goto BattleScript_FlowerShieldMoveTargetEnd BattleScript_FlowerShieldLoop2: @@ -1503,6 +1506,7 @@ BattleScript_FlowerShieldMoveTargetEnd: moveendto MOVEEND_NEXT_TARGET jumpifnexttargetvalid BattleScript_FlowerShieldLoop restoretarget + moveendfrom MOVEEND_ITEM_EFFECTS_ATTACKER end BattleScript_EffectRototiller:: diff --git a/test/battle/move_effect/flower_shield.c b/test/battle/move_effect/flower_shield.c index 9eac08eb4d..d690869fa3 100644 --- a/test/battle/move_effect/flower_shield.c +++ b/test/battle/move_effect/flower_shield.c @@ -36,4 +36,38 @@ DOUBLE_BATTLE_TEST("Flower Shield raises the defense of all Grass-type Pokémon" } } -TO_DO_BATTLE_TEST("Flower Shield fails if there's no Grass-type Pokémon on the field") +SINGLE_BATTLE_TEST("Flower Shield fails if there's no Grass-type Pokémon on the field") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_FLOWER_SHIELD); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_FLOWER_SHIELD, player); + } +} + +DOUBLE_BATTLE_TEST("Flower Shield doesn't affect Grass-type Pokémon that are in a semi-invulnerable position") +{ + GIVEN { + PLAYER(SPECIES_BULBASAUR); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_BULBASAUR); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { + MOVE(opponentLeft, MOVE_FLY, target: playerLeft); + MOVE(playerLeft, MOVE_FLOWER_SHIELD); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLY, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLOWER_SHIELD, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + NONE_OF { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + } + } +} From dae8aea26c1df5888ae43baea78add04e7ce9052 Mon Sep 17 00:00:00 2001 From: grintoul <166724814+grintoul1@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:42:33 +0100 Subject: [PATCH 06/25] Initial Lash Out tests (#7769) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- test/battle/move_effect/lash_out.c | 186 ++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/test/battle/move_effect/lash_out.c b/test/battle/move_effect/lash_out.c index dd8e9eb0e9..4ceb0c02a8 100644 --- a/test/battle/move_effect/lash_out.c +++ b/test/battle/move_effect/lash_out.c @@ -1,4 +1,188 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("TODO: Write Lash Out (Move Effect) test titles") +SINGLE_BATTLE_TEST("Lash Out damage is boosted if the user's stats are dropped the turn it is used") +{ + s16 damage[2]; + u32 move = MOVE_NONE; + + PARAMETRIZE { move = MOVE_TACKLE; } + PARAMETRIZE { move = MOVE_GROWL; } + PARAMETRIZE { move = MOVE_LEER; } + PARAMETRIZE { move = MOVE_STRING_SHOT; } + PARAMETRIZE { move = MOVE_CONFIDE; } + PARAMETRIZE { move = MOVE_SAND_ATTACK; } + PARAMETRIZE { move = MOVE_SWEET_SCENT; } + PARAMETRIZE { move = MOVE_CHARM; } + PARAMETRIZE { move = MOVE_SCREECH; } + PARAMETRIZE { move = MOVE_SCARY_FACE; } + PARAMETRIZE { move = MOVE_CAPTIVATE; } + PARAMETRIZE { move = MOVE_EERIE_IMPULSE; } + PARAMETRIZE { move = MOVE_FAKE_TEARS; } + PARAMETRIZE { move = MOVE_NOBLE_ROAR; } + PARAMETRIZE { move = MOVE_LUNGE; } + PARAMETRIZE { move = MOVE_FIRE_LASH; } + PARAMETRIZE { move = MOVE_BULLDOZE; } + PARAMETRIZE { move = MOVE_MYSTICAL_FIRE; } + PARAMETRIZE { move = MOVE_BUG_BUZZ; } + + GIVEN { + PLAYER(SPECIES_GRIMMSNARL) { Gender(MON_MALE); Speed(1); Moves(MOVE_LASH_OUT); } + OPPONENT(SPECIES_GOLEM) { Gender(MON_FEMALE); Speed(2); Moves(move, MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(player, MOVE_LASH_OUT); MOVE(opponent, MOVE_CELEBRATE); } + TURN { MOVE(player, MOVE_LASH_OUT); MOVE(opponent, move); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[0]); + + ANIMATION(ANIM_TYPE_MOVE, move, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[1]); + } THEN { + if (move == MOVE_TACKLE || move == MOVE_CHARM) + EXPECT_EQ(damage[0], damage[1]); + else if (move == MOVE_GROWL || move == MOVE_LUNGE || move == MOVE_NOBLE_ROAR) + EXPECT_MUL_EQ(damage[0], UQ_4_12(1.33), damage[1]); + else + EXPECT_MUL_EQ(damage[0], UQ_4_12(2.00), damage[1]); + } +} + +SINGLE_BATTLE_TEST("Lash Out damage is only boosted on the turn that Intimidate switches in") +{ + s16 damage[3] = {0}; + u32 move = MOVE_NONE; + + PARAMETRIZE { move = MOVE_LASH_OUT; } + PARAMETRIZE { move = MOVE_SPLASH; } + + GIVEN { + PLAYER(SPECIES_GRIMMSNARL) { Moves(move, MOVE_CELEBRATE, MOVE_LASH_OUT); } + OPPONENT(SPECIES_INCINEROAR) { Ability(ABILITY_BLAZE); Moves(MOVE_CELEBRATE); } + OPPONENT(SPECIES_INCINEROAR) { Ability(ABILITY_INTIMIDATE); Moves(MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(player, MOVE_LASH_OUT); MOVE(opponent, MOVE_CELEBRATE); } + TURN { MOVE(player, move); SWITCH(opponent, 1); } + TURN { MOVE(player, MOVE_LASH_OUT); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[0]); + + ANIMATION(ANIM_TYPE_MOVE, move, player); + if (move == MOVE_LASH_OUT) + HP_BAR(opponent, captureDamage: &damage[1]); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[2]); + } THEN { + EXPECT_EQ(damage[0], damage[2]); + EXPECT_MUL_EQ(damage[0], (move == MOVE_LASH_OUT ? UQ_4_12(1.33) : UQ_4_12(0.00)), damage[1]); + } +} + +SINGLE_BATTLE_TEST("Lash Out damage is boosted on turn 1 by switch in abilities") +{ + s16 damage[2] = {0}; + u32 species = SPECIES_NONE, ability = ABILITY_NONE; + + PARAMETRIZE { species = SPECIES_INCINEROAR, ability = ABILITY_BLAZE; } + PARAMETRIZE { species = SPECIES_INCINEROAR, ability = ABILITY_INTIMIDATE; } + PARAMETRIZE { species = SPECIES_HYDRAPPLE, ability = ABILITY_REGENERATOR; } + PARAMETRIZE { species = SPECIES_HYDRAPPLE, ability = ABILITY_SUPERSWEET_SYRUP; } + + GIVEN { + PLAYER(SPECIES_GRIMMSNARL) { Moves(MOVE_LASH_OUT); } + OPPONENT(species) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_LASH_OUT); } + TURN { MOVE(player, MOVE_LASH_OUT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[0]); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, player); + HP_BAR(opponent, captureDamage: &damage[1]); + + } THEN { + if (ability == ABILITY_INTIMIDATE) + EXPECT_MUL_EQ(damage[0], UQ_4_12(1.33) , damage[1]); + else if (ability == ABILITY_SUPERSWEET_SYRUP) + EXPECT_MUL_EQ(damage[0], UQ_4_12(2.00) , damage[1]); + else + EXPECT_EQ(damage[0], damage[1]); + } +} + +DOUBLE_BATTLE_TEST("Lash Out damage is boosted by Cotton Down activation in doubles") +{ + s16 damage[2] = {0}; + u32 ability = ABILITY_NONE; + + PARAMETRIZE { ability = ABILITY_REGENERATOR; } + PARAMETRIZE { ability = ABILITY_COTTON_DOWN; } + + GIVEN { + PLAYER(SPECIES_GRIMMSNARL) { Speed(1); Moves(MOVE_LASH_OUT); } + PLAYER(SPECIES_RATTATA) { Speed(2); Moves(MOVE_TACKLE, MOVE_CELEBRATE); } + OPPONENT(SPECIES_ELDEGOSS) { Speed(3); Ability(ability); } + OPPONENT(SPECIES_GOLEM) { Speed(4); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_LASH_OUT, target:opponentRight); MOVE(playerRight, MOVE_TACKLE, target:opponentLeft); } + TURN { MOVE(playerLeft, MOVE_LASH_OUT, target:opponentRight); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, playerRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, playerLeft); + HP_BAR(opponentRight, captureDamage: &damage[0]); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, playerLeft); + HP_BAR(opponentRight, captureDamage: &damage[1]); + + } THEN { + if (ability == ABILITY_COTTON_DOWN) + EXPECT_MUL_EQ(damage[0], UQ_4_12(2.00) , damage[1]); + else + EXPECT_EQ(damage[0], damage[1]); + } +} + +DOUBLE_BATTLE_TEST("Lash Out damage is not boosted by Treasure of Ruin ability activation in doubles") +{ + s16 damage[2] = {0}; + u32 species = SPECIES_NONE, ability = ABILITY_NONE; + + PARAMETRIZE { species = SPECIES_KANGASKHAN, ability = ABILITY_INNER_FOCUS; } + PARAMETRIZE { species = SPECIES_HYDRAPPLE, ability = ABILITY_SUPERSWEET_SYRUP; } + PARAMETRIZE { species = SPECIES_WO_CHIEN, ability = ABILITY_TABLETS_OF_RUIN; } + PARAMETRIZE { species = SPECIES_CHIEN_PAO, ability = ABILITY_SWORD_OF_RUIN; } + PARAMETRIZE { species = SPECIES_TING_LU, ability = ABILITY_VESSEL_OF_RUIN; } + PARAMETRIZE { species = SPECIES_CHI_YU, ability = ABILITY_BEADS_OF_RUIN; } + + GIVEN { + PLAYER(SPECIES_GRIMMSNARL) { Moves(MOVE_LASH_OUT); } + PLAYER(SPECIES_RATTATA); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GOLEM); + OPPONENT(species) { Ability(ability); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_LASH_OUT, target:opponentRight); } + TURN { MOVE(playerLeft, MOVE_LASH_OUT, target:opponentRight); SWITCH(opponentLeft, 2); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, playerLeft); + HP_BAR(opponentRight, captureDamage: &damage[0]); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASH_OUT, playerLeft); + HP_BAR(opponentRight, captureDamage: &damage[1]); + + } THEN { + if (ability == ABILITY_SUPERSWEET_SYRUP) + EXPECT_MUL_EQ(damage[0], UQ_4_12(2.00) , damage[1]); + else if (ability == ABILITY_TABLETS_OF_RUIN) + EXPECT_MUL_EQ(damage[0], UQ_4_12(0.75) , damage[1]); + else if (ability == ABILITY_SWORD_OF_RUIN) + EXPECT_MUL_EQ(damage[0], UQ_4_12(1.33) , damage[1]); + else + EXPECT_EQ(damage[0], damage[1]); + } +} From 5fad32deccffbaaa42a395d0834e2972ce40e80f Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:00:34 +0100 Subject: [PATCH 07/25] Fixes Helping Hand boosts not stacking with each other (#7775) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- include/battle.h | 6 +- src/battle_main.c | 2 +- src/battle_script_commands.c | 5 +- src/battle_util.c | 3 +- test/battle/move_effect/helping_hand.c | 121 +++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 8 deletions(-) diff --git a/include/battle.h b/include/battle.h index 393da86ad1..13316eada9 100644 --- a/include/battle.h +++ b/include/battle.h @@ -141,7 +141,6 @@ struct ProtectStruct { u32 protected:7; // 126 protect options u32 noValidMoves:1; - u32 helpingHand:1; u32 bounceMove:1; u32 stealMove:1; u32 nonVolatileStatusImmobility:1; @@ -163,13 +162,14 @@ struct ProtectStruct u32 shellTrap:1; u32 eatMirrorHerb:1; u32 activateOpportunist:2; // 2 - to copy stats. 1 - stats copied (do not repeat). 0 - no stats to copy - // End of 32-bit bitfield u16 usedAllySwitch:1; + // End of 32-bit bitfield + u32 helpingHand:3; u16 lashOutAffected:1; u16 assuranceDoubled:1; u16 myceliumMight:1; u16 laggingTail:1; - u16 padding:11; + u16 padding:9; // End of 16-bit bitfield u16 physicalDmg; u16 specialDmg; diff --git a/src/battle_main.c b/src/battle_main.c index f51b063ba6..c39ea52905 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3325,7 +3325,7 @@ const u8* FaintClearSetData(u32 battler) gProtectStructs[battler].quash = FALSE; gProtectStructs[battler].noValidMoves = FALSE; - gProtectStructs[battler].helpingHand = FALSE; + gProtectStructs[battler].helpingHand = 0; gProtectStructs[battler].bounceMove = FALSE; gProtectStructs[battler].stealMove = FALSE; gProtectStructs[battler].nonVolatileStatusImmobility = FALSE; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 56aeb7fee7..03c9d028fd 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -12760,10 +12760,9 @@ static void Cmd_trysethelpinghand(void) if (IsDoubleBattle() && !(gAbsentBattlerFlags & (1u << gBattlerTarget)) - && !gProtectStructs[gBattlerAttacker].helpingHand - && !gProtectStructs[gBattlerTarget].helpingHand) + && gCurrentTurnActionNumber < GetBattlerTurnOrderNum(gBattlerTarget)) { - gProtectStructs[gBattlerTarget].helpingHand = TRUE; + gProtectStructs[gBattlerTarget].helpingHand++; gBattlescriptCurrInstr = cmd->nextInstr; } else diff --git a/src/battle_util.c b/src/battle_util.c index f5f40d4496..6d1f4efa4f 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -8329,8 +8329,9 @@ static inline u32 CalcMoveBasePowerAfterModifiers(struct DamageContext *ctx) } // various effects - if (gProtectStructs[battlerAtk].helpingHand) + for (u32 i = 0; i < gProtectStructs[battlerAtk].helpingHand; i++) modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + if (gSpecialStatuses[battlerAtk].gemBoost) modifier = uq4_12_multiply(modifier, uq4_12_add(UQ_4_12(1.0), PercentToUQ4_12(gSpecialStatuses[battlerAtk].gemParam))); if (gBattleMons[battlerAtk].volatiles.charge && moveType == TYPE_ELECTRIC) diff --git a/test/battle/move_effect/helping_hand.c b/test/battle/move_effect/helping_hand.c index 81d64e113d..6d4d6e0eb5 100644 --- a/test/battle/move_effect/helping_hand.c +++ b/test/battle/move_effect/helping_hand.c @@ -1,4 +1,125 @@ #include "global.h" #include "test/battle.h" +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_HELPING_HAND) == EFFECT_HELPING_HAND); +} + +SINGLE_BATTLE_TEST("Helping Hand fails in a Single Battle") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_HELPING_HAND); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_HELPING_HAND, player); + MESSAGE("But it failed!"); + } +} + +DOUBLE_BATTLE_TEST("Helping Hand fails if ally already acted") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_HELPING_HAND, target: playerRight); MOVE(playerRight, MOVE_HELPING_HAND, target: playerLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HELPING_HAND, playerLeft); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_HELPING_HAND, playerRight); + } +} + +DOUBLE_BATTLE_TEST("Helping Hand boosts the power of attacking moves by 50%", s16 damage) +{ + bool32 useHelpingHand; + + PARAMETRIZE { useHelpingHand = FALSE; } + PARAMETRIZE { useHelpingHand = TRUE; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (useHelpingHand) + TURN { MOVE(playerRight, MOVE_HELPING_HAND, target: playerLeft); MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); } + else + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + HP_BAR(opponentLeft, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} + +DOUBLE_BATTLE_TEST("Helping Hand still boosts moves used due to Instruct", s16 damage) +{ + bool32 useHelpingHand; + + PARAMETRIZE { useHelpingHand = FALSE; } + PARAMETRIZE { useHelpingHand = TRUE; } + + GIVEN { + ASSUME(GetMoveEffect(MOVE_INSTRUCT) == EFFECT_INSTRUCT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (useHelpingHand) + { + TURN { MOVE(playerRight, MOVE_HELPING_HAND, target: playerLeft); + MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); + MOVE(opponentLeft, MOVE_INSTRUCT, target: playerLeft); } + } + else + { + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); + MOVE(opponentLeft, MOVE_INSTRUCT, target: playerLeft); } + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + HP_BAR(opponentLeft, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} + +DOUBLE_BATTLE_TEST("Helping Hand boosts the power of attacking moves by 125% if Instructed into using it again", s16 damage) +{ + bool32 useHelpingHandTwice; + + PARAMETRIZE { useHelpingHandTwice = FALSE; } + PARAMETRIZE { useHelpingHandTwice = TRUE; } + + GIVEN { + ASSUME(GetMoveEffect(MOVE_INSTRUCT) == EFFECT_INSTRUCT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (useHelpingHandTwice) + TURN { MOVE(playerRight, MOVE_HELPING_HAND, target: playerLeft); + MOVE(opponentLeft, MOVE_INSTRUCT, target: playerRight); + MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); } + else + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + HP_BAR(opponentLeft, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.25), results[1].damage); + } +} + TO_DO_BATTLE_TEST("TODO: Write Helping Hand (Move Effect) test titles") From 1d11a2cb0a2bebb9368d6e168a982b9dae333643 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Mon, 22 Sep 2025 20:44:12 +0200 Subject: [PATCH 08/25] Improve how test involving ball throw work (#7774) --- include/random.h | 2 ++ include/test/battle.h | 2 ++ src/battle_script_commands.c | 16 ++++++---------- test/battle/ability/ball_fetch.c | 11 ++++++----- test/test_runner_battle.c | 3 +++ 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/include/random.h b/include/random.h index de0f406fe5..df3e1579ad 100644 --- a/include/random.h +++ b/include/random.h @@ -215,6 +215,8 @@ enum RandomTag RNG_AI_REFRESH_TRICK_ROOM_ON_LAST_TURN, RNG_AI_APPLY_TAILWIND_ON_LAST_TURN_OF_TRICK_ROOM, RNG_WRAP, + RNG_BALLTHROW_CRITICAL, + RNG_BALLTHROW_SHAKE, }; #define RandomWeighted(tag, ...) \ diff --git a/include/test/battle.h b/include/test/battle.h index d909aa0332..9cd1d23b1f 100644 --- a/include/test/battle.h +++ b/include/test/battle.h @@ -1005,6 +1005,8 @@ struct ItemContext u16 explicitPartyIndex:1; u16 move; u16 explicitMove:1; + struct TurnRNG rng; + u16 explicitRNG:1; }; void OpenTurn(u32 sourceLine); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 03c9d028fd..5ad5a1c642 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -13943,19 +13943,16 @@ static void Cmd_handleballthrow(void) { odds = Sqrt(Sqrt(16711680 / odds)); odds = 1048560 / odds; - for (shakes = 0; shakes < maxShakes && Random() < odds; shakes++); + for (shakes = 0; shakes < maxShakes; shakes++) + { + if (RandomUniform(RNG_BALLTHROW_SHAKE, 0, MAX_u16) < odds) + break; + } } BtlController_EmitBallThrowAnim(gBattlerAttacker, B_COMM_TO_CONTROLLER, shakes); MarkBattlerForControllerExec(gBattlerAttacker); - #if TESTING - if (gTestRunnerEnabled) - { - shakes = 0; // Force failure for tests. TODO: make capture RNG flag - } - #endif - if (shakes == maxShakes) // mon caught, copy of the code above { if (IsCriticalCapture()) @@ -14752,8 +14749,7 @@ static bool32 CriticalCapture(u32 odds) odds = (odds * (100 + B_CATCHING_CHARM_BOOST)) / 100; odds /= 6; - - if ((Random() % 255) < odds) + if (RandomUniform(RNG_BALLTHROW_CRITICAL, 0, MAX_u8) < odds) return TRUE; return FALSE; diff --git a/test/battle/ability/ball_fetch.c b/test/battle/ability/ball_fetch.c index 040aa7cb08..a7513a701c 100644 --- a/test/battle/ability/ball_fetch.c +++ b/test/battle/ability/ball_fetch.c @@ -15,7 +15,8 @@ WILD_BATTLE_TEST("Ball Fetch causes the Pokémon to pick up the last failed Ball PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, item); } + TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, 0) );} + TURN {} } SCENE { if (item != ITEM_X_ACCURACY) ABILITY_POPUP(player, ABILITY_BALL_FETCH); @@ -40,7 +41,7 @@ WILD_BATTLE_TEST("Ball Fetch doesn't trigger if the Pokémon is already holding PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); Item(item); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, ITEM_GREAT_BALL); } + TURN { USE_ITEM(player, ITEM_GREAT_BALL, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } } SCENE { if (item == ITEM_NONE) { @@ -79,9 +80,9 @@ WILD_BATTLE_TEST("Ball Fetch only picks up the first failed ball, once per battl PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, item); } + TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } TURN { MOVE(player, MOVE_BESTOW); } - TURN { USE_ITEM(player, item2); } + TURN { USE_ITEM(player, item2, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } } SCENE { MESSAGE("You used Great Ball!"); ABILITY_POPUP(player, ABILITY_BALL_FETCH); @@ -109,7 +110,7 @@ SINGLE_BATTLE_TEST("Ball Fetch doesn't trigger in Trainer Battles") PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, item); } + TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } } SCENE { NOT ABILITY_POPUP(player, ABILITY_BALL_FETCH); } THEN { diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index c71e74bb48..2f9dd037fb 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -2520,6 +2520,9 @@ void UseItem(u32 sourceLine, struct BattlePokemon *battler, struct ItemContext c { i = 0; } + + if (ctx.explicitRNG) + DATA.battleRecordTurns[DATA.turns][battlerId].rng = ctx.rng; PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_USE_ITEM); PushBattlerAction(sourceLine, battlerId, RECORDED_ITEM_ID, (ctx.itemId >> 8) & 0xFF); PushBattlerAction(sourceLine, battlerId, RECORDED_ITEM_ID, ctx.itemId & 0xFF); From 4963c4317c6c4b15fd3074751c5dfc22b3f7ffdf Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Tue, 23 Sep 2025 14:30:07 +0200 Subject: [PATCH 09/25] Fix TRAINER_TYPE_SEE_ALL_DIRECTIONS (#7779) --- src/trainer_see.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trainer_see.c b/src/trainer_see.c index cefb22eef4..71237b4e29 100644 --- a/src/trainer_see.c +++ b/src/trainer_see.c @@ -373,7 +373,7 @@ bool8 CheckForTrainersWantingBattle(void) if (!gObjectEvents[i].active) continue; - if (gObjectEvents[i].trainerType != TRAINER_TYPE_NORMAL && gObjectEvents[i].trainerType != TRAINER_TYPE_BURIED) + if (gObjectEvents[i].trainerType != TRAINER_TYPE_NORMAL && gObjectEvents[i].trainerType != TRAINER_TYPE_SEE_ALL_DIRECTIONS && gObjectEvents[i].trainerType != TRAINER_TYPE_BURIED) continue; numTrainers = CheckTrainer(i); From 36fae275994d89d83c11987c509479811b67e475 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Tue, 23 Sep 2025 17:50:48 +0200 Subject: [PATCH 10/25] Fix catch bug introduced in #7774 (#7782) --- src/battle_script_commands.c | 2 +- test/battle/ability/ball_fetch.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 5ad5a1c642..84d58d6f64 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -13945,7 +13945,7 @@ static void Cmd_handleballthrow(void) odds = 1048560 / odds; for (shakes = 0; shakes < maxShakes; shakes++) { - if (RandomUniform(RNG_BALLTHROW_SHAKE, 0, MAX_u16) < odds) + if (RandomUniform(RNG_BALLTHROW_SHAKE, 0, MAX_u16) >= odds) break; } } diff --git a/test/battle/ability/ball_fetch.c b/test/battle/ability/ball_fetch.c index a7513a701c..762225c0f0 100644 --- a/test/battle/ability/ball_fetch.c +++ b/test/battle/ability/ball_fetch.c @@ -15,7 +15,7 @@ WILD_BATTLE_TEST("Ball Fetch causes the Pokémon to pick up the last failed Ball PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, 0) );} + TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, MAX_u16) );} TURN {} } SCENE { if (item != ITEM_X_ACCURACY) @@ -41,7 +41,7 @@ WILD_BATTLE_TEST("Ball Fetch doesn't trigger if the Pokémon is already holding PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); Item(item); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, ITEM_GREAT_BALL, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } + TURN { USE_ITEM(player, ITEM_GREAT_BALL, WITH_RNG(RNG_BALLTHROW_SHAKE, MAX_u16)); } } SCENE { if (item == ITEM_NONE) { @@ -80,9 +80,9 @@ WILD_BATTLE_TEST("Ball Fetch only picks up the first failed ball, once per battl PLAYER(SPECIES_YAMPER) { Ability(ABILITY_BALL_FETCH); } OPPONENT(SPECIES_METAGROSS); } WHEN { - TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } + TURN { USE_ITEM(player, item, WITH_RNG(RNG_BALLTHROW_SHAKE, MAX_u16)); } TURN { MOVE(player, MOVE_BESTOW); } - TURN { USE_ITEM(player, item2, WITH_RNG(RNG_BALLTHROW_SHAKE, 0)); } + TURN { USE_ITEM(player, item2, WITH_RNG(RNG_BALLTHROW_SHAKE, MAX_u16)); } } SCENE { MESSAGE("You used Great Ball!"); ABILITY_POPUP(player, ABILITY_BALL_FETCH); From b244740f52d526f8b4f376d22e58848cb8be72a3 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:34:08 +0200 Subject: [PATCH 11/25] Fixes OHKO moves calculating accuracy twice (#7785) --- data/battle_scripts_1.s | 1 - src/battle_script_commands.c | 35 +++++++++++++++++++++------------ test/battle/move_effect/ohko.c | 36 +++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index b0b0c61fcd..7c33bfef0f 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -3219,7 +3219,6 @@ BattleScript_EffectOHKO:: attackcanceler attackstring ppreduce - accuracycheck BattleScript_ButItFailed, ACC_CURR_MOVE typecalc jumpifmovehadnoeffect BattleScript_HitFromAtkAnimation tryKO BattleScript_KOFail diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 84d58d6f64..37f25637d4 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -10857,6 +10857,11 @@ static void Cmd_setlightscreen(void) gBattlescriptCurrInstr = cmd->nextInstr; } +// for var lands +#define NO_HIT 0 +#define CALC_ACC 1 +#define SURE_HIT 2 +// for var endured #define NOT_ENDURED 0 #define FOCUS_SASHED 1 #define FOCUS_BANDED 2 @@ -10865,7 +10870,6 @@ static void Cmd_tryKO(void) { CMD_ARGS(const u8 *failInstr); - bool32 lands = FALSE; enum BattleMoveEffects effect = GetMoveEffect(gCurrentMove); enum ItemHoldEffect holdEffect = GetBattlerHoldEffect(gBattlerTarget, TRUE); u16 targetAbility = GetBattlerAbility(gBattlerTarget); @@ -10911,24 +10915,28 @@ static void Cmd_tryKO(void) } else { - if (gBattleMons[gBattlerAttacker].level >= gBattleMons[gBattlerTarget].level - && ((gBattleMons[gBattlerTarget].volatiles.lockOn - && gDisableStructs[gBattlerTarget].battlerWithSureHit == gBattlerAttacker) - || IsAbilityAndRecord(gBattlerAttacker, GetBattlerAbility(gBattlerAttacker), ABILITY_NO_GUARD) - || IsAbilityAndRecord(gBattlerTarget, targetAbility, ABILITY_NO_GUARD))) - { - lands = TRUE; - } - else + u32 lands = NO_HIT; + if (gBattleMons[gBattlerTarget].level > gBattleMons[gBattlerAttacker].level) + lands = NO_HIT; + else if ((gBattleMons[gBattlerTarget].volatiles.lockOn && gDisableStructs[gBattlerTarget].battlerWithSureHit == gBattlerAttacker) + || IsAbilityAndRecord(gBattlerAttacker, GetBattlerAbility(gBattlerAttacker), ABILITY_NO_GUARD) + || IsAbilityAndRecord(gBattlerTarget, targetAbility, ABILITY_NO_GUARD)) + lands = SURE_HIT; + else if (IsSemiInvulnerable(gBattlerTarget, CHECK_ALL)) + lands = NO_HIT; + else if (!JumpIfMoveAffectedByProtect(gCurrentMove, gBattlerTarget, TRUE, cmd->failInstr)) + lands = CALC_ACC; + + if (lands == CALC_ACC) { u16 odds = GetMoveAccuracy(gCurrentMove) + (gBattleMons[gBattlerAttacker].level - gBattleMons[gBattlerTarget].level); if (B_SHEER_COLD_ACC >= GEN_7 && effect == EFFECT_SHEER_COLD && !IS_BATTLER_OF_TYPE(gBattlerAttacker, TYPE_ICE)) odds -= 10; if (RandomPercentage(RNG_ACCURACY, odds) && gBattleMons[gBattlerAttacker].level >= gBattleMons[gBattlerTarget].level) - lands = TRUE; + lands = SURE_HIT; } - if (lands) + if (lands == SURE_HIT) { if (gDisableStructs[gBattlerTarget].endured) { @@ -10964,6 +10972,9 @@ static void Cmd_tryKO(void) } } } +#undef NO_HIT +#undef CALC_ACC +#undef SURE_HIT #undef NOT_ENDURED #undef FOCUS_SASHED #undef FOCUS_BANDED diff --git a/test/battle/move_effect/ohko.c b/test/battle/move_effect/ohko.c index 580219e3db..81847a1ba5 100644 --- a/test/battle/move_effect/ohko.c +++ b/test/battle/move_effect/ohko.c @@ -21,6 +21,18 @@ SINGLE_BATTLE_TEST("OHKO moves can hit semi-invulnerable mons when the user has } } +SINGLE_BATTLE_TEST("OHKO moves can not hit semi-invulnerable") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_FLY); MOVE(player, MOVE_FISSURE); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_FISSURE, player); + } +} + SINGLE_BATTLE_TEST("OHKO moves can can be endured by Focus Sash") { GIVEN { @@ -48,7 +60,29 @@ SINGLE_BATTLE_TEST("OHKO moves can can be endured by Sturdy") } } +SINGLE_BATTLE_TEST("OHKO moves always fails if the target has a higher level than the user") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(1); } + OPPONENT(SPECIES_WOBBUFFET) { Level(2); } + } WHEN { + TURN { MOVE(player, MOVE_FISSURE); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_FISSURE, player); + } +} + +SINGLE_BATTLE_TEST("OHKO moves fail if target protects") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_FISSURE); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_FISSURE, player); + } +} TO_DO_BATTLE_TEST("OHKO moves faints the target, skipping regular damage calculations") -TO_DO_BATTLE_TEST("OHKO moves always fails if the target has a higher level than the user") TO_DO_BATTLE_TEST("OHKO moves's accuracy increases by 1% for every level the user has over the target") TO_DO_BATTLE_TEST("OHKO moves's ignores non-stage accuracy modifiers") // Gravity, Wide Lens, Compound Eyes From 58b9642cb11dddd5356aa471e577aac017167d66 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:11:45 +0100 Subject: [PATCH 12/25] Fixes Instructed moves looking at the wrong turn order number (#7788) --- include/battle_script_commands.h | 3 +- src/battle_script_commands.c | 51 ++++++++++++++++---------- src/battle_util.c | 6 +-- test/battle/move_effect/sucker_punch.c | 19 ++++++++++ test/battle/move_effect/upper_hand.c | 22 +++++++++++ 5 files changed, 78 insertions(+), 23 deletions(-) diff --git a/include/battle_script_commands.h b/include/battle_script_commands.h index df7163840f..4ba801b98c 100644 --- a/include/battle_script_commands.h +++ b/include/battle_script_commands.h @@ -44,7 +44,8 @@ s32 CalcCritChanceStage(u32 battlerAtk, u32 battlerDef, u32 move, bool32 recordA s32 CalcCritChanceStageGen1(u32 battlerAtk, u32 battlerDef, u32 move, bool32 recordAbility, u32 abilityAtk, u32 abilityDef, enum ItemHoldEffect holdEffectAtk); s32 GetCritHitOdds(s32 critChanceIndex); u32 GetTotalAccuracy(u32 battlerAtk, u32 battlerDef, u32 move, u32 atkAbility, u32 defAbility, u32 atkHoldEffect, u32 defHoldEffect); -u8 GetBattlerTurnOrderNum(u8 battler); +bool32 HasBattlerActedThisTurn(u32 battler); +u32 GetBattlerTurnOrderNum(u32 battler); bool32 NoAliveMonsForBattlerSide(u32 battler); bool32 NoAliveMonsForPlayer(void); bool32 NoAliveMonsForEitherParty(void); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 37f25637d4..df620dd3f7 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -2804,9 +2804,20 @@ static void Cmd_printselectionstringfromtable(void) } } -u8 GetBattlerTurnOrderNum(u8 battler) +bool32 HasBattlerActedThisTurn(u32 battler) { - s32 i; + u32 i; + for (i = 0; i < gCurrentTurnActionNumber; i++) + { + if (gBattlerByTurnOrder[i] == battler) + return TRUE; + } + return FALSE; +} + +u32 GetBattlerTurnOrderNum(u32 battler) +{ + u32 i; for (i = 0; i < gBattlersCount; i++) { if (gBattlerByTurnOrder[i] == battler) @@ -3091,8 +3102,8 @@ void SetMoveEffect(u32 battler, u32 effectBattler, bool32 primary, bool32 certai { gBattlescriptCurrInstr++; } - else if (GetBattlerTurnOrderNum(gEffectBattler) > gCurrentTurnActionNumber - && !(GetActiveGimmick(gEffectBattler) == GIMMICK_DYNAMAX)) + else if (!HasBattlerActedThisTurn(gEffectBattler) + && GetActiveGimmick(gEffectBattler) != GIMMICK_DYNAMAX) { gBattleMons[gEffectBattler].volatiles.flinched = TRUE; gBattlescriptCurrInstr++; @@ -3425,7 +3436,7 @@ void SetMoveEffect(u32 battler, u32 effectBattler, bool32 primary, bool32 certai } break; case MOVE_EFFECT_CORE_ENFORCER: - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget) + if (HasBattlerActedThisTurn(gBattlerTarget) && !NoAliveMonsForEitherParty()) { BattleScriptPush(gBattlescriptCurrInstr + 1); @@ -9455,10 +9466,12 @@ static bool32 ChangeOrderTargetAfterAttacker(void) u32 i; u8 data[MAX_BATTLERS_COUNT]; u8 actionsData[MAX_BATTLERS_COUNT]; + u32 attackerTurnOrderNum = GetBattlerTurnOrderNum(gBattlerAttacker); + u32 targetTurnOrderNum = GetBattlerTurnOrderNum(gBattlerTarget); - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + if (attackerTurnOrderNum > targetTurnOrderNum) return FALSE; - if (GetBattlerTurnOrderNum(gBattlerAttacker) + 1 == GetBattlerTurnOrderNum(gBattlerTarget)) + if (attackerTurnOrderNum + 1 == targetTurnOrderNum) return GetGenConfig(GEN_CONFIG_AFTER_YOU_TURN_ORDER) >= GEN_8; for (i = 0; i < MAX_BATTLERS_COUNT; i++) @@ -9466,14 +9479,14 @@ static bool32 ChangeOrderTargetAfterAttacker(void) data[i] = gBattlerByTurnOrder[i]; actionsData[i] = gActionsByTurnOrder[i]; } - if (GetBattlerTurnOrderNum(gBattlerAttacker) == 0 && GetBattlerTurnOrderNum(gBattlerTarget) == 2) + if (attackerTurnOrderNum == 0 && targetTurnOrderNum == 2) { gBattlerByTurnOrder[1] = gBattlerTarget; gActionsByTurnOrder[1] = actionsData[2]; gBattlerByTurnOrder[2] = data[1]; gActionsByTurnOrder[2] = actionsData[1]; } - else if (GetBattlerTurnOrderNum(gBattlerAttacker) == 0 && GetBattlerTurnOrderNum(gBattlerTarget) == 3) + else if (attackerTurnOrderNum == 0 && targetTurnOrderNum == 3) { gBattlerByTurnOrder[1] = gBattlerTarget; gActionsByTurnOrder[1] = actionsData[3]; @@ -9482,7 +9495,7 @@ static bool32 ChangeOrderTargetAfterAttacker(void) gBattlerByTurnOrder[3] = data[2]; gActionsByTurnOrder[3] = actionsData[2]; } - else // Attacker == 1, Target == 3 + else // attackerTurnOrderNum == 1, targetTurnOrderNum == 3 { gBattlerByTurnOrder[2] = gBattlerTarget; gActionsByTurnOrder[2] = actionsData[3]; @@ -11436,7 +11449,7 @@ static void Cmd_trysetencore(void) gDisableStructs[gBattlerTarget].encoredMove = gBattleMons[gBattlerTarget].moves[i]; gDisableStructs[gBattlerTarget].encoredMovePos = i; // Encore always lasts 3 turns, but we need to account for a scenario where Encore changes the move during the same turn. - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + if (HasBattlerActedThisTurn(gBattlerTarget)) gDisableStructs[gBattlerTarget].encoreTimer = 4; else gDisableStructs[gBattlerTarget].encoreTimer = 3; @@ -12742,7 +12755,7 @@ static void Cmd_settaunt(void) if (B_TAUNT_TURNS >= GEN_5) { turns = 4; - if (GetBattlerTurnOrderNum(gBattlerTarget) > GetBattlerTurnOrderNum(gBattlerAttacker)) + if (!HasBattlerActedThisTurn(gBattlerTarget)) turns--; // If the target hasn't yet moved this turn, Taunt lasts for only three turns (source: Bulbapedia) } else if (B_TAUNT_TURNS >= GEN_4) @@ -12771,7 +12784,7 @@ static void Cmd_trysethelpinghand(void) if (IsDoubleBattle() && !(gAbsentBattlerFlags & (1u << gBattlerTarget)) - && gCurrentTurnActionNumber < GetBattlerTurnOrderNum(gBattlerTarget)) + && !HasBattlerActedThisTurn(gBattlerTarget)) { gProtectStructs[gBattlerTarget].helpingHand++; gBattlescriptCurrInstr = cmd->nextInstr; @@ -15401,7 +15414,7 @@ void BS_SetPledge(void) else if ((gChosenActionByBattler[partner] == B_ACTION_USE_MOVE) && IsDoubleBattle() && IsBattlerAlive(partner) - && GetBattlerTurnOrderNum(gBattlerAttacker) < GetBattlerTurnOrderNum(partner) + && !HasBattlerActedThisTurn(partner) && !(gHitMarker & HITMARKER_UNABLE_TO_USE_MOVE) && gCurrentMove != partnerMove && GetMoveEffect(partnerMove) == EFFECT_PLEDGE) @@ -15600,7 +15613,7 @@ void BS_TryUpperHand(void) u32 abilityDef = GetBattlerAbility(gBattlerTarget); u32 prio = GetChosenMovePriority(gBattlerTarget, abilityDef); - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget) + if (HasBattlerActedThisTurn(gBattlerTarget) || gChosenMoveByBattler[gBattlerTarget] == MOVE_NONE || IsBattleMoveStatus(gChosenMoveByBattler[gBattlerTarget]) || prio < 1 @@ -15817,7 +15830,7 @@ void BS_TryQuash(void) u32 i, j; // It's true if foe is faster, has a bigger priority, or switches - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + if (HasBattlerActedThisTurn(gBattlerTarget)) { gBattlescriptCurrInstr = cmd->failInstr; return; @@ -17346,7 +17359,7 @@ void BS_SuckerPunchCheck(void) NATIVE_ARGS(const u8 *failInstr); if (gProtectStructs[gBattlerTarget].protected == PROTECT_OBSTRUCT) gBattlescriptCurrInstr = cmd->failInstr; - else if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + else if (HasBattlerActedThisTurn(gBattlerTarget)) gBattlescriptCurrInstr = cmd->failInstr; else if (IsBattleMoveStatus(gBattleMons[gBattlerTarget].moves[gBattleStruct->chosenMovePositions[gBattlerTarget]]) && !gProtectStructs[gBattlerTarget].noValidMoves) gBattlescriptCurrInstr = cmd->failInstr; @@ -17433,7 +17446,7 @@ void BS_TryMeFirst(void) NATIVE_ARGS(const u8 *failInstr); u16 move = gBattleMons[gBattlerTarget].moves[gBattleStruct->chosenMovePositions[gBattlerTarget]]; if (IsBattleMoveStatus(move) || IsMoveMeFirstBanned(move) - || GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + || HasBattlerActedThisTurn(gBattlerTarget)) { gBattlescriptCurrInstr = cmd->failInstr; } @@ -17457,7 +17470,7 @@ void BS_TryMeFirst(void) void BS_TryElectrify(void) { NATIVE_ARGS(const u8 *failInstr); - if (GetBattlerTurnOrderNum(gBattlerAttacker) > GetBattlerTurnOrderNum(gBattlerTarget)) + if (HasBattlerActedThisTurn(gBattlerTarget)) { gBattlescriptCurrInstr = cmd->failInstr; } diff --git a/src/battle_util.c b/src/battle_util.c index 6d1f4efa4f..e633a20bad 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -8169,12 +8169,12 @@ static inline u32 CalcMoveBasePower(struct DamageContext *ctx) } break; case EFFECT_PAYBACK: - if (GetBattlerTurnOrderNum(battlerAtk) > GetBattlerTurnOrderNum(battlerDef) + if (HasBattlerActedThisTurn(battlerDef) && (B_PAYBACK_SWITCH_BOOST < GEN_5 || gDisableStructs[battlerDef].isFirstTurn != 2)) basePower *= 2; break; case EFFECT_BOLT_BEAK: - if (GetBattlerTurnOrderNum(battlerAtk) < GetBattlerTurnOrderNum(battlerDef) + if (!HasBattlerActedThisTurn(battlerDef) || gDisableStructs[battlerDef].isFirstTurn == 2) basePower *= 2; break; @@ -11818,7 +11818,7 @@ u32 GetTotalAccuracy(u32 battlerAtk, u32 battlerDef, u32 move, u32 atkAbility, u calc = (calc * (100 + atkParam)) / 100; break; case HOLD_EFFECT_ZOOM_LENS: - if (GetBattlerTurnOrderNum(battlerAtk) > GetBattlerTurnOrderNum(battlerDef)) + if (HasBattlerActedThisTurn(battlerDef)) calc = (calc * (100 + atkParam)) / 100; break; } diff --git a/test/battle/move_effect/sucker_punch.c b/test/battle/move_effect/sucker_punch.c index 40ba135b6a..0fd2273607 100644 --- a/test/battle/move_effect/sucker_punch.c +++ b/test/battle/move_effect/sucker_punch.c @@ -51,3 +51,22 @@ SINGLE_BATTLE_TEST("Sucker Punch doesn't hit targets that has already moved") } } } + +DOUBLE_BATTLE_TEST("Sucker Punch fails if the target has attempted to act even if previously successful") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_INSTRUCT) == EFFECT_INSTRUCT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_SCRATCH, target: playerLeft); MOVE(playerLeft, MOVE_SUCKER_PUNCH, target: opponentLeft); MOVE(playerRight, MOVE_INSTRUCT, target: playerLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUCKER_PUNCH, playerLeft); + HP_BAR(opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, playerRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_SUCKER_PUNCH, playerLeft); + } +} diff --git a/test/battle/move_effect/upper_hand.c b/test/battle/move_effect/upper_hand.c index ae63fc9569..36b35781e3 100644 --- a/test/battle/move_effect/upper_hand.c +++ b/test/battle/move_effect/upper_hand.c @@ -132,3 +132,25 @@ AI_SINGLE_BATTLE_TEST("AI won't use Upper Hand unless it has seen a priority mov TURN { MOVE(player, move); EXPECT_MOVE(opponent, move == MOVE_QUICK_ATTACK ? MOVE_UPPER_HAND : MOVE_KARATE_CHOP); } } } + +DOUBLE_BATTLE_TEST("Upper Hand fails if the target has attempted to act even if previously successful") +{ + GIVEN { + ASSUME(GetMoveCategory(MOVE_EXTREME_SPEED) == DAMAGE_CATEGORY_PHYSICAL); + ASSUME(GetMovePriority(MOVE_EXTREME_SPEED) == 2); + ASSUME(GetMoveEffect(MOVE_INSTRUCT) == EFFECT_INSTRUCT); + PLAYER(SPECIES_MIENSHAO); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_EXTREME_SPEED, target: playerLeft); MOVE(playerLeft, MOVE_UPPER_HAND, target: opponentLeft); MOVE(playerRight, MOVE_INSTRUCT, target: playerLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_UPPER_HAND, playerLeft); + HP_BAR(opponentLeft); + MESSAGE("The opposing Wobbuffet flinched and couldn't move!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_EXTREME_SPEED, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_INSTRUCT, playerRight); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_UPPER_HAND, playerLeft); + } +} From 27b1a17d0973b87207a4d6aa26aa4d3f5d3205a4 Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Wed, 24 Sep 2025 20:21:49 +0200 Subject: [PATCH 13/25] Adjusted line break substring breaking (#7789) Co-authored-by: Hedara --- src/line_break.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/line_break.c b/src/line_break.c index 169d68332b..a4dd6475af 100644 --- a/src/line_break.c +++ b/src/line_break.c @@ -36,10 +36,10 @@ void BreakStringAutomatic(u8 *src, u32 maxWidth, u32 screenLines, u8 fontId, enu { if (src[currIndex] == CHAR_PROMPT_CLEAR) { - u8 replacedChar = src[currIndex + 1]; - src[currIndex + 1] = EOS; + u8 replacedChar = src[currIndex]; + src[currIndex] = EOS; BreakSubStringAutomatic(currSrc, maxWidth, screenLines, fontId, toggleScrollPrompt); - src[currIndex + 1] = replacedChar; + src[currIndex] = replacedChar; currSrc = &src[currIndex + 1]; } currIndex++; From 27b542466f80b0da2aa2d5d32d9607457a495054 Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Thu, 25 Sep 2025 10:39:56 +0200 Subject: [PATCH 14/25] Fix Flame Burst timeout if primary target is fainted (#7793) Co-authored-by: Hedara --- data/battle_scripts_1.s | 1 - .../move_effect_secondary/flame_burst.c | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 7c33bfef0f..ebdfc59e0d 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -1582,7 +1582,6 @@ BattleScript_EffectAfterYou:: goto BattleScript_MoveEnd BattleScript_MoveEffectFlameBurst:: - tryfaintmon BS_TARGET printstring STRINGID_BURSTINGFLAMESHIT waitmessage B_WAIT_TIME_LONG orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_PASSIVE_HP_UPDATE diff --git a/test/battle/move_effect_secondary/flame_burst.c b/test/battle/move_effect_secondary/flame_burst.c index df0199e116..494a7bf9c0 100644 --- a/test/battle/move_effect_secondary/flame_burst.c +++ b/test/battle/move_effect_secondary/flame_burst.c @@ -22,3 +22,35 @@ DOUBLE_BATTLE_TEST("Flame Burst Substitute") NOT MESSAGE("The substitute took damage for the opposing Wynaut!"); } } + +DOUBLE_BATTLE_TEST("Flame Burst doesn't crash, opponent to player") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentRight, MOVE_FLAME_BURST, target: playerLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLAME_BURST, opponentRight); + HP_BAR(playerRight); + MESSAGE("Wobbuffet fainted!"); + } +} + +DOUBLE_BATTLE_TEST("Flame Burst doesn't crash, player to opponent") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_FLAME_BURST, target: opponentLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLAME_BURST, playerLeft); + HP_BAR(opponentRight); + MESSAGE("The opposing Wynaut fainted!"); + } +} From 68e2bb5ab538312e7582ecdfef9ab6a8914904e0 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:41:27 +0200 Subject: [PATCH 15/25] Fixes Leppa Berry timings (#7787) Co-authored-by: Bassoonian --- data/battle_scripts_1.s | 8 ++++---- src/battle_util.c | 4 ++++ test/battle/hold_effect/restore_pp.c | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 test/battle/hold_effect/restore_pp.c diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index ebdfc59e0d..0fa06c7772 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -8509,15 +8509,15 @@ BattleScript_ItemHealHP_RemoveItemEnd2_Anim: end2 BattleScript_BerryPPHealRet:: - jumpifability BS_ATTACKER, ABILITY_RIPEN, BattleScript_BerryPPHeal_AbilityPopup + jumpifability BS_SCRIPTING, ABILITY_RIPEN, BattleScript_BerryPPHeal_AbilityPopup goto BattleScript_BerryPPHeal_Anim BattleScript_BerryPPHeal_AbilityPopup: - call BattleScript_AbilityPopUp + call BattleScript_AbilityPopUpScripting BattleScript_BerryPPHeal_Anim: - playanimation BS_ATTACKER, B_ANIM_HELD_ITEM_EFFECT + playanimation BS_SCRIPTING, B_ANIM_HELD_ITEM_EFFECT printstring STRINGID_PKMNSITEMRESTOREDPP waitmessage B_WAIT_TIME_LONG - removeitem BS_ATTACKER + removeitem BS_SCRIPTING return BattleScript_BerryPPHealEnd2:: diff --git a/src/battle_util.c b/src/battle_util.c index e633a20bad..3ab160c03b 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -6145,6 +6145,7 @@ static u32 ItemRestorePp(u32 battler, u32 itemId, enum ItemCaseId caseID) else BattleScriptCall(BattleScript_BerryPPHealRet); + gBattleScripting.battler = battler; BtlController_EmitSetMonData(battler, B_COMM_TO_CONTROLLER, i + REQUEST_PPMOVE1_BATTLE, 0, 1, &changedPP); MarkBattlerForControllerExec(battler); if (MOVE_IS_PERMANENT(battler, i)) @@ -6729,6 +6730,9 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler) if (B_BERRIES_INSTANT >= GEN_4) effect = ItemHealHp(battler, gLastUsedItem, caseID, TRUE); break; + case HOLD_EFFECT_RESTORE_PP: + effect = ItemRestorePp(battler, gLastUsedItem, caseID); + break; case HOLD_EFFECT_AIR_BALLOON: effect = ITEM_EFFECT_OTHER; gBattleScripting.battler = battler; diff --git a/test/battle/hold_effect/restore_pp.c b/test/battle/hold_effect/restore_pp.c new file mode 100644 index 0000000000..c3810652a4 --- /dev/null +++ b/test/battle/hold_effect/restore_pp.c @@ -0,0 +1,23 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gItemsInfo[ITEM_LEPPA_BERRY].holdEffect == HOLD_EFFECT_RESTORE_PP); +} + +SINGLE_BATTLE_TEST("Restore PP berry activates immediately on switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LEPPA_BERRY); MovesWithPP({MOVE_SCRATCH, 0}, {MOVE_CELEBRATE, 20}); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_POUND); MOVE(player, MOVE_CELEBRATE); } + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_POUND, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + } THEN { + EXPECT(player->item == ITEM_NONE); + } +} From 2f69b44f813ff89045fa16be72f6951d7210afc0 Mon Sep 17 00:00:00 2001 From: Estellar <137097857+estellarc@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:52:00 -0300 Subject: [PATCH 16/25] Added missing `LOCALID_NONE` (#7783) --- src/battle_setup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/battle_setup.c b/src/battle_setup.c index 01e95d45a1..6a6f529ea6 100644 --- a/src/battle_setup.c +++ b/src/battle_setup.c @@ -1004,7 +1004,7 @@ void TrainerBattleLoadArgsSecondTrainer(const u8 *data) void SetMapVarsToTrainerA(void) { - if (TRAINER_BATTLE_PARAM.objEventLocalIdA != 0) + if (TRAINER_BATTLE_PARAM.objEventLocalIdA != LOCALID_NONE) { gSpecialVar_LastTalked = TRAINER_BATTLE_PARAM.objEventLocalIdA; gSelectedObjectEvent = GetObjectEventIdByLocalIdAndMap(TRAINER_BATTLE_PARAM.objEventLocalIdA, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup); From d1bd4edc57550ca9ffc082ba6d18fa6167318709 Mon Sep 17 00:00:00 2001 From: Bassoonian Date: Thu, 25 Sep 2025 16:03:17 +0200 Subject: [PATCH 17/25] Fix Forecast and Flower Gift corruption (#7796) --- src/battle_script_commands.c | 2 +- test/battle/form_change/faint.c | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index df620dd3f7..2ab77334db 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -15194,7 +15194,7 @@ void BS_ItemRestorePP(void) void BS_TryRevertWeatherForm(void) { NATIVE_ARGS(); - if (TryBattleFormChange(gBattlerTarget, FORM_CHANGE_BATTLE_WEATHER)) + if (IsBattlerAlive(gBattlerTarget) && TryBattleFormChange(gBattlerTarget, FORM_CHANGE_BATTLE_WEATHER)) { gBattleScripting.battler = gBattlerTarget; BattleScriptPush(cmd->nextInstr); diff --git a/test/battle/form_change/faint.c b/test/battle/form_change/faint.c index dffabb44de..8fc95cdc65 100644 --- a/test/battle/form_change/faint.c +++ b/test/battle/form_change/faint.c @@ -3,8 +3,9 @@ SINGLE_BATTLE_TEST("Aegislash reverts to Shield Form upon fainting") { + KNOWN_FAILING; GIVEN { - PLAYER(SPECIES_AEGISLASH_SHIELD) { HP(1); } + PLAYER(SPECIES_AEGISLASH_BLADE) { HP(1); } PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { @@ -16,3 +17,26 @@ SINGLE_BATTLE_TEST("Aegislash reverts to Shield Form upon fainting") EXPECT_EQ(GetMonData(&PLAYER_PARTY[0], MON_DATA_SPECIES), SPECIES_AEGISLASH_SHIELD); } } + +DOUBLE_BATTLE_TEST("Causing a Forecast or Flower Gift Pokémon to faint should not cause a message") // issue 7795 +{ + u32 species; + PARAMETRIZE { species = SPECIES_CASTFORM; } + PARAMETRIZE { species = SPECIES_CHERRIM; } + GIVEN { + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_VULPIX) { Ability(ABILITY_DROUGHT); } + OPPONENT(species) { HP(1); } + } WHEN { + TURN { MOVE(playerRight, MOVE_GYRO_BALL, target: opponentRight); } + } SCENE { + if (species == SPECIES_CASTFORM) { + MESSAGE("The opposing Castform fainted!"); + NOT MESSAGE("The opposing Castform transformed!"); + } else { + MESSAGE("The opposing Cherrim fainted!"); + NOT MESSAGE("The opposing Cherrim transformed!"); + } + } +} From 512471268e06aab0efa5e9060c7d93ce08af7839 Mon Sep 17 00:00:00 2001 From: Bassoonian Date: Thu, 25 Sep 2025 16:59:39 +0200 Subject: [PATCH 18/25] Fix Salt Cure in double battles (#7797) --- data/battle_scripts_1.s | 4 +++- src/battle_message.c | 2 +- test/battle/move_effect_secondary/salt_cure.c | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 0fa06c7772..533eca06ac 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -402,7 +402,9 @@ BattleScript_MoveEffectSaltCure:: BattleScript_SaltCureExtraDamage:: playanimation BS_ATTACKER, B_ANIM_SALT_CURE_DAMAGE, NULL waitanimation - call BattleScript_HurtTarget_NoString + orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_PASSIVE_HP_UPDATE + healthbarupdate BS_ATTACKER + datahpupdate BS_ATTACKER printstring STRINGID_TARGETISHURTBYSALTCURE waitmessage B_WAIT_TIME_LONG tryfaintmon BS_ATTACKER diff --git a/src/battle_message.c b/src/battle_message.c index 4b581c01d8..0eb8c85791 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -827,7 +827,7 @@ const u8 *const gBattleStringsTable[STRINGID_COUNT] = [STRINGID_TEAMGAINEDEXP] = COMPOUND_STRING("The rest of your team gained Exp. Points thanks to the Exp. Share!\p"), [STRINGID_CURRENTMOVECANTSELECT] = COMPOUND_STRING("{B_BUFF1} cannot be used!\p"), [STRINGID_TARGETISBEINGSALTCURED] = COMPOUND_STRING("{B_DEF_NAME_WITH_PREFIX} is being salt cured!"), - [STRINGID_TARGETISHURTBYSALTCURE] = COMPOUND_STRING("{B_DEF_NAME_WITH_PREFIX} is hurt by {B_BUFF1}!"), + [STRINGID_TARGETISHURTBYSALTCURE] = COMPOUND_STRING("{B_ATK_NAME_WITH_PREFIX} is hurt by {B_BUFF1}!"), [STRINGID_TARGETCOVEREDINSTICKYCANDYSYRUP] = COMPOUND_STRING("{B_DEF_NAME_WITH_PREFIX} got covered in sticky candy syrup!"), [STRINGID_SHARPSTEELFLOATS] = COMPOUND_STRING("Sharp-pointed pieces of steel started floating around {B_DEF_TEAM2} Pokémon!"), [STRINGID_SHARPSTEELDMG] = COMPOUND_STRING("The sharp steel bit into {B_DEF_NAME_WITH_PREFIX2}!"), diff --git a/test/battle/move_effect_secondary/salt_cure.c b/test/battle/move_effect_secondary/salt_cure.c index 94e3ead5cc..e304d2d447 100644 --- a/test/battle/move_effect_secondary/salt_cure.c +++ b/test/battle/move_effect_secondary/salt_cure.c @@ -132,3 +132,19 @@ SINGLE_BATTLE_TEST("If Salt Cure faints the target, messages will be applied in MESSAGE("The opposing Wobbuffet fainted!"); } } + +DOUBLE_BATTLE_TEST("Salt Cure works in double battles") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_SALT_CURE, target: opponentLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SALT_CURE, playerLeft); + HP_BAR(opponentLeft); + HP_BAR(opponentLeft); + } +} From b995857b443bd185b52b96cef341545d721bac0f Mon Sep 17 00:00:00 2001 From: Zimmermann Gyula Date: Fri, 26 Sep 2025 16:35:53 +0200 Subject: [PATCH 19/25] Fixup add-new-trainer-front-pic tutorial. (#7802) --- docs/tutorials/how_to_trainer_front_pic.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/how_to_trainer_front_pic.md b/docs/tutorials/how_to_trainer_front_pic.md index 092679c196..1b3d303bf9 100644 --- a/docs/tutorials/how_to_trainer_front_pic.md +++ b/docs/tutorials/how_to_trainer_front_pic.md @@ -19,7 +19,7 @@ If you've done this before and just need a quick lookup, here's what files you n ## The Graphics ### 1. Edit the sprites -We will start with a graphic that we want to use for our new trainer pic. Unlike with adding Pokémon, the trainer sprites aren't sorted in individual folders, but rather in one folder: [`graphics/trainers/front_pics`](./graphics/trainers/front_pics). **Trainers sprites cannot be more than 16 - this includes the color that will be transparent, which is the first slot of the palette.** +We will start with a graphic that we want to use for our new trainer pic. Unlike with adding Pokémon, the trainer sprites aren't sorted in individual folders, but rather in one folder: [`graphics/trainers/front_pics`](./graphics/trainers/front_pics). **Trainers sprites cannot have more than 16 colors - this includes the color that will be transparent, which is the first slot of the palette.** ### 2. Register the sprites Sadly, just putting the image files into the graphics folder is not enough. To use the sprites we have to register them by linking the graphic files in [`src/data/graphics/trainers`](./data/graphics/trainers.h): From 567a1290d4144628cbe9e6af46ae81712e52fb40 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sat, 27 Sep 2025 19:55:12 +0200 Subject: [PATCH 20/25] Fixes Effects activating when move wasn't successful (#7803) Co-authored-by: Bassoonian --- src/battle_script_commands.c | 9 +++++++-- src/battle_util.c | 1 + test/battle/hold_effect/throat_spray.c | 16 ++++++++++++++++ test/battle/move_effect/ceaseless_edge.c | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 2ab77334db..114da2aed7 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -5698,6 +5698,7 @@ static bool32 HandleMoveEndMoveBlock(u32 moveEffect) case EFFECT_KNOCK_OFF: if (gBattleStruct->battlerState[gBattlerTarget].itemCanBeKnockedOff && gBattleMons[gBattlerTarget].item != ITEM_NONE + && IsBattlerTurnDamaged(gBattlerTarget) && IsBattlerAlive(gBattlerAttacker)) { u32 side = GetBattlerSide(gBattlerTarget); @@ -5873,7 +5874,9 @@ static bool32 HandleMoveEndMoveBlock(u32 moveEffect) } break; case EFFECT_STONE_AXE: - if (!IsHazardOnSide(GetBattlerSide(gBattlerTarget), HAZARDS_STEALTH_ROCK) && IsBattlerAlive(gBattlerAttacker)) + if (!IsHazardOnSide(GetBattlerSide(gBattlerTarget), HAZARDS_STEALTH_ROCK) + && IsBattlerTurnDamaged(gBattlerTarget) + && IsBattlerAlive(gBattlerAttacker)) { gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_POINTEDSTONESFLOAT; BattleScriptPushCursor(); @@ -5882,7 +5885,9 @@ static bool32 HandleMoveEndMoveBlock(u32 moveEffect) } break; case EFFECT_CEASELESS_EDGE: - if (gSideTimers[GetBattlerSide(gBattlerTarget)].spikesAmount < 3 && IsBattlerAlive(gBattlerAttacker)) + if (gSideTimers[GetBattlerSide(gBattlerTarget)].spikesAmount < 3 + && IsBattlerTurnDamaged(gBattlerTarget) + && IsBattlerAlive(gBattlerAttacker)) { gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_SPIKESSCATTERED; BattleScriptPush(gBattlescriptCurrInstr + 1); diff --git a/src/battle_util.c b/src/battle_util.c index 3ab160c03b..d80864cd36 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -7091,6 +7091,7 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler) break; case HOLD_EFFECT_THROAT_SPRAY: // Does NOT need to be a damaging move if (IsSoundMove(gCurrentMove) + && !(gHitMarker & HITMARKER_UNABLE_TO_USE_MOVE) && IsBattlerAlive(gBattlerAttacker) && IsAnyTargetAffected(gBattlerAttacker) && CompareStat(gBattlerAttacker, STAT_SPATK, MAX_STAT_STAGE, CMP_LESS_THAN) diff --git a/test/battle/hold_effect/throat_spray.c b/test/battle/hold_effect/throat_spray.c index 67e596c277..99b2da3e59 100644 --- a/test/battle/hold_effect/throat_spray.c +++ b/test/battle/hold_effect/throat_spray.c @@ -88,3 +88,19 @@ SINGLE_BATTLE_TEST("Throat Spray does not activate if move fails") } } } + +SINGLE_BATTLE_TEST("Throat Spray does not activate if user flinches") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_THROAT_SPRAY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_FAKE_OUT); MOVE(player, MOVE_HYPER_VOICE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FAKE_OUT, opponent); + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPER_VOICE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + } + } +} diff --git a/test/battle/move_effect/ceaseless_edge.c b/test/battle/move_effect/ceaseless_edge.c index 5a7e7f4a1a..288045945f 100644 --- a/test/battle/move_effect/ceaseless_edge.c +++ b/test/battle/move_effect/ceaseless_edge.c @@ -78,3 +78,19 @@ SINGLE_BATTLE_TEST("Ceaseless Edge fails to set up hazards if user faints") NOT MESSAGE("Spikes were scattered on the ground all around the opposing team!"); } } + +SINGLE_BATTLE_TEST("Ceaseless Edge does not set up hazards if target was not hit") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_CEASELESS_EDGE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, opponent); + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CEASELESS_EDGE, player); + MESSAGE("Spikes were scattered on the ground all around the opposing team!"); + } + } +} From 027a4380b6e20b94406c2a10354f67a9fd0a8d81 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sat, 27 Sep 2025 20:03:05 +0200 Subject: [PATCH 21/25] Fixes Effects activating when move wasn't successful (#7803) Co-authored-by: Bassoonian From e5e0800b9037b1bc18d414a33d6d41f73160e497 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:59:31 +0200 Subject: [PATCH 22/25] Fixes Throat Spray being blocked by Sheer Force (#7808) --- include/constants/battle_script_commands.h | 6 ++--- src/battle_util.c | 30 +++++++++++----------- test/battle/hold_effect/throat_spray.c | 13 ++++++++++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/include/constants/battle_script_commands.h b/include/constants/battle_script_commands.h index 78c2200018..2b4d590286 100644 --- a/include/constants/battle_script_commands.h +++ b/include/constants/battle_script_commands.h @@ -155,14 +155,14 @@ enum MoveEndEffects MOVEEND_ITEM_EFFECTS_ATTACKER, MOVEEND_ABILITY_BLOCK, MOVEEND_SHEER_FORCE, // If move is Sheer Force affected, skip until Opportunist - MOVEEND_RED_CARD, // Red Card triggers before Eject Pack + MOVEEND_RED_CARD, MOVEEND_EJECT_BUTTON, - MOVEEND_LIFEORB_SHELLBELL, // Includes shell bell, throat spray, etc + MOVEEND_LIFEORB_SHELLBELL, MOVEEND_FORM_CHANGE, MOVEEND_EMERGENCY_EXIT, MOVEEND_EJECT_PACK, MOVEEND_HIT_ESCAPE, - MOVEEND_OPPORTUNIST, // Occurs after other stat change items/abilities to try and copy the boosts + MOVEEND_OPPORTUNIST, MOVEEND_PICKPOCKET, MOVEEND_WHITE_HERB, MOVEEND_THIRD_MOVE_BLOCK, diff --git a/src/battle_util.c b/src/battle_util.c index d80864cd36..61d8ef0535 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -6501,6 +6501,21 @@ static u8 ItemEffectMoveEnd(u32 battler, enum ItemHoldEffect holdEffect) case HOLD_EFFECT_MIRROR_HERB: effect = TryConsumeMirrorHerb(battler, ITEMEFFECT_NONE); break; + case HOLD_EFFECT_THROAT_SPRAY: + if (IsSoundMove(gCurrentMove) + && !(gHitMarker & HITMARKER_UNABLE_TO_USE_MOVE) + && IsBattlerAlive(gBattlerAttacker) + && IsAnyTargetAffected(gBattlerAttacker) + && CompareStat(gBattlerAttacker, STAT_SPATK, MAX_STAT_STAGE, CMP_LESS_THAN) + && !NoAliveMonsForEitherParty()) // Don't activate if battle will end + { + gLastUsedItem = gBattleMons[gBattlerAttacker].item; + gBattleScripting.battler = gBattlerAttacker; + SET_STATCHANGER(STAT_SPATK, 1, FALSE); + effect = ITEM_STATS_CHANGE; + BattleScriptCall(BattleScript_AttackerItemStatRaise); + } + break; default: break; } @@ -7089,21 +7104,6 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler) gLastUsedItem = atkItem; } break; - case HOLD_EFFECT_THROAT_SPRAY: // Does NOT need to be a damaging move - if (IsSoundMove(gCurrentMove) - && !(gHitMarker & HITMARKER_UNABLE_TO_USE_MOVE) - && IsBattlerAlive(gBattlerAttacker) - && IsAnyTargetAffected(gBattlerAttacker) - && CompareStat(gBattlerAttacker, STAT_SPATK, MAX_STAT_STAGE, CMP_LESS_THAN) - && !NoAliveMonsForEitherParty()) // Don't activate if battle will end - { - gLastUsedItem = atkItem; - gBattleScripting.battler = gBattlerAttacker; - SET_STATCHANGER(STAT_SPATK, 1, FALSE); - effect = ITEM_STATS_CHANGE; - BattleScriptCall(BattleScript_AttackerItemStatRaise); - } - break; default: break; } diff --git a/test/battle/hold_effect/throat_spray.c b/test/battle/hold_effect/throat_spray.c index 99b2da3e59..a8e6cc5c10 100644 --- a/test/battle/hold_effect/throat_spray.c +++ b/test/battle/hold_effect/throat_spray.c @@ -104,3 +104,16 @@ SINGLE_BATTLE_TEST("Throat Spray does not activate if user flinches") } } } + +SINGLE_BATTLE_TEST("Throat Spray is not blocked by Sheer Force") +{ + GIVEN { + PLAYER(SPECIES_NIDOKING) { Ability(ABILITY_SHEER_FORCE); Item(ITEM_THROAT_SPRAY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BUG_BUZZ); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_BUG_BUZZ, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + } +} From a4ecd7338bb503dfd7082c87784c6e1dca3aaaa3 Mon Sep 17 00:00:00 2001 From: khbsd Date: Sun, 28 Sep 2025 02:53:55 -0500 Subject: [PATCH 23/25] fix: seen flags for first mon in enemy party (#7791) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- include/battle.h | 3 ++- include/config/battle.h | 1 + include/pokemon.h | 1 + src/battle_ai_switch_items.c | 6 +++--- src/battle_controller_opponent.c | 2 ++ src/battle_controller_player_partner.c | 3 ++- src/battle_main.c | 29 ++++++++++++++++++++++---- src/battle_script_commands.c | 8 +------ src/pokemon.c | 8 +++++++ 9 files changed, 45 insertions(+), 16 deletions(-) diff --git a/include/battle.h b/include/battle.h index 13316eada9..8fe4fa8cce 100644 --- a/include/battle.h +++ b/include/battle.h @@ -600,7 +600,8 @@ struct PartyState u32 supersweetSyrup:1; u32 timesGotHit:5; u32 changedSpecies:11; // For forms when multiple mons can change into the same pokemon. - u32 padding:10; + u32 sentOut:1; + u32 padding:9; }; // Cleared at the beginning of the battle. Fields need to be cleared when needed manually otherwise. diff --git a/include/config/battle.h b/include/config/battle.h index d6de6397fe..61603ba5b8 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -311,6 +311,7 @@ #define B_TOXIC_REVERSAL GEN_LATEST // In Gen5+, bad poison will change to regular poison at the end of battles. #define B_TRY_CATCH_TRAINER_BALL GEN_LATEST // In Gen4+, trying to catch a Trainer's Pokémon does not consume the Poké Ball. #define B_SLEEP_CLAUSE FALSE // Enables Sleep Clause all the time in every case, overriding B_FLAG_SLEEP_CLAUSE. Use that for modularity. +#define B_PARTNER_MONS_MARKED_SEEN FALSE // If TRUE, if your double battle partner sends out a Pokémon you haven't encountered yet, it will be marked as SEEN in your Pokédex. #define NUM_BEEPS_GEN_LATEST 4 // Loops 4 times #define NUM_BEEPS_GEN_3 -1 // Loops infinitely diff --git a/include/pokemon.h b/include/pokemon.h index ba459480e1..1e31c425b6 100644 --- a/include/pokemon.h +++ b/include/pokemon.h @@ -849,6 +849,7 @@ u8 GetOpposingLinkMultiBattlerId(bool8 rightSide, u8 multiplayerId); u16 FacilityClassToPicIndex(u16 facilityClass); u16 PlayerGenderToFrontTrainerPicId(u8 playerGender); void HandleSetPokedexFlag(enum NationalDexOrder nationalNum, u8 caseId, u32 personality); +void HandleSetPokedexFlagFromMon(struct Pokemon *mon, u32 caseId); bool8 HasTwoFramesAnimation(u16 species); struct MonSpritesGfxManager *CreateMonSpritesGfxManager(u8 managerId, u8 mode); void DestroyMonSpritesGfxManager(u8 managerId); diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 6340b921c6..ed81c19659 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -1522,7 +1522,7 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva return bestMonId; } -static u32 GetFirstNonIvalidMon(u32 firstId, u32 lastId, u32 invalidMons, u32 battlerIn1, u32 battlerIn2) +static u32 GetFirstNonInvalidMon(u32 firstId, u32 lastId, u32 invalidMons, u32 battlerIn1, u32 battlerIn2) { if (!IsDoubleBattle()) return PARTY_SIZE; @@ -2297,7 +2297,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, return aceMonId; // Fallback - u32 bestMonId = GetFirstNonIvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2); + u32 bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2); if (bestMonId != PARTY_SIZE) return bestMonId; @@ -2419,7 +2419,7 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType) return aceMonId; // Fallback - bestMonId = GetFirstNonIvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2); + bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2); if (bestMonId != PARTY_SIZE) return bestMonId; diff --git a/src/battle_controller_opponent.c b/src/battle_controller_opponent.c index 84b3d3c0a8..8c469733c1 100644 --- a/src/battle_controller_opponent.c +++ b/src/battle_controller_opponent.c @@ -567,12 +567,14 @@ static void OpponentHandleChoosePokemon(u32 battler) } } gBattleStruct->monToSwitchIntoId[battler] = chosenMonId; + GetBattlerPartyState(battler)->sentOut = TRUE; } else { chosenMonId = gBattleStruct->AI_monToSwitchIntoId[battler]; gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE; gBattleStruct->monToSwitchIntoId[battler] = chosenMonId; + GetBattlerPartyState(battler)->sentOut = TRUE; } #if TESTING TestRunner_Battle_CheckSwitch(battler, chosenMonId); diff --git a/src/battle_controller_player_partner.c b/src/battle_controller_player_partner.c index 5426f0239f..58911f1518 100644 --- a/src/battle_controller_player_partner.c +++ b/src/battle_controller_player_partner.c @@ -285,7 +285,6 @@ static void PlayerPartnerHandleChoosePokemon(u32 battler) else if (gBattleStruct->monToSwitchIntoId[battler] >= PARTY_SIZE || !IsValidForBattle(&gPlayerParty[gBattleStruct->monToSwitchIntoId[battler]])) { chosenMonId = GetMostSuitableMonToSwitchInto(battler, SWITCH_AFTER_KO); - if (chosenMonId == PARTY_SIZE || !IsValidForBattle(&gPlayerParty[chosenMonId])) // just switch to the next mon { s32 firstId = (IsAiVsAiBattle()) ? 0 : (PARTY_SIZE / 2); @@ -303,12 +302,14 @@ static void PlayerPartnerHandleChoosePokemon(u32 battler) } } gBattleStruct->monToSwitchIntoId[battler] = chosenMonId; + GetBattlerPartyState(battler)->sentOut = TRUE; } else // Mon to switch out has been already chosen. { chosenMonId = gBattleStruct->monToSwitchIntoId[battler]; gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE; gBattleStruct->monToSwitchIntoId[battler] = chosenMonId; + GetBattlerPartyState(battler)->sentOut = TRUE; } BtlController_EmitChosenMonReturnValue(battler, B_COMM_TO_ENGINE, chosenMonId, NULL); BtlController_Complete(battler); diff --git a/src/battle_main.c b/src/battle_main.c index c39ea52905..967520f9fb 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3735,6 +3735,10 @@ static void DoBattleIntro(void) gBattleStruct->overworldWeatherDone = FALSE; Ai_InitPartyStruct(); // Save mons party counts, and first 2/4 mons on the battlefield. + // mark all battlers as sent out + for (battler = 0; battler < gBattlersCount; battler++) + GetBattlerPartyState(battler)->sentOut = TRUE; + // Try to set a status to start the battle with gBattleStruct->startingStatus = 0; if (gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS && GetTrainerStartingStatusFromId(TRAINER_BATTLE_PARAM.opponentB)) @@ -5562,14 +5566,31 @@ static void HandleEndTurn_FinishBattle(void) GetMonData(GetBattlerMon(battler), MON_DATA_NICKNAME, gBattleResults.playerMon2Name); } } - else if (!IsOnPlayerSide(battler)) - { - HandleSetPokedexFlag(SpeciesToNationalPokedexNum(gBattleMons[battler].species), FLAG_SET_SEEN, gBattleMons[battler].personality); - } } TryPutPokemonTodayOnAir(); } + if (!(gBattleTypeFlags & (BATTLE_TYPE_LINK + | BATTLE_TYPE_EREADER_TRAINER + | BATTLE_TYPE_RECORDED_LINK + | BATTLE_TYPE_TRAINER_HILL + | BATTLE_TYPE_FRONTIER))) + { + for (u32 side = 0; side < NUM_BATTLE_SIDES; side++) + { + struct Pokemon *party = GetSideParty(side); + + if (side == B_SIDE_PLAYER && !B_PARTNER_MONS_MARKED_SEEN) + continue; + + for (u32 partySlot = 0; partySlot < PARTY_SIZE; partySlot++) + { + if (gBattleStruct->partyState[side][partySlot].sentOut) + HandleSetPokedexFlagFromMon(&party[partySlot], FLAG_SET_SEEN); + } + } + } + if (!(gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 114da2aed7..a1b66e83f5 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -7176,13 +7176,7 @@ static void Cmd_switchinanim(void) battler = GetBattlerForBattleScript(cmd->battler); - if (!IsOnPlayerSide(battler) - && !(gBattleTypeFlags & (BATTLE_TYPE_LINK - | BATTLE_TYPE_EREADER_TRAINER - | BATTLE_TYPE_RECORDED_LINK - | BATTLE_TYPE_TRAINER_HILL - | BATTLE_TYPE_FRONTIER))) - HandleSetPokedexFlag(SpeciesToNationalPokedexNum(gBattleMons[battler].species), FLAG_SET_SEEN, gBattleMons[battler].personality); + GetBattlerPartyState(battler)->sentOut = TRUE; gAbsentBattlerFlags &= ~(1u << battler); diff --git a/src/pokemon.c b/src/pokemon.c index 61270d72c3..eb2428f2fb 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -6349,6 +6349,14 @@ void HandleSetPokedexFlag(enum NationalDexOrder nationalNum, u8 caseId, u32 pers } } +void HandleSetPokedexFlagFromMon(struct Pokemon *mon, u32 caseId) +{ + u32 personality = GetMonData(mon, MON_DATA_PERSONALITY); + enum NationalDexOrder nationalNum = SpeciesToNationalPokedexNum(GetMonData(mon, MON_DATA_SPECIES)); + + HandleSetPokedexFlag(nationalNum, caseId, personality); +} + bool8 HasTwoFramesAnimation(u16 species) { return P_TWO_FRAME_FRONT_SPRITES From e9911d4d19c12ecc399e98170fe91f2b939acad1 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sun, 28 Sep 2025 17:39:08 +0200 Subject: [PATCH 24/25] Fixes inaccurate save / restore in Fling script (#7811) --- data/battle_scripts_1.s | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 533eca06ac..386258c716 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -792,13 +792,13 @@ BattleScript_FlingLightBall: goto BattleScript_FlingEnd BattleScript_FlingMentalHerb: curecertainstatuses - savetarget + saveattacker copybyte gBattlerAttacker, gBattlerTarget playanimation BS_ATTACKER, B_ANIM_HELD_ITEM_EFFECT, NULL printfromtable gMentalHerbCureStringIds waitmessage B_WAIT_TIME_LONG updatestatusicon BS_ATTACKER - restoretarget + restoreattacker goto BattleScript_FlingEnd BattleScript_FlingPoisonBarb: seteffectsecondary BS_ATTACKER, BS_TARGET, MOVE_EFFECT_POISON From ac75fe04d88d493759b373a46dc46ab01e2a1293 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sun, 28 Sep 2025 20:47:22 +0200 Subject: [PATCH 25/25] Fix test exit prints for stored battlers (#7807) --- data/battle_scripts_1.s | 2 +- src/battle_script_commands.c | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 386258c716..4248807b01 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -216,7 +216,7 @@ BattleScript_EffectDoodle_AfterCopy: printstring STRINGID_PKMNCOPIEDFOE waitmessage B_WAIT_TIME_LONG switchinabilities BS_ATTACKER - jumpifbyte CMP_NOT_EQUAL, gBattleCommunication, 0x0, BattleScript_MoveEnd + jumpifbyte CMP_NOT_EQUAL, gBattleCommunication, 0x0, BattleScript_EffectDoodleMoveEnd addbyte gBattleCommunication, 1 jumpifnoally BS_ATTACKER, BattleScript_EffectDoodleMoveEnd setallytonextattacker BattleScript_EffectDoodle_CopyAbility diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index a1b66e83f5..f2b03a382e 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6881,20 +6881,19 @@ static void Cmd_moveend(void) if (gBattleStruct->savedAttackerCount > 0) { - // #if TESTING - // Test_ExitWithResult(TEST_RESULT_ERROR, "savedAttackerCount is greater than 0! More calls to SaveBattlerAttacker than RestoreBattlerAttacker!"); - // #else - DebugPrintfLevel(MGBA_LOG_WARN, "savedAttackerCount is greater than 0! More calls to SaveBattlerAttacker than RestoreBattlerAttacker!"); - // #endif + if (TESTING) + Test_ExitWithResult(TEST_RESULT_ERROR, 0, "savedAttackerCount is greater than 0! More calls to SaveBattlerAttacker than RestoreBattlerAttacker!", __FILE__, __LINE__); + else + DebugPrintfLevel(MGBA_LOG_WARN, "savedAttackerCount is greater than 0! More calls to SaveBattlerAttacker than RestoreBattlerAttacker!"); } if (gBattleStruct->savedTargetCount > 0) { - // #if TESTING - // Test_ExitWithResult(TEST_RESULT_ERROR, "savedTargetCount is greater than 0! More calls to SaveBattlerTarget than RestoreBattlerTarget!"); - // #else - DebugPrintfLevel(MGBA_LOG_WARN, "savedTargetCount is greater than 0! More calls to SaveBattlerTarget than RestoreBattlerTarget!"); - // #endif + if (TESTING) + Test_ExitWithResult(TEST_RESULT_ERROR, 0, "savedTargetCount is greater than 0! More calls to SaveBattlerTarget than RestoreBattlerTarget!", __FILE__, __LINE__); + else + DebugPrintfLevel(MGBA_LOG_WARN, "savedTargetCount is greater than 0! More calls to SaveBattlerTarget than RestoreBattlerTarget!"); } + gProtectStructs[gBattlerAttacker].shellTrap = FALSE; gBattleStruct->battlerState[gBattlerAttacker].ateBoost = FALSE; gSpecialStatuses[gBattlerAttacker].gemBoost = FALSE;