Switch AI refactor + considers free switches (#5379)

* Switch AI considers free switches from pivot move

* Fix Baton Pass refactor

* Some cleanup and comments

* Mon fainting to hazards is a 0HKO

* Revert "Mon fainting to hazards is a 0HKO"

This reverts commit 446f738226.

* Cleanup speed check / Eject Pack

* Move eject trackers to AiLogicData

* Review feedback and WhoStrikesFirst changes

* This linebreak will bug me lol

* Also this comment, heck

* Last bit of comment cleanup
This commit is contained in:
Pawkkie 2024-10-01 17:10:02 -04:00 committed by GitHub
parent 5a8a393dcd
commit ded97e5296
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 211 additions and 103 deletions

View file

@ -374,6 +374,8 @@ struct AiLogicData
bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once.
u8 mostSuitableMonId[MAX_BATTLERS_COUNT]; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId.
struct SwitchinCandidate switchinCandidate; // Struct used for deciding which mon to switch to in battle_ai_switch_items.c
bool8 ejectButtonSwitch; // Tracks whether current switch out was from Eject Button
bool8 ejectPackSwitch; // Tracks whether current switch out was from Eject Pack
u8 shouldSwitch; // Stores result of ShouldSwitch, which decides whether a mon should be switched out
};

View file

@ -31,6 +31,7 @@ void RecordItemEffectBattle(u32 battlerId, u32 itemEffect);
void ClearBattlerItemEffectHistory(u32 battlerId);
void SaveBattlerData(u32 battlerId);
void SetBattlerData(u32 battlerId);
void SetBattlerAiData(u32 battlerId, struct AiLogicData *aiData);
void RestoreBattlerData(u32 battlerId);
u32 GetAIChosenMove(u32 battlerId);
u32 GetTotalBaseStat(u32 species);
@ -140,6 +141,7 @@ bool32 HasThawingMove(u32 battler);
bool32 IsStatRaisingEffect(u32 effect);
bool32 IsStatLoweringEffect(u32 effect);
bool32 IsSelfStatLoweringEffect(u32 effect);
bool32 IsSwitchOutEffect(u32 effect);
bool32 IsAttackBoostMoveEffect(u32 effect);
bool32 IsUngroundingEffect(u32 effect);
bool32 IsSemiInvulnerable(u32 battlerDef, u32 move);
@ -201,6 +203,7 @@ void IncreaseConfusionScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score
void IncreaseFrostbiteScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score);
s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool32 isPartyMonAttacker, enum DamageRollType rollType);
u32 AI_WhoStrikesFirstPartyMon(u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, u32 moveConsidered);
s32 AI_TryToClearStats(u32 battlerAtk, u32 battlerDef, bool32 isDoubleBattle);
bool32 AI_ShouldCopyStatChanges(u32 battlerAtk, u32 battlerDef);
bool32 AI_ShouldSetUpHazards(u32 battlerAtk, u32 battlerDef, struct AiLogicData *aiData);

View file

@ -389,7 +389,7 @@ void Ai_UpdateFaintData(u32 battler)
aiMon->isFainted = TRUE;
}
static void SetBattlerAiData(u32 battler, struct AiLogicData *aiData)
void SetBattlerAiData(u32 battler, struct AiLogicData *aiData)
{
u32 ability, holdEffect;

View file

@ -1789,30 +1789,60 @@ static bool32 CanAbilityTrapOpponent(u16 ability, u32 opponent)
return FALSE;
}
// This function splits switching behaviour mid-battle from after a KO.
// Mid battle, it integrates GetBestMonTypeMatchup (vanilla with modifications), GetBestMonDefensive (custom), and GetBestMonBatonPass (vanilla with modifications)
// After a KO, integrates GetBestMonRevengeKiller (custom), GetBestMonTypeMatchup (vanilla with modifications), GetBestMonBatonPass (vanilla with modifications), and GetBestMonDmg (vanilla)
// the Type Matchup code will prioritize switching into a mon with the best type matchup and also a super effective move, or just best type matchup if no super effective move is found
// the Most Defensive code will prioritize switching into the mon that takes the most hits to KO, with a minimum of 4 hits required to be considered a valid option
// the Baton Pass code will prioritize switching into a mon with Baton Pass if it can get in, boost, and BP out without being KO'd, and randomizes between multiple valid options
// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd
// the Most Damage code will prioritize switching into whatever mon deals the most damage, which is generally not as good as having a good Type Matchup
// Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you!
static inline bool32 IsFreeSwitch(bool32 isSwitchAfterKO, u32 battlerSwitchingOut, u32 opposingBattler)
{
bool32 movedSecond = GetBattlerTurnOrderNum(battlerSwitchingOut) > GetBattlerTurnOrderNum(opposingBattler) ? TRUE : FALSE;
// Switch out effects
if (!IsDoubleBattle()) // Not handling doubles' additional complexity
{
if (IsSwitchOutEffect(gMovesInfo[gLastUsedMove].effect) && movedSecond)
return TRUE;
if (AI_DATA->ejectButtonSwitch)
return TRUE;
if (AI_DATA->ejectPackSwitch)
{
// If faster, not a free switch; likely lowered own stats
if (!movedSecond)
return FALSE;
// Otherwise, free switch
return TRUE;
}
}
// Post KO check has to be last because the GetMostSuitableMonToSwitchInto call in OpponentHandleChoosePokemon assumes a KO rather than a forced switch choice
if (isSwitchAfterKO)
return TRUE;
else
return FALSE;
}
static inline bool32 CanSwitchinWin1v1(u32 hitsToKOAI, u32 hitsToKOPlayer, bool32 isSwitchinFirst, bool32 isFreeSwitch)
{
// Free switch, need to outspeed or take 1 extra hit
if (isFreeSwitch)
{
if (hitsToKOAI > hitsToKOPlayer || (hitsToKOAI == hitsToKOPlayer && isSwitchinFirst))
return TRUE;
}
// Mid battle switch, need to take 1 or 2 extra hits depending on speed
if (hitsToKOAI > hitsToKOPlayer + 1 || (hitsToKOAI == hitsToKOPlayer + 1 && isSwitchinFirst))
return TRUE;
return FALSE;
}
// This function splits switching behaviour depending on whether the switch is free.
// Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you!
static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u32 battlerIn1, u32 battlerIn2, bool32 isSwitchAfterKO)
{
int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE;
int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE, trapperId = PARTY_SIZE;
int i, j, aliveCount = 0, bits = 0;
s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed
u32 aiMove, hitsToKOAI, hitsToKOPlayer, hitsToKOAIThreshold, maxHitsToKO = 0;
s32 playerMonSpeed = gBattleMons[opposingBattler].speed, playerMonHP = gBattleMons[opposingBattler].hp, aiMonSpeed, aiMovePriority = 0, maxDamageDealt = 0, damageDealt = 0;
s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0;
u32 aiMove, hitsToKOAI, maxHitsToKO = 0;
u16 bestResist = UQ_4_12(1.0), bestResistEffective = UQ_4_12(1.0), typeMatchup;
if (isSwitchAfterKO)
hitsToKOAIThreshold = 1; // After a KO, mons at minimum need to not be 1-shot, as they switch in for free
else
hitsToKOAIThreshold = 2; // When switching in otherwise need to not be 2-shot, as they do not switch in for free
bool32 isFreeSwitch = IsFreeSwitch(isSwitchAfterKO, battlerIn1, opposingBattler), isSwitchinFirst, canSwitchinWin1v1;
// Iterate through mons
for (i = firstId; i < lastId; i++)
@ -1841,10 +1871,11 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))
continue;
// Get max number of hits for player to KO AI mon
// Get max number of hits for player to KO AI mon and type matchup for defensive switching
hitsToKOAI = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon), battler);
typeMatchup = GetSwitchinTypeMatchup(opposingBattler, AI_DATA->switchinCandidate.battleMon);
// Track max hits to KO and set GetBestMonDefensive if applicable
// Track max hits to KO and set defensive mon
if(hitsToKOAI > maxHitsToKO)
{
maxHitsToKO = hitsToKOAI;
@ -1852,28 +1883,12 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
defensiveMonId = i;
}
typeMatchup = GetSwitchinTypeMatchup(opposingBattler, AI_DATA->switchinCandidate.battleMon);
// Check that good type matchups gets at least two turns and set GetBestMonTypeMatchup if applicable
if (typeMatchup < bestResist)
{
if ((hitsToKOAI > hitsToKOAIThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed) || hitsToKOAI > hitsToKOAIThreshold + 1) // Need to take an extra hit if slower
{
bestResist = typeMatchup;
typeMatchupId = i;
}
}
aiMonSpeed = AI_DATA->switchinCandidate.battleMon.speed;
// Check through current mon's moves
for (j = 0; j < MAX_MON_MOVES; j++)
{
aiMove = AI_DATA->switchinCandidate.battleMon.moves[j];
aiMovePriority = gMovesInfo[aiMove].priority;
// Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn
if (isSwitchAfterKO && aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0)
if (aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0)
{
if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_CONSERVATIVE)
damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_LOWEST);
@ -1881,19 +1896,35 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_DEFAULT);
}
// Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not
if (aiMove == MOVE_BATON_PASS && ((hitsToKOAI > hitsToKOAIThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKOAI > hitsToKOAIThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed)))
bits |= 1u << i;
// Offensive switchin decisions are based on which whether switchin moves first and whether it can win a 1v1
isSwitchinFirst = AI_WhoStrikesFirstPartyMon(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, aiMove);
canSwitchinWin1v1 = CanSwitchinWin1v1(hitsToKOAI, GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler), isSwitchinFirst, isFreeSwitch);
// Check for mon with resistance and super effective move for GetBestMonTypeMatchup
// Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not
if (aiMove == MOVE_BATON_PASS)
{
if ((isSwitchinFirst && hitsToKOAI > 1) || hitsToKOAI > 2) // Need to take an extra hit if slower
bits |= 1u << i;
}
// Check that good type matchups get at least two turns and set best type matchup mon
if (typeMatchup < bestResist)
{
if (canSwitchinWin1v1)
{
bestResist = typeMatchup;
typeMatchupId = i;
}
}
// Check for mon with resistance and super effective move for best type matchup mon with effective move
if (aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0)
{
if (typeMatchup < bestResistEffective)
{
if (AI_GetTypeEffectiveness(aiMove, battler, opposingBattler) >= UQ_4_12(2.0))
{
// Assuming a super effective move would do significant damage or scare the player out, so not being as conservative here
if (hitsToKOAI > hitsToKOAIThreshold)
if (canSwitchinWin1v1)
{
bestResistEffective = typeMatchup;
typeMatchupEffectiveId = i;
@ -1905,10 +1936,10 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (gMovesInfo[aiMove].effect == EFFECT_EXPLOSION && damageDealt < playerMonHP)
continue;
// Check that mon isn't one shot and set GetBestMonDmg if applicable
// Check that mon isn't one shot and set best damage mon
if (damageDealt > maxDamageDealt)
{
if(hitsToKOAI > hitsToKOAIThreshold)
if((isFreeSwitch && hitsToKOAI > 1) || hitsToKOAI > 2) // This is a "default", we have uniquely low standards
{
maxDamageDealt = damageDealt;
damageMonId = i;
@ -1919,74 +1950,40 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
// If AI mon can one shot
if (damageDealt > playerMonHP)
{
// If AI mon outspeeds and doesn't die to hazards
if ((((aiMonSpeed > playerMonSpeed && !(gFieldStatuses & STATUS_FIELD_TRICK_ROOM)) || aiMovePriority > 0) // Outspeed if not Trick Room
|| ((gFieldStatuses & STATUS_FIELD_TRICK_ROOM) // Trick Room
&& (aiMonSpeed < playerMonSpeed || (ItemId_GetHoldEffect(AI_DATA->switchinCandidate.battleMon.item) == HOLD_EFFECT_ROOM_SERVICE && aiMonSpeed * 2 / 3 < playerMonSpeed)))) // Trick Room speeds
&& AI_DATA->switchinCandidate.battleMon.hp > GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon)) // Hazards
if (canSwitchinWin1v1)
{
// We have a revenge killer
revengeKillerId = i;
}
// If AI mon is outsped
else
{
// If AI mon can't be OHKO'd
if (hitsToKOAI > hitsToKOAIThreshold)
{
// We have a slow revenge killer
if (isSwitchinFirst)
revengeKillerId = i;
else
slowRevengeKillerId = i;
}
}
}
// If AI mon can two shot
if (damageDealt > playerMonHP / 2)
{
// If AI mon outspeeds
if (((aiMonSpeed > playerMonSpeed && !(gFieldStatuses & STATUS_FIELD_TRICK_ROOM)) || aiMovePriority > 0) // Outspeed if not Trick Room
|| (((gFieldStatuses & STATUS_FIELD_TRICK_ROOM) && gFieldTimers.trickRoomTimer > 1) // Trick Room has at least 2 turns left
&& (aiMonSpeed < playerMonSpeed || (ItemId_GetHoldEffect(AI_DATA->switchinCandidate.battleMon.item) == HOLD_EFFECT_ROOM_SERVICE && aiMonSpeed * 2/ 3 < playerMonSpeed)))) // Trick Room speeds
if (canSwitchinWin1v1)
{
// If AI mon can't be OHKO'd
if (hitsToKOAI > hitsToKOAIThreshold)
{
// We have a fast threaten
if (isSwitchinFirst)
fastThreatenId = i;
}
}
// If AI mon is outsped
else
{
// If AI mon can't be 2HKO'd
if (hitsToKOAI > hitsToKOAIThreshold + 1)
{
// We have a slow threaten
else
slowThreatenId = i;
}
}
}
// If mon can trap
if (CanAbilityTrapOpponent(AI_DATA->switchinCandidate.battleMon.ability, opposingBattler))
{
hitsToKOPlayer = GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler);
if (CountUsablePartyMons(opposingBattler) > 0
&& (((hitsToKOAI > hitsToKOPlayer && isSwitchAfterKO) // If can 1v1 after a KO
|| (hitsToKOAI == hitsToKOPlayer && isSwitchAfterKO && (aiMonSpeed > playerMonSpeed || aiMovePriority > 0)))
|| ((hitsToKOAI > hitsToKOPlayer + 1 && !isSwitchAfterKO) // If can 1v1 after mid battle
|| (hitsToKOAI == hitsToKOPlayer + 1 && !isSwitchAfterKO && (aiMonSpeed > playerMonSpeed || aiMovePriority > 0)))))
trapperId = i;
}
if (CanAbilityTrapOpponent(AI_DATA->switchinCandidate.battleMon.ability, opposingBattler)
&& CountUsablePartyMons(opposingBattler) > 0
&& canSwitchinWin1v1)
trapperId = i;
}
}
}
batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i);
// Different switching priorities depending on switching mid battle vs switching after a KO
if (isSwitchAfterKO)
// Different switching priorities depending on switching mid battle vs switching after a KO or slow switch
if (isFreeSwitch)
{
// Return Trapper > Revenge Killer > Type Matchup > Baton Pass > Best Damage
if (trapperId != PARTY_SIZE) return trapperId;
@ -2009,8 +2006,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
else if (batonPassId != PARTY_SIZE) return batonPassId;
}
// If ace mon is the last available Pokemon and U-Turn/Volt Switch was used - switch to the mon.
if (aceMonId != PARTY_SIZE
&& (gMovesInfo[gLastUsedMove].effect == EFFECT_HIT_ESCAPE || gMovesInfo[gLastUsedMove].effect == EFFECT_PARTING_SHOT || gMovesInfo[gLastUsedMove].effect == EFFECT_BATON_PASS || gMovesInfo[gLastUsedMove].effect == EFFECT_CHILLY_RECEPTION || gMovesInfo[gLastUsedMove].effect == EFFECT_SHED_TAIL))
if (aceMonId != PARTY_SIZE && IsSwitchOutEffect(gMovesInfo[gLastUsedMove].effect))
return aceMonId;
return PARTY_SIZE;
@ -2082,7 +2078,6 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd)
return bestMonId;
}
// Split ideal mon decision between after previous mon KO'd (prioritize offensive options) and after switching active mon out (prioritize defensive options), and expand the scope of both.
// Only use better mon selection if AI_FLAG_SMART_MON_CHOICES is set for the trainer.
if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_MON_CHOICES && !IsDoubleBattle()) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic
{
@ -2103,11 +2098,11 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd)
|| gBattlerPartyIndexes[battlerIn2] == i
|| i == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| i == gBattleStruct->monToSwitchIntoId[battlerIn2]
|| (GetMonAbility(&party[i]) == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))) // While not really invalid per se, not really wise to switch into this mon.)
|| (GetMonAbility(&party[i]) == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))) // While not really invalid per se, not really wise to switch into this mon.
{
invalidMons |= 1u << i;
}
else if (IsAceMon(battler, i))// Save Ace Pokemon for last.
else if (IsAceMon(battler, i)) // Save Ace Pokemon for last.
{
aceMonId = i;
invalidMons |= 1u << i;

View file

@ -2375,6 +2375,22 @@ bool32 IsSelfStatLoweringEffect(u32 effect)
}
}
bool32 IsSwitchOutEffect(u32 effect)
{
// Switch out effects like U-Turn, Volt Switch, etc.
switch (effect)
{
case EFFECT_HIT_ESCAPE:
case EFFECT_PARTING_SHOT:
case EFFECT_BATON_PASS:
case EFFECT_CHILLY_RECEPTION:
case EFFECT_SHED_TAIL:
return TRUE;
default:
return FALSE;
}
}
bool32 HasDamagingMove(u32 battlerId)
{
u32 i;
@ -3492,14 +3508,14 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl
{
gBattleMons[battlerAtk] = switchinCandidate;
AI_THINKING_STRUCT->saved[battlerDef].saved = TRUE;
SetBattlerData(battlerDef); // set known opposing battler data
SetBattlerAiData(battlerDef, AI_DATA); // set known opposing battler data
AI_THINKING_STRUCT->saved[battlerDef].saved = FALSE;
}
else
{
gBattleMons[battlerDef] = switchinCandidate;
AI_THINKING_STRUCT->saved[battlerAtk].saved = TRUE;
SetBattlerData(battlerAtk); // set known opposing battler data
SetBattlerAiData(battlerAtk, AI_DATA); // set known opposing battler data
AI_THINKING_STRUCT->saved[battlerAtk].saved = FALSE;
}
@ -3509,6 +3525,18 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl
return dmg.expected;
}
u32 AI_WhoStrikesFirstPartyMon(u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, u32 moveConsidered)
{
struct BattlePokemon *savedBattleMons = AllocSaveBattleMons();
gBattleMons[battlerAtk] = switchinCandidate;
SetBattlerAiData(battlerAtk, AI_DATA);
u32 aiMonFaster = AI_IsFaster(battlerAtk, battlerDef, moveConsidered);
FreeRestoreBattleMons(savedBattleMons);
return aiMonFaster;
}
s32 CountUsablePartyMons(u32 battlerId)
{
s32 battlerOnField1, battlerOnField2, i, ret;

View file

@ -3239,6 +3239,10 @@ void SwitchInClearSetData(u32 battler)
gSpecialStatuses[battler].specialDmg = 0;
gBattleStruct->enduredDamage &= ~(1u << battler);
// Reset Eject Button / Eject Pack switch detection
AI_DATA->ejectButtonSwitch = FALSE;
AI_DATA->ejectPackSwitch = FALSE;
// Reset G-Max Chi Strike boosts.
gBattleStruct->bonusCritStages[battler] = 0;

View file

@ -6296,10 +6296,12 @@ static void Cmd_moveend(void)
if (ejectButtonBattlers & (1u << battler))
{
gBattlescriptCurrInstr = BattleScript_EjectButtonActivates;
AI_DATA->ejectButtonSwitch = TRUE;
}
else // Eject Pack
{
gBattlescriptCurrInstr = BattleScript_EjectPackActivates;
AI_DATA->ejectPackSwitch = TRUE;
// Are these 2 lines below needed?
gProtectStructs[battler].statFell = FALSE;
gSpecialStatuses[gBattlerAttacker].preventLifeOrbDamage = TRUE;

View file

@ -215,23 +215,97 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); }
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron
OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); }
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); } // Mid battle, AI sends out Aron
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53);}
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, 1); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize offensive options after slow U-Turn")
{
GIVEN {
ASSUME(gMovesInfo[MOVE_FALSE_SWIPE].effect == EFFECT_FALSE_SWIPE);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_FALSE_SWIPE, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_U_TURN); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53); }
} WHEN {
TURN { MOVE(player, MOVE_FALSE_SWIPE); EXPECT_SEND_OUT(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize offensive options after Eject Button")
{
GIVEN {
ASSUME(gItemsInfo[ITEM_EJECT_BUTTON].holdEffect == HOLD_EFFECT_EJECT_BUTTON);
ASSUME(gMovesInfo[MOVE_FALSE_SWIPE].effect == EFFECT_FALSE_SWIPE);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_FALSE_SWIPE, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Item(ITEM_EJECT_BUTTON); Moves(MOVE_TACKLE); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53); }
} WHEN {
TURN { MOVE(player, MOVE_FALSE_SWIPE); EXPECT_SEND_OUT(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize offensive options after Eject Pack")
{
GIVEN {
ASSUME(gItemsInfo[ITEM_EJECT_PACK].holdEffect == HOLD_EFFECT_EJECT_PACK);
ASSUME(gMovesInfo[MOVE_GROWL].effect == EFFECT_ATTACK_DOWN);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_GROWL, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Item(ITEM_EJECT_PACK); Moves(MOVE_TACKLE); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53); }
} WHEN {
TURN { MOVE(player, MOVE_GROWL); EXPECT_SEND_OUT(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive options after Eject Pack if mon outspeeds")
{
GIVEN {
ASSUME(gItemsInfo[ITEM_EJECT_PACK].holdEffect == HOLD_EFFECT_EJECT_PACK);
ASSUME(MoveHasAdditionalEffectSelf(MOVE_OVERHEAT, MOVE_EFFECT_SP_ATK_MINUS_2) == TRUE);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Item(ITEM_EJECT_PACK); Moves(MOVE_OVERHEAT); Speed(6); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53); }
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SEND_OUT(opponent, 1); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize offensive options after Eject Pack if mon outspeeds but was Intimidate'd")
{
GIVEN {
ASSUME(gItemsInfo[ITEM_EJECT_PACK].holdEffect == HOLD_EFFECT_EJECT_PACK);
ASSUME(MoveHasAdditionalEffectSelf(MOVE_OVERHEAT, MOVE_EFFECT_SP_ATK_MINUS_2) == TRUE);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_STARAPTOR) { Level(30); Ability(ABILITY_INTIMIDATE); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Item(ITEM_EJECT_PACK); Moves(MOVE_OVERHEAT); Speed(6); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); SpDefense(53); }
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Post-KO switches prioritize offensive options")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_TACKLE); Speed(4); }
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron
OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); }
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); } // Mid battle, AI sends out Aron
OPPONENT(SPECIES_ELECTRODE) { Level(30); Ability(ABILITY_STATIC); Moves(MOVE_CHARGE_BEAM); Speed(6); }
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SEND_OUT(opponent, 2); }
}