From ded97e529664fc816a12a042ff5d94fb7a2cf4f1 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:10:02 -0400 Subject: [PATCH] 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 446f73822651af55ee1a36aeb61f3f0c9317d244. * 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 --- include/battle.h | 2 + include/battle_ai_util.h | 3 + src/battle_ai_main.c | 2 +- src/battle_ai_switch_items.c | 185 +++++++++++++++++----------------- src/battle_ai_util.c | 32 +++++- src/battle_main.c | 4 + src/battle_script_commands.c | 2 + test/battle/ai/ai_switching.c | 84 ++++++++++++++- 8 files changed, 211 insertions(+), 103 deletions(-) diff --git a/include/battle.h b/include/battle.h index 16df7c500a..f463a2440b 100644 --- a/include/battle.h +++ b/include/battle.h @@ -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 }; diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 5fecdd5f47..69cf92ed0d 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -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); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index e686b52071..a47d17839f 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -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; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 3f13a8c3f4..f4ff355967 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -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; diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 05dbbe916a..3ef613d1fc 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -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; diff --git a/src/battle_main.c b/src/battle_main.c index d30d7a927f..9a430952c3 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -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; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 897bf1dab3..a13e84265a 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -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; diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index 6fb9b499d9..cc5c7aafa9 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -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); } }