From fec1f15fbaf96eebe82d77bfa5e11ec1d19bcadf Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Thu, 11 May 2023 12:56:19 -0400 Subject: [PATCH 01/12] add opportunist --- data/battle_scripts_1.s | 19 ++++++++----------- include/battle.h | 1 + include/battle_scripts.h | 1 + include/battle_util.h | 1 + include/constants/battle_script_commands.h | 5 +++-- include/constants/battle_string_ids.h | 3 ++- src/battle_main.c | 8 ++++++-- src/battle_message.c | 2 ++ src/battle_script_commands.c | 19 ++++++++++++++++--- src/battle_util.c | 22 ++++++++++++++++++++++ 10 files changed, 62 insertions(+), 19 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index fdc7c8a3fd33..d460d44a9358 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -8899,17 +8899,6 @@ BattleScript_ActivateTerrainEffects_Increment: restoretarget return -BattleScript_ActivateSwitchInAbilities: - copybyte sBATTLER, gBattlerAttacker - setbyte gBattlerAttacker, 0 -BattleScript_ActivateSwitchInAbilities_Loop: - switchinabilities BS_ATTACKER -BattleScript_ActivateSwitchInAbilities_Increment: - addbyte gBattlerAttacker, 1 - jumpifbytenotequal gBattlerAttacker, gBattlersCount, BattleScript_ActivateSwitchInAbilities_Loop - copybyte gBattlerAttacker, sBATTLER - return - BattleScript_ElectricSurgeActivates:: pause B_WAIT_TIME_SHORT call BattleScript_AbilityPopUp @@ -10054,6 +10043,14 @@ BattleScript_MirrorHerbCopyStatChange:: copybyte gBattlerAttacker, sSAVED_BATTLER @ restore the original attacker just to be safe return +BattleScript_OpportunistCopyStatChange:: + call BattleScript_AbilityPopUp + printstring STRINGID_OPPORTUNISTCOPIED + waitmessage B_WAIT_TIME_LONG + call BattleScript_TotemVar_Ret + copybyte gBattlerAttacker, sSAVED_BATTLER @ restore the original attacker just to be safe + end3 + BattleScript_TotemVar:: call BattleScript_TotemVar_Ret end2 diff --git a/include/battle.h b/include/battle.h index 00c7081fb8fe..fe03077a7dbc 100644 --- a/include/battle.h +++ b/include/battle.h @@ -148,6 +148,7 @@ struct ProtectStruct u16 shellTrap:1; u16 silkTrapped:1; u16 eatMirrorHerb:1; + u16 activateOpportunist:2; // 2 - to copy stats. 1 - stats copied (do not repeat). 0 - no stats to copy u32 physicalDmg; u32 specialDmg; u8 physicalBattlerId; diff --git a/include/battle_scripts.h b/include/battle_scripts.h index 94625566dd04..bfd32273a85c 100644 --- a/include/battle_scripts.h +++ b/include/battle_scripts.h @@ -1,6 +1,7 @@ #ifndef GUARD_BATTLE_SCRIPTS_H #define GUARD_BATTLE_SCRIPTS_H +extern const u8 BattleScript_OpportunistCopyStatChange[]; extern const u8 BattleScript_MirrorHerbCopyStatChange[]; extern const u8 BattleScript_MirrorHerbCopyStatChangeEnd2[]; extern const u8 BattleScript_NotAffected[]; diff --git a/include/battle_util.h b/include/battle_util.h index e28e41aad7de..bd277380e337 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -35,6 +35,7 @@ #define ABILITYEFFECT_FIELD_SPORT 13 // Only used if B_SPORT_TURNS < GEN_6 #define ABILITYEFFECT_ON_WEATHER 14 #define ABILITYEFFECT_ON_TERRAIN 15 +#define ABILITYEFFECT_OPPORTUNIST 16 // Special cases #define ABILITYEFFECT_MUD_SPORT 252 // Only used if B_SPORT_TURNS < GEN_6 #define ABILITYEFFECT_WATER_SPORT 253 // Only used if B_SPORT_TURNS < GEN_6 diff --git a/include/constants/battle_script_commands.h b/include/constants/battle_script_commands.h index 2be15be581d5..f3d9bfd47c1a 100644 --- a/include/constants/battle_script_commands.h +++ b/include/constants/battle_script_commands.h @@ -336,8 +336,9 @@ #define MOVEEND_DANCER 31 #define MOVEEND_EMERGENCY_EXIT 32 #define MOVEEND_SYMBIOSIS 33 -#define MOVEEND_CLEAR_BITS 34 -#define MOVEEND_COUNT 35 +#define MOVEEND_OPPORTUNIST 34 // Occurs after other stat change items/abilities to try and copy the boosts +#define MOVEEND_CLEAR_BITS 35 +#define MOVEEND_COUNT 36 // switch cases #define B_SWITCH_NORMAL 0 diff --git a/include/constants/battle_string_ids.h b/include/constants/battle_string_ids.h index 174c611e2fa8..c726cbd7064f 100644 --- a/include/constants/battle_string_ids.h +++ b/include/constants/battle_string_ids.h @@ -664,8 +664,9 @@ #define STRINGID_SNOWCONTINUES 662 #define STRINGID_SNOWSTOPPED 663 #define STRINGID_SNOWWARNINGSNOW 664 +#define STRINGID_OPPORTUNISTCOPIED 665 -#define BATTLESTRINGS_COUNT 665 +#define BATTLESTRINGS_COUNT 666 // 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 7270e0f36915..56b2f27630b2 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3823,14 +3823,13 @@ static void TryDoEventsBeforeFirstTurn(void) // Totem boosts for (i = 0; i < gBattlersCount; i++) { - if (gTotemBoosts[i].stats != 0) + if (gTotemBoosts[i].stats != 0 && !gProtectStructs[i].eatMirrorHerb && gProtectStructs[i].activateOpportunist == 0) { gBattlerAttacker = i; BattleScriptExecute(BattleScript_TotemVar); return; } } - memset(gTotemBoosts, 0, sizeof(gTotemBoosts)); // erase all totem boosts just to be safe // Check neutralizing gas if (AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, 0, 0, 0, 0) != 0) @@ -3859,6 +3858,9 @@ static void TryDoEventsBeforeFirstTurn(void) if (ItemBattleEffects(ITEMEFFECT_ON_SWITCH_IN, gBattlerByTurnOrder[gBattleStruct->switchInItemsCounter++], FALSE)) return; } + + if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, 0, 0, 0, 0)) + return; for (i = 0; i < MAX_BATTLERS_COUNT; i++) { @@ -3892,6 +3894,8 @@ static void TryDoEventsBeforeFirstTurn(void) gMoveResultFlags = 0; gRandomTurnNumber = Random(); + + memset(gTotemBoosts, 0, sizeof(gTotemBoosts)); // erase all totem boosts just to be safe GetAiLogicData(); // get assumed abilities, hold effects, etc of all battlers diff --git a/src/battle_message.c b/src/battle_message.c index 94dc0d67077f..be2dcc0cad59 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -799,9 +799,11 @@ static const u8 sText_ItemCuredSpeciesStatus[] = _("{B_BUFF1} had\nits status he static const u8 sText_ItemRestoredSpeciesPP[] = _("{B_BUFF1} had its\nPP restored!"); static const u8 sText_AtkTrappedDef[] = _("{B_ATK_NAME_WITH_PREFIX} trapped\nthe {B_DEF_NAME_WITH_PREFIX}!"); static const u8 sText_MirrorHerbCopied[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} used its {B_LAST_ITEM}\nto mirror its opponent's stat changes!"); +static const u8 sText_OpportunistCopied[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} copied its\nopponent's stat changes!"); const u8 *const gBattleStringsTable[BATTLESTRINGS_COUNT] = { + [STRINGID_OPPORTUNISTCOPIED - BATTLESTRINGS_TABLE_START] = sText_OpportunistCopied, [STRINGID_MIRRORHERBCOPIED - BATTLESTRINGS_TABLE_START] = sText_MirrorHerbCopied, [STRINGID_THUNDERCAGETRAPPED - BATTLESTRINGS_TABLE_START] = sText_AtkTrappedDef, [STRINGID_ITEMRESTOREDSPECIESHEALTH - BATTLESTRINGS_TABLE_START] = sText_ItemRestoredSpeciesHealth, diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 955970539c4a..4f9d51628e36 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -5672,6 +5672,12 @@ static void Cmd_moveend(void) effect = TRUE; gBattleScripting.moveendState++; break; + case MOVEEND_OPPORTUNIST: + if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, 0, 0, 0, 0)) + effect = TRUE; // it loops through all battlers, so we increment after its done with all battlers + else + gBattleScripting.moveendState++; + break; case MOVEEND_STATUS_IMMUNITY_ABILITIES: // status immunities if (AbilityBattleEffects(ABILITYEFFECT_IMMUNITY, 0, 0, 0, 0)) effect = TRUE; // it loops through all battlers, so we increment after its done with all battlers @@ -7209,6 +7215,8 @@ static void Cmd_switchineffects(void) { if (DoSwitchInAbilitiesItems(gActiveBattler)) return; + else if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, gActiveBattler, 0, 0, 0)) + return; } gSideStatuses[GetBattlerSide(gActiveBattler)] &= ~(SIDE_STATUS_SPIKES_DAMAGED | SIDE_STATUS_TOXIC_SPIKES_DAMAGED | SIDE_STATUS_STEALTH_ROCK_DAMAGED | SIDE_STATUS_STICKY_WEB_DAMAGED); @@ -9436,6 +9444,7 @@ static void Cmd_various(void) AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, gActiveBattler, 0, 0, 0); AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN, gActiveBattler, 0, 0, 0); AbilityBattleEffects(ABILITYEFFECT_TRACE2, gActiveBattler, 0, 0, 0); + AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, gActiveBattler, 0, 0, 0); return; } case VARIOUS_SAVE_TARGET: @@ -12133,15 +12142,19 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr gBattleCommunication[MULTISTRING_CHOOSER] = (gBattlerTarget == gActiveBattler); gProtectStructs[gActiveBattler].statRaised = TRUE; - // check mirror herb + // check mirror herb / opportunist for (index = 0; index < gBattlersCount; index++) { if (GetBattlerSide(index) == GetBattlerSide(gActiveBattler)) continue; // Only triggers on opposing side - if (GetBattlerHoldEffect(index, TRUE) == HOLD_EFFECT_MIRROR_HERB + if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[gActiveBattler].activateOpportunist == 0) { + gProtectStructs[index].activateOpportunist = 2; // set stats to copy + gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk + gTotemBoosts[index].statChanges[statId - 1] = statValue; + } else if (GetBattlerHoldEffect(index, TRUE) == HOLD_EFFECT_MIRROR_HERB && gBattleMons[index].statStages[statId] < MAX_STAT_STAGE) { - gProtectStructs[index].eatMirrorHerb = 1; + gProtectStructs[index].eatMirrorHerb = TRUE; gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk gTotemBoosts[index].statChanges[statId - 1] = statValue; } diff --git a/src/battle_util.c b/src/battle_util.c index e8c9b8d8cd35..1958d093c356 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5890,6 +5890,27 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move break; } break; + case ABILITYEFFECT_OPPORTUNIST: + /* Similar to ABILITYEFFECT_IMMUNITY in that it loops through all battlers. + * Is called after ABILITYEFFECT_ON_SWITCHIN to copy any boosts + * from switch in abilities e.g. intrepid sword, as + */ + for (battler = 0; battler < gBattlersCount; battler++) + { + switch (GetBattlerAbility(battler)) + { + case ABILITY_OPPORTUNIST: + if (gProtectStructs[battler].activateOpportunist == 2) { + gBattleScripting.savedBattler = gBattlerAttacker; + gBattleScripting.battler = gBattlerAttacker = gBattlerAbility = battler; + gProtectStructs[battler].activateOpportunist--; // TODO if after switch in, this should be set to 0 + BattleScriptPushCursorAndCallback(BattleScript_OpportunistCopyStatChange); + effect = 1; + } + break; + } + } + break; case ABILITYEFFECT_IMMUNITY: // 5 for (battler = 0; battler < gBattlersCount; battler++) { @@ -5948,6 +5969,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move effect = 4; break; } + if (effect != 0) { switch (effect) From 0d1c0a8a918d09f234ab65fc3b08ca48fcc8a257 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Thu, 11 May 2023 13:01:31 -0400 Subject: [PATCH 02/12] opportunist, mirror herb cumulative boosts to gTotemBoosts to handle multiple opponent stat boosts --- src/battle_script_commands.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 4f9d51628e36..187f51d1c48f 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -12150,13 +12150,13 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[gActiveBattler].activateOpportunist == 0) { gProtectStructs[index].activateOpportunist = 2; // set stats to copy gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gTotemBoosts[index].statChanges[statId - 1] = statValue; + gTotemBoosts[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) { gProtectStructs[index].eatMirrorHerb = TRUE; gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gTotemBoosts[index].statChanges[statId - 1] = statValue; + gTotemBoosts[index].statChanges[statId - 1] += statValue; } } } From 5c0f0696c3a02a13ca489a23d8d806b6a4cf31d2 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Thu, 11 May 2023 13:04:08 -0400 Subject: [PATCH 03/12] remove TODO comment --- src/battle_util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/battle_util.c b/src/battle_util.c index 1958d093c356..962b18b0409f 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5903,7 +5903,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move if (gProtectStructs[battler].activateOpportunist == 2) { gBattleScripting.savedBattler = gBattlerAttacker; gBattleScripting.battler = gBattlerAttacker = gBattlerAbility = battler; - gProtectStructs[battler].activateOpportunist--; // TODO if after switch in, this should be set to 0 + gProtectStructs[battler].activateOpportunist--; BattleScriptPushCursorAndCallback(BattleScript_OpportunistCopyStatChange); effect = 1; } From 410fc73fb994eba3c58bfa42744a65b1a71c7d8c Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Fri, 12 May 2023 10:05:15 -0400 Subject: [PATCH 04/12] add first opportunist test --- test/ability_opportunist.c | 41 ++++++++++++++++++++++++++++++++++ test/hold_effect_mirror_herb.c | 4 ++-- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 test/ability_opportunist.c diff --git a/test/ability_opportunist.c b/test/ability_opportunist.c new file mode 100644 index 000000000000..8e01b004aba9 --- /dev/null +++ b/test/ability_opportunist.c @@ -0,0 +1,41 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].abilities[2] == ABILITY_OPPORTUNIST); +} + +SINGLE_BATTLE_TEST("Opportunist copies all of foe's positive stat changes in a turn", s16 damage) +{ + u32 ability; + PARAMETRIZE { ability = ABILITY_NONE; } + PARAMETRIZE { ability = ABILITY_OPPORTUNIST; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(4); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(5); Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_SHELL_SMASH); } + TURN { MOVE(player, MOVE_TACKLE); MOVE(opponent, MOVE_TACKLE); } + } SCENE { + if (ability == ABILITY_NONE) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &results[i].damage); + } else { + 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); + // stat boosts should be the same + EXPECT_EQ(player->statStages[STAT_ATK], opponent->statStages[STAT_ATK]); + EXPECT_EQ(player->statStages[STAT_SPATK], opponent->statStages[STAT_SPATK]); + EXPECT_EQ(player->statStages[STAT_SPEED], opponent->statStages[STAT_SPEED]); + // opportunist should not copy stat drops from shell smash + EXPECT_LT(opponent->statStages[STAT_DEF], player->statStages[STAT_DEF]); + EXPECT_LT(opponent->statStages[STAT_SPDEF], player->statStages[STAT_SPDEF]); + } +} + diff --git a/test/hold_effect_mirror_herb.c b/test/hold_effect_mirror_herb.c index 22f564b85007..638aacfb0403 100644 --- a/test/hold_effect_mirror_herb.c +++ b/test/hold_effect_mirror_herb.c @@ -6,7 +6,7 @@ ASSUMPTIONS ASSUME(gItems[ITEM_MIRROR_HERB].holdEffect == HOLD_EFFECT_MIRROR_HERB); } -SINGLE_BATTLE_TEST("Mirror Herb copies all of foe's stat changes in a turn", s16 damage) +SINGLE_BATTLE_TEST("Mirror Herb copies all of foe's positive stat changes in a turn", s16 damage) { u32 item; PARAMETRIZE { item = ITEM_NONE; } @@ -34,7 +34,7 @@ SINGLE_BATTLE_TEST("Mirror Herb copies all of foe's stat changes in a turn", s16 } } -SINGLE_BATTLE_TEST("Mirror Herb copies all of of Stuff Cheeks") +SINGLE_BATTLE_TEST("Mirror Herb copies all of Stuff Cheeks' stat boosts") { GIVEN { ASSUME(gItems[ITEM_LIECHI_BERRY].holdEffect == HOLD_EFFECT_ATTACK_UP); From 0947989e191ea7b3fcfefd57aff1f85d847c7b7f Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Fri, 16 Jun 2023 11:07:42 -0400 Subject: [PATCH 05/12] rename gTotemBoosts to gQueuedStatBoosts --- include/battle.h | 4 ++-- src/battle_main.c | 12 ++++++------ src/battle_script_commands.c | 24 ++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/include/battle.h b/include/battle.h index b7149c2aafde..5298a4b463e3 100644 --- a/include/battle.h +++ b/include/battle.h @@ -878,7 +878,7 @@ struct MonSpritesGfx u16 *buffer; }; -struct TotemBoost +struct QueuedStatBoost { u8 stats; // bitfield for each battle stat that is set if the stat changes s8 statChanges[NUM_BATTLE_STATS - 1]; // highest bit being set decreases the stat @@ -994,7 +994,7 @@ extern u32 gFieldStatuses; extern struct FieldTimer gFieldTimers; extern u8 gBattlerAbility; extern u16 gPartnerSpriteId; -extern struct TotemBoost gTotemBoosts[MAX_BATTLERS_COUNT]; +extern struct QueuedStatBoost gQueuedStatBoosts[MAX_BATTLERS_COUNT]; extern void (*gPreBattleCallback1)(void); extern void (*gBattleMainFunc)(void); diff --git a/src/battle_main.c b/src/battle_main.c index 7051e5b6114b..f3aa4b395755 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -239,7 +239,7 @@ EWRAM_DATA u32 gFieldStatuses = 0; EWRAM_DATA struct FieldTimer gFieldTimers = {0}; EWRAM_DATA u8 gBattlerAbility = 0; EWRAM_DATA u16 gPartnerSpriteId = 0; -EWRAM_DATA struct TotemBoost gTotemBoosts[MAX_BATTLERS_COUNT] = {0}; +EWRAM_DATA struct QueuedStatBoost gQueuedStatBoosts[MAX_BATTLERS_COUNT] = {0}; EWRAM_DATA bool8 gHasFetchedBall = FALSE; EWRAM_DATA u8 gLastUsedBall = 0; EWRAM_DATA u16 gLastThrownBall = 0; @@ -3823,7 +3823,7 @@ static void TryDoEventsBeforeFirstTurn(void) // Totem boosts for (i = 0; i < gBattlersCount; i++) { - if (gTotemBoosts[i].stats != 0 && !gProtectStructs[i].eatMirrorHerb && gProtectStructs[i].activateOpportunist == 0) + if (gQueuedStatBoosts[i].stats != 0 && !gProtectStructs[i].eatMirrorHerb && gProtectStructs[i].activateOpportunist == 0) { gBattlerAttacker = i; BattleScriptExecute(BattleScript_TotemVar); @@ -3895,7 +3895,7 @@ static void TryDoEventsBeforeFirstTurn(void) gRandomTurnNumber = Random(); - memset(gTotemBoosts, 0, sizeof(gTotemBoosts)); // erase all totem boosts just to be safe + memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); // erase all totem boosts just to be safe GetAiLogicData(); // get assumed abilities, hold effects, etc of all battlers @@ -5740,9 +5740,9 @@ void SetTotemBoost(void) { if (*(&gSpecialVar_0x8001 + i)) { - gTotemBoosts[battlerId].stats |= (1 << i); - gTotemBoosts[battlerId].statChanges[i] = *(&gSpecialVar_0x8001 + i); - gTotemBoosts[battlerId].stats |= 0x80; // used as a flag for the "totem flared to life" script + gQueuedStatBoosts[battlerId].stats |= (1 << i); + gQueuedStatBoosts[battlerId].statChanges[i] = *(&gSpecialVar_0x8001 + i); + gQueuedStatBoosts[battlerId].stats |= 0x80; // used as a flag for the "totem flared to life" script } } } diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 59f505167134..631fe20d0994 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -10307,7 +10307,7 @@ static void Cmd_various(void) { VARIOUS_ARGS(const u8 *jumpInstr); gActiveBattler = gBattlerAttacker; - if (gTotemBoosts[gActiveBattler].stats == 0) + if (gQueuedStatBoosts[gActiveBattler].stats == 0) { gBattlescriptCurrInstr = cmd->nextInstr; // stats done, exit } @@ -10315,19 +10315,19 @@ static void Cmd_various(void) { for (i = 0; i < (NUM_BATTLE_STATS - 1); i++) { - if (gTotemBoosts[gActiveBattler].stats & (1 << i)) + if (gQueuedStatBoosts[gActiveBattler].stats & (1 << i)) { - if (gTotemBoosts[gActiveBattler].statChanges[i] <= -1) - SET_STATCHANGER(i + 1, abs(gTotemBoosts[gActiveBattler].statChanges[i]), TRUE); + if (gQueuedStatBoosts[gActiveBattler].statChanges[i] <= -1) + SET_STATCHANGER(i + 1, abs(gQueuedStatBoosts[gActiveBattler].statChanges[i]), TRUE); else - SET_STATCHANGER(i + 1, gTotemBoosts[gActiveBattler].statChanges[i], FALSE); + SET_STATCHANGER(i + 1, gQueuedStatBoosts[gActiveBattler].statChanges[i], FALSE); - gTotemBoosts[gActiveBattler].stats &= ~(1 << i); + gQueuedStatBoosts[gActiveBattler].stats &= ~(1 << i); gBattleScripting.battler = gActiveBattler; gBattlerTarget = gActiveBattler; - if (gTotemBoosts[gActiveBattler].stats & 0x80) + if (gQueuedStatBoosts[gActiveBattler].stats & 0x80) { - gTotemBoosts[gActiveBattler].stats &= ~0x80; // set 'aura flared to life' flag + gQueuedStatBoosts[gActiveBattler].stats &= ~0x80; // set 'aura flared to life' flag gBattlescriptCurrInstr = BattleScript_TotemFlaredToLife; } else @@ -12101,14 +12101,14 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr continue; // Only triggers on opposing side if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[gActiveBattler].activateOpportunist == 0) { gProtectStructs[index].activateOpportunist = 2; // set stats to copy - gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gTotemBoosts[index].statChanges[statId - 1] += statValue; // cumulative in case of multiple opponent boosts + 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) { gProtectStructs[index].eatMirrorHerb = TRUE; - gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gTotemBoosts[index].statChanges[statId - 1] += statValue; + gQueuedStatBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk + gQueuedStatBoosts[index].statChanges[statId - 1] += statValue; } } } From f05f7261b7c43793cb5d7e866cca5be25eda3964 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Mon, 25 Sep 2023 14:05:29 -0400 Subject: [PATCH 06/12] fix a few errors --- src/battle_script_commands.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 8feaac07f1bc..b4305e8b425e 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6981,7 +6981,7 @@ static void Cmd_switchineffects(void) { if (DoSwitchInAbilitiesItems(battler)) return; - else if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, gActiveBattler, 0, 0, 0)) + else if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, battler, 0, 0, 0)) return; } @@ -11643,8 +11643,8 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr && gBattleMons[index].statStages[statId] < MAX_STAT_STAGE) { gProtectStructs[index].eatMirrorHerb = 1; - gTotemBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk - gTotemBoosts[index].statChanges[statId - 1] = statValue; + gQueuedStatBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk + gQueuedStatBoosts[index].statChanges[statId - 1] = statValue; } } } From baa08f3fcf0b7f043a8dc79d78dd4dca7a3abdbd Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Mon, 25 Sep 2023 16:21:08 -0400 Subject: [PATCH 07/12] fix opportunist test --- .../ability/opportunist.c} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename test/{ability_opportunist.c => battle/ability/opportunist.c} (81%) diff --git a/test/ability_opportunist.c b/test/battle/ability/opportunist.c similarity index 81% rename from test/ability_opportunist.c rename to test/battle/ability/opportunist.c index 8e01b004aba9..2721b1e043e7 100644 --- a/test/ability_opportunist.c +++ b/test/battle/ability/opportunist.c @@ -1,12 +1,12 @@ #include "global.h" -#include "test_battle.h" +#include "test/battle.h" ASSUMPTIONS { ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].abilities[2] == ABILITY_OPPORTUNIST); } -SINGLE_BATTLE_TEST("Opportunist copies all of foe's positive stat changes in a turn", s16 damage) +SINGLE_BATTLE_TEST("Opportunist only copies foe's positive stat changes in a turn", s16 damage) { u32 ability; PARAMETRIZE { ability = ABILITY_NONE; } @@ -19,7 +19,7 @@ SINGLE_BATTLE_TEST("Opportunist copies all of foe's positive stat changes in a t TURN { MOVE(player, MOVE_TACKLE); MOVE(opponent, MOVE_TACKLE); } } SCENE { if (ability == ABILITY_NONE) { - ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SHELL_SMASH, player); ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); HP_BAR(player, captureDamage: &results[i].damage); } else { @@ -34,8 +34,8 @@ SINGLE_BATTLE_TEST("Opportunist copies all of foe's positive stat changes in a t EXPECT_EQ(player->statStages[STAT_SPATK], opponent->statStages[STAT_SPATK]); EXPECT_EQ(player->statStages[STAT_SPEED], opponent->statStages[STAT_SPEED]); // opportunist should not copy stat drops from shell smash - EXPECT_LT(opponent->statStages[STAT_DEF], player->statStages[STAT_DEF]); - EXPECT_LT(opponent->statStages[STAT_SPDEF], player->statStages[STAT_SPDEF]); + EXPECT_LT(player->statStages[STAT_DEF], opponent->statStages[STAT_DEF]); + EXPECT_LT(player->statStages[STAT_SPDEF], opponent->statStages[STAT_SPDEF]); } } From 2d4678aa988224043d3c2be0b6d8c6c81fc01fc2 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Thu, 28 Sep 2023 11:44:08 -0400 Subject: [PATCH 08/12] fix activateOpportunist battler id check --- src/battle_script_commands.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 54fac18b4a7d..e0f3e90f877e 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -11650,7 +11650,7 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr { if (GetBattlerSide(index) == GetBattlerSide(battler)) continue; // Only triggers on opposing side - if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[battler].activateOpportunist == 0) + if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[index].activateOpportunist == 0) { gProtectStructs[index].activateOpportunist = 2; // set stats to copy gQueuedStatBoosts[index].stats |= (1 << (statId - 1)); // -1 to start at atk From 5b15f0417318011bad31b3dc43ad5537cace74f0 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Thu, 28 Sep 2023 12:18:17 -0400 Subject: [PATCH 09/12] revert last fix but add comment about why its needed --- src/battle_script_commands.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index e0f3e90f877e..6b2e3708cd3c 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -11650,7 +11650,8 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr { if (GetBattlerSide(index) == GetBattlerSide(battler)) continue; // Only triggers on opposing side - if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST && gProtectStructs[index].activateOpportunist == 0) + if (GetBattlerAbility(index) == ABILITY_OPPORTUNIST + && 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 From 74479b9430047d5c5fa4c989d1358f554e31f4cf Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Fri, 6 Oct 2023 09:58:46 -0400 Subject: [PATCH 10/12] add opportunist intimidate contrary test --- test/battle/ability/opportunist.c | 72 ++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/test/battle/ability/opportunist.c b/test/battle/ability/opportunist.c index 2721b1e043e7..fdcb4203be69 100644 --- a/test/battle/ability/opportunist.c +++ b/test/battle/ability/opportunist.c @@ -1,11 +1,6 @@ #include "global.h" #include "test/battle.h" -ASSUMPTIONS -{ - ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].abilities[2] == ABILITY_OPPORTUNIST); -} - SINGLE_BATTLE_TEST("Opportunist only copies foe's positive stat changes in a turn", s16 damage) { u32 ability; @@ -39,3 +34,70 @@ SINGLE_BATTLE_TEST("Opportunist only copies foe's positive stat changes in a tur } } + +DOUBLE_BATTLE_TEST("Opportunist raises Attack only once when partner has Intimidate against Contrary foe in a double battle", s16 damageLeft, s16 damageRight) +{ + u32 abilityLeft, abilityRight; + + PARAMETRIZE { abilityLeft = ABILITY_CONTRARY; abilityRight = ABILITY_CONTRARY; } + PARAMETRIZE { abilityLeft = ABILITY_TANGLED_FEET; abilityRight = ABILITY_TANGLED_FEET; } + PARAMETRIZE { abilityLeft = ABILITY_CONTRARY; abilityRight = ABILITY_TANGLED_FEET; } + PARAMETRIZE { abilityLeft = ABILITY_TANGLED_FEET; abilityRight = ABILITY_CONTRARY; } + + GIVEN { + PLAYER(SPECIES_MIGHTYENA) { Ability(ABILITY_INTIMIDATE); } + PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_OPPORTUNIST); } + OPPONENT(SPECIES_SPINDA) { Ability(abilityLeft); } + OPPONENT(SPECIES_SPINDA) { Ability(abilityRight); } + } WHEN { + TURN { MOVE(opponentLeft, MOVE_TACKLE, target: playerLeft); MOVE(opponentRight, MOVE_TACKLE, target: playerRight); } + } SCENE { + ABILITY_POPUP(playerLeft, ABILITY_INTIMIDATE); + if (abilityLeft == ABILITY_CONTRARY) { + ABILITY_POPUP(opponentLeft, ABILITY_CONTRARY); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + MESSAGE("Foe Spinda's Attack rose!"); + } else { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + MESSAGE("Mightyena's Intimidate cuts Foe Spinda's attack!"); + } + if (abilityRight == ABILITY_CONTRARY) { + ABILITY_POPUP(opponentRight, ABILITY_CONTRARY); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + MESSAGE("Foe Spinda's Attack rose!"); + } else { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + MESSAGE("Mightyena's Intimidate cuts Foe Spinda's attack!"); + } + + if ((abilityLeft == ABILITY_CONTRARY && abilityRight != ABILITY_CONTRARY) + || (abilityLeft != ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY)) { + ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); + MESSAGE("Wobbuffet copied its opponent's stat changes!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + MESSAGE("Wobbuffet's Attack rose!"); + } else if (abilityLeft == ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY) { + ABILITY_POPUP(playerRight, ABILITY_OPPORTUNIST); + MESSAGE("Wobbuffet copied its opponent's stat changes!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerRight); + MESSAGE("Wobbuffet's Attack sharply rose!"); + } + + HP_BAR(playerLeft, captureDamage: &results[i].damageLeft); + HP_BAR(playerRight, captureDamage: &results[i].damageRight); + } THEN { + EXPECT_EQ(opponentLeft->statStages[STAT_ATK], (abilityLeft == ABILITY_CONTRARY) ? DEFAULT_STAT_STAGE+1 : DEFAULT_STAT_STAGE-1); + EXPECT_EQ(opponentRight->statStages[STAT_ATK], (abilityRight == ABILITY_CONTRARY) ? DEFAULT_STAT_STAGE+1 : DEFAULT_STAT_STAGE-1); + if ((abilityLeft == ABILITY_CONTRARY && abilityRight != ABILITY_CONTRARY) + || (abilityLeft != ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY)) { + EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 1); + } else if (abilityLeft == ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY) { + EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 2); + } + } + FINALLY { + EXPECT_MUL_EQ(results[1].damageLeft, Q_4_12(2.25), results[0].damageLeft); + EXPECT_MUL_EQ(results[1].damageRight, Q_4_12(2.25), results[0].damageRight); + } +} + From 574e2ad28a905faea11dff280708d2a1c6805307 Mon Sep 17 00:00:00 2001 From: ghoulslash Date: Fri, 6 Oct 2023 09:59:50 -0400 Subject: [PATCH 11/12] add opportunist todo tests --- test/battle/ability/opportunist.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/battle/ability/opportunist.c b/test/battle/ability/opportunist.c index fdcb4203be69..d2b43e8d5fdd 100644 --- a/test/battle/ability/opportunist.c +++ b/test/battle/ability/opportunist.c @@ -101,3 +101,6 @@ DOUBLE_BATTLE_TEST("Opportunist raises Attack only once when partner has Intimid } } +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"); From 844597d1d1007773a5e4c19149fb72eef902feca Mon Sep 17 00:00:00 2001 From: ghoulslash <41651341+ghoulslash@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:36:10 -0600 Subject: [PATCH 12/12] Update test/battle/ability/opportunist.c Co-authored-by: Eduardo Quezada D'Ottone --- test/battle/ability/opportunist.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/battle/ability/opportunist.c b/test/battle/ability/opportunist.c index d2b43e8d5fdd..f344bd026ff2 100644 --- a/test/battle/ability/opportunist.c +++ b/test/battle/ability/opportunist.c @@ -86,8 +86,8 @@ DOUBLE_BATTLE_TEST("Opportunist raises Attack only once when partner has Intimid HP_BAR(playerLeft, captureDamage: &results[i].damageLeft); HP_BAR(playerRight, captureDamage: &results[i].damageRight); } THEN { - EXPECT_EQ(opponentLeft->statStages[STAT_ATK], (abilityLeft == ABILITY_CONTRARY) ? DEFAULT_STAT_STAGE+1 : DEFAULT_STAT_STAGE-1); - EXPECT_EQ(opponentRight->statStages[STAT_ATK], (abilityRight == ABILITY_CONTRARY) ? DEFAULT_STAT_STAGE+1 : DEFAULT_STAT_STAGE-1); + EXPECT_EQ(opponentLeft->statStages[STAT_ATK], DEFAULT_STAT_STAGE + (abilityLeft == ABILITY_CONTRARY ? 1 : - 1)); + EXPECT_EQ(opponentRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + (abilityRight == ABILITY_CONTRARY ? 1 : - 1)); if ((abilityLeft == ABILITY_CONTRARY && abilityRight != ABILITY_CONTRARY) || (abilityLeft != ABILITY_CONTRARY && abilityRight == ABILITY_CONTRARY)) { EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 1);