Opportunist Ability (#2994)

Co-authored-by: Eduardo Quezada D'Ottone <eduardo602002@gmail.com>
Co-authored-by: ghoulslash <pokevoyager0@gmail.com>
This commit is contained in:
ghoulslash 2023-10-09 21:35:00 -04:00 committed by GitHub
parent 2fac60055f
commit 70fbf9e9df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 46 deletions

View file

@ -8712,28 +8712,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_ActivateTerrainAbilities:
savetarget
setbyte gBattlerTarget, 0
BattleScript_ActivateTerrainAbilities_Loop:
activateterrainchangeabilities BS_ATTACKER
BattleScript_ActivateTerrainAbilities_Increment:
addbyte gBattlerTarget, 1
jumpifbytenotequal gBattlerTarget, gBattlersCount, BattleScript_ActivateTerrainAbilities_Loop
restoretarget
return
BattleScript_ElectricSurgeActivates::
pause B_WAIT_TIME_SHORT
call BattleScript_AbilityPopUp
@ -9902,6 +9880,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

View file

@ -149,6 +149,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;
@ -901,7 +902,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
@ -1014,7 +1015,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);

View file

@ -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[];

View file

@ -38,6 +38,7 @@
#define ABILITYEFFECT_ON_TERRAIN 15
#define ABILITYEFFECT_SWITCH_IN_TERRAIN 16
#define ABILITYEFFECT_SWITCH_IN_WEATHER 17
#define ABILITYEFFECT_OPPORTUNIST 18
// 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

View file

@ -323,8 +323,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

View file

@ -670,8 +670,9 @@
#define STRINGID_CURRENTMOVECANTSELECT 668
#define STRINGID_TARGETISBEINGSALTCURED 669
#define STRINGID_TARGETISHURTBYSALTCURE 670
#define STRINGID_OPPORTUNISTCOPIED 671
#define BATTLESTRINGS_COUNT 671
#define BATTLESTRINGS_COUNT 672
// This is the string id that gBattleStringsTable starts with.
// String ids before this (e.g. STRINGID_INTROMSG) are not in the table,

View file

@ -236,7 +236,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;
@ -3790,14 +3790,13 @@ static void TryDoEventsBeforeFirstTurn(void)
// Totem boosts
for (i = 0; i < gBattlersCount; i++)
{
if (gTotemBoosts[i].stats != 0)
if (gQueuedStatBoosts[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)
@ -3821,6 +3820,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++)
{
@ -3854,6 +3856,8 @@ static void TryDoEventsBeforeFirstTurn(void)
gMoveResultFlags = 0;
gRandomTurnNumber = Random();
memset(gQueuedStatBoosts, 0, sizeof(gQueuedStatBoosts)); // erase all totem boosts just to be safe
SetAiLogicDataForTurn(AI_DATA); // get assumed abilities, hold effects, etc of all battlers
@ -5737,9 +5741,9 @@ void SetTotemBoost(void)
{
if (*(&gSpecialVar_0x8001 + i))
{
gTotemBoosts[battler].stats |= (1 << i);
gTotemBoosts[battler].statChanges[i] = *(&gSpecialVar_0x8001 + i);
gTotemBoosts[battler].stats |= 0x80; // used as a flag for the "totem flared to life" script
gQueuedStatBoosts[battler].stats |= (1 << i);
gQueuedStatBoosts[battler].statChanges[i] = *(&gSpecialVar_0x8001 + i);
gQueuedStatBoosts[battler].stats |= 0x80; // used as a flag for the "totem flared to life" script
}
}
}

View file

@ -807,9 +807,11 @@ 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!");
const u8 *const gBattleStringsTable[BATTLESTRINGS_COUNT] =
{
[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,

View file

@ -5429,6 +5429,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
@ -6963,6 +6969,8 @@ static void Cmd_switchineffects(void)
{
if (DoSwitchInAbilitiesItems(battler))
return;
else if (AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, battler, 0, 0, 0))
return;
}
gDisableStructs[battler].stickyWebDone = FALSE;
@ -9062,6 +9070,7 @@ static void Cmd_various(void)
AbilityBattleEffects(ABILITYEFFECT_NEUTRALIZINGGAS, battler, 0, 0, 0);
AbilityBattleEffects(ABILITYEFFECT_ON_SWITCHIN, battler, 0, 0, 0);
AbilityBattleEffects(ABILITYEFFECT_TRACE2, battler, 0, 0, 0);
AbilityBattleEffects(ABILITYEFFECT_OPPORTUNIST, battler, 0, 0, 0);
return;
}
case VARIOUS_SAVE_TARGET:
@ -9883,7 +9892,7 @@ static void Cmd_various(void)
{
VARIOUS_ARGS(const u8 *jumpInstr);
battler = gBattlerAttacker;
if (gTotemBoosts[battler].stats == 0)
if (gQueuedStatBoosts[battler].stats == 0)
{
gBattlescriptCurrInstr = cmd->nextInstr; // stats done, exit
}
@ -9891,19 +9900,19 @@ static void Cmd_various(void)
{
for (i = 0; i < (NUM_BATTLE_STATS - 1); i++)
{
if (gTotemBoosts[battler].stats & (1 << i))
if (gQueuedStatBoosts[battler].stats & (1 << i))
{
if (gTotemBoosts[battler].statChanges[i] <= -1)
SET_STATCHANGER(i + 1, abs(gTotemBoosts[battler].statChanges[i]), TRUE);
if (gQueuedStatBoosts[battler].statChanges[i] <= -1)
SET_STATCHANGER(i + 1, abs(gQueuedStatBoosts[battler].statChanges[i]), TRUE);
else
SET_STATCHANGER(i + 1, gTotemBoosts[battler].statChanges[i], FALSE);
SET_STATCHANGER(i + 1, gQueuedStatBoosts[battler].statChanges[i], FALSE);
gTotemBoosts[battler].stats &= ~(1 << i);
gQueuedStatBoosts[battler].stats &= ~(1 << i);
gBattleScripting.battler = battler;
gBattlerTarget = battler;
if (gTotemBoosts[battler].stats & 0x80)
if (gQueuedStatBoosts[battler].stats & 0x80)
{
gTotemBoosts[battler].stats &= ~0x80; // set 'aura flared to life' flag
gQueuedStatBoosts[battler].stats &= ~0x80; // set 'aura flared to life' flag
gBattlescriptCurrInstr = BattleScript_TotemFlaredToLife;
}
else
@ -11617,12 +11626,19 @@ 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 (GetBattlerHoldEffect(index, TRUE) == HOLD_EFFECT_MIRROR_HERB
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
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 = 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;
}
}
}

View file

@ -5789,6 +5789,27 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32
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--;
BattleScriptPushCursorAndCallback(BattleScript_OpportunistCopyStatChange);
effect = 1;
}
break;
}
}
break;
case ABILITYEFFECT_IMMUNITY: // 5
for (battler = 0; battler < gBattlersCount; battler++)
{
@ -5847,6 +5868,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32
effect = 4;
break;
}
if (effect != 0)
{
switch (effect)

View file

@ -0,0 +1,106 @@
#include "global.h"
#include "test/battle.h"
SINGLE_BATTLE_TEST("Opportunist only copies 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_SHELL_SMASH, 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(player->statStages[STAT_DEF], opponent->statStages[STAT_DEF]);
EXPECT_LT(player->statStages[STAT_SPDEF], opponent->statStages[STAT_SPDEF]);
}
}
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], 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);
} 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);
}
}
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");

View file

@ -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);