From 552e2768daa85e5a11078c72ebd3593d7640ccdd Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:01:46 +0200 Subject: [PATCH] Fixes to Opportunist and Mirror Herb adjustments (#4928) * Replace Opportunist todo tests with proper tests * add failing test * desc * Fixes to Opportunist and Mirror Herb adjustments * more tests * some fixes * first turn events switch * simple enum + revert test desc --- asm/macros/battle_script.inc | 6 + data/battle_scripts_1.s | 21 +- include/battle.h | 4 +- include/battle_main.h | 14 ++ include/constants/battle_string_ids.h | 75 ++++---- src/battle_main.c | 265 ++++++++++++++------------ src/battle_message.c | 2 - src/battle_script_commands.c | 58 +++++- src/battle_util.c | 75 +++++++- test/battle/ability/opportunist.c | 183 +++++++++++++++++- test/battle/hold_effect/mirror_herb.c | 15 ++ 11 files changed, 532 insertions(+), 186 deletions(-) diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index b805b4b3fe..021dd12bce 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1639,6 +1639,12 @@ .4byte \failInstr .endm + .macro copyfoesstatincrease battler:req, failInstr:req + callnative BS_CopyFoesStatIncrease + .byte \battler + .4byte \failInstr + .endm + @ various command changed to more readable macros .macro cancelmultiturnmoves battler:req various \battler, VARIOUS_CANCEL_MULTI_TURN_MOVES diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index b2ae0f2f41..b349dea985 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -9100,7 +9100,6 @@ BattleScript_TotemFlaredToLife:: call BattleScript_ApplyTotemVarBoost end2 -@ remove the mirror herb, do totem loop BattleScript_MirrorHerbCopyStatChangeEnd2:: call BattleScript_MirrorHerbCopyStatChange end2 @@ -9110,16 +9109,24 @@ BattleScript_MirrorHerbCopyStatChange:: printstring STRINGID_MIRRORHERBCOPIED waitmessage B_WAIT_TIME_LONG removeitem BS_SCRIPTING - call BattleScript_TotemVar_Ret - copybyte gBattlerAttacker, sSAVED_BATTLER @ restore the original attacker just to be safe + playanimation BS_SCRIPTING, B_ANIM_STATS_CHANGE, sB_ANIM_ARG1 +BattleScript_MirrorHerbStartCopyStats: + copyfoesstatincrease BS_SCRIPTING, BattleScript_MirrorHerbStartReturn + statbuffchange STAT_CHANGE_ALLOW_PTR, BattleScript_MirrorHerbStartReturn + goto BattleScript_MirrorHerbStartCopyStats +BattleScript_MirrorHerbStartReturn: return BattleScript_OpportunistCopyStatChange:: - call BattleScript_AbilityPopUp - printstring STRINGID_OPPORTUNISTCOPIED + call BattleScript_AbilityPopUpScripting + playanimation BS_SCRIPTING, B_ANIM_STATS_CHANGE, sB_ANIM_ARG1 +BattleScript_OpportunistStartCopyStats: + copyfoesstatincrease BS_SCRIPTING, BattleScript_OpportunistCopyStatChangeEnd + statbuffchange STAT_CHANGE_ALLOW_PTR, BattleScript_OpportunistCopyStatChangeEnd + printfromtable gStatUpStringIds waitmessage B_WAIT_TIME_LONG - call BattleScript_TotemVar_Ret - copybyte gBattlerAttacker, sSAVED_BATTLER @ restore the original attacker just to be safe + goto BattleScript_OpportunistStartCopyStats +BattleScript_OpportunistCopyStatChangeEnd: end3 BattleScript_TotemVar:: diff --git a/include/battle.h b/include/battle.h index 92da6ad05e..73ae60831f 100644 --- a/include/battle.h +++ b/include/battle.h @@ -628,7 +628,7 @@ struct BattleStruct u8 moneyMultiplierItem:1; u8 moneyMultiplierMove:1; u8 savedTurnActionNumber; - u8 switchInAbilitiesCounter; + u8 eventsBeforeFirstTurnState; u8 faintedActionsState; u8 faintedActionsBattlerId; u8 scriptPartyIdx; // for printing the nickname @@ -672,7 +672,7 @@ struct BattleStruct u16 chosenItem[MAX_BATTLERS_COUNT]; u16 choicedMove[MAX_BATTLERS_COUNT]; u16 changedItems[MAX_BATTLERS_COUNT]; - u8 switchInItemsCounter; + u8 switchInBattlerCounter; u8 arenaTurnCounter; u8 turnSideTracker; u16 lastTakenMoveFrom[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT]; // a 2-D array [target][attacker] diff --git a/include/battle_main.h b/include/battle_main.h index 9469e63efe..e313ec6629 100644 --- a/include/battle_main.h +++ b/include/battle_main.h @@ -23,6 +23,20 @@ struct MultiPartnerMenuPokemon #define BOUNCE_MON 0x0 #define BOUNCE_HEALTHBOX 0x1 +enum { + FIRST_TURN_EVENTS_START, + FIRST_TURN_EVENTS_OVERWORLD_WEATHER, + FIRST_TURN_EVENTS_TERRAIN, + FIRST_TURN_EVENTS_STARTING_STATUS, + FIRST_TURN_EVENTS_TOTEM_BOOST, + FIRST_TURN_EVENTS_NEUTRALIZING_GAS, + FIRST_TURN_EVENTS_SWITCH_IN_ABILITIES, + FIRST_TURN_EVENTS_OPPORTUNIST_1, + FIRST_TURN_EVENTS_ITEM_EFFECTS, + FIRST_TURN_EVENTS_OPPORTUNIST_2, + FIRST_TURN_EVENTS_END, +}; + void CB2_InitBattle(void); void BattleMainCB2(void); void CB2_QuitRecordedBattle(void); diff --git a/include/constants/battle_string_ids.h b/include/constants/battle_string_ids.h index 6f5b0db9ef..8d5cde7c2e 100644 --- a/include/constants/battle_string_ids.h +++ b/include/constants/battle_string_ids.h @@ -670,45 +670,44 @@ #define STRINGID_CURRENTMOVECANTSELECT 668 #define STRINGID_TARGETISBEINGSALTCURED 669 #define STRINGID_TARGETISHURTBYSALTCURE 670 -#define STRINGID_OPPORTUNISTCOPIED 671 -#define STRINGID_TARGETCOVEREDINSTICKYCANDYSYRUP 672 -#define STRINGID_SHARPSTEELFLOATS 673 -#define STRINGID_SHARPSTEELDMG 674 -#define STRINGID_PKMNBLEWAWAYSHARPSTEEL 675 -#define STRINGID_SHARPSTEELDISAPPEAREDFROMTEAM 676 -#define STRINGID_TEAMTRAPPEDWITHVINES 677 -#define STRINGID_PKMNHURTBYVINES 678 -#define STRINGID_TEAMCAUGHTINVORTEX 679 -#define STRINGID_PKMNHURTBYVORTEX 680 -#define STRINGID_TEAMSURROUNDEDBYFIRE 681 -#define STRINGID_PKMNBURNINGUP 682 -#define STRINGID_TEAMSURROUNDEDBYROCKS 683 -#define STRINGID_PKMNHURTBYROCKSTHROWN 684 -#define STRINGID_MOVEBLOCKEDBYDYNAMAX 685 -#define STRINGID_ZEROTOHEROTRANSFORMATION 686 -#define STRINGID_THETWOMOVESBECOMEONE 687 -#define STRINGID_ARAINBOWAPPEAREDONSIDE 688 -#define STRINGID_THERAINBOWDISAPPEARED 689 -#define STRINGID_WAITINGFORPARTNERSMOVE 690 -#define STRINGID_SEAOFFIREENVELOPEDSIDE 691 -#define STRINGID_HURTBYTHESEAOFFIRE 692 -#define STRINGID_THESEAOFFIREDISAPPEARED 693 -#define STRINGID_SWAMPENVELOPEDSIDE 694 -#define STRINGID_THESWAMPDISAPPEARED 695 -#define STRINGID_PKMNTELLCHILLINGRECEPTIONJOKE 696 -#define STRINGID_HOSPITALITYRESTORATION 697 -#define STRINGID_ELECTROSHOTCHARGING 698 -#define STRINGID_ITEMWASUSEDUP 699 -#define STRINGID_ATTACKERLOSTITSTYPE 700 -#define STRINGID_SHEDITSTAIL 701 -#define STRINGID_CLOAKEDINAHARSHLIGHT 702 -#define STRINGID_SUPERSWEETAROMAWAFTS 703 -#define STRINGID_DIMENSIONSWERETWISTED 704 -#define STRINGID_BIZARREARENACREATED 705 -#define STRINGID_BIZARREAREACREATED 706 -#define STRINGID_TIDYINGUPCOMPLETE 707 +#define STRINGID_TARGETCOVEREDINSTICKYCANDYSYRUP 671 +#define STRINGID_SHARPSTEELFLOATS 672 +#define STRINGID_SHARPSTEELDMG 673 +#define STRINGID_PKMNBLEWAWAYSHARPSTEEL 674 +#define STRINGID_SHARPSTEELDISAPPEAREDFROMTEAM 675 +#define STRINGID_TEAMTRAPPEDWITHVINES 676 +#define STRINGID_PKMNHURTBYVINES 677 +#define STRINGID_TEAMCAUGHTINVORTEX 678 +#define STRINGID_PKMNHURTBYVORTEX 679 +#define STRINGID_TEAMSURROUNDEDBYFIRE 680 +#define STRINGID_PKMNBURNINGUP 681 +#define STRINGID_TEAMSURROUNDEDBYROCKS 682 +#define STRINGID_PKMNHURTBYROCKSTHROWN 683 +#define STRINGID_MOVEBLOCKEDBYDYNAMAX 684 +#define STRINGID_ZEROTOHEROTRANSFORMATION 685 +#define STRINGID_THETWOMOVESBECOMEONE 686 +#define STRINGID_ARAINBOWAPPEAREDONSIDE 687 +#define STRINGID_THERAINBOWDISAPPEARED 688 +#define STRINGID_WAITINGFORPARTNERSMOVE 689 +#define STRINGID_SEAOFFIREENVELOPEDSIDE 690 +#define STRINGID_HURTBYTHESEAOFFIRE 691 +#define STRINGID_THESEAOFFIREDISAPPEARED 692 +#define STRINGID_SWAMPENVELOPEDSIDE 693 +#define STRINGID_THESWAMPDISAPPEARED 694 +#define STRINGID_PKMNTELLCHILLINGRECEPTIONJOKE 695 +#define STRINGID_HOSPITALITYRESTORATION 696 +#define STRINGID_ELECTROSHOTCHARGING 697 +#define STRINGID_ITEMWASUSEDUP 698 +#define STRINGID_ATTACKERLOSTITSTYPE 699 +#define STRINGID_SHEDITSTAIL 700 +#define STRINGID_CLOAKEDINAHARSHLIGHT 701 +#define STRINGID_SUPERSWEETAROMAWAFTS 702 +#define STRINGID_DIMENSIONSWERETWISTED 703 +#define STRINGID_BIZARREARENACREATED 704 +#define STRINGID_BIZARREAREACREATED 705 +#define STRINGID_TIDYINGUPCOMPLETE 706 -#define BATTLESTRINGS_COUNT 708 +#define BATTLESTRINGS_COUNT 707 // This is the string id that gBattleStringsTable starts with. // String ids before this (e.g. STRINGID_INTROMSG) are not in the table, diff --git a/src/battle_main.c b/src/battle_main.c index 0e16ffc11e..f5714d0ce2 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3995,8 +3995,8 @@ static void DoBattleIntro(void) } } - gBattleStruct->switchInAbilitiesCounter = 0; - gBattleStruct->switchInItemsCounter = 0; + gBattleStruct->eventsBeforeFirstTurnState = 0; + gBattleStruct->switchInBattlerCounter = 0; gBattleStruct->overworldWeatherDone = FALSE; SetAiLogicDataForTurn(AI_DATA); // get assumed abilities, hold effects, etc of all battlers Ai_InitPartyStruct(); // Save mons party counts, and first 2/4 mons on the battlefield. @@ -4031,34 +4031,35 @@ static void TryDoEventsBeforeFirstTurn(void) if (gBattleControllerExecFlags) return; - // Set invalid mons as absent(for example when starting a double battle with only one pokemon). - if (!(gBattleTypeFlags & BATTLE_TYPE_SAFARI)) + switch (gBattleStruct->eventsBeforeFirstTurnState) { - for (i = 0; i < gBattlersCount; i++) + case FIRST_TURN_EVENTS_START: + // Set invalid mons as absent(for example when starting a double battle with only one pokemon). + if (!(gBattleTypeFlags & BATTLE_TYPE_SAFARI)) { - struct Pokemon *party = GetBattlerParty(i); - struct Pokemon *mon = &party[gBattlerPartyIndexes[i]]; - if (gBattleMons[i].hp == 0 || gBattleMons[i].species == SPECIES_NONE || GetMonData(mon, MON_DATA_IS_EGG)) - gAbsentBattlerFlags |= gBitTable[i]; + for (i = 0; i < gBattlersCount; i++) + { + struct Pokemon *party = GetBattlerParty(i); + struct Pokemon *mon = &party[gBattlerPartyIndexes[i]]; + if (gBattleMons[i].hp == 0 || gBattleMons[i].species == SPECIES_NONE || GetMonData(mon, MON_DATA_IS_EGG)) + gAbsentBattlerFlags |= gBitTable[i]; + } } - } - // Allow for illegal abilities within tests. - #if TESTING - if (gTestRunnerEnabled && gBattleStruct->switchInAbilitiesCounter == 0) - { - for (i = 0; i < gBattlersCount; ++i) + // Allow for illegal abilities within tests. + #if TESTING + if (gTestRunnerEnabled) { - u32 side = GetBattlerSide(i); - u32 partyIndex = gBattlerPartyIndexes[i]; - if (TestRunner_Battle_GetForcedAbility(side, partyIndex)) - gBattleMons[i].ability = gBattleStruct->overwrittenAbilities[i] = TestRunner_Battle_GetForcedAbility(side, partyIndex); + for (i = 0; i < gBattlersCount; ++i) + { + u32 side = GetBattlerSide(i); + u32 partyIndex = gBattlerPartyIndexes[i]; + if (TestRunner_Battle_GetForcedAbility(side, partyIndex)) + gBattleMons[i].ability = gBattleStruct->overwrittenAbilities[i] = TestRunner_Battle_GetForcedAbility(side, partyIndex); + } } - } - #endif // TESTING + #endif // TESTING - if (gBattleStruct->switchInAbilitiesCounter == 0) - { for (i = 0; i < gBattlersCount; i++) gBattlerByTurnOrder[i] = i; for (i = 0; i < gBattlersCount - 1; i++) @@ -4069,109 +4070,135 @@ static void TryDoEventsBeforeFirstTurn(void) SwapTurnOrder(i, j); } } - } - if (!gBattleStruct->overworldWeatherDone - && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_WEATHER, 0, 0, ABILITYEFFECT_SWITCH_IN_WEATHER, 0) != 0) - { - gBattleStruct->overworldWeatherDone = TRUE; - return; - } - - if (!gBattleStruct->terrainDone && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_TERRAIN, 0, 0, ABILITYEFFECT_SWITCH_IN_TERRAIN, 0) != 0) - { - gBattleStruct->terrainDone = TRUE; - return; - } - - if (!gBattleStruct->startingStatusDone - && gBattleStruct->startingStatus - && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_STATUSES, 0, 0, ABILITYEFFECT_SWITCH_IN_STATUSES, 0) != 0) - { - gBattleStruct->startingStatusDone = TRUE; - return; - } - - // Totem boosts - for (i = 0; i < gBattlersCount; i++) - { - if (gQueuedStatBoosts[i].stats != 0 && !gProtectStructs[i].eatMirrorHerb && gProtectStructs[i].activateOpportunist == 0) + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_OVERWORLD_WEATHER: + if (!gBattleStruct->overworldWeatherDone + && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_WEATHER, 0, 0, ABILITYEFFECT_SWITCH_IN_WEATHER, 0) != 0) { - gBattlerAttacker = i; - BattleScriptExecute(BattleScript_TotemVar); + gBattleStruct->overworldWeatherDone = TRUE; return; } - } - - // Check neutralizing gas - if (AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, 0, 0, 0, 0) != 0) - return; - - // Check all switch in abilities happening from the fastest mon to slowest. - while (gBattleStruct->switchInAbilitiesCounter < gBattlersCount) - { - gBattlerAttacker = gBattlerByTurnOrder[gBattleStruct->switchInAbilitiesCounter++]; - - if (TryPrimalReversion(gBattlerAttacker)) + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_TERRAIN: + if (!gBattleStruct->terrainDone + && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_TERRAIN, 0, 0, ABILITYEFFECT_SWITCH_IN_TERRAIN, 0) != 0) + { + gBattleStruct->terrainDone = TRUE; return; - if (AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN, gBattlerAttacker, 0, 0, 0) != 0) + } + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_STARTING_STATUS: + if (!gBattleStruct->startingStatusDone + && gBattleStruct->startingStatus + && AbilityBattleEffects(ABILITYEFFECT_SWITCH_IN_STATUSES, 0, 0, ABILITYEFFECT_SWITCH_IN_STATUSES, 0) != 0) + { + gBattleStruct->startingStatusDone = TRUE; return; - } - // Check all switch in items having effect from the fastest mon to slowest. - while (gBattleStruct->switchInItemsCounter < gBattlersCount) - { - if (ItemBattleEffects(ITEMEFFECT_ON_SWITCH_IN, gBattlerByTurnOrder[gBattleStruct->switchInItemsCounter++], FALSE)) + } + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_TOTEM_BOOST: + for (i = 0; i < gBattlersCount; i++) + { + if (gQueuedStatBoosts[i].stats != 0 && !gProtectStructs[i].eatMirrorHerb && gProtectStructs[i].activateOpportunist == 0) + { + gBattlerAttacker = i; + BattleScriptExecute(BattleScript_TotemVar); + return; + } + } + memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); // erase all totem boosts for Mirror Herb and Opportunist + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_NEUTRALIZING_GAS: + if (AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, 0, 0, 0, 0) != 0) return; + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_SWITCH_IN_ABILITIES: + while (gBattleStruct->switchInBattlerCounter < gBattlersCount) // From fastest to slowest + { + gBattlerAttacker = gBattlerByTurnOrder[gBattleStruct->switchInBattlerCounter++]; + + if (TryPrimalReversion(gBattlerAttacker)) + return; + if (AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN, gBattlerAttacker, 0, 0, 0) != 0) + return; + } + gBattleStruct->switchInBattlerCounter = 0; + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_OPPORTUNIST_1: + if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, 0, 0, 0, 0)) + return; + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_ITEM_EFFECTS: + while (gBattleStruct->switchInBattlerCounter < gBattlersCount) // From fastest to slowest + { + if (ItemBattleEffects(ITEMEFFECT_ON_SWITCH_IN, gBattlerByTurnOrder[gBattleStruct->switchInBattlerCounter++], FALSE)) + return; + } + gBattleStruct->switchInBattlerCounter = 0; + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_OPPORTUNIST_2: + if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, 0, 0, 0, 0)) + return; + gBattleStruct->eventsBeforeFirstTurnState++; + break; + case FIRST_TURN_EVENTS_END: + for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { + *(gBattleStruct->monToSwitchIntoId + i) = PARTY_SIZE; + gChosenActionByBattler[i] = B_ACTION_NONE; + gChosenMoveByBattler[i] = MOVE_NONE; + } + TurnValuesCleanUp(FALSE); + SpecialStatusesClear(); + *(&gBattleStruct->absentBattlerFlags) = gAbsentBattlerFlags; + BattlePutTextOnWindow(gText_EmptyString3, B_WIN_MSG); + gBattleMainFunc = HandleTurnActionSelectionState; + ResetSentPokesToOpponentValue(); + + for (i = 0; i < BATTLE_COMMUNICATION_ENTRIES_COUNT; i++) + gBattleCommunication[i] = 0; + + for (i = 0; i < gBattlersCount; i++) + { + gBattleMons[i].status2 &= ~STATUS2_FLINCHED; + // Record party slots of player's mons that appeared in battle + if (!BattlerHasAi(i)) + gBattleStruct->appearedInBattle |= gBitTable[gBattlerPartyIndexes[i]]; + } + + *(&gBattleStruct->turnEffectsTracker) = 0; + *(&gBattleStruct->turnEffectsBattlerId) = 0; + *(&gBattleStruct->wishPerishSongState) = 0; + *(&gBattleStruct->wishPerishSongBattlerId) = 0; + gBattleScripting.moveendState = 0; + gBattleStruct->faintedActionsState = 0; + gBattleStruct->turnCountersTracker = 0; + gMoveResultFlags = 0; + + memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); + SetShellSideArmCategory(); + SetAiLogicDataForTurn(AI_DATA); // get assumed abilities, hold effects, etc of all battlers + + if (gBattleTypeFlags & BATTLE_TYPE_ARENA) + { + StopCryAndClearCrySongs(); + BattleScriptExecute(BattleScript_ArenaTurnBeginning); + } + + if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_BEFORE_FIRST_TURN))) + BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); + gBattleStruct->eventsBeforeFirstTurnState = 0; + break; } - - if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, 0, 0, 0, 0)) - return; - - for (i = 0; i < MAX_BATTLERS_COUNT; i++) - { - *(gBattleStruct->monToSwitchIntoId + i) = PARTY_SIZE; - gChosenActionByBattler[i] = B_ACTION_NONE; - gChosenMoveByBattler[i] = MOVE_NONE; - } - TurnValuesCleanUp(FALSE); - SpecialStatusesClear(); - *(&gBattleStruct->absentBattlerFlags) = gAbsentBattlerFlags; - BattlePutTextOnWindow(gText_EmptyString3, B_WIN_MSG); - gBattleMainFunc = HandleTurnActionSelectionState; - ResetSentPokesToOpponentValue(); - - for (i = 0; i < BATTLE_COMMUNICATION_ENTRIES_COUNT; i++) - gBattleCommunication[i] = 0; - - for (i = 0; i < gBattlersCount; i++) - { - gBattleMons[i].status2 &= ~STATUS2_FLINCHED; - // Record party slots of player's mons that appeared in battle - if (!BattlerHasAi(i)) - gBattleStruct->appearedInBattle |= gBitTable[gBattlerPartyIndexes[i]]; - } - - *(&gBattleStruct->turnEffectsTracker) = 0; - *(&gBattleStruct->turnEffectsBattlerId) = 0; - *(&gBattleStruct->wishPerishSongState) = 0; - *(&gBattleStruct->wishPerishSongBattlerId) = 0; - gBattleScripting.moveendState = 0; - gBattleStruct->faintedActionsState = 0; - gBattleStruct->turnCountersTracker = 0; - gMoveResultFlags = 0; - - memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); // erase all totem boosts just to be safe - - SetShellSideArmCategory(); - SetAiLogicDataForTurn(AI_DATA); // get assumed abilities, hold effects, etc of all battlers - - if (gBattleTypeFlags & BATTLE_TYPE_ARENA) - { - StopCryAndClearCrySongs(); - BattleScriptExecute(BattleScript_ArenaTurnBeginning); - } - - if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_BEFORE_FIRST_TURN))) - BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); } static void HandleEndTurn_ContinueBattle(void) diff --git a/src/battle_message.c b/src/battle_message.c index eecec7d2a4..f57d29f913 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -824,7 +824,6 @@ static const u8 sText_TeamGainedEXP[] = _("The rest of your team gained EXP.\nPo static const u8 sText_CurrentMoveCantSelect[] = _("{B_BUFF1} cannot be used!\p"); static const u8 sText_TargetIsBeingSaltCured[] = _("{B_DEF_NAME_WITH_PREFIX} is being salt cured!"); static const u8 sText_TargetIsHurtBySaltCure[] = _("{B_DEF_NAME_WITH_PREFIX} is hurt by {B_BUFF1}!"); -static const u8 sText_OpportunistCopied[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} copied its\nopponent's stat changes!"); static const u8 sText_TargetCoveredInStickyCandySyrup[] = _("{B_DEF_NAME_WITH_PREFIX} got covered\nin sticky syrup!"); static const u8 sText_PkmnTellChillingReceptionJoke[] = _("{B_ATK_NAME_WITH_PREFIX} is preparing to tell a\nchillingly bad joke!"); static const u8 sText_ZeroToHeroTransformation[] = _("{B_ATK_NAME_WITH_PREFIX} underwent a heroic\ntransformation!"); @@ -864,7 +863,6 @@ const u8 *const gBattleStringsTable[BATTLESTRINGS_COUNT] = [STRINGID_ZEROTOHEROTRANSFORMATION - BATTLESTRINGS_TABLE_START] = sText_ZeroToHeroTransformation, [STRINGID_PKMNTELLCHILLINGRECEPTIONJOKE - BATTLESTRINGS_TABLE_START] = sText_PkmnTellChillingReceptionJoke, [STRINGID_MOVEBLOCKEDBYDYNAMAX - BATTLESTRINGS_TABLE_START] = sText_MoveBlockedByDynamax, - [STRINGID_OPPORTUNISTCOPIED - BATTLESTRINGS_TABLE_START] = sText_OpportunistCopied, [STRINGID_TARGETISHURTBYSALTCURE - BATTLESTRINGS_TABLE_START] = sText_TargetIsHurtBySaltCure, [STRINGID_TARGETISBEINGSALTCURED - BATTLESTRINGS_TABLE_START] = sText_TargetIsBeingSaltCured, [STRINGID_CURRENTMOVECANTSELECT - BATTLESTRINGS_TABLE_START] = sText_CurrentMoveCantSelect, diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 64bf348478..95970d1db0 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6412,6 +6412,7 @@ static void Cmd_moveend(void) gBattleStruct->bouncedMoveIsUsed = FALSE; gBattleStruct->enduredDamage = 0; gBattleStruct->additionalEffectsCounter = 0; + memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); gBattleScripting.moveendState++; break; case MOVEEND_COUNT: @@ -11786,27 +11787,35 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr } else { + u32 statIncrease; + if ((statValue + gBattleMons[battler].statStages[statId]) > MAX_STAT_STAGE) + statIncrease = MAX_STAT_STAGE - gBattleMons[battler].statStages[statId]; + else + statIncrease = statValue; + gBattleCommunication[MULTISTRING_CHOOSER] = (gBattlerTarget == battler); gProtectStructs[battler].statRaised = TRUE; - // check mirror herb + // Check Mirror Herb / Opportunist for (index = 0; index < gBattlersCount; index++) { if (GetBattlerSide(index) == GetBattlerSide(battler)) continue; // Only triggers on opposing side + if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST - && gProtectStructs[battler].activateOpportunist == 0) // don't activate opportunist on other mon's opportunist raises + && gProtectStructs[battler].activateOpportunist == 0) // don't activate opportunist on other mon's opportunist raises { gProtectStructs[index].activateOpportunist = 2; // set stats to copy - gQueuedStatBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gQueuedStatBoosts[index].statChanges[statId - 1] += statValue; // cumulative in case of multiple opponent boosts } - else if (GetBattlerHoldEffect(index, TRUE) == HOLD_EFFECT_MIRROR_HERB - && gBattleMons[index].statStages[statId] < MAX_STAT_STAGE) + if (GetBattlerHoldEffect(index, TRUE) == HOLD_EFFECT_MIRROR_HERB) { gProtectStructs[index].eatMirrorHerb = 1; + } + + if (gProtectStructs[index].activateOpportunist == 2 || gProtectStructs[index].eatMirrorHerb == 1) + { gQueuedStatBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gQueuedStatBoosts[index].statChanges[statId - 1] = statValue; + gQueuedStatBoosts[index].statChanges[statId - 1] += statIncrease; } } } @@ -17005,3 +17014,38 @@ void BS_TryQuash(void) } gBattlescriptCurrInstr = cmd->nextInstr; } + +void BS_CopyFoesStatIncrease(void) +{ + NATIVE_ARGS(u8 battler, const u8 *jumpInstr); + u32 stat = 0; + u32 battler = GetBattlerForBattleScript(cmd->battler); + + if (gQueuedStatBoosts[battler].stats == 0) + { + for (stat = 0; stat < (NUM_BATTLE_STATS - 1); stat++) + { + if (gQueuedStatBoosts[battler].statChanges[stat] != 0) + gQueuedStatBoosts[battler].stats |= (1 << stat); + } + gBattlescriptCurrInstr = cmd->jumpInstr; + return; + } + + for (stat = 0; stat < (NUM_BATTLE_STATS - 1); stat++) + { + if (gQueuedStatBoosts[battler].stats & (1 << stat)) + { + if (gQueuedStatBoosts[battler].statChanges[stat] <= -1) + SET_STATCHANGER(stat + 1, abs(gQueuedStatBoosts[battler].statChanges[stat]), TRUE); + else + SET_STATCHANGER(stat + 1, gQueuedStatBoosts[battler].statChanges[stat], FALSE); + + gQueuedStatBoosts[battler].stats &= ~(1 << stat); + gBattlerTarget = battler; + gBattlescriptCurrInstr = cmd->nextInstr; + return; + } + } + gBattlescriptCurrInstr = cmd->jumpInstr; +} diff --git a/src/battle_util.c b/src/battle_util.c index 755b75f6be..18a81aa1b5 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5874,9 +5874,40 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 case ABILITY_OPPORTUNIST: if (gProtectStructs[battler].activateOpportunist == 2) { - gBattleScripting.savedBattler = gBattlerAttacker; - gBattleScripting.battler = gBattlerAttacker = gBattlerAbility = battler; + bool32 statBuffMoreThan1 = FALSE; + bool32 handleSpeedAnimLater = FALSE; + gBattleScripting.animArg1 = 0; + gBattleScripting.battler = battler; gProtectStructs[battler].activateOpportunist--; + + for (i = 0; i < (NUM_BATTLE_STATS - 1); i++) + { + if ((gQueuedStatBoosts[battler].stats & (1 << i)) == 0) + continue; + + if (i == STAT_SPEED) + { + handleSpeedAnimLater = TRUE; + continue; + } + + if (!statBuffMoreThan1) + statBuffMoreThan1 = ((gQueuedStatBoosts[battler].stats & (1 << i)) > 1); + + if (gBattleScripting.animArg1 != 0) //Already set in a different stat so now boosting multiple stats + gBattleScripting.animArg1 = (!statBuffMoreThan1 ? STAT_ANIM_MULTIPLE_PLUS1 : STAT_ANIM_MULTIPLE_PLUS2); + else + gBattleScripting.animArg1 = GET_STAT_BUFF_ID((i + 1)) + (!statBuffMoreThan1 ? STAT_ANIM_PLUS1 : STAT_ANIM_PLUS2); + + } + if (handleSpeedAnimLater) + { + if (gBattleScripting.animArg1 != 0) //Already set in a different stat so now boosting multiple stats + gBattleScripting.animArg1 = (!statBuffMoreThan1 ? STAT_ANIM_MULTIPLE_PLUS1 : STAT_ANIM_MULTIPLE_PLUS2); + else + gBattleScripting.animArg1 = GET_STAT_BUFF_ID((STAT_SPEED + 1)) + (!statBuffMoreThan1 ? STAT_ANIM_PLUS1 : STAT_ANIM_PLUS2); + } + BattleScriptPushCursorAndCallback(BattleScript_OpportunistCopyStatChange); effect = 1; } @@ -6815,10 +6846,43 @@ static u8 TryConsumeMirrorHerb(u32 battler, bool32 execute) if (gProtectStructs[battler].eatMirrorHerb) { + u32 i; + bool32 statBuffMoreThan1 = FALSE; + bool32 handleSpeedAnimLater = FALSE; + gBattleScripting.animArg1 = 0; + gLastUsedItem = gBattleMons[battler].item; - gBattleScripting.savedBattler = gBattlerAttacker; - gBattleScripting.battler = gBattlerAttacker = battler; + gBattleScripting.battler = battler; gProtectStructs[battler].eatMirrorHerb = 0; + + for (i = 0; i < (NUM_BATTLE_STATS - 1); i++) + { + if ((gQueuedStatBoosts[battler].stats & (1 << i)) == 0) + continue; + + if (i == STAT_SPEED) + { + handleSpeedAnimLater = TRUE; + continue; + } + + if (!statBuffMoreThan1) + statBuffMoreThan1 = ((gQueuedStatBoosts[battler].stats & (1 << i)) > 1); + + if (gBattleScripting.animArg1 != 0) //Already set in a different stat so now boosting multiple stats + gBattleScripting.animArg1 = (!statBuffMoreThan1 ? STAT_ANIM_MULTIPLE_PLUS1 : STAT_ANIM_MULTIPLE_PLUS2); + else + gBattleScripting.animArg1 = GET_STAT_BUFF_ID((i + 1)) + (!statBuffMoreThan1 ? STAT_ANIM_PLUS1 : STAT_ANIM_PLUS2); + + } + if (handleSpeedAnimLater) + { + if (gBattleScripting.animArg1 != 0) //Already set in a different stat so now boosting multiple stats + gBattleScripting.animArg1 = (!statBuffMoreThan1 ? STAT_ANIM_MULTIPLE_PLUS1 : STAT_ANIM_MULTIPLE_PLUS2); + else + gBattleScripting.animArg1 = GET_STAT_BUFF_ID((STAT_SPEED + 1)) + (!statBuffMoreThan1 ? STAT_ANIM_PLUS1 : STAT_ANIM_PLUS2); + } + if (execute) { BattleScriptExecute(BattleScript_MirrorHerbCopyStatChangeEnd2); @@ -7345,6 +7409,9 @@ u8 ItemBattleEffects(u8 caseID, u32 battler, bool32 moveTurn) BattleScriptPushCursorAndCallback(BattleScript_BerserkGeneRet); effect = ITEM_STATS_CHANGE; break; + case HOLD_EFFECT_MIRROR_HERB: + effect = TryConsumeMirrorHerb(battler, TRUE); + break; } if (effect != 0) { diff --git a/test/battle/ability/opportunist.c b/test/battle/ability/opportunist.c index f2a9f5a37a..b627b8ec6e 100644 --- a/test/battle/ability/opportunist.c +++ b/test/battle/ability/opportunist.c @@ -16,16 +16,16 @@ SINGLE_BATTLE_TEST("Opportunist only copies foe's positive stat changes in a tur OPPONENT(SPECIES_ESPATHRA) { Speed(5); Ability(ability); } } WHEN { TURN { MOVE(player, MOVE_SHELL_SMASH); } - TURN { MOVE(player, MOVE_TACKLE); MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } } SCENE { if (ability == ABILITY_FRISK) { ANIMATION(ANIM_TYPE_MOVE, MOVE_SHELL_SMASH, player); ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); HP_BAR(player, captureDamage: &results[i].damage); } else { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SHELL_SMASH, player); ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); HP_BAR(player, captureDamage: &results[i].damage); - ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); } } FINALLY { EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.0), results[1].damage); @@ -78,12 +78,10 @@ DOUBLE_BATTLE_TEST("Opportunist raises Attack only once when partner has Intimid if ((abilityLeft == ABILITY_CONTRARY && abilityRight != ABILITY_CONTRARY) || (abilityLeft != ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY)) { ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); - MESSAGE("Espathra copied its opponent's stat changes!"); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); MESSAGE("Espathra's Attack rose!"); } else if (abilityLeft == ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY) { ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); - MESSAGE("Espathra copied its opponent's stat changes!"); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); MESSAGE("Espathra's Attack sharply rose!"); } @@ -124,6 +122,177 @@ SINGLE_BATTLE_TEST("Opportunist does not accumulate opposing mon's stat changes" } } -TO_DO_BATTLE_TEST("Opportunist doesn't copy ally stat increases"); -TO_DO_BATTLE_TEST("Opportunist doesn't copy foe stat increases gained via Opportunist"); -TO_DO_BATTLE_TEST("Opportunist copies foe stat increased gained via Swagger and Flatter"); +SINGLE_BATTLE_TEST("Opportunist copies each stat increase individually from ability and move") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + OPPONENT(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + } WHEN { + TURN { SWITCH(player, 1); } + TURN { MOVE(player, MOVE_SWORDS_DANCE); } + } SCENE { + ABILITY_POPUP(player, ABILITY_INTREPID_SWORD); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 3); + } +} + +SINGLE_BATTLE_TEST("Opportunist doesn't copy foe stat increases gained via Opportunist") +{ + GIVEN { + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + } WHEN { + TURN { MOVE(player, MOVE_SWORDS_DANCE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + NOT ABILITY_POPUP(player, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], player->statStages[STAT_ATK]); + } +} + +SINGLE_BATTLE_TEST("Opportunist copies foe stat increase gained via Swagger and Flatter") +{ + GIVEN { + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + } WHEN { + TURN { MOVE(opponent, MOVE_FLATTER); } + TURN { MOVE(opponent, MOVE_SWAGGER); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FLATTER, opponent); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWAGGER, opponent); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_SPATK], DEFAULT_STAT_STAGE + 1); + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + } +} + +DOUBLE_BATTLE_TEST("Opportunist doesn't copy ally stat increases") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_SWORDS_DANCE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, playerLeft); + NOT ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(playerRight->statStages[STAT_SPATK], DEFAULT_STAT_STAGE ); + } +} + +DOUBLE_BATTLE_TEST("Opportunist copies the stat increase of each opposing mon") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentRight, MOVE_SWORDS_DANCE); MOVE(opponentLeft, MOVE_SWORDS_DANCE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, opponentRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, opponentLeft); + ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 4); + } +} + + +DOUBLE_BATTLE_TEST("Opportunist copies the stat of each pokemon that were raised at the same time") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + OPPONENT(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(opponentLeft, ABILITY_INTREPID_SWORD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + ABILITY_POPUP(opponentRight, ABILITY_INTREPID_SWORD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); + MESSAGE("Espathra's Attack sharply rose!"); + } THEN { + EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + } +} + +SINGLE_BATTLE_TEST("Opportunist copies the increase not the stages") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + } WHEN { + TURN { MOVE(player, MOVE_CHARM); MOVE(opponent, MOVE_CHARM); } + TURN { MOVE(player, MOVE_CHARM); MOVE(opponent, MOVE_CHARM); } + TURN { MOVE(player, MOVE_CHARM); MOVE(opponent, MOVE_GROWL); } + TURN { MOVE(player, MOVE_BELLY_DRUM); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CHARM, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CHARM, opponent); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_CHARM, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CHARM, opponent); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_CHARM, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_GROWL, opponent); + + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 5); // + 11 + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 6); // + 11 + } +} + +SINGLE_BATTLE_TEST("Opportunist copies the stat increase from the incoming mon") +{ + GIVEN { + PLAYER(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); SEND_OUT(opponent, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + ABILITY_POPUP(opponent, ABILITY_INTREPID_SWORD); + ABILITY_POPUP(player, ABILITY_OPPORTUNIST); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 1); + } +} + +SINGLE_BATTLE_TEST("Opportunist and Mirror Herb stack stat increases") +{ + GIVEN { + PLAYER(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + OPPONENT(SPECIES_ESPATHRA) { Ability(ABILITY_OPPORTUNIST); Item(ITEM_MIRROR_HERB); } + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(player, ABILITY_INTREPID_SWORD); + ABILITY_POPUP(opponent, ABILITY_OPPORTUNIST); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_SPATK], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_SPDEF], DEFAULT_STAT_STAGE); + } +} diff --git a/test/battle/hold_effect/mirror_herb.c b/test/battle/hold_effect/mirror_herb.c index 135ec270b3..52326cc60f 100644 --- a/test/battle/hold_effect/mirror_herb.c +++ b/test/battle/hold_effect/mirror_herb.c @@ -73,3 +73,18 @@ DOUBLE_BATTLE_TEST("Mirror Herb does not trigger for Ally's Soul Heart's stat ra EXPECT_EQ(playerLeft->statStages[STAT_SPATK], DEFAULT_STAT_STAGE); } } + +SINGLE_BATTLE_TEST("Mirror Herb copies the boost gained by an ability") +{ + GIVEN { + PLAYER(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_MIRROR_HERB); } + } WHEN { + TURN { } + } SCENE { + ABILITY_POPUP(player, ABILITY_INTREPID_SWORD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 1); + } +}