From d220459a016983541b9536b7d0ce56974e17bc0b Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:33:29 +0200 Subject: [PATCH 01/13] Fixes Intimidate / Eject Pack interaction (#6645) Co-authored-by: Bassoonian --- asm/macros/battle_script.inc | 8 +++ data/battle_scripts_1.s | 15 ++--- include/battle.h | 3 +- include/battle_scripts.h | 2 + src/battle_main.c | 3 +- src/battle_script_commands.c | 91 +++++++++++++++++++++++++++- src/battle_util.c | 5 +- test/battle/ability/intimidate.c | 49 ++++++++++++++- test/battle/hold_effect/eject_pack.c | 71 ++++++++++++++++++++++ 9 files changed, 227 insertions(+), 20 deletions(-) diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index 2af5c1fcc8..425ba1cac9 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1367,6 +1367,10 @@ callnative BS_RestoreAttacker .endm + .macro jumpifintimidateabilityprevented + callnative BS_JumpIfIntimidateAbilityPrevented + .endm + .macro metalburstdamagecalculator failInstr:req callnative BS_CalcMetalBurstDmg .4byte \failInstr @@ -1383,6 +1387,10 @@ .byte \battler .endm + .macro tryintimidatejectpack + callnative BS_TryIntimidatEjectpack + .endm + .macro allyswitchswapbattlers callnative BS_AllySwitchSwapBattler .endm diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 2c82112b1a..a4f13be0b9 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -7735,13 +7735,7 @@ BattleScript_IntimidateLoop: jumpiftargetally BattleScript_IntimidateLoopIncrement jumpifabsent BS_TARGET, BattleScript_IntimidateLoopIncrement jumpifstatus2 BS_TARGET, STATUS2_SUBSTITUTE, BattleScript_IntimidateLoopIncrement -.if B_UPDATED_INTIMIDATE >= GEN_8 @These abilties specifically prevent just intimidate, without blocking stat decreases - jumpifability BS_TARGET, ABILITY_INNER_FOCUS, BattleScript_IntimidatePrevented - jumpifability BS_TARGET, ABILITY_SCRAPPY, BattleScript_IntimidatePrevented - jumpifability BS_TARGET, ABILITY_OWN_TEMPO, BattleScript_IntimidatePrevented - jumpifability BS_TARGET, ABILITY_OBLIVIOUS, BattleScript_IntimidatePrevented -.endif - jumpifability BS_TARGET, ABILITY_GUARD_DOG, BattleScript_IntimidateInReverse + jumpifintimidateabilityprevented BattleScript_IntimidateEffect: copybyte sBATTLER, gBattlerAttacker setstatchanger STAT_ATK, 1, TRUE @@ -7766,9 +7760,10 @@ BattleScript_IntimidateLoopIncrement: destroyabilitypopup restoretarget pause B_WAIT_TIME_MED + tryintimidatejectpack end3 -BattleScript_IntimidatePrevented: +BattleScript_IntimidatePrevented:: copybyte sBATTLER, gBattlerTarget call BattleScript_AbilityPopUp printstring STRINGID_PKMNPREVENTSSTATLOSSWITH @@ -7788,7 +7783,7 @@ BattleScript_IntimidateContrary_WontIncrease: printstring STRINGID_TARGETSTATWONTGOHIGHER goto BattleScript_IntimidateEffect_WaitString -BattleScript_IntimidateInReverse: +BattleScript_IntimidateInReverse:: copybyte sBATTLER, gBattlerTarget call BattleScript_AbilityPopUpTarget pause B_WAIT_TIME_SHORT @@ -9533,7 +9528,7 @@ BattleScript_EjectButtonActivates:: printstring STRINGID_EJECTBUTTONACTIVATE waitmessage B_WAIT_TIME_LONG removeitem BS_SCRIPTING - undodynamax BS_SCRIPTING + undodynamax BS_SCRIPTING makeinvisible BS_SCRIPTING openpartyscreen BS_SCRIPTING, BattleScript_EjectButtonEnd copybyte sSAVED_BATTLER, sBATTLER diff --git a/include/battle.h b/include/battle.h index 823e4813e9..1189ada4ea 100644 --- a/include/battle.h +++ b/include/battle.h @@ -183,7 +183,8 @@ struct ProtectStruct u16 eatMirrorHerb:1; u16 activateOpportunist:2; // 2 - to copy stats. 1 - stats copied (do not repeat). 0 - no stats to copy u16 usedAllySwitch:1; - u16 padding:2; + u16 lashOutAffected:1; + u16 padding:1; // End of 16-bit bitfield u32 physicalDmg; u32 specialDmg; diff --git a/include/battle_scripts.h b/include/battle_scripts.h index d11d2b814e..20ea1ccd49 100644 --- a/include/battle_scripts.h +++ b/include/battle_scripts.h @@ -170,6 +170,8 @@ extern const u8 BattleScript_RainDishActivates[]; extern const u8 BattleScript_SandstreamActivates[]; extern const u8 BattleScript_ShedSkinActivates[]; extern const u8 BattleScript_IntimidateActivates[]; +extern const u8 BattleScript_IntimidatePrevented[]; +extern const u8 BattleScript_IntimidateInReverse[]; extern const u8 BattleScript_DroughtActivates[]; extern const u8 BattleScript_TookAttack[]; extern const u8 BattleScript_SturdyPreventsOHKO[]; diff --git a/src/battle_main.c b/src/battle_main.c index 4834e26796..02cd58172d 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3751,6 +3751,7 @@ static void TryDoEventsBeforeFirstTurn(void) { for (i = 0; i < gBattlersCount; i++) { + gBattleStruct->monToSwitchIntoId[i] = PARTY_SIZE; // Included here because switches can happen before during set ups (eg. eject pack) struct Pokemon *party = GetBattlerParty(i); struct Pokemon *mon = &party[gBattlerPartyIndexes[i]]; if (!IsBattlerAlive(i) || gBattleMons[i].species == SPECIES_NONE || GetMonData(mon, MON_DATA_IS_EGG)) @@ -4846,7 +4847,6 @@ s8 GetBattleMovePriority(u32 battler, u16 move) return priority; } -// Function for AI with variables provided as arguments to speed the computation time s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMoves, u32 ability1, u32 ability2, u32 holdEffectBattler1, u32 holdEffectBattler2, u32 speedBattler1, u32 speedBattler2, s32 priority1, s32 priority2) { @@ -5135,6 +5135,7 @@ static void TurnValuesCleanUp(bool8 var0) gSpecialStatuses[i].parentalBondState = PARENTAL_BOND_OFF; gBattleStruct->battlerState[i].usedEjectItem = FALSE; + gProtectStructs[i].lashOutAffected = FALSE; } gSideStatuses[B_SIDE_PLAYER] &= ~(SIDE_STATUS_QUICK_GUARD | SIDE_STATUS_WIDE_GUARD | SIDE_STATUS_CRAFTY_SHIELD | SIDE_STATUS_MAT_BLOCK); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index f743879fc6..915cb16758 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -7149,6 +7149,9 @@ static void Cmd_moveend(void) if (numEjectPackBattlers > 1) SortBattlersBySpeed(battlers, FALSE); + for (i = 0; i < gBattlersCount; i++) + gProtectStructs[i].statFell = FALSE; // restore for every possible eject pack battler + for (i = 0; i < gBattlersCount; i++) { u32 battler = battlers[i]; @@ -7167,7 +7170,6 @@ static void Cmd_moveend(void) BattleScriptPushCursor(); gBattlescriptCurrInstr = BattleScript_EjectPackActivates; AI_DATA->ejectPackSwitch = TRUE; - gProtectStructs[battler].statFell = FALSE; break; // Only the fastest Eject item activates } } @@ -12588,7 +12590,8 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr } else { - gProtectStructs[battler].statFell = TRUE; // Eject pack, lash out + gProtectStructs[battler].statFell = TRUE; + gProtectStructs[battler].lashOutAffected = TRUE; gBattleCommunication[MULTISTRING_CHOOSER] = (gBattlerTarget == battler); // B_MSG_ATTACKER_STAT_FELL or B_MSG_DEFENDER_STAT_FELL } } @@ -18811,3 +18814,87 @@ void BS_SetSteelsurge(void) gBattlescriptCurrInstr = cmd->nextInstr; } } + +void BS_JumpIfIntimidateAbilityPrevented(void) +{ + NATIVE_ARGS(); + + u32 hasAbility = FALSE; + u32 ability = GetBattlerAbility(gBattlerTarget); + + switch (ability) + { + case ABILITY_INNER_FOCUS: + case ABILITY_SCRAPPY: + case ABILITY_OWN_TEMPO: + case ABILITY_OBLIVIOUS: + if (B_UPDATED_INTIMIDATE >= GEN_8) + { + hasAbility = TRUE; + gBattlescriptCurrInstr = BattleScript_IntimidatePrevented; + } + else + { + gBattlescriptCurrInstr = cmd->nextInstr; + } + break; + case ABILITY_GUARD_DOG: + hasAbility = TRUE; + gBattlescriptCurrInstr = BattleScript_IntimidateInReverse; + break; + default: + gBattlescriptCurrInstr = cmd->nextInstr; + break; + } + + if (hasAbility) + { + gLastUsedAbility = ability; + gBattlerAbility = gBattlerTarget; + RecordAbilityBattle(gBattlerTarget, gLastUsedAbility); + } +} + +void BS_TryIntimidatEjectpack(void) +{ + NATIVE_ARGS(); + + u32 affectedBattler = 0xFF; + u32 battler = BATTLE_OPPOSITE(gBattlerAttacker); + u32 partnerBattler = BATTLE_PARTNER(battler); + + bool32 ejectPackBattler = CanEjectPackTrigger(gBattlerAttacker, battler, MOVE_NONE); + bool32 ejectPackPartnerBattler = CanEjectPackTrigger(gBattlerAttacker, partnerBattler, MOVE_NONE); + + if (ejectPackBattler && ejectPackPartnerBattler) + { + u32 battlerSpeed = GetBattlerTotalSpeedStat(battler); + u32 partnerbattlerSpeed = GetBattlerTotalSpeedStat(partnerBattler); + + if (battlerSpeed >= partnerbattlerSpeed) + affectedBattler = battler; + else + affectedBattler = partnerBattler; + } + else if (ejectPackBattler) + { + affectedBattler = battler; + } + else if (ejectPackPartnerBattler) + { + affectedBattler = partnerBattler; + } + + gBattlescriptCurrInstr = cmd->nextInstr; + if (affectedBattler != 0xFF) + { + gProtectStructs[battler].statFell = FALSE; + gProtectStructs[partnerBattler].statFell = FALSE; + AI_DATA->ejectPackSwitch = TRUE; + gBattleScripting.battler = affectedBattler; + gLastUsedItem = gBattleMons[affectedBattler].item; + RecordItemEffectBattle(affectedBattler, HOLD_EFFECT_EJECT_PACK); + BattleScriptPushCursor(); + gBattlescriptCurrInstr = BattleScript_EjectPackActivate_Ret; + } +} diff --git a/src/battle_util.c b/src/battle_util.c index 05f5b83602..b1e479f06b 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -8449,9 +8449,6 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler, bool32 moveTurn) gBattlescriptCurrInstr = BattleScript_WhiteHerbRet; } break; - case HOLD_EFFECT_EJECT_PACK: - effect = TryEjectPack(battler, ITEMEFFECT_ON_SWITCH_IN); - break; } break; } @@ -9229,7 +9226,7 @@ static inline u32 CalcMoveBasePower(struct DamageCalculationData *damageCalcData basePower *= 2; break; case EFFECT_LASH_OUT: - if (gProtectStructs[battlerAtk].statFell) + if (gProtectStructs[battlerAtk].lashOutAffected) basePower *= 2; break; case EFFECT_EXPLOSION: diff --git a/test/battle/ability/intimidate.c b/test/battle/ability/intimidate.c index dd1f761dee..e5571b23fc 100644 --- a/test/battle/ability/intimidate.c +++ b/test/battle/ability/intimidate.c @@ -104,7 +104,7 @@ DOUBLE_BATTLE_TEST("Intimidate doesn't activate on an empty field in a double ba } } -SINGLE_BATTLE_TEST("Intimidate and Eject Button force the opponent to Attack") +SINGLE_BATTLE_TEST("Intimidate and Eject Button don't force the opponent to Attack") { GIVEN { ASSUME(gItemsInfo[ITEM_EJECT_BUTTON].holdEffect == HOLD_EFFECT_EJECT_BUTTON); @@ -365,6 +365,7 @@ DOUBLE_BATTLE_TEST("Intimidate will correctly decrease the attack of the second } SCENE { ABILITY_POPUP(opponentLeft, ABILITY_INTIMIDATE); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); ABILITY_POPUP(playerLeft, ABILITY_PROTOSYNTHESIS); @@ -372,7 +373,6 @@ DOUBLE_BATTLE_TEST("Intimidate will correctly decrease the attack of the second ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); } - ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); } } @@ -390,3 +390,48 @@ SINGLE_BATTLE_TEST("Intimdate does not lose timing after mega evolution and swit ABILITY_POPUP(opponent, ABILITY_INTIMIDATE); } } + +DOUBLE_BATTLE_TEST("Intimidate drop down both opposing atk before eject pack has the chance to activate") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_EKANS) { Ability(ABILITY_INTIMIDATE); } + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_EKANS) { Ability(ABILITY_INTIMIDATE); } + } WHEN { + TURN { SWITCH(opponentLeft, 2); SEND_OUT(playerLeft, 2); } + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + ABILITY_POPUP(playerLeft, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + } +} + +DOUBLE_BATTLE_TEST("Intimidate will not miss timing for competitive") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_MILOTIC) { Ability(ABILITY_COMPETITIVE); } + PLAYER(SPECIES_EKANS) { Ability(ABILITY_INTIMIDATE); } + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_EKANS) { Ability(ABILITY_INTIMIDATE); } + } WHEN { + TURN { SWITCH(opponentLeft, 2); SEND_OUT(playerLeft, 2); } + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + ABILITY_POPUP(playerRight, ABILITY_COMPETITIVE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + ABILITY_POPUP(playerLeft, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + } +} diff --git a/test/battle/hold_effect/eject_pack.c b/test/battle/hold_effect/eject_pack.c index da053ec5e6..0a21362a99 100644 --- a/test/battle/hold_effect/eject_pack.c +++ b/test/battle/hold_effect/eject_pack.c @@ -160,3 +160,74 @@ SINGLE_BATTLE_TEST("Eject Pack will miss timing to switch out user if Eject Butt EXPECT(opponent->species == SPECIES_WYNAUT); } } + +DOUBLE_BATTLE_TEST("Eject Pack: Only the fastest Eject Pack will activate after intimidate") +{ + u32 speed; + + PARAMETRIZE { speed = 1; } + PARAMETRIZE { speed = 11; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(10); Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_WYNAUT) { Speed(speed); Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_WYNAUT) { Speed(4); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(5); } + OPPONENT(SPECIES_EKANS) { Speed(6); Ability(ABILITY_INTIMIDATE); } + } WHEN { + TURN { + SWITCH(opponentLeft, 2); + if (speed == 11) + SEND_OUT(playerRight, 2); + else + SEND_OUT(playerLeft, 2); + } + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + if (speed == 11) { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerRight); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + } else { + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerRight); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + } + } +} + +DOUBLE_BATTLE_TEST("Eject Pack: Only the fastest Eject Pack will activate after a move stat drop") +{ + u32 speed; + + PARAMETRIZE { speed = 1; } + PARAMETRIZE { speed = 11; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(10); Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_WYNAUT) { Speed(speed); Item(ITEM_EJECT_PACK); } + PLAYER(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_WYNAUT) { Speed(4); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(5); } + } WHEN { + TURN { + MOVE(opponentLeft, MOVE_BUBBLE); + if (speed == 11) + SEND_OUT(playerRight, 2); + else + SEND_OUT(playerLeft, 2); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_BUBBLE, opponentLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + if (speed == 11) { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerRight); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + } else { + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerRight); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, playerLeft); + } + } +} From 007d7e712e19a1a740d3d2b318a360991eaa4d3d Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:14:53 +0100 Subject: [PATCH 02/13] Fixes Neutralizing Gas activating again after switch-in (#6667) --- src/battle_script_commands.c | 1 + test/battle/ability/teraform_zero.c | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 915cb16758..225646ee29 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -8179,6 +8179,7 @@ static bool32 DoSwitchInEffectsForBattler(u32 battler) { gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_SWITCHIN_NEUTRALIZING_GAS; gSpecialStatuses[battler].announceNeutralizingGas = TRUE; + gDisableStructs[battler].neutralizingGas = TRUE; gBattlerAbility = battler; BattleScriptPushCursor(); gBattlescriptCurrInstr = BattleScript_SwitchInAbilityMsgRet; diff --git a/test/battle/ability/teraform_zero.c b/test/battle/ability/teraform_zero.c index 09ce921931..96186f2047 100644 --- a/test/battle/ability/teraform_zero.c +++ b/test/battle/ability/teraform_zero.c @@ -84,7 +84,6 @@ SINGLE_BATTLE_TEST("Teraform Zero cannot be copied") DOUBLE_BATTLE_TEST("Teraform Zero shouldn't cause Neutralizing Gas to show it's popup when trying to activate") { - KNOWN_FAILING; // #5010 GIVEN { PLAYER(SPECIES_TERAPAGOS_TERASTAL); PLAYER(SPECIES_ABSOL) {Ability(ABILITY_PRESSURE); } @@ -95,8 +94,8 @@ DOUBLE_BATTLE_TEST("Teraform Zero shouldn't cause Neutralizing Gas to show it's TURN { SWITCH(playerRight, 2); MOVE(playerLeft, MOVE_CELEBRATE, gimmick: GIMMICK_TERA); } } SCENE { MESSAGE("Terapagos is storing energy!"); - MESSAGE("Terapagos terastalized into the Stellar type!"); + MESSAGE("Terapagos terastallized into the Stellar type!"); NOT ABILITY_POPUP(playerRight, ABILITY_NEUTRALIZING_GAS); - MESSAGE("Terapagos used Celebreate!"); + MESSAGE("Terapagos used Celebrate!"); } } From 72839fc002e805d4f38986c8b80e83814f31c742 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Tue, 22 Apr 2025 00:38:39 +0200 Subject: [PATCH 03/13] Fixes Bug Bite eaten berry not ignoring Unnerve (#6666) --- src/battle_main.c | 3 +++ src/battle_util.c | 3 +++ test/battle/move_effect_secondary/bug_bite.c | 15 +++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/battle_main.c b/src/battle_main.c index 02cd58172d..85194a0d54 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -5106,6 +5106,9 @@ static void TurnValuesCleanUp(bool8 var0) gProtectStructs[i].spikyShielded = FALSE; gProtectStructs[i].kingsShielded = FALSE; gProtectStructs[i].banefulBunkered = FALSE; + gProtectStructs[i].obstructed = FALSE; + gProtectStructs[i].silkTrapped = FALSE; + gProtectStructs[i].burningBulwarked = FALSE; gProtectStructs[i].quash = FALSE; gProtectStructs[i].usedCustapBerry = FALSE; gProtectStructs[i].quickDraw = FALSE; diff --git a/src/battle_util.c b/src/battle_util.c index b1e479f06b..d9e64c969c 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -7234,6 +7234,9 @@ static u32 ItemHealHp(u32 battler, u32 itemId, enum ItemCaseId caseID, bool32 pe static bool32 UnnerveOn(u32 battler, u32 itemId) { + if (gBattleScripting.overrideBerryRequirements > 0) // Berries that aren't eaten naturally ignore unnerve + return FALSE; + if (ItemId_GetPocket(itemId) == POCKET_BERRIES && IsUnnerveAbilityOnOpposingSide(battler)) return TRUE; return FALSE; diff --git a/test/battle/move_effect_secondary/bug_bite.c b/test/battle/move_effect_secondary/bug_bite.c index e086941a5e..70e0966caa 100644 --- a/test/battle/move_effect_secondary/bug_bite.c +++ b/test/battle/move_effect_secondary/bug_bite.c @@ -133,3 +133,18 @@ SINGLE_BATTLE_TEST("Tanga Berry activates before Bug Bite") EXPECT_EQ(player->item, ITEM_NONE); } } + +SINGLE_BATTLE_TEST("Bug Bite ignores Unnerve") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_TYRANITAR) { Ability(ABILITY_UNNERVE); Item(ITEM_ORAN_BERRY); } + } WHEN { + TURN { MOVE(player, MOVE_BUG_BITE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_BUG_BITE, player); + HP_BAR(player); + } THEN { + EXPECT_EQ(opponent->item, ITEM_NONE); + } +} From 91fc406b0a733930b83e884a92cc164308c1df7c Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:54:04 +0100 Subject: [PATCH 04/13] Fixes Mirror Armor stat drops not being stopped by Substitute (#6675) --- data/battle_scripts_1.s | 9 ++++++++- test/battle/ability/mirror_armor.c | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index a4f13be0b9..c6d6075ad5 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -3202,7 +3202,7 @@ BattleScript_StatDownEnd:: BattleScript_MirrorArmorReflect:: pause B_WAIT_TIME_SHORT call BattleScript_AbilityPopUp - jumpifsubstituteblocks BattleScript_AbilityNoSpecificStatLoss + jumpifstatus2 BS_ATTACKER, STATUS2_SUBSTITUTE, BattleScript_MirrorArmorDoesntAffect BattleScript_MirrorArmorReflectStatLoss: statbuffchange MOVE_EFFECT_AFFECTS_USER | STAT_CHANGE_MIRROR_ARMOR | STAT_CHANGE_NOT_PROTECT_AFFECTED | STAT_CHANGE_ALLOW_PTR, BattleScript_MirrorArmorReflectEnd jumpifbyte CMP_LESS_THAN, cMULTISTRING_CHOOSER, B_MSG_STAT_WONT_DECREASE, BattleScript_MirrorArmorReflectAnim @@ -3216,6 +3216,13 @@ BattleScript_MirrorArmorReflectPrintString: BattleScript_MirrorArmorReflectEnd: return +BattleScript_MirrorArmorDoesntAffect: + swapattackerwithtarget + printstring STRINGID_ITDOESNTAFFECT + waitmessage B_WAIT_TIME_LONG + swapattackerwithtarget + return + BattleScript_MirrorArmorReflectWontFall: copybyte gBattlerTarget, gBattlerAttacker @ STRINGID_STATSWONTDECREASE uses target goto BattleScript_MirrorArmorReflectPrintString diff --git a/test/battle/ability/mirror_armor.c b/test/battle/ability/mirror_armor.c index 0918682d76..97e7616c94 100644 --- a/test/battle/ability/mirror_armor.c +++ b/test/battle/ability/mirror_armor.c @@ -102,10 +102,8 @@ SINGLE_BATTLE_TEST("Mirror Armor lowers the Attack of Pokemon with Intimidate") } } -// Unsure whether this should or should not fail, as Showdown has conflicting information. Needs testing in gen8 games. SINGLE_BATTLE_TEST("Mirror Armor doesn't lower the stats of an attacking Pokemon behind Substitute") { - KNOWN_FAILING; GIVEN { PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } OPPONENT(SPECIES_WYNAUT); From 71cfab64774b5148be27e0677015257d16022e26 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:01:00 +0200 Subject: [PATCH 05/13] Fixes Magician Life Orb interaction (#6676) --- include/battle.h | 4 +--- include/battle_util.h | 1 + src/battle_script_commands.c | 2 -- src/battle_util.c | 32 +++++++++++--------------------- test/battle/ability/magician.c | 8 +++----- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/include/battle.h b/include/battle.h index 1189ada4ea..185e565ed2 100644 --- a/include/battle.h +++ b/include/battle.h @@ -228,13 +228,11 @@ struct SpecialStatus // End of byte u8 dancerUsedMove:1; u8 dancerOriginalTarget:3; - u8 preventLifeOrbDamage:1; // So that Life Orb doesn't activate various effects. u8 distortedTypeMatchups:1; u8 teraShellAbilityDone:1; u8 criticalHit:1; - // End of byte u8 enduredDamage:1; - u8 padding:7; + // End of byte }; struct SideTimer diff --git a/include/battle_util.h b/include/battle_util.h index 5fef1b4f60..e4a54990c0 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -351,5 +351,6 @@ bool32 HasWeatherEffect(void); bool32 IsMovePowderBlocked(u32 battlerAtk, u32 battlerDef, u32 move); bool32 EmergencyExitCanBeTriggered(u32 battler); u32 RestoreWhiteHerbStats(u32 battler); +bool32 IsFutureSightAttackerInParty(u32 battlerAtk, u32 battlerDef); #endif // GUARD_BATTLE_UTIL_H diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 225646ee29..c812df97c8 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -7064,7 +7064,6 @@ static void Cmd_moveend(void) gEffectBattler = gBattlerTarget; BattleScriptPushCursor(); gBattlescriptCurrInstr = BattleScript_MagicianActivates; - gSpecialStatuses[gBattlerAttacker].preventLifeOrbDamage = TRUE; effect = TRUE; } gBattleScripting.moveendState++; @@ -7404,7 +7403,6 @@ static void Cmd_moveend(void) gBattleStruct->ateBoost[gBattlerAttacker] = FALSE; gStatuses3[gBattlerAttacker] &= ~STATUS3_ME_FIRST; gSpecialStatuses[gBattlerAttacker].gemBoost = FALSE; - gSpecialStatuses[gBattlerAttacker].preventLifeOrbDamage = 0; gSpecialStatuses[gBattlerTarget].berryReduced = FALSE; gSpecialStatuses[gBattlerTarget].distortedTypeMatchups = FALSE; gBattleScripting.moveEffect = 0; diff --git a/src/battle_util.c b/src/battle_util.c index d9e64c969c..0093550a47 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -2930,8 +2930,6 @@ bool32 HandleWishPerishSongOnTurnEnd(void) if (gWishFutureKnock.futureSightCounter[battler] == gBattleTurnCounter && !(gAbsentBattlerFlags & (1u << battler))) { - struct Pokemon *party; - if (gWishFutureKnock.futureSightCounter[battler] == gBattleTurnCounter && gWishFutureKnock.futureSightCounter[BATTLE_PARTNER(battler)] <= gBattleTurnCounter) { @@ -2952,8 +2950,7 @@ bool32 HandleWishPerishSongOnTurnEnd(void) gBattlerAttacker = gWishFutureKnock.futureSightBattlerIndex[battler]; gCurrentMove = gWishFutureKnock.futureSightMove[battler]; - party = GetSideParty(GetBattlerSide(gBattlerAttacker)); - if (&party[gWishFutureKnock.futureSightPartyIndex[gBattlerTarget]] == &party[gBattlerPartyIndexes[gBattlerAttacker]]) + if (!IsFutureSightAttackerInParty(gBattlerAttacker, gBattlerTarget)) SetTypeBeforeUsingMove(gCurrentMove, gBattlerAttacker); BattleScriptExecute(BattleScript_MonTookFutureAttack); @@ -5879,12 +5876,9 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 && IsBattlerAlive(gBattlerAttacker)) { // Prevent Innards Out effect if Future Sight user is currently not on field - if (GetMoveEffect(gCurrentMove) == EFFECT_FUTURE_SIGHT) - { - if (gWishFutureKnock.futureSightPartyIndex[gBattlerTarget] != gBattlerPartyIndexes[gBattlerAttacker] - && gWishFutureKnock.futureSightPartyIndex[gBattlerTarget] != BATTLE_PARTNER(gBattlerPartyIndexes[gBattlerAttacker])) - break; - } + if (GetMoveEffect(gCurrentMove) == EFFECT_FUTURE_SIGHT + && IsFutureSightAttackerInParty(gBattlerAttacker, gBattlerTarget)) + break; gBattleScripting.battler = gBattlerTarget; gBattleStruct->moveDamage[gBattlerAttacker] = gBattleStruct->moveDamage[gBattlerTarget]; @@ -8202,7 +8196,7 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler, bool32 moveTurn) && !(TestIfSheerForceAffected(gBattlerAttacker, gCurrentMove)) && GetBattlerAbility(gBattlerAttacker) != ABILITY_MAGIC_GUARD && !gProtectStructs[gBattlerAttacker].confusionSelfDmg - && !gSpecialStatuses[gBattlerAttacker].preventLifeOrbDamage) + && !IsFutureSightAttackerInParty(gBattlerAttacker, gBattlerTarget)) { gBattleStruct->moveDamage[gBattlerAttacker] = GetNonDynamaxMaxHP(gBattlerAttacker) / 10; if (gBattleStruct->moveDamage[gBattlerAttacker] == 0) @@ -10510,8 +10504,6 @@ static inline s32 DoFutureSightAttackDamageCalcVars(struct DamageCalculationData if (dmg == 0) dmg = 1; - gSpecialStatuses[battlerAtk].preventLifeOrbDamage = TRUE; - return dmg; } @@ -10538,14 +10530,11 @@ static u32 GetWeather(void) return gBattleWeather; } -static inline bool32 IsFutureSightAttackerInParty(struct DamageCalculationData *damageCalcData) +bool32 IsFutureSightAttackerInParty(u32 battlerAtk, u32 battlerDef) { - if (GetMoveEffect(damageCalcData->move) != EFFECT_FUTURE_SIGHT) - return FALSE; - - struct Pokemon *party = GetSideParty(GetBattlerSide(damageCalcData->battlerAtk)); - return &party[gWishFutureKnock.futureSightPartyIndex[damageCalcData->battlerDef]] - != &party[gBattlerPartyIndexes[damageCalcData->battlerAtk]]; + struct Pokemon *party = GetSideParty(GetBattlerSide(battlerAtk)); + return &party[gWishFutureKnock.futureSightPartyIndex[battlerDef]] != &party[gBattlerPartyIndexes[battlerAtk]] + && &party[gWishFutureKnock.futureSightPartyIndex[battlerDef]] != &party[BATTLE_PARTNER(gBattlerPartyIndexes[battlerAtk])]; } s32 CalculateMoveDamage(struct DamageCalculationData *damageCalcData, u32 fixedBasePower) @@ -10557,7 +10546,8 @@ s32 CalculateMoveDamage(struct DamageCalculationData *damageCalcData, u32 fixedB GetBattlerAbility(damageCalcData->battlerDef), damageCalcData->updateFlags); - if (IsFutureSightAttackerInParty(damageCalcData)) + if (GetMoveEffect(damageCalcData->move) == EFFECT_FUTURE_SIGHT + && IsFutureSightAttackerInParty(damageCalcData->battlerAtk, damageCalcData->battlerDef)) return DoFutureSightAttackDamageCalc(damageCalcData, typeEffectivenessMultiplier, GetWeather()); return DoMoveDamageCalc(damageCalcData, fixedBasePower, typeEffectivenessMultiplier, GetWeather()); diff --git a/test/battle/ability/magician.c b/test/battle/ability/magician.c index f622ac07df..d12fe920b8 100644 --- a/test/battle/ability/magician.c +++ b/test/battle/ability/magician.c @@ -1,7 +1,7 @@ #include "global.h" #include "test/battle.h" -SINGLE_BATTLE_TEST("Magician does not get self-damage recoil after stealing Life Orb") +SINGLE_BATTLE_TEST("Magician gets self-damage recoil after stealing Life Orb") { GIVEN { ASSUME(gItemsInfo[ITEM_LIFE_ORB].holdEffect == HOLD_EFFECT_LIFE_ORB); @@ -16,10 +16,8 @@ SINGLE_BATTLE_TEST("Magician does not get self-damage recoil after stealing Life MESSAGE("Delphox used Tackle!"); ABILITY_POPUP(player, ABILITY_MAGICIAN); MESSAGE("Delphox stole the opposing Wobbuffet's Life Orb!"); - NONE_OF { - HP_BAR(player); - MESSAGE("Delphox was hurt by the Life Orb!"); - } + HP_BAR(player); + MESSAGE("Delphox was hurt by the Life Orb!"); // 2nd turn - Life Orb recoil happens now MESSAGE("Delphox used Tackle!"); HP_BAR(player); From 6fdb6a9e34ac2490fff306090c01f776561aea0a Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Wed, 23 Apr 2025 12:17:54 +0200 Subject: [PATCH 06/13] Fixed broken palette load for shiny followers in the sprite visualizer (#6683) Co-authored-by: Hedara --- src/event_object_movement.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/event_object_movement.c b/src/event_object_movement.c index a8d1605438..cdc2c66f8e 100644 --- a/src/event_object_movement.c +++ b/src/event_object_movement.c @@ -1840,13 +1840,10 @@ u8 CreateObjectGraphicsSprite(u16 graphicsId, void (*callback)(struct Sprite *), const struct ObjectEventGraphicsInfo *graphicsInfo = GetObjectEventGraphicsInfo(graphicsId); struct Sprite *sprite; u8 spriteId; - bool32 isShiny = graphicsId & OBJ_EVENT_MON_SHINY; spriteTemplate = Alloc(sizeof(struct SpriteTemplate)); CopyObjectGraphicsInfoToSpriteTemplate(graphicsId, callback, spriteTemplate, &subspriteTables); - if (isShiny) - graphicsId -= SPECIES_SHINY_TAG; if (OW_GFX_COMPRESS) { From 20799d5835f8d86db878aa802cbc1473e904285a Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:24:25 +0200 Subject: [PATCH 07/13] Remove misleading comment (#6684) --- src/battle_script_commands.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index c812df97c8..ede0a7749d 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -4594,7 +4594,6 @@ void SetMoveEffect(bool32 primary, bool32 certain) static bool32 CanApplyAdditionalEffect(const struct AdditionalEffect *additionalEffect) { - // Self-targeting move effects only apply after the last mon has been hit if (additionalEffect->self && NumAffectedSpreadMoveTargets() > 1 && GetNextTarget(GetBattlerMoveTargetType(gBattlerAttacker, gCurrentMove), TRUE) != MAX_BATTLERS_COUNT) From 34e91dd96ff2949683ee7cc125ed11622fc5c888 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:26:23 +0100 Subject: [PATCH 08/13] Fixes Unburden doubling speed when affected by Neutralizing Gas (#6691) --- src/battle_main.c | 4 +- test/battle/ability/unburden.c | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 test/battle/ability/unburden.c diff --git a/src/battle_main.c b/src/battle_main.c index 85194a0d54..7bb13fa5c2 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4750,6 +4750,8 @@ u32 GetBattlerTotalSpeedStatArgs(u32 battler, u32 ability, u32 holdEffect) speed = (GetHighestStatId(battler) == STAT_SPEED) ? (speed * 150) / 100 : speed; else if (ability == ABILITY_QUARK_DRIVE && !(gBattleMons[battler].status2 & STATUS2_TRANSFORMED) && (gFieldStatuses & STATUS_FIELD_ELECTRIC_TERRAIN || gDisableStructs[battler].boosterEnergyActivates)) speed = (GetHighestStatId(battler) == STAT_SPEED) ? (speed * 150) / 100 : speed; + else if (ability == ABILITY_UNBURDEN && gDisableStructs[battler].unburdenActive) + speed *= 2; // stat stages speed *= gStatStageRatios[gBattleMons[battler].statStages[STAT_SPEED]][0]; @@ -4776,8 +4778,6 @@ u32 GetBattlerTotalSpeedStatArgs(u32 battler, u32 ability, u32 holdEffect) // various effects if (gSideStatuses[GetBattlerSide(battler)] & SIDE_STATUS_TAILWIND) speed *= 2; - if (gDisableStructs[battler].unburdenActive) - speed *= 2; // paralysis drop if (gBattleMons[battler].status1 & STATUS1_PARALYSIS && ability != ABILITY_QUICK_FEET) diff --git a/test/battle/ability/unburden.c b/test/battle/ability/unburden.c new file mode 100644 index 0000000000..63a72e2ace --- /dev/null +++ b/test/battle/ability/unburden.c @@ -0,0 +1,101 @@ +#include "global.h" +#include "test/battle.h" + +SINGLE_BATTLE_TEST("Unburden doubles speed once user uses item") +{ + GIVEN { + ASSUME(ItemId_GetHoldEffect(ITEM_GRASSY_SEED) == HOLD_EFFECT_SEEDS); + ASSUME(GetMoveEffect(MOVE_U_TURN) == EFFECT_HIT_ESCAPE); + PLAYER(SPECIES_DRIFBLIM) { Ability(ABILITY_UNBURDEN); Item(ITEM_GRASSY_SEED); Speed(5); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(7); } + OPPONENT(SPECIES_RILLABOOM) { Speed(7); Ability(ABILITY_GRASSY_SURGE); } + } WHEN { + TURN { MOVE(opponent, MOVE_U_TURN); SEND_OUT(opponent, 1); } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, opponent); + ABILITY_POPUP(opponent, ABILITY_GRASSY_SURGE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 2, doubled speed + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + } +} + +SINGLE_BATTLE_TEST("Unburden doubles speed once user gets their item knocked off") +{ + GIVEN { + ASSUME(MoveHasAdditionalEffect(MOVE_KNOCK_OFF, MOVE_EFFECT_KNOCK_OFF)); + PLAYER(SPECIES_DRIFBLIM) { Ability(ABILITY_UNBURDEN); Item(ITEM_POTION); Speed(5); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(7); } + } WHEN { + TURN { MOVE(opponent, MOVE_KNOCK_OFF); } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_KNOCK_OFF, opponent); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_ITEM_KNOCKOFF, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 2, doubled speed + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + } +} + +SINGLE_BATTLE_TEST("Unburden doesn't activate when item is consumed in Neutralizing Gas") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_U_TURN) == EFFECT_HIT_ESCAPE); + ASSUME(MoveHasAdditionalEffect(MOVE_KNOCK_OFF, MOVE_EFFECT_KNOCK_OFF)); + PLAYER(SPECIES_DRIFBLIM) { Ability(ABILITY_UNBURDEN); Item(ITEM_POTION); Speed(5); } + OPPONENT(SPECIES_WEEZING) { Speed(7); Ability(ABILITY_NEUTRALIZING_GAS); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(7); } + } WHEN { + TURN { MOVE(opponent, MOVE_KNOCK_OFF); } + TURN { MOVE(opponent, MOVE_U_TURN); SEND_OUT(opponent, 1); } + TURN { } + } SCENE { + ABILITY_POPUP(opponent, ABILITY_NEUTRALIZING_GAS); + ANIMATION(ANIM_TYPE_MOVE, MOVE_KNOCK_OFF, opponent); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_ITEM_KNOCKOFF, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 2, no speed increase + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, opponent); + MESSAGE("The effects of the neutralizing gas wore off!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 3, no speed increase + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + } +} + +SINGLE_BATTLE_TEST("Unburden doubling speed effect is ignored by Neutralizing Gas") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_U_TURN) == EFFECT_HIT_ESCAPE); + ASSUME(MoveHasAdditionalEffect(MOVE_KNOCK_OFF, MOVE_EFFECT_KNOCK_OFF)); + PLAYER(SPECIES_DRIFBLIM) { Ability(ABILITY_UNBURDEN); Item(ITEM_POTION); Speed(5); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(7); } + OPPONENT(SPECIES_WEEZING) { Speed(7); Ability(ABILITY_NEUTRALIZING_GAS); } + } WHEN { + TURN { MOVE(opponent, MOVE_KNOCK_OFF); } + TURN { MOVE(opponent, MOVE_U_TURN); SEND_OUT(opponent, 1); } + TURN { MOVE(opponent, MOVE_U_TURN); SEND_OUT(opponent, 0); } + TURN { } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_KNOCK_OFF, opponent); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_ITEM_KNOCKOFF, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 2, doubled speed + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, opponent); + ABILITY_POPUP(opponent, ABILITY_NEUTRALIZING_GAS); + // Turn 3, no speed increase + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, opponent); + MESSAGE("The effects of the neutralizing gas wore off!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + // Turn 4, doubled speed + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + } +} From 5cd7bd77b32187ee82d0bb60611980b16299f1cb Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Thu, 24 Apr 2025 17:06:47 +0200 Subject: [PATCH 09/13] Fix various Toxic Spikes interactions (#6690) Co-authored-by: Hedara Co-authored-by: PhallenTree <168426989+PhallenTree@users.noreply.github.com> --- src/battle_script_commands.c | 8 +------ test/battle/ability/comatose.c | 32 ++++++++++++++++++++++++++++ test/battle/ability/immunity.c | 17 +++++++++++++++ test/battle/ability/purifying_salt.c | 17 +++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index ede0a7749d..72e05cfb68 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -8240,13 +8240,7 @@ static bool32 DoSwitchInEffectsForBattler(u32 battler) else if (IsBattlerAffectedByHazards(battler, TRUE)) { i = GetBattlerAbility(battler); - if (!(gBattleMons[battler].status1 & STATUS1_ANY) - && !IS_BATTLER_OF_TYPE(battler, TYPE_STEEL) - && i != ABILITY_IMMUNITY - && i != ABILITY_PURIFYING_SALT - && !IsAbilityOnSide(battler, ABILITY_PASTEL_VEIL) - && !(gSideStatuses[GetBattlerSide(battler)] & SIDE_STATUS_SAFEGUARD) - && !(gFieldStatuses & STATUS_FIELD_MISTY_TERRAIN)) + if (CanBePoisoned(gBattlerAttacker, battler, i)) { if (gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount >= 2) gBattleMons[battler].status1 |= STATUS1_TOXIC_POISON; diff --git a/test/battle/ability/comatose.c b/test/battle/ability/comatose.c index b23a6ca85f..d714ee5e3d 100644 --- a/test/battle/ability/comatose.c +++ b/test/battle/ability/comatose.c @@ -55,3 +55,35 @@ SINGLE_BATTLE_TEST("Comatose may be suppressed if pokemon transformed into a pok else if (move == MOVE_SLEEP_POWDER) { STATUS_ICON(opponent, sleep: TRUE); } } } + +SINGLE_BATTLE_TEST("Comatose pokemon doesn't get poisoned by Toxic Spikes on switch-in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(player, 1); } + } SCENE { + NOT STATUS_ICON(player, STATUS1_POISON); + ABILITY_POPUP(player, ABILITY_COMATOSE); + NOT HP_BAR(player); + } +} + +SINGLE_BATTLE_TEST("Comatose pokemon don't get poisoned by Toxic Spikes on switch-in if forced in by phazing with Mold Breaker") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); } + OPPONENT(SPECIES_PINSIR) { Ability(ABILITY_MOLD_BREAKER); } + } WHEN { + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponent, MOVE_DRAGON_TAIL); } + } SCENE { + NOT STATUS_ICON(player, STATUS1_POISON); + ABILITY_POPUP(player, ABILITY_COMATOSE); + NOT HP_BAR(player); + } +} diff --git a/test/battle/ability/immunity.c b/test/battle/ability/immunity.c index 92e32d31f3..365b5d0d50 100644 --- a/test/battle/ability/immunity.c +++ b/test/battle/ability/immunity.c @@ -45,3 +45,20 @@ SINGLE_BATTLE_TEST("Immunity prevents Toxic Spikes poison") NOT STATUS_ICON(opponent, poison: TRUE); } } + +SINGLE_BATTLE_TEST("Immunity doesn't prevent pokemon from being poisoned by Toxic Spikes on switch-in if forced in by phazing with Mold Breaker, but it cures it immediately") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_DRAGON_TAIL) == EFFECT_HIT_SWITCH_TARGET); + ASSUME(GetMoveEffect(MOVE_TOXIC_SPIKES) == EFFECT_TOXIC_SPIKES); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); } + OPPONENT(SPECIES_PINSIR) { Ability(ABILITY_MOLD_BREAKER); } + } WHEN { + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponent, MOVE_DRAGON_TAIL); } + } SCENE { + STATUS_ICON(player, STATUS1_POISON); + NOT HP_BAR(player); + } +} diff --git a/test/battle/ability/purifying_salt.c b/test/battle/ability/purifying_salt.c index cb8fc6ca56..85d2e9c009 100644 --- a/test/battle/ability/purifying_salt.c +++ b/test/battle/ability/purifying_salt.c @@ -100,3 +100,20 @@ SINGLE_BATTLE_TEST("Purifying Salt user can't be poisoned by Toxic Spikes") EXPECT_EQ(player->status1, STATUS1_NONE); } } + +SINGLE_BATTLE_TEST("Purifying Salt doesn't prevent pokemon from being poisoned by Toxic Spikes on switch-in if forced in by phazing with Mold Breaker") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_DRAGON_TAIL) == EFFECT_HIT_SWITCH_TARGET); + ASSUME(GetMoveEffect(MOVE_TOXIC_SPIKES) == EFFECT_TOXIC_SPIKES); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_GARGANACL) { Ability(ABILITY_PURIFYING_SALT); } + OPPONENT(SPECIES_PINSIR) { Ability(ABILITY_MOLD_BREAKER); } + } WHEN { + TURN { MOVE(opponent, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponent, MOVE_DRAGON_TAIL); } + } SCENE { + STATUS_ICON(player, STATUS1_POISON); + HP_BAR(player); + } +} From d53c616e068c73713ffc1932947a4e4d60f8aedf Mon Sep 17 00:00:00 2001 From: Bassoonian Date: Thu, 24 Apr 2025 20:13:03 +0200 Subject: [PATCH 10/13] Adds a whole bunch of new tests (#6685) --- test/battle/ability/analytic.c | 20 +++- test/battle/move_effect/aqua_ring.c | 30 +++++- test/battle/move_effect/attract.c | 106 +++++++++++++++++++- test/battle/move_effect/bestow.c | 134 ++++++++++++++++++++++++-- test/battle/move_effect/captivate.c | 113 +++++++++++++++++++++- test/battle/move_effect/entrainment.c | 48 ++++++++- 6 files changed, 430 insertions(+), 21 deletions(-) diff --git a/test/battle/ability/analytic.c b/test/battle/ability/analytic.c index 44c42ae2d5..526e0983f4 100644 --- a/test/battle/ability/analytic.c +++ b/test/battle/ability/analytic.c @@ -1,7 +1,25 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Analytic increases the power of moves by 30% if it's the last one that uses its move"); +SINGLE_BATTLE_TEST("Analytic increases the power of moves by 30% if it's the last one that uses its move", s16 damage) +{ + u32 speed; + + PARAMETRIZE { speed = 3; } + PARAMETRIZE { speed = 1; } + + GIVEN { + PLAYER(SPECIES_MAGNEMITE) { Ability(ABILITY_ANALYTIC); Speed(speed); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.3), results[1].damage); + } +} + TO_DO_BATTLE_TEST("Analytic takes into account modifications to speeed an priority (Gen 5-8)"); //Eg. Paralysis, Power Weight, Stall TO_DO_BATTLE_TEST("Analytic does not take into account modifications to speeed an priority (Gen 8)"); //Eg. Paralysis, Power Weight, Stall TO_DO_BATTLE_TEST("Analytic takes into account the turn order of what fainted Pokémon would've moved"); diff --git a/test/battle/move_effect/aqua_ring.c b/test/battle/move_effect/aqua_ring.c index 158c839d79..d137a35276 100644 --- a/test/battle/move_effect/aqua_ring.c +++ b/test/battle/move_effect/aqua_ring.c @@ -1,6 +1,32 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Aqua Ring recovers 1/16th HP at end of turn"); -TO_DO_BATTLE_TEST("Aqua Ring can be used under Heal Block but will not heal the user"); +SINGLE_BATTLE_TEST("Aqua Ring recovers 1/16th HP at end of turn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_AQUA_RING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + } THEN { + EXPECT(player->hp == 58); + } +} + +SINGLE_BATTLE_TEST("Aqua Ring can be used under Heal Block but will not heal the user") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(50); MaxHP(128); Speed(50); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(100); } + } WHEN { + TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_AQUA_RING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AQUA_RING, player); + } THEN { + EXPECT(player->hp == 50); + } +} + TO_DO_BATTLE_TEST("Baton Pass passes Aqua Ring's effect"); diff --git a/test/battle/move_effect/attract.c b/test/battle/move_effect/attract.c index 748a88a950..be30483358 100644 --- a/test/battle/move_effect/attract.c +++ b/test/battle/move_effect/attract.c @@ -1,7 +1,105 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Attract causes the target to become infatuated with the user if they have opposite genders"); -TO_DO_BATTLE_TEST("Attract ignores type immunity"); -TO_DO_BATTLE_TEST("Attract bypasses Substitute"); -TO_DO_BATTLE_TEST("Attract fails if the target is already infatuated"); +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_ATTRACT) == EFFECT_ATTRACT); + ASSUME(gSpeciesInfo[SPECIES_NIDOKING].genderRatio == MON_MALE); + ASSUME(gSpeciesInfo[SPECIES_NIDOQUEEN].genderRatio == MON_FEMALE); +} + +SINGLE_BATTLE_TEST("Attract causes the target to become infatuated with the user if they have opposite genders") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_NIDOKING); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ATTRACT, player); + MESSAGE("The opposing Nidoking fell in love!"); + } THEN { + EXPECT(opponent->status2 & STATUS2_INFATUATION); + } +} + +SINGLE_BATTLE_TEST("Attract ignores type immunity") +{ + GIVEN { + ASSUME(GetMoveType(MOVE_ATTRACT) == TYPE_NORMAL); + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_MISDREAVUS) { Gender(MON_MALE); } + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ATTRACT, player); + MESSAGE("The opposing Misdreavus fell in love!"); + } THEN { + EXPECT(opponent->status2 & STATUS2_INFATUATION); + } +} + +SINGLE_BATTLE_TEST("Attract bypasses Substitute") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN) { Speed(90); } + OPPONENT(SPECIES_NIDOKING) { Speed(100); } + } WHEN { + TURN { MOVE(opponent, MOVE_SUBSTITUTE); } + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ATTRACT, player); + MESSAGE("The opposing Nidoking fell in love!"); + } THEN { + EXPECT(opponent->status2 & STATUS2_INFATUATION); + } +} + +SINGLE_BATTLE_TEST("Attract fails if the target is already infatuated") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_NIDOKING); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ATTRACT, player); + MESSAGE("The opposing Nidoking fell in love!"); + MESSAGE("Nidoqueen used Attract!"); + MESSAGE("But it failed!"); + } THEN { + EXPECT(opponent->status2 & STATUS2_INFATUATION); + } +} + +SINGLE_BATTLE_TEST("Attract fails when used on a Pokémon of the same gender") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_NIDOQUEEN); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + MESSAGE("Nidoqueen used Attract!"); + MESSAGE("But it failed!"); + } THEN { + EXPECT(!(opponent->status2 & STATUS2_INFATUATION)); + } +} + +SINGLE_BATTLE_TEST("Attract fails when used on a genderless Pokémon") +{ + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_STARMIE].genderRatio == MON_GENDERLESS); + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_STARMIE); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + MESSAGE("Nidoqueen used Attract!"); + MESSAGE("But it failed!"); + } THEN { + EXPECT(!(opponent->status2 & STATUS2_INFATUATION)); + } +} diff --git a/test/battle/move_effect/bestow.c b/test/battle/move_effect/bestow.c index 1161cdf8ae..24abdb3bea 100644 --- a/test/battle/move_effect/bestow.c +++ b/test/battle/move_effect/bestow.c @@ -1,10 +1,130 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Bestow transfers its held item to the target"); -TO_DO_BATTLE_TEST("Bestow fails if the user has no held item"); -TO_DO_BATTLE_TEST("Bestow fails if the target already has a held item"); -TO_DO_BATTLE_TEST("Bestow fails if the target is behind a Substitute"); -TO_DO_BATTLE_TEST("Bestow fails if the user is holding Mail"); -TO_DO_BATTLE_TEST("Bestow fails if the user's held item changes its form"); -TO_DO_BATTLE_TEST("Bestow fails if the user's held item is a Z-Crystal"); +SINGLE_BATTLE_TEST("Bestow transfers its held item to the target") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SITRUS_BERRY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } THEN { + EXPECT(player->item == ITEM_NONE); + EXPECT(opponent->item == ITEM_SITRUS_BERRY); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the user has no held item") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_NONE); + EXPECT(opponent->item == ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the target already has a held item") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SITRUS_BERRY); } + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_LUM_BERRY); } + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_SITRUS_BERRY); + EXPECT(opponent->item == ITEM_LUM_BERRY); + } +} + +#include "mail.h" +SINGLE_BATTLE_TEST("Bestow fails if the user is holding Mail") +{ + KNOWN_FAILING; + + GIVEN { + ASSUME(ItemIsMail(ITEM_ORANGE_MAIL)); + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_ORANGE_MAIL); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_ORANGE_MAIL); + EXPECT(opponent->item == ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the user's held item is a Mega Stone") +{ + GIVEN { + PLAYER(SPECIES_BLAZIKEN) { Item(ITEM_BLAZIKENITE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_BLAZIKENITE); + EXPECT(opponent->item == ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the user's held item is a Z-Crystal") +{ + GIVEN { + ASSUME(ItemId_GetHoldEffect(ITEM_FIGHTINIUM_Z) == HOLD_EFFECT_Z_CRYSTAL); + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_FIGHTINIUM_Z); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_FIGHTINIUM_Z); + EXPECT(opponent->item == ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the target is behind a Substitute") +{ + KNOWN_FAILING; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SITRUS_BERRY); Speed(50); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(100); } + } WHEN { + TURN { MOVE(opponent, MOVE_SUBSTITUTE); MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_SITRUS_BERRY); + EXPECT(opponent->item == ITEM_NONE); + } +} + +SINGLE_BATTLE_TEST("Bestow fails if the user's held item changes its form") +{ + KNOWN_FAILING; + + GIVEN { + PLAYER(SPECIES_GIRATINA_ORIGIN) { Item(ITEM_GRISEOUS_ORB); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BESTOW); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->item == ITEM_GRISEOUS_ORB); + EXPECT(opponent->item == ITEM_NONE); + } +} + diff --git a/test/battle/move_effect/captivate.c b/test/battle/move_effect/captivate.c index 0da58983c7..224e6bef07 100644 --- a/test/battle/move_effect/captivate.c +++ b/test/battle/move_effect/captivate.c @@ -1,7 +1,112 @@ #include "global.h" #include "test/battle.h" -TO_DO_BATTLE_TEST("Captivate decreases the target's Sp. Attack if they're opposite gender from the user"); -TO_DO_BATTLE_TEST("Captivate fails if the target and user share gender"); -TO_DO_BATTLE_TEST("Captivate fails if the target is genderless"); -TO_DO_BATTLE_TEST("Captivate fails if the user is genderless"); +ASSUMPTIONS +{ + ASSUME(GetMoveEffect(MOVE_CAPTIVATE) == EFFECT_CAPTIVATE); + ASSUME(gSpeciesInfo[SPECIES_NIDOKING].genderRatio == MON_MALE); + ASSUME(gSpeciesInfo[SPECIES_NIDOQUEEN].genderRatio == MON_FEMALE); + ASSUME(gSpeciesInfo[SPECIES_STARMIE].genderRatio == MON_GENDERLESS); +} + +SINGLE_BATTLE_TEST("Captivate decreases the target's Sp. Attack if they're opposite gender from the user") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_NIDOKING); + } WHEN { + TURN { MOVE(player, MOVE_CAPTIVATE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CAPTIVATE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("The opposing Nidoking's Sp. Atk harshly fell!"); + } THEN { + EXPECT(opponent->statStages[STAT_SPATK] == 4); + } +} + +SINGLE_BATTLE_TEST("Captivate fails if the target and user share gender") +{ + GIVEN { + PLAYER(SPECIES_NIDOKING); + OPPONENT(SPECIES_NIDOKING); + } WHEN { + TURN { MOVE(player, MOVE_CAPTIVATE); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(opponent->statStages[STAT_SPATK] == 6); + } +} + +SINGLE_BATTLE_TEST("Captivate fails if the target is genderless") +{ + GIVEN { + PLAYER(SPECIES_NIDOQUEEN); + OPPONENT(SPECIES_STARMIE); + } WHEN { + TURN { MOVE(player, MOVE_CAPTIVATE); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(opponent->statStages[STAT_SPATK] == 6); + } +} + +SINGLE_BATTLE_TEST("Captivate fails if the user is genderless") +{ + GIVEN { + PLAYER(SPECIES_STARMIE); + OPPONENT(SPECIES_NIDOQUEEN); + } WHEN { + TURN { MOVE(player, MOVE_CAPTIVATE); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(opponent->statStages[STAT_SPATK] == 6); + } +} + +SINGLE_BATTLE_TEST("Captivate fails if both the user and the opponent are genderless") +{ + GIVEN { + PLAYER(SPECIES_STARMIE); + OPPONENT(SPECIES_STARMIE); + } WHEN { + TURN { MOVE(player, MOVE_CAPTIVATE); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(opponent->statStages[STAT_SPATK] == 6); + } +} + +SINGLE_BATTLE_TEST("Attract fails when used by a genderless Pokémon") +{ + GIVEN { + PLAYER(SPECIES_STARMIE); + OPPONENT(SPECIES_NIDOQUEEN); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + MESSAGE("Starmie used Attract!"); + MESSAGE("But it failed!"); + } THEN { + EXPECT(!(opponent->status2 & STATUS2_INFATUATION)); + } +} + +SINGLE_BATTLE_TEST("Attract fails if both the user and the target are genderless") +{ + GIVEN { + PLAYER(SPECIES_STARMIE); + OPPONENT(SPECIES_STARMIE); + } WHEN { + TURN { MOVE(player, MOVE_ATTRACT); } + } SCENE { + MESSAGE("Starmie used Attract!"); + MESSAGE("But it failed!"); + } THEN { + EXPECT(!(opponent->status2 & STATUS2_INFATUATION)); + } +} diff --git a/test/battle/move_effect/entrainment.c b/test/battle/move_effect/entrainment.c index cccae86759..fd47937f04 100644 --- a/test/battle/move_effect/entrainment.c +++ b/test/battle/move_effect/entrainment.c @@ -14,6 +14,48 @@ AI_DOUBLE_BATTLE_TEST("AI prefers Entrainment'ing good abilities onto partner wi } } -TO_DO_BATTLE_TEST("Entrainment changes the target's Ability to match the user's"); -TO_DO_BATTLE_TEST("Entrainment fails if the user's ability has cantBeCopied flag"); -TO_DO_BATTLE_TEST("Entrainment fails if the targets's ability has cantBeOverwritten flag"); +SINGLE_BATTLE_TEST("Entrainment changes the target's Ability to match the user's") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_TELEPATHY); } + OPPONENT(SPECIES_WOBBUFFET) { Ability(ABILITY_SHADOW_TAG); } + } WHEN { + TURN { MOVE(player, MOVE_ENTRAINMENT); } + } THEN { + EXPECT(opponent->ability == ABILITY_TELEPATHY); + } +} + +SINGLE_BATTLE_TEST("Entrainment fails if the user's ability has cantBeCopied flag") +{ + GIVEN { + ASSUME(gAbilitiesInfo[ABILITY_MULTITYPE].cantBeCopied); + PLAYER(SPECIES_ARCEUS) { Ability(ABILITY_MULTITYPE); } + OPPONENT(SPECIES_WOBBUFFET) { Ability(ABILITY_SHADOW_TAG); } + } WHEN { + TURN { MOVE(player, MOVE_ENTRAINMENT); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->ability == ABILITY_MULTITYPE); + EXPECT(opponent->ability == ABILITY_SHADOW_TAG); + } +} + +SINGLE_BATTLE_TEST("Entrainment fails if the target's ability has cantBeOverwritten flag") +{ + GIVEN { + ASSUME(gAbilitiesInfo[ABILITY_MULTITYPE].cantBeOverwritten); + PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_TELEPATHY); } + OPPONENT(SPECIES_ARCEUS) { Ability(ABILITY_MULTITYPE); } + } WHEN { + TURN { MOVE(player, MOVE_ENTRAINMENT); } + } SCENE { + MESSAGE("But it failed!"); + } THEN { + EXPECT(player->ability == ABILITY_TELEPATHY); + EXPECT(opponent->ability == ABILITY_MULTITYPE); + } +} + +TO_DO_BATTLE_TEST("Entrainment fails on Dynamaxed Pokémon"); From 1cd4d2396943b1176630e9c719501de841c72cfe Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sat, 26 Apr 2025 21:46:41 +0200 Subject: [PATCH 11/13] Fixes Z and Max moves dmg reduction against side protection (#6697) --- include/battle.h | 13 +++-- src/battle_script_commands.c | 2 +- test/battle/move_effect/protect.c | 81 ++++++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/include/battle.h b/include/battle.h index 185e565ed2..86b71eb89f 100644 --- a/include/battle.h +++ b/include/battle.h @@ -906,10 +906,6 @@ static inline bool32 IsBattleMoveRecoil(u32 move) } #define IS_BATTLER_PROTECTED(battlerId)(gProtectStructs[battlerId].protected \ - || gSideStatuses[GetBattlerSide(battlerId)] & SIDE_STATUS_WIDE_GUARD \ - || gSideStatuses[GetBattlerSide(battlerId)] & SIDE_STATUS_QUICK_GUARD \ - || gSideStatuses[GetBattlerSide(battlerId)] & SIDE_STATUS_CRAFTY_SHIELD \ - || gSideStatuses[GetBattlerSide(battlerId)] & SIDE_STATUS_MAT_BLOCK \ || gProtectStructs[battlerId].spikyShielded \ || gProtectStructs[battlerId].kingsShielded \ || gProtectStructs[battlerId].banefulBunkered \ @@ -1294,5 +1290,12 @@ static inline bool32 IsBattlerInvalidForSpreadMove(u32 battlerAtk, u32 battlerDe || (battlerDef == BATTLE_PARTNER(battlerAtk) && (moveTarget == MOVE_TARGET_BOTH)); } -#endif // GUARD_BATTLE_H +static inline bool32 IsBattlerSideProtected(u32 battler) +{ + return gSideStatuses[GetBattlerSide(battler)] & (SIDE_STATUS_WIDE_GUARD + | SIDE_STATUS_QUICK_GUARD + | SIDE_STATUS_CRAFTY_SHIELD + | SIDE_STATUS_MAT_BLOCK); +} +#endif // GUARD_BATTLE_H diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 72e05cfb68..87cb89f58d 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -3893,7 +3893,7 @@ void SetMoveEffect(bool32 primary, bool32 certain) } break; case MOVE_EFFECT_FEINT: - if (IS_BATTLER_PROTECTED(gBattlerTarget)) + if (IS_BATTLER_PROTECTED(gBattlerTarget) || IsBattlerSideProtected(gBattlerTarget)) { gProtectStructs[gBattlerTarget].protected = FALSE; gSideStatuses[GetBattlerSide(gBattlerTarget)] &= ~SIDE_STATUS_WIDE_GUARD; diff --git a/test/battle/move_effect/protect.c b/test/battle/move_effect/protect.c index 82a29ef81b..b0cd3e424a 100644 --- a/test/battle/move_effect/protect.c +++ b/test/battle/move_effect/protect.c @@ -20,7 +20,7 @@ ASSUMPTIONS ASSUME(!(MoveMakesContact(MOVE_WATER_GUN))); } -SINGLE_BATTLE_TEST("Protect, Detect, Spiky Shield, Baneful Bunker and Burning Bulwark protect from all moves") +SINGLE_BATTLE_TEST("Protect: Protect, Detect, Spiky Shield, Baneful Bunker and Burning Bulwark protect from all moves") { u32 j; static const u16 protectMoves[] = { @@ -59,7 +59,7 @@ SINGLE_BATTLE_TEST("Protect, Detect, Spiky Shield, Baneful Bunker and Burning Bu } } -SINGLE_BATTLE_TEST("King's Shield, Silk Trap and Obstruct protect from damaging moves and lower stats on contact") +SINGLE_BATTLE_TEST("Protect: King's Shield, Silk Trap and Obstruct protect from damaging moves and lower stats on contact") { u32 j; static const u16 protectMoves[][3] = @@ -125,7 +125,7 @@ SINGLE_BATTLE_TEST("King's Shield, Silk Trap and Obstruct protect from damaging } } -SINGLE_BATTLE_TEST("Spiky Shield does 1/8 dmg of max hp of attackers making contact and may faint them") +SINGLE_BATTLE_TEST("Protect: Spiky Shield does 1/8 dmg of max hp of attackers making contact and may faint them") { u16 usedMove = MOVE_NONE; u16 hp = 400, maxHp = 400; @@ -162,7 +162,7 @@ SINGLE_BATTLE_TEST("Spiky Shield does 1/8 dmg of max hp of attackers making cont } } -SINGLE_BATTLE_TEST("Baneful Bunker poisons pokemon for moves making contact") +SINGLE_BATTLE_TEST("Protect: Baneful Bunker poisons pokemon for moves making contact") { u16 usedMove = MOVE_NONE; @@ -194,7 +194,7 @@ SINGLE_BATTLE_TEST("Baneful Bunker poisons pokemon for moves making contact") } } -SINGLE_BATTLE_TEST("Burning Bulwark burns pokemon for moves making contact") +SINGLE_BATTLE_TEST("Protect: Burning Bulwark burns pokemon for moves making contact") { u16 usedMove = MOVE_NONE; @@ -226,7 +226,7 @@ SINGLE_BATTLE_TEST("Burning Bulwark burns pokemon for moves making contact") } } -SINGLE_BATTLE_TEST("Recoil damage is not applied if target was protected") +SINGLE_BATTLE_TEST("Protect: Recoil damage is not applied if target was protected") { u32 j, k; static const u16 protectMoves[] = { MOVE_PROTECT, MOVE_DETECT, MOVE_KINGS_SHIELD, MOVE_BANEFUL_BUNKER, MOVE_SILK_TRAP, MOVE_OBSTRUCT, MOVE_SPIKY_SHIELD }; @@ -269,7 +269,7 @@ SINGLE_BATTLE_TEST("Recoil damage is not applied if target was protected") } } -SINGLE_BATTLE_TEST("Multi-hit moves don't hit a protected target and fail only once") +SINGLE_BATTLE_TEST("Protect: Multi-hit moves don't hit a protected target and fail only once") { u16 move = MOVE_NONE; @@ -316,7 +316,7 @@ SINGLE_BATTLE_TEST("Multi-hit moves don't hit a protected target and fail only o } } -DOUBLE_BATTLE_TEST("Wide Guard protects self and ally from multi-target moves") +DOUBLE_BATTLE_TEST("Protect: Wide Guard protects self and ally from multi-target moves") { u16 move = MOVE_NONE; @@ -358,7 +358,7 @@ DOUBLE_BATTLE_TEST("Wide Guard protects self and ally from multi-target moves") } } -DOUBLE_BATTLE_TEST("Wide Guard can not fail on consecutive turns") +DOUBLE_BATTLE_TEST("Protect: Wide Guard can not fail on consecutive turns") { u8 turns; @@ -386,7 +386,7 @@ DOUBLE_BATTLE_TEST("Wide Guard can not fail on consecutive turns") } } -DOUBLE_BATTLE_TEST("Quick Guard protects self and ally from priority moves") +DOUBLE_BATTLE_TEST("Protect: Quick Guard protects self and ally from priority moves") { u16 move = MOVE_NONE; struct BattlePokemon *targetOpponent = NULL; @@ -421,7 +421,7 @@ DOUBLE_BATTLE_TEST("Quick Guard protects self and ally from priority moves") } } -DOUBLE_BATTLE_TEST("Quick Guard can not fail on consecutive turns") +DOUBLE_BATTLE_TEST("Protect: Quick Guard can not fail on consecutive turns") { u8 turns; @@ -446,7 +446,7 @@ DOUBLE_BATTLE_TEST("Quick Guard can not fail on consecutive turns") } } -DOUBLE_BATTLE_TEST("Crafty Shield protects self and ally from status moves") +DOUBLE_BATTLE_TEST("Protect: Crafty Shield protects self and ally from status moves") { u16 move = MOVE_NONE; struct BattlePokemon *targetOpponent = NULL; @@ -487,7 +487,7 @@ DOUBLE_BATTLE_TEST("Crafty Shield protects self and ally from status moves") } } -SINGLE_BATTLE_TEST("Protect does not block Confide or Decorate") +SINGLE_BATTLE_TEST("Protect: Protect does not block Confide or Decorate") { u32 move; PARAMETRIZE { move = MOVE_CONFIDE; } @@ -563,3 +563,58 @@ DOUBLE_BATTLE_TEST("Crafty Shield does not protect against moves that target all MESSAGE("The opposing Sunflora's Defense rose!"); } } + +SINGLE_BATTLE_TEST("Protect: Quick Guard, Wide Guard and Crafty Shield don't reduce Max Move demage", s16 damage) +{ + s16 dmg[2]; + u32 move; + + PARAMETRIZE { move = MOVE_WIDE_GUARD; } + PARAMETRIZE { move = MOVE_QUICK_GUARD; } + PARAMETRIZE { move = MOVE_CRAFTY_SHIELD; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_TACKLE, gimmick: GIMMICK_DYNAMAX); } + TURN { MOVE(player, MOVE_TACKLE); MOVE(opponent, move); } + } SCENE { + HP_BAR(opponent, captureDamage: &dmg[0]); + HP_BAR(opponent, captureDamage: &dmg[1]); + } FINALLY { + EXPECT_EQ(dmg[0], dmg[1]); + } +} + +SINGLE_BATTLE_TEST("Protect: Quick Guard, Wide Guard and Crafty Shield don't reduce Z-Move demage", s16 damage) +{ + bool32 protected; + u32 move; + + PARAMETRIZE { protected = TRUE; move = MOVE_WIDE_GUARD; } + PARAMETRIZE { protected = FALSE; move = MOVE_WIDE_GUARD; } + + PARAMETRIZE { protected = TRUE; move = MOVE_QUICK_GUARD; } + PARAMETRIZE { protected = FALSE; move = MOVE_QUICK_GUARD; } + + PARAMETRIZE { protected = TRUE; move = MOVE_CRAFTY_SHIELD; } + PARAMETRIZE { protected = FALSE; move = MOVE_CRAFTY_SHIELD; } + + GIVEN { + ASSUME(GetMoveType(MOVE_TACKLE) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (protected) + TURN { MOVE(player, MOVE_TACKLE, gimmick: GIMMICK_Z_MOVE); MOVE(opponent, move); } + else + TURN { MOVE(player, MOVE_TACKLE, gimmick: GIMMICK_Z_MOVE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_EQ(results[0].damage, results[1].damage); + EXPECT_EQ(results[2].damage, results[3].damage); + EXPECT_EQ(results[4].damage, results[5].damage); + } +} From 1c5d9b470809f3f23e01f5c23448f5be58342d90 Mon Sep 17 00:00:00 2001 From: AlexOn1ine Date: Sun, 27 Apr 2025 10:03:31 +0200 Subject: [PATCH 12/13] unused var --- src/battle_end_turn.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/battle_end_turn.c b/src/battle_end_turn.c index 08f5e51ac2..16c817b79f 100644 --- a/src/battle_end_turn.c +++ b/src/battle_end_turn.c @@ -357,7 +357,6 @@ static bool32 HandleEndTurnFutureSight(u32 battler) gBattlerAttacker = gWishFutureKnock.futureSightBattlerIndex[battler]; gCurrentMove = gWishFutureKnock.futureSightMove[battler]; - party = GetSideParty(GetBattlerSide(gBattlerAttacker)); if (!IsFutureSightAttackerInParty(gBattlerAttacker, gBattlerTarget)) SetTypeBeforeUsingMove(gCurrentMove, gBattlerAttacker); From 19fb962173f2e45efb5d32849f0526909fcf0343 Mon Sep 17 00:00:00 2001 From: AlexOn1ine Date: Sun, 27 Apr 2025 14:56:03 +0200 Subject: [PATCH 13/13] remove more unused --- src/battle_end_turn.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/battle_end_turn.c b/src/battle_end_turn.c index 16c817b79f..d6905e9065 100644 --- a/src/battle_end_turn.c +++ b/src/battle_end_turn.c @@ -335,8 +335,6 @@ static bool32 HandleEndTurnFutureSight(u32 battler) if (gWishFutureKnock.futureSightCounter[battler] == gBattleTurnCounter) { - struct Pokemon *party; - if (gWishFutureKnock.futureSightCounter[battler] == gBattleTurnCounter && gWishFutureKnock.futureSightCounter[BATTLE_PARTNER(battler)] <= gBattleTurnCounter) {