#include "global.h" #include "battle.h" #include "constants/battle_ai.h" #include "battle_ai_main.h" #include "battle_ai_util.h" #include "battle_util.h" #include "battle_anim.h" #include "battle_controllers.h" #include "battle_main.h" #include "battle_setup.h" #include "data.h" #include "item.h" #include "party_menu.h" #include "pokemon.h" #include "random.h" #include "util.h" #include "constants/abilities.h" #include "constants/item_effects.h" #include "constants/battle_move_effects.h" #include "constants/items.h" #include "constants/moves.h" // this file's functions static bool32 CanUseSuperEffectiveMoveAgainstOpponents(u32 battler); static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 moduloPercent); static bool32 ShouldUseItem(u32 battler); static bool32 AiExpectsToFaintPlayer(u32 battler); static bool32 AI_ShouldHeal(u32 battler, u32 healAmount); static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount); static u32 GetSwitchinHazardsDamage(u32 battler, struct BattlePokemon *battleMon); static bool32 CanAbilityTrapOpponent(enum Ability ability, u32 opponent); static u32 GetHPHealAmount(u8 itemEffectParam, struct Pokemon *mon); static u32 GetBattleMonTypeMatchup(struct BattlePokemon opposingBattleMon, struct BattlePokemon battleMon); static void InitializeSwitchinCandidate(struct Pokemon *mon) { PokemonToBattleMon(mon, &gAiLogicData->switchinCandidate.battleMon); gAiLogicData->switchinCandidate.hypotheticalStatus = FALSE; } u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario) { // Modify these cases if you want unique behaviour based on other data (trainer class, difficulty, etc.) switch(shouldSwitchScenario) { case SHOULD_SWITCH_WONDER_GUARD: return SHOULD_SWITCH_WONDER_GUARD_PERCENTAGE; case SHOULD_SWITCH_ABSORBS_MOVE: return SHOULD_SWITCH_ABSORBS_MOVE_PERCENTAGE; case SHOULD_SWITCH_TRAPPER: return SHOULD_SWITCH_TRAPPER_PERCENTAGE; case SHOULD_SWITCH_FREE_TURN: return SHOULD_SWITCH_FREE_TURN_PERCENTAGE; case SHOULD_SWITCH_TRUANT: return SHOULD_SWITCH_TRUANT_PERCENTAGE; case SHOULD_SWITCH_ALL_MOVES_BAD: return SHOULD_SWITCH_ALL_MOVES_BAD_PERCENTAGE; case SHOULD_SWITCH_PERISH_SONG: return SHOULD_SWITCH_PERISH_SONG_PERCENTAGE; case SHOULD_SWITCH_YAWN: return SHOULD_SWITCH_YAWN_PERCENTAGE; case SHOULD_SWITCH_BADLY_POISONED: return SHOULD_SWITCH_BADLY_POISONED_PERCENTAGE; case SHOULD_SWITCH_BADLY_POISONED_STATS_RAISED: return SHOULD_SWITCH_BADLY_POISONED_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_CURSED: return SHOULD_SWITCH_CURSED_PERCENTAGE; case SHOULD_SWITCH_CURSED_STATS_RAISED: return SHOULD_SWITCH_CURSED_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_NIGHTMARE: return SHOULD_SWITCH_NIGHTMARE_PERCENTAGE; case SHOULD_SWITCH_NIGHTMARE_STATS_RAISED: return SHOULD_SWITCH_NIGHTMARE_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_SEEDED: return SHOULD_SWITCH_SEEDED_PERCENTAGE; case SHOULD_SWITCH_SEEDED_STATS_RAISED: return SHOULD_SWITCH_SEEDED_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_INFATUATION: return SHOULD_SWITCH_INFATUATION_PERCENTAGE; case SHOULD_SWITCH_HASBADODDS: return SHOULD_SWITCH_HASBADODDS_PERCENTAGE; case SHOULD_SWITCH_NATURAL_CURE_STRONG: return SHOULD_SWITCH_NATURAL_CURE_STRONG_PERCENTAGE; case SHOULD_SWITCH_NATURAL_CURE_STRONG_STATS_RAISED: return SHOULD_SWITCH_NATURAL_CURE_STRONG_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_NATURAL_CURE_WEAK: return SHOULD_SWITCH_NATURAL_CURE_WEAK_PERCENTAGE; case SHOULD_SWITCH_NATURAL_CURE_WEAK_STATS_RAISED: return SHOULD_SWITCH_NATURAL_CURE_WEAK_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_REGENERATOR: return SHOULD_SWITCH_REGENERATOR_PERCENTAGE; case SHOULD_SWITCH_REGENERATOR_STATS_RAISED: return SHOULD_SWITCH_REGENERATOR_STATS_RAISED_PERCENTAGE; case SHOULD_SWITCH_ENCORE_STATUS: return SHOULD_SWITCH_ENCORE_STATUS_PERCENTAGE; case SHOULD_SWITCH_ENCORE_DAMAGE: return SHOULD_SWITCH_ENCORE_DAMAGE_PERCENTAGE; case SHOULD_SWITCH_CHOICE_LOCKED: return SHOULD_SWITCH_CHOICE_LOCKED_PERCENTAGE; case SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO: return SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE; case SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS: return SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE; case SHOULD_SWITCH_ALL_SCORES_BAD: return SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE; default: return 100; } } static bool32 IsAceMon(u32 battler, u32 monPartyId) { if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && monPartyId == CalculateEnemyPartyCountInSide(battler)-1) return TRUE; if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_DOUBLE_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && (monPartyId == CalculateEnemyPartyCount()-1 || monPartyId == CalculateEnemyPartyCount()-2)) return TRUE; return FALSE; } static bool32 AreStatsRaised(u32 battler) { u8 buffedStatsValue = 0; s32 i; for (i = 0; i < NUM_BATTLE_STATS; i++) { if (gBattleMons[battler].statStages[i] > DEFAULT_STAT_STAGE) buffedStatsValue += gBattleMons[battler].statStages[i] - DEFAULT_STAT_STAGE; } return (buffedStatsValue > STAY_IN_STATS_RAISED); } void GetAIPartyIndexes(u32 battler, s32 *firstId, s32 *lastId) { if (BATTLE_TWO_VS_ONE_OPPONENT && (battler & BIT_SIDE) == B_SIDE_OPPONENT) { *firstId = 0, *lastId = PARTY_SIZE; } else if (gBattleTypeFlags & (BATTLE_TYPE_TWO_OPPONENTS | BATTLE_TYPE_INGAME_PARTNER | BATTLE_TYPE_TOWER_LINK_MULTI)) { if ((battler & BIT_FLANK) == B_FLANK_LEFT) *firstId = 0, *lastId = PARTY_SIZE / 2; else *firstId = PARTY_SIZE / 2, *lastId = PARTY_SIZE; } else { *firstId = 0, *lastId = PARTY_SIZE; } } static inline bool32 SetSwitchinAndSwitch(u32 battler, u32 switchinId) { gBattleStruct->AI_monToSwitchIntoId[battler] = switchinId; return TRUE; } static bool32 AI_DoesChoiceEffectBlockMove(u32 battler, u32 move) { // Choice locked into something else if (gAiLogicData->lastUsedMove[battler] != MOVE_NONE && gAiLogicData->lastUsedMove[battler] != move && (IsHoldEffectChoice(GetBattlerHoldEffect(battler) && IsBattlerItemEnabled(battler)) || gBattleMons[battler].ability == ABILITY_GORILLA_TACTICS)) return TRUE; return FALSE; } static inline bool32 CanBattlerWin1v1(u32 hitsToKOAI, u32 hitsToKOPlayer, bool32 isBattlerFirst) { // Player's best move deals 0 damage if (hitsToKOAI == 0 && hitsToKOPlayer > 0) return TRUE; // AI's best move deals 0 damage if (hitsToKOPlayer == 0 && hitsToKOAI > 0) return FALSE; // Neither mon can damage the other if (hitsToKOPlayer == 0 && hitsToKOAI == 0) return FALSE; // Different KO thresholds depending on who goes first if (isBattlerFirst) { if (hitsToKOAI >= hitsToKOPlayer) return TRUE; } else { if (hitsToKOAI > hitsToKOPlayer) return TRUE; } return FALSE; } // Note that as many return statements as possible are INTENTIONALLY put after all of the loops; // the function can take a max of about 0.06s to run, and this prevents the player from identifying // whether the mon will switch or not by seeing how long the delay is before they select a move static bool32 ShouldSwitchIfHasBadOdds(u32 battler) { //Variable initialization u8 opposingPosition; s32 i, damageDealt = 0, maxDamageDealt = 0, damageTaken = 0, maxDamageTaken = 0, maxDamageTakenPriority = 0; u32 aiMove, playerMove, bestPlayerPriorityMove = MOVE_NONE, bestPlayerMove = MOVE_NONE, expectedMove = MOVE_NONE, aiAbility = gAiLogicData->abilities[battler], opposingBattler; bool32 getsOneShot = FALSE, hasStatusMove = FALSE, hasSuperEffectiveMove = FALSE; u32 typeMatchup; enum BattleMoveEffects aiMoveEffect; u32 hitsToKoAI = 0, hitsToKoAIPriority = 0, hitsToKoPlayer = 0; bool32 canBattlerWin1v1 = FALSE, isBattlerFirst, isBattlerFirstPriority; // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic if (IsDoubleBattle()) return FALSE; opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler)); opposingBattler = GetBattlerAtPosition(opposingPosition); u16 *playerMoves = GetMovesArray(opposingBattler); // Get max damage mon could take for (i = 0; i < MAX_MON_MOVES; i++) { playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[i] : playerMoves[i]; if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH && gBattleMons[opposingBattler].pp[i] > 0) { damageTaken = AI_GetDamage(opposingBattler, battler, i, AI_DEFENDING, gAiLogicData); if (damageTaken > maxDamageTaken && !AI_DoesChoiceEffectBlockMove(opposingBattler, playerMove)) { maxDamageTaken = damageTaken; bestPlayerMove = playerMove; } if (GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], playerMove) > 0 && damageTaken > maxDamageTakenPriority && !AI_DoesChoiceEffectBlockMove(opposingBattler, playerMove)) { maxDamageTakenPriority = damageTaken; bestPlayerPriorityMove = playerMove; } } } hitsToKoAI = GetNoOfHitsToKOBattlerDmg(maxDamageTaken, battler); hitsToKoAIPriority = GetNoOfHitsToKOBattlerDmg(maxDamageTakenPriority, battler); expectedMove = gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_MOVE ? GetIncomingMove(battler, opposingBattler, gAiLogicData) : bestPlayerMove; for (i = 0; i < MAX_MON_MOVES; i++) { aiMove = gBattleMons[battler].moves[i]; aiMoveEffect = GetMoveEffect(aiMove); if (aiMove != MOVE_NONE && gBattleMons[battler].pp[i] > 0) { u32 nonVolatileStatus = GetMoveNonVolatileStatus(aiMove); // Check if mon has an "important" status move if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN || aiMoveEffect == EFFECT_SPIKES || aiMoveEffect == EFFECT_TOXIC_SPIKES || aiMoveEffect == EFFECT_STEALTH_ROCK || aiMoveEffect == EFFECT_STICKY_WEB || aiMoveEffect == EFFECT_LEECH_SEED || aiMoveEffect == EFFECT_EXPLOSION || aiMoveEffect == EFFECT_MISTY_EXPLOSION || nonVolatileStatus == MOVE_EFFECT_SLEEP || nonVolatileStatus == MOVE_EFFECT_TOXIC || nonVolatileStatus == MOVE_EFFECT_PARALYSIS || nonVolatileStatus == MOVE_EFFECT_BURN || aiMoveEffect == EFFECT_YAWN || aiMoveEffect == EFFECT_TRICK || aiMoveEffect == EFFECT_TRICK_ROOM || aiMoveEffect== EFFECT_WONDER_ROOM || aiMoveEffect == EFFECT_PSYCHO_SHIFT || aiMoveEffect == EFFECT_FIRST_TURN_ONLY ) { hasStatusMove = TRUE; } // Only check damage if it's a damaging move if (!IsBattleMoveStatus(aiMove) && !AI_DoesChoiceEffectBlockMove(battler, aiMove)) { // Check if mon has a super effective move if (gAiLogicData->effectiveness[battler][opposingBattler][i] >= UQ_4_12(2.0)) hasSuperEffectiveMove = TRUE; // Get maximum damage mon can deal damageDealt = AI_GetDamage(battler, opposingBattler, i, AI_ATTACKING, gAiLogicData); if (damageDealt > maxDamageDealt) maxDamageDealt = damageDealt; if (!canBattlerWin1v1 ) // Once we can win a 1v1 we don't need to track this, but want to run the rest of the function to keep the runtime the same regardless of when we find the winning move { hitsToKoPlayer = GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler); isBattlerFirst = AI_IsFaster(battler, opposingBattler, aiMove, expectedMove, CONSIDER_PRIORITY); isBattlerFirstPriority = AI_IsFaster(battler, opposingBattler, aiMove, bestPlayerPriorityMove, CONSIDER_PRIORITY); canBattlerWin1v1 = CanBattlerWin1v1(hitsToKoAI, hitsToKoPlayer, isBattlerFirst) && CanBattlerWin1v1(hitsToKoAIPriority, hitsToKoPlayer, isBattlerFirstPriority); } } } } // Calculate type advantage typeMatchup = GetBattleMonTypeMatchup(gBattleMons[opposingBattler], gBattleMons[battler]); // Check if mon gets one shot if (maxDamageTaken > gBattleMons[battler].hp && !(gItemsInfo[gBattleMons[battler].item].holdEffect == HOLD_EFFECT_FOCUS_SASH || (!IsMoldBreakerTypeAbility(opposingBattler, gAiLogicData->abilities[opposingBattler]) && GetConfig(B_STURDY) >= GEN_5 && aiAbility == ABILITY_STURDY))) { getsOneShot = TRUE; } // Check if current mon can 1v1 in spite of bad matchup, and don't switch out if it can if (canBattlerWin1v1) return FALSE; // If we don't have any other viable options, don't switch out if (gAiLogicData->mostSuitableMonId[battler] == PARTY_SIZE) return FALSE; // Start assessing whether or not mon has bad odds // Jump straight to switching out in cases where mon gets OHKO'd if ((getsOneShot && !canBattlerWin1v1) && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator || (aiAbility == ABILITY_REGENERATOR && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) { // 50% chance to stay in regardless if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !gAiLogicData->aiPredictionInProgress) return FALSE; // Switch mon out return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // General bad type matchups have more wiggle room if (typeMatchup > UQ_4_12(2.0)) // If the player has favourable offensive matchup (2.0 is neutral, this must be worse) { if (!hasSuperEffectiveMove // If the AI doesn't have a super effective move && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator || (aiAbility == ABILITY_REGENERATOR && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) { // Then check if they have an important status move, which is worth using even in a bad matchup if (hasStatusMove) return FALSE; // 50% chance to stay in regardless if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !gAiLogicData->aiPredictionInProgress) return FALSE; // Switch mon out return SetSwitchinAndSwitch(battler, PARTY_SIZE); } } return FALSE; } static bool32 ShouldSwitchIfTruant(u32 battler) { // Switch if mon with truant is bodied by Protect or invulnerability spam if (gAiLogicData->abilities[battler] == ABILITY_TRUANT && IsTruantMonVulnerable(battler, gBattlerTarget) && gDisableStructs[battler].truantCounter && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE) { if (RandomPercentage(RNG_AI_SWITCH_TRUANT, GetSwitchChance(SHOULD_SWITCH_TRUANT))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } static u32 FindMonWithMoveOfEffectiveness(u32 battler, u32 opposingBattler, uq4_12_t effectiveness) { u32 move, i, j; s32 firstId; s32 lastId; // + 1 struct Pokemon *party = NULL; // Get party information. GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); // Find a Pokémon in the party that has a super effective move. for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battler]) continue; if (IsAceMon(battler, i)) continue; for (j = 0; j < MAX_MON_MOVES; j++) { move = GetMonData(&party[i], MON_DATA_MOVE1 + j); if (move != MOVE_NONE && AI_GetMoveEffectiveness(move, battler, opposingBattler) >= effectiveness && GetMovePower(move) != 0) return SetSwitchinAndSwitch(battler, i); } } return FALSE; // There is not a single Pokémon in the party that has a move with this effectiveness threshold } static bool32 ShouldSwitchIfAllMovesBad(u32 battler) { u32 moveIndex; u32 opposingBattler = GetOppositeBattler(battler); u32 aiMove; // Switch if no moves affect opponents if (IsDoubleBattle()) { u32 opposingPartner = GetBattlerAtPosition(BATTLE_PARTNER(opposingBattler)); for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { aiMove = gBattleMons[battler].moves[moveIndex]; if (aiMove == MOVE_NONE) continue; if (gAiLogicData->effectiveness[battler][opposingBattler][moveIndex] > UQ_4_12(0.0) || gAiLogicData->effectiveness[battler][opposingPartner][moveIndex] > UQ_4_12(0.0)) return FALSE; } } else { for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { aiMove = gBattleMons[battler].moves[moveIndex]; if (gAiLogicData->effectiveness[battler][opposingBattler][moveIndex] > UQ_4_12(0.0) && aiMove != MOVE_NONE && !CanAbilityAbsorbMove(battler, opposingBattler, gAiLogicData->abilities[opposingBattler], aiMove, GetBattleMoveType(aiMove), AI_CHECK) && !CanAbilityBlockMove(battler, opposingBattler, gBattleMons[battler].ability, gAiLogicData->abilities[opposingBattler], aiMove, AI_CHECK) && (!ALL_MOVES_BAD_STATUS_MOVES_BAD || GetMovePower(aiMove) != 0)) // If using ALL_MOVES_BAD_STATUS_MOVES_BAD, then need power to be non-zero return FALSE; } } if (RandomPercentage(RNG_AI_SWITCH_ALL_MOVES_BAD, GetSwitchChance(SHOULD_SWITCH_ALL_MOVES_BAD))) { if (gAiLogicData->mostSuitableMonId[battler] == PARTY_SIZE) // No good candidate mons, find any one that can deal damage return FindMonWithMoveOfEffectiveness(battler, opposingBattler, UQ_4_12(1.0)); else // Good candidate mon, send that in return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } static bool32 ShouldSwitchIfWonderGuard(u32 battler) { u32 opposingBattler = GetOppositeBattler(battler); u32 i; if (IsDoubleBattle()) return FALSE; if (gAiLogicData->abilities[opposingBattler] != ABILITY_WONDER_GUARD) return FALSE; // Check if Pokémon has a super effective move. for (i = 0; i < MAX_MON_MOVES; i++) { if (gBattleMons[battler].moves[i] != MOVE_NONE && gAiLogicData->effectiveness[battler][opposingBattler][i] >= UQ_4_12(2.0)) return FALSE; } if (RandomPercentage(RNG_AI_SWITCH_WONDER_GUARD, GetSwitchChance(SHOULD_SWITCH_WONDER_GUARD))) { if (gAiLogicData->mostSuitableMonId[battler] == PARTY_SIZE) // No good candidate mons, find any one that can deal damage return FindMonWithMoveOfEffectiveness(battler, opposingBattler, UQ_4_12(2.0)); else // Good candidate mon, send that in return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) { u8 battlerIn1, battlerIn2; u8 numAbsorbingAbilities = 0; enum Ability absorbingTypeAbilities[8]; // Max needed for type + move property absorbers s32 firstId; s32 lastId; struct Pokemon *party; enum Ability monAbility; u16 aiMove; u32 opposingBattler = GetOppositeBattler(battler); u32 incomingMove = GetIncomingMove(battler, opposingBattler, gAiLogicData); enum Type incomingType = CheckDynamicMoveType(GetBattlerMon(opposingBattler), incomingMove, opposingBattler, MON_IN_BATTLE); bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); s32 i, j; if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; if (GetMoveEffect(incomingMove) == EFFECT_HIDDEN_POWER && RandomPercentage(RNG_AI_SWITCH_ABSORBING_HIDDEN_POWER, SHOULD_SWITCH_ABSORBS_HIDDEN_POWER_PERCENTAGE)) return FALSE; if (gBattleStruct->prevTurnSpecies[battler] != gBattleMons[battler].species && !(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_MOVE)) // AI mon has changed, player's behaviour no longer reliable; override this if using AI_FLAG_PREDICT_MOVE return FALSE; if (CanUseSuperEffectiveMoveAgainstOpponents(battler) && (RandomPercentage(RNG_AI_SWITCH_ABSORBING_STAY_IN, STAY_IN_ABSORBING_PERCENTAGE) || gAiLogicData->aiPredictionInProgress)) return FALSE; if (AreStatsRaised(battler)) return FALSE; if (IsMoldBreakerTypeAbility(opposingBattler, gAiLogicData->abilities[opposingBattler])) return FALSE; // Don't switch if mon could OHKO for (i = 0; i < MAX_MON_MOVES; i++) { aiMove = gBattleMons[battler].moves[i]; if (aiMove != MOVE_NONE) { // Only check damage if it's a damaging move if (!IsBattleMoveStatus(aiMove)) { if (!AI_DoesChoiceEffectBlockMove(battler, aiMove) && AI_GetDamage(battler, opposingBattler, i, AI_ATTACKING, gAiLogicData) > gBattleMons[opposingBattler].hp) return FALSE; } } } if (IsDoubleBattle()) { battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler))) battlerIn2 = battler; else battlerIn2 = GetPartnerBattler(battler); } else { battlerIn1 = battler; battlerIn2 = battler; } // Create an array of possible absorb abilities so the AI considers all of them if (incomingType == TYPE_FIRE) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_FLASH_FIRE; } if (incomingType == TYPE_WATER || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_WATER)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WATER_ABSORB; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_DRY_SKIN; if (GetConfig(B_REDIRECT_ABILITY_IMMUNITY) >= GEN_5) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_STORM_DRAIN; } if (incomingType == TYPE_ELECTRIC || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_ELECTRIC)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_VOLT_ABSORB; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_MOTOR_DRIVE; if (GetConfig(B_REDIRECT_ABILITY_IMMUNITY) >= GEN_5) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LIGHTNING_ROD; } if (incomingType == TYPE_GRASS || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GRASS)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SAP_SIPPER; } if (incomingType == TYPE_GROUND || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GROUND)) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_EARTH_EATER; absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LEVITATE; } if (IsSoundMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsSoundMove(incomingMove))) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SOUNDPROOF; } if (IsBallisticMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsBallisticMove(incomingMove))) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_BULLETPROOF; } if (IsWindMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsWindMove(incomingMove))) { absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WIND_RIDER; } if (IsPowderMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsPowderMove(incomingMove))) { if (GetConfig(B_POWDER_OVERCOAT) >= GEN_6) absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_OVERCOAT; } if (numAbsorbingAbilities == 0) { return FALSE; } // Check current mon for all absorbing abilities for (i = 0; i < numAbsorbingAbilities; i++) { if (gBattleMons[battler].ability == absorbingTypeAbilities[i]) return FALSE; } // Check party for mon with ability that absorbs move GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battlerIn1]) continue; if (i == gBattlerPartyIndexes[battlerIn2]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, i)) continue; monAbility = GetMonAbility(&party[i]); for (j = 0; j < numAbsorbingAbilities; j++) { // Found a mon if (absorbingTypeAbilities[j] == monAbility && RandomPercentage(RNG_AI_SWITCH_ABSORBING, GetSwitchChance(SHOULD_SWITCH_ABSORBS_MOVE))) return SetSwitchinAndSwitch(battler, i); } } return FALSE; } static bool32 ShouldSwitchIfOpponentChargingOrInvulnerable(u32 battler) { u32 opposingBattler = GetOppositeBattler(battler); u32 incomingMove = GetIncomingMove(battler, opposingBattler, gAiLogicData); bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); if (IsDoubleBattle() || !(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // In a world with a unified ShouldSwitch function, also want to check whether we already win 1v1 and if we do don't switch; not worth doubling the HasBadOdds computation for now if (isOpposingBattlerChargingOrInvulnerable && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_FREE_TURN, GetSwitchChance(SHOULD_SWITCH_FREE_TURN))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); return FALSE; } static bool32 ShouldSwitchIfTrapperInParty(u32 battler) { s32 firstId; s32 lastId; struct Pokemon *party; s32 i; enum Ability monAbility; s32 opposingBattler = GetOppositeBattler(battler); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Check if current mon has an ability that traps opponent if (CanAbilityTrapOpponent(gBattleMons[battler].ability, opposingBattler)) return FALSE; // Check party for mon with ability that traps opponent GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { if (IsAceMon(battler, i)) return FALSE; monAbility = GetMonAbility(&party[i]); if (CanAbilityTrapOpponent(monAbility, opposingBattler) || (CanAbilityTrapOpponent(gAiLogicData->abilities[opposingBattler], opposingBattler) && monAbility == ABILITY_TRACE)) { // If mon in slot i is the most suitable switchin candidate, then it's a trapper than wins 1v1 if (i == gAiLogicData->mostSuitableMonId[battler] && RandomPercentage(RNG_AI_SWITCH_TRAPPER, GetSwitchChance(SHOULD_SWITCH_TRAPPER))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } } return FALSE; } static bool32 ShouldSwitchIfBadlyStatused(u32 battler) { bool32 switchMon = FALSE; enum Ability monAbility = gAiLogicData->abilities[battler]; enum HoldEffect holdEffect = gAiLogicData->holdEffects[battler]; u8 opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler)); u8 opposingBattler = GetBattlerAtPosition(opposingPosition); bool32 hasStatRaised = AnyStatIsRaised(battler); //Perish Song if (gBattleMons[battler].volatiles.perishSong && gDisableStructs[battler].perishSongTimer == 0 && monAbility != ABILITY_SOUNDPROOF && RandomPercentage(RNG_AI_SWITCH_PERISH_SONG, GetSwitchChance(SHOULD_SWITCH_PERISH_SONG))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) { //Yawn if (gBattleMons[battler].volatiles.yawn && CanBeSlept(battler, battler, monAbility, BLOCKED_BY_SLEEP_CLAUSE) // TODO: ask for help from pawwkie && gBattleMons[battler].hp > gBattleMons[battler].maxHP / 3 && RandomPercentage(RNG_AI_SWITCH_YAWN, GetSwitchChance(SHOULD_SWITCH_YAWN))) { switchMon = TRUE; // If we don't have a good switchin, not worth switching if (gAiLogicData->mostSuitableMonId[battler] == PARTY_SIZE) switchMon = FALSE; // Check if Active Pokemon can KO opponent instead of switching // Will still fall asleep, but take out opposing Pokemon first if (AiExpectsToFaintPlayer(battler)) switchMon = FALSE; // Checks to see if active Pokemon can do something against sleep if ((monAbility == ABILITY_NATURAL_CURE || monAbility == ABILITY_SHED_SKIN || monAbility == ABILITY_EARLY_BIRD) || holdEffect == (HOLD_EFFECT_CURE_SLP | HOLD_EFFECT_CURE_STATUS) || HasMove(battler, MOVE_SLEEP_TALK) || (HasMove(battler, MOVE_SNORE) && gAiLogicData->effectiveness[battler][opposingBattler][GetIndexInMoveArray(battler, MOVE_SNORE)] >= UQ_4_12(2.0)) || (IsBattlerGrounded(battler, monAbility, gAiLogicData->holdEffects[battler]) && (HasMove(battler, MOVE_MISTY_TERRAIN) || HasMove(battler, MOVE_ELECTRIC_TERRAIN))) ) switchMon = FALSE; // Check if Active Pokemon evasion boosted and might be able to dodge until awake if (gBattleMons[battler].statStages[STAT_EVASION] > (DEFAULT_STAT_STAGE + 3) && gAiLogicData->abilities[opposingBattler] != ABILITY_UNAWARE && gAiLogicData->abilities[opposingBattler] != ABILITY_KEEN_EYE && gAiLogicData->abilities[opposingBattler] != ABILITY_MINDS_EYE && (GetConfig(B_ILLUMINATE_EFFECT) >= GEN_9 && gAiLogicData->abilities[opposingBattler] != ABILITY_ILLUMINATE) && !gBattleMons[battler].volatiles.foresight && !gBattleMons[battler].volatiles.miracleEye) switchMon = FALSE; if (switchMon) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // Secondary Damage if (monAbility != ABILITY_MAGIC_GUARD && !AiExpectsToFaintPlayer(battler) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE) { //Toxic if (((gBattleMons[battler].status1 & STATUS1_TOXIC_COUNTER) >= STATUS1_TOXIC_TURN(2)) && gBattleMons[battler].hp >= (gBattleMons[battler].maxHP / 3) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_BADLY_POISONED, GetSwitchChance(SHOULD_SWITCH_BADLY_POISONED_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_BADLY_POISONED, GetSwitchChance(SHOULD_SWITCH_BADLY_POISONED)))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); //Cursed if (gBattleMons[battler].volatiles.cursed && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_CURSED, GetSwitchChance(SHOULD_SWITCH_CURSED_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_CURSED, GetSwitchChance(SHOULD_SWITCH_CURSED)))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); //Nightmare if (gBattleMons[battler].volatiles.nightmare && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_NIGHTMARE, GetSwitchChance(SHOULD_SWITCH_NIGHTMARE_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_NIGHTMARE, GetSwitchChance(SHOULD_SWITCH_NIGHTMARE)))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); //Leech Seed if (gBattleMons[battler].volatiles.leechSeed && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_SEEDED, GetSwitchChance(SHOULD_SWITCH_SEEDED_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_SEEDED, GetSwitchChance(SHOULD_SWITCH_SEEDED)))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // Infatuation if (gBattleMons[battler].volatiles.infatuation && !AiExpectsToFaintPlayer(battler) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_INFATUATION, GetSwitchChance(SHOULD_SWITCH_INFATUATION))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } static bool32 ShouldSwitchIfAbilityBenefit(u32 battler) { bool32 hasStatRaised = AnyStatIsRaised(battler); //Check if ability is blocked if (gBattleMons[battler].volatiles.gastroAcid || IsNeutralizingGasOnField()) return FALSE; switch(gAiLogicData->abilities[battler]) { case ABILITY_NATURAL_CURE: //Attempt to cure bad ailment if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_NATURAL_CURE, GetSwitchChance(SHOULD_SWITCH_NATURAL_CURE_STRONG_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_NATURAL_CURE, GetSwitchChance(SHOULD_SWITCH_NATURAL_CURE_STRONG)))) break; //Attempt to cure lesser ailment if ((gBattleMons[battler].status1 & STATUS1_ANY) && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_NATURAL_CURE, GetSwitchChance(SHOULD_SWITCH_NATURAL_CURE_WEAK_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_NATURAL_CURE, GetSwitchChance(SHOULD_SWITCH_NATURAL_CURE_WEAK)))) break; return FALSE; case ABILITY_REGENERATOR: //Don't switch if ailment if (gBattleMons[battler].status1 & STATUS1_ANY) return FALSE; if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3)) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (hasStatRaised ? RandomPercentage(RNG_AI_SWITCH_REGENERATOR, GetSwitchChance(SHOULD_SWITCH_REGENERATOR_STATS_RAISED)) : RandomPercentage(RNG_AI_SWITCH_REGENERATOR, GetSwitchChance(SHOULD_SWITCH_REGENERATOR)))) break; return FALSE; case ABILITY_ZERO_TO_HERO: // Want to activate Palafin-Zero at all costs if (gBattleMons[battler].species == SPECIES_PALAFIN_ZERO) break; default: return FALSE; } return SetSwitchinAndSwitch(battler, PARTY_SIZE); } static bool32 CanUseSuperEffectiveMoveAgainstOpponents(u32 battler) { s32 i; u16 move; u32 opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler)); u32 opposingBattler = GetBattlerAtPosition(opposingPosition); if (!(gAbsentBattlerFlags & (1u << opposingBattler))) { for (i = 0; i < MAX_MON_MOVES; i++) { move = gBattleMons[battler].moves[i]; if (move == MOVE_NONE || AI_DoesChoiceEffectBlockMove(battler, move)) continue; if (gAiLogicData->effectiveness[battler][opposingBattler][i] >= UQ_4_12(2.0)) return TRUE; } } if (!IsDoubleBattle()) return FALSE; opposingBattler = GetBattlerAtPosition(BATTLE_PARTNER(opposingPosition)); if (!(gAbsentBattlerFlags & (1u << opposingBattler))) { for (i = 0; i < MAX_MON_MOVES; i++) { move = gBattleMons[battler].moves[i]; if (move == MOVE_NONE || AI_DoesChoiceEffectBlockMove(battler, move)) continue; if (gAiLogicData->effectiveness[battler][opposingBattler][i] >= UQ_4_12(2.0)) return TRUE; } } return FALSE; } static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 percentChance) { u32 battlerIn1, battlerIn2; s32 firstId; s32 lastId; // + 1 struct Pokemon *party; s32 i, j; u16 move; // Similar functionality handled more thoroughly by ShouldSwitchIfHasBadOdds if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) return FALSE; if (gLastLandedMoves[battler] == MOVE_NONE) return FALSE; if (gLastLandedMoves[battler] == MOVE_UNAVAILABLE) return FALSE; if (gLastHitBy[battler] == 0xFF) return FALSE; if (IsBattleMoveStatus(gLastLandedMoves[battler])) return FALSE; if (IsDoubleBattle()) { battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler))) battlerIn2 = battler; else battlerIn2 = GetPartnerBattler(battler); } else { battlerIn1 = battler; battlerIn2 = battler; } GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { u16 species; enum Ability monAbility; uq4_12_t typeMultiplier; u16 moveFlags = 0; if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battlerIn1]) continue; if (i == gBattlerPartyIndexes[battlerIn2]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, i)) continue; species = GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG); monAbility = GetMonAbility(&party[i]); typeMultiplier = CalcPartyMonTypeEffectivenessMultiplier(gLastLandedMoves[battler], species, monAbility); UpdateMoveResultFlags(typeMultiplier, &moveFlags); if (moveFlags & flags) { battlerIn1 = gLastHitBy[battler]; for (j = 0; j < MAX_MON_MOVES; j++) { move = GetMonData(&party[i], MON_DATA_MOVE1 + j); if (move == MOVE_NONE) continue; if (AI_GetMoveEffectiveness(move, battler, battlerIn1) >= UQ_4_12(2.0) && (RandomPercentage(RNG_AI_SWITCH_SE_DEFENSIVE, percentChance) || gAiLogicData->aiPredictionInProgress)) return SetSwitchinAndSwitch(battler, i); } } } return FALSE; } static bool32 CanMonSurviveHazardSwitchin(u32 battler) { u32 battlerIn1, battlerIn2; u32 hazardDamage = 0, battlerHp = gBattleMons[battler].hp; enum Ability ability = gAiLogicData->abilities[battler], aiMove; s32 firstId, lastId, i, j; struct Pokemon *party; if (ability == ABILITY_REGENERATOR) battlerHp = (battlerHp * 133) / 100; // Account for Regenerator healing hazardDamage = GetSwitchinHazardsDamage(battler, &gBattleMons[battler]); // Battler will faint to hazards, check to see if another mon can clear them if (hazardDamage > battlerHp) { if (IsDoubleBattle()) { battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler))) battlerIn2 = battler; else battlerIn2 = GetPartnerBattler(battler); } else { battlerIn1 = battler; battlerIn2 = battler; } GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battlerIn1]) continue; if (i == gBattlerPartyIndexes[battlerIn2]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, i)) continue; for (j = 0; j < MAX_MON_MOVES; j++) { aiMove = GetMonData(&party[i], MON_DATA_MOVE1 + j, NULL); if (IsHazardClearingMove(aiMove)) // Have a mon that can clear the hazards, so switching out is okay return TRUE; } } // Faints to hazards and party can't clear them, don't switch out return FALSE; } return TRUE; } static bool32 ShouldSwitchIfEncored(u32 battler) { u32 encoredMove = gDisableStructs[battler].encoredMove; u32 opposingBattler = GetOppositeBattler(battler); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // If not Encore'd don't switch if (encoredMove == MOVE_NONE) return FALSE; // Switch out if status move if (GetMoveCategory(encoredMove) == DAMAGE_CATEGORY_STATUS && RandomPercentage(RNG_AI_SWITCH_ENCORE, GetSwitchChance(SHOULD_SWITCH_ENCORE_STATUS))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); // Stay in if effective move else if (gAiLogicData->effectiveness[battler][opposingBattler][GetIndexInMoveArray(battler, encoredMove)] >= UQ_4_12(2.0)) return FALSE; // Switch out 50% of the time otherwise else if ((RandomPercentage(RNG_AI_SWITCH_ENCORE, GetSwitchChance(SHOULD_SWITCH_ENCORE_DAMAGE)) || gAiLogicData->aiPredictionInProgress) && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE) return SetSwitchinAndSwitch(battler, PARTY_SIZE); return FALSE; } static bool32 ShouldSwitchIfBadChoiceLock(u32 battler) { enum HoldEffect holdEffect = GetBattlerHoldEffect(battler); u32 lastUsedMove = gAiLogicData->lastUsedMove[battler]; u32 opposingBattler = GetOppositeBattler(battler); bool32 moveAffectsTarget = TRUE; if (lastUsedMove != MOVE_NONE && (AI_GetMoveEffectiveness(lastUsedMove, battler, opposingBattler) == UQ_4_12(0.0) || CanAbilityAbsorbMove(battler, opposingBattler, gAiLogicData->abilities[opposingBattler], lastUsedMove, CheckDynamicMoveType(GetBattlerMon(battler), lastUsedMove, battler, MON_IN_BATTLE), AI_CHECK) || CanAbilityBlockMove(battler, opposingBattler, gAiLogicData->abilities[battler], gAiLogicData->abilities[opposingBattler], lastUsedMove, AI_CHECK))) moveAffectsTarget = FALSE; if (IsHoldEffectChoice(holdEffect) && IsBattlerItemEnabled(battler)) { if ((GetMoveCategory(lastUsedMove) == DAMAGE_CATEGORY_STATUS || !moveAffectsTarget) && RandomPercentage(RNG_AI_SWITCH_CHOICE_LOCKED, GetSwitchChance(SHOULD_SWITCH_CHOICE_LOCKED))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } // AI should switch if it's become setup fodder and has something better to switch to static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) { s8 attackingStage = gBattleMons[battler].statStages[STAT_ATK]; s8 spAttackingStage = gBattleMons[battler].statStages[STAT_SPATK]; // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Physical attacker if (gBattleMons[battler].attack > gBattleMons[battler].spAttack) { // Don't switch if attack isn't below -1 if (attackingStage > DEFAULT_STAT_STAGE - 2) return FALSE; // 50% chance if attack at -2 and have a good candidate mon else if (attackingStage == DEFAULT_STAT_STAGE - 2) { if (gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || gAiLogicData->aiPredictionInProgress)) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // If at -3 or worse, switch out regardless else if ((attackingStage < DEFAULT_STAT_STAGE - 2) && RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // Special attacker else { // Don't switch if attack isn't below -1 if (spAttackingStage > DEFAULT_STAT_STAGE - 2) return FALSE; // 50% chance if attack at -2 and have a good candidate mon else if (spAttackingStage == DEFAULT_STAT_STAGE - 2) { if (gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || gAiLogicData->aiPredictionInProgress)) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // If at -3 or worse, switch out regardless else if ((spAttackingStage < DEFAULT_STAT_STAGE - 2) && RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } return FALSE; } static bool32 CanBattlerConsiderSwitch(u32 battler) { if (gBattleMons[battler].volatiles.wrapped) return FALSE; if (gBattleMons[battler].volatiles.escapePrevention) return FALSE; if (gBattleMons[battler].volatiles.root) return FALSE; if (IsAbilityPreventingEscape(battler)) return FALSE; if (gBattleStruct->battlerState[battler].commanderSpecies) return FALSE; if (gBattleTypeFlags & BATTLE_TYPE_ARENA) return FALSE; return TRUE; } bool32 ShouldSwitch(u32 battler) { u32 battlerIn1, battlerIn2; s32 firstId; s32 lastId; // + 1 struct Pokemon *party; s32 i; s32 availableToSwitch; if (!CanBattlerConsiderSwitch(battler)) return FALSE; // Sequence Switching AI never switches mid-battle if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) return FALSE; availableToSwitch = 0; if (IsDoubleBattle()) { u32 partner = BATTLE_PARTNER(battler); battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << partner)) battlerIn2 = battler; else battlerIn2 = partner; } else { battlerIn1 = battler; battlerIn2 = battler; } GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battlerIn1]) continue; if (i == gBattlerPartyIndexes[battlerIn2]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, i)) continue; availableToSwitch++; } if (availableToSwitch == 0) return FALSE; // NOTE: The sequence of the below functions matter! Do not change unless you have carefully considered the outcome. // Since the order is sequential, and some of these functions prompt switch to specific party members. // FindMon functions can prompt a switch to specific party members that override GetMostSuitableMonToSwitchInto // The rest can prompt a switch to party member returned by GetMostSuitableMonToSwitchInto if (ShouldSwitchIfWonderGuard(battler)) return TRUE; if ((gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) return FALSE; if (ShouldSwitchIfTrapperInParty(battler)) return TRUE; if (FindMonThatAbsorbsOpponentsMove(battler)) return TRUE; if (ShouldSwitchIfOpponentChargingOrInvulnerable(battler)) return TRUE; if (ShouldSwitchIfTruant(battler)) return TRUE; if (ShouldSwitchIfAllMovesBad(battler)) return TRUE; if (ShouldSwitchIfBadlyStatused(battler)) return TRUE; if (ShouldSwitchIfAbilityBenefit(battler)) return TRUE; if (ShouldSwitchIfHasBadOdds(battler)) return TRUE; if (ShouldSwitchIfEncored(battler)) return TRUE; if (ShouldSwitchIfBadChoiceLock(battler)) return TRUE; if (ShouldSwitchIfAttackingStatsLowered(battler)) return TRUE; // Removing switch capabilites under specific conditions // These Functions prevent the "FindMonWithFlagsAndSuperEffective" from getting out of hand. // We don't use FindMonWithFlagsAndSuperEffective with AI_FLAG_SMART_SWITCHING, so we can bail early. if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) return FALSE; if (CanUseSuperEffectiveMoveAgainstOpponents(battler)) return FALSE; if (AreStatsRaised(battler)) return FALSE; // Default Function // Can prompt switch if AI has a pokemon in party that resists current opponent & has super effective move if (FindMonWithFlagsAndSuperEffective(battler, MOVE_RESULT_DOESNT_AFFECT_FOE, 50) || FindMonWithFlagsAndSuperEffective(battler, MOVE_RESULT_NOT_VERY_EFFECTIVE, 33)) return TRUE; return FALSE; } bool32 ShouldSwitchIfAllScoresBad(u32 battler) { u32 i, score, opposingBattler = GetOppositeBattler(battler); if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; for (i = 0; i < MAX_MON_MOVES; i++) { score = gAiBattleData->finalScore[battler][opposingBattler][i]; if (score > AI_BAD_SCORE_THRESHOLD) return FALSE; } if (RandomPercentage(RNG_AI_SWITCH_ALL_SCORES_BAD, GetSwitchChance(SHOULD_SWITCH_ALL_SCORES_BAD))) return TRUE; return FALSE; } bool32 ShouldStayInToUseMove(u32 battler) { u32 i, aiMove, opposingBattler = GetOppositeBattler(battler); enum BattleMoveEffects aiMoveEffect; for (i = 0; i < MAX_MON_MOVES; i++) { aiMove = gBattleMons[battler].moves[i]; aiMoveEffect = GetMoveEffect(aiMove); if (aiMoveEffect == EFFECT_REVIVAL_BLESSING || IsSwitchOutEffect(aiMoveEffect)) { if (gAiBattleData->finalScore[battler][opposingBattler][i] > AI_GOOD_SCORE_THRESHOLD) return TRUE; } } return FALSE; } void ModifySwitchAfterMoveScoring(u32 battler) { u32 battlerIn1, battlerIn2; s32 firstId; s32 lastId; // + 1 struct Pokemon *party; s32 i; s32 availableToSwitch; if (!CanBattlerConsiderSwitch(battler)) return; // Sequence Switching AI never switches mid-battle if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) return; availableToSwitch = 0; if (IsDoubleBattle()) { u32 partner = BATTLE_PARTNER(battler); battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << partner)) battlerIn2 = battler; else battlerIn2 = partner; } else { battlerIn1 = battler; battlerIn2 = battler; } GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i])) continue; if (i == gBattlerPartyIndexes[battlerIn1]) continue; if (i == gBattlerPartyIndexes[battlerIn2]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, i)) continue; availableToSwitch++; } if (availableToSwitch == 0) return; if (ShouldSwitchIfAllScoresBad(battler)) gAiLogicData->shouldSwitch |= (1u << battler); else if (ShouldStayInToUseMove(battler)) gAiLogicData->shouldSwitch &= ~(1u << battler); } bool32 IsSwitchinValid(u32 battler) { // Edge case: See if partner already chose to switch into the same mon if (IsDoubleBattle()) { u32 partner = BATTLE_PARTNER(battler); if (gBattleStruct->AI_monToSwitchIntoId[battler] == PARTY_SIZE) // Generic switch { if ((gAiLogicData->shouldSwitch & (1u << partner)) && gAiLogicData->monToSwitchInId[partner] == gAiLogicData->mostSuitableMonId[battler]) { return FALSE; } } else // Override switch { if ((gAiLogicData->shouldSwitch & (1u << partner)) && gAiLogicData->monToSwitchInId[partner] == gBattleStruct->AI_monToSwitchIntoId[battler]) { return FALSE; } } } return TRUE; } void AI_TrySwitchOrUseItem(u32 battler) { struct Pokemon *party; u8 battlerIn1, battlerIn2; s32 firstId; s32 lastId; // + 1 u8 battlerPosition = GetBattlerPosition(battler); party = GetBattlerParty(battler); if (gBattleTypeFlags & BATTLE_TYPE_TRAINER) { if (gAiLogicData->shouldSwitch & (1u << battler) && IsSwitchinValid(battler)) { BtlController_EmitTwoReturnValues(battler, B_COMM_TO_ENGINE, B_ACTION_SWITCH, 0); SetAIUsingGimmick(battler, NO_GIMMICK); if (gBattleStruct->AI_monToSwitchIntoId[battler] == PARTY_SIZE) { s32 monToSwitchId = gAiLogicData->mostSuitableMonId[battler]; if (monToSwitchId == PARTY_SIZE) { if (!IsDoubleBattle()) { battlerIn1 = GetBattlerAtPosition(battlerPosition); battlerIn2 = battlerIn1; } else { battlerIn1 = GetBattlerAtPosition(battlerPosition); battlerIn2 = GetBattlerAtPosition(BATTLE_PARTNER(battlerPosition)); } GetAIPartyIndexes(battler, &firstId, &lastId); for (monToSwitchId = (lastId-1); monToSwitchId >= firstId; monToSwitchId--) { if (!IsValidForBattle(&party[monToSwitchId])) continue; if (monToSwitchId == gBattlerPartyIndexes[battlerIn1]) continue; if (monToSwitchId == gBattlerPartyIndexes[battlerIn2]) continue; if (monToSwitchId == gBattleStruct->monToSwitchIntoId[battlerIn1]) continue; if (monToSwitchId == gBattleStruct->monToSwitchIntoId[battlerIn2]) continue; if (IsAceMon(battler, monToSwitchId)) continue; break; } } gBattleStruct->AI_monToSwitchIntoId[battler] = monToSwitchId; } gBattleStruct->monToSwitchIntoId[battler] = gBattleStruct->AI_monToSwitchIntoId[battler]; gAiLogicData->monToSwitchInId[battler] = gBattleStruct->AI_monToSwitchIntoId[battler]; return; } else if (ShouldUseItem(battler)) { SetAIUsingGimmick(battler, NO_GIMMICK); return; } } BtlController_EmitTwoReturnValues(battler, B_COMM_TO_ENGINE, B_ACTION_USE_MOVE, BATTLE_OPPOSITE(battler) << 8); } // If there are two(or more) mons to choose from, always choose one that has baton pass // as most often it can't do much on its own. static u32 GetBestMonBatonPass(struct Pokemon *party, int firstId, int lastId, u8 invalidMons, int aliveCount, u32 battler, u32 opposingBattler) { int i, j, bits = 0; for (i = firstId; i < lastId; i++) { if (invalidMons & (1u << i)) continue; for (j = 0; j < MAX_MON_MOVES; j++) { if (GetMonData(&party[i], MON_DATA_MOVE1 + j, NULL) == MOVE_BATON_PASS) { bits |= 1u << i; break; } } } if ((aliveCount == 2 || (aliveCount > 2 && Random() % 3 == 0)) && bits) { do { i = (Random() % (lastId - firstId)) + firstId; } while (!(bits & (1 << i))); return i; } return PARTY_SIZE; } static u32 GetBestMonTypeMatchup(struct Pokemon *party, int firstId, int lastId, u8 invalidMons, u32 battler, u32 opposingBattler) { int i, bits = 0; while (bits != 0x3F) // All mons were checked. { u32 bestResist = UQ_4_12(2.0); int bestMonId = PARTY_SIZE; // Find the mon whose type is the most suitable defensively. for (i = firstId; i < lastId; i++) { if (!((1u << i) & invalidMons) && !((1u << i) & bits)) { InitializeSwitchinCandidate(&party[i]); u32 typeEffectiveness = GetBattleMonTypeMatchup(gBattleMons[opposingBattler], gAiLogicData->switchinCandidate.battleMon); if (typeEffectiveness < bestResist) { bestResist = typeEffectiveness; bestMonId = i; } } } // Ok, we know the mon has the right typing but does it have at least one super effective move? if (bestMonId != PARTY_SIZE) { for (i = 0; i < MAX_MON_MOVES; i++) { u32 move = GetMonData(&party[bestMonId], MON_DATA_MOVE1 + i); if (move != MOVE_NONE && AI_GetMoveEffectiveness(move, battler, opposingBattler) >= UQ_4_12(2.0)) break; } if (i != MAX_MON_MOVES) return bestMonId; // Has both the typing and at least one super effective move. bits |= (1u << bestMonId); // Sorry buddy, we want something better. } else { bits = 0x3F; // No viable mon to switch. } } return PARTY_SIZE; } static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 invalidMons, u32 battler, u32 opposingBattler) { int i, j; int dmg, bestDmg = 0; int bestMonId = PARTY_SIZE; u32 aiMove; uq4_12_t effectiveness; // If we couldn't find the best mon in terms of typing, find the one that deals most damage. for (i = firstId; i < lastId; i++) { if ((1 << (i)) & invalidMons) continue; InitializeSwitchinCandidate(&party[i]); for (j = 0; j < MAX_MON_MOVES; j++) { aiMove = gAiLogicData->switchinCandidate.battleMon.moves[j]; if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) { aiMove = GetMonData(&party[i], MON_DATA_MOVE1 + j); dmg = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, &effectiveness, AI_ATTACKING); if (bestDmg < dmg) { bestDmg = dmg; bestMonId = i; } } } } return bestMonId; } static u32 GetFirstNonInvalidMon(u32 firstId, u32 lastId, u32 invalidMons) { u32 chosenMonId = PARTY_SIZE; for (u32 i = (lastId-1); i > firstId; i--) { if (!((1 << i) & invalidMons)) { // first non invalid mon found chosenMonId = i; break; } } return chosenMonId; } bool32 IsMonGrounded(enum HoldEffect heldItemEffect, enum Ability ability, enum Type type1, enum Type type2) { // List that makes mon not grounded if (type1 == TYPE_FLYING || type2 == TYPE_FLYING || ability == ABILITY_LEVITATE || (heldItemEffect == HOLD_EFFECT_AIR_BALLOON && !(ability == ABILITY_KLUTZ || (gFieldStatuses & STATUS_FIELD_MAGIC_ROOM)))) { // List that overrides being off the ground if ((heldItemEffect == HOLD_EFFECT_IRON_BALL && !(ability == ABILITY_KLUTZ || (gFieldStatuses & STATUS_FIELD_MAGIC_ROOM))) || (gFieldStatuses & STATUS_FIELD_GRAVITY) || (gFieldStatuses & STATUS_FIELD_MAGIC_ROOM)) return TRUE; else return FALSE; } else return TRUE; } // Gets hazard damage static u32 GetSwitchinHazardsDamage(u32 battler, struct BattlePokemon *battleMon) { enum Type defType1 = battleMon->types[0], defType2 = battleMon->types[1]; u8 tSpikesLayers; u16 heldItemEffect = GetItemHoldEffect(battleMon->item); u32 maxHP = battleMon->maxHP; enum Ability ability = battleMon->ability, status = battleMon->status1; u32 spikesDamage = 0, tSpikesDamage = 0, hazardDamage = 0; u32 side = GetBattlerSide(battler); // Check ways mon might avoid all hazards if (ability != ABILITY_MAGIC_GUARD || (heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS && !((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ))) { // Stealth Rock if (IsHazardOnSide(side, HAZARDS_STEALTH_ROCK) && heldItemEffect != HOLD_EFFECT_HEAVY_DUTY_BOOTS) hazardDamage += GetStealthHazardDamageByTypesAndHP(TYPE_SIDE_HAZARD_POINTED_STONES, defType1, defType2, battleMon->maxHP); // G-Max Steelsurge if (IsHazardOnSide(side, HAZARDS_STEELSURGE) && heldItemEffect != HOLD_EFFECT_HEAVY_DUTY_BOOTS) hazardDamage += GetStealthHazardDamageByTypesAndHP(TYPE_SIDE_HAZARD_SHARP_STEEL, defType1, defType2, battleMon->maxHP); // Spikes if (IsHazardOnSide(side, HAZARDS_SPIKES) && IsMonGrounded(heldItemEffect, ability, defType1, defType2)) { spikesDamage = maxHP / ((5 - gSideTimers[GetBattlerSide(battler)].spikesAmount) * 2); if (spikesDamage == 0) spikesDamage = 1; hazardDamage += spikesDamage; } if (IsHazardOnSide(side, HAZARDS_TOXIC_SPIKES) && (defType1 != TYPE_POISON && defType2 != TYPE_POISON && defType1 != TYPE_STEEL && defType2 != TYPE_STEEL && ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL && ability != ABILITY_COMATOSE && status == 0 && !(gSideStatuses[GetBattlerSide(battler)] & SIDE_STATUS_SAFEGUARD) && !IsAbilityOnSide(battler, ABILITY_PASTEL_VEIL) && !IsBattlerTerrainAffected(battler, ability, gAiLogicData->holdEffects[battler], STATUS_FIELD_MISTY_TERRAIN) && !IsAbilityStatusProtected(battler, ability) && heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS && IsMonGrounded(heldItemEffect, ability, defType1, defType2))) { tSpikesLayers = gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount; if (tSpikesLayers == 1) { tSpikesDamage = maxHP / 8; if (tSpikesDamage == 0) tSpikesDamage = 1; } else if (tSpikesLayers >= 2) { tSpikesDamage = maxHP / 16; if (tSpikesDamage == 0) tSpikesDamage = 1; } hazardDamage += tSpikesDamage; } } return hazardDamage; } // Gets damage / healing from weather static s32 GetSwitchinWeatherImpact(void) { s32 weatherImpact = 0, maxHP = gAiLogicData->switchinCandidate.battleMon.maxHP; enum Ability ability = gAiLogicData->switchinCandidate.battleMon.ability; enum HoldEffect holdEffect = GetItemHoldEffect(gAiLogicData->switchinCandidate.battleMon.item); if (HasWeatherEffect()) { // Damage if (holdEffect != HOLD_EFFECT_SAFETY_GOGGLES && ability != ABILITY_MAGIC_GUARD && ability != ABILITY_OVERCOAT) { if ((gBattleWeather & B_WEATHER_HAIL) && (gAiLogicData->switchinCandidate.battleMon.types[0] != TYPE_ICE || gAiLogicData->switchinCandidate.battleMon.types[1] != TYPE_ICE) && ability != ABILITY_SNOW_CLOAK && ability != ABILITY_ICE_BODY) { weatherImpact = maxHP / 16; if (weatherImpact == 0) weatherImpact = 1; } else if ((gBattleWeather & B_WEATHER_SANDSTORM) && (gAiLogicData->switchinCandidate.battleMon.types[0] != TYPE_GROUND && gAiLogicData->switchinCandidate.battleMon.types[1] != TYPE_GROUND && gAiLogicData->switchinCandidate.battleMon.types[0] != TYPE_ROCK && gAiLogicData->switchinCandidate.battleMon.types[1] != TYPE_ROCK && gAiLogicData->switchinCandidate.battleMon.types[0] != TYPE_STEEL && gAiLogicData->switchinCandidate.battleMon.types[1] != TYPE_STEEL && ability != ABILITY_SAND_VEIL && ability != ABILITY_SAND_RUSH && ability != ABILITY_SAND_FORCE)) { weatherImpact = maxHP / 16; if (weatherImpact == 0) weatherImpact = 1; } } if ((gBattleWeather & B_WEATHER_SUN) && holdEffect != HOLD_EFFECT_UTILITY_UMBRELLA && (ability == ABILITY_SOLAR_POWER || ability == ABILITY_DRY_SKIN)) { weatherImpact = maxHP / 8; if (weatherImpact == 0) weatherImpact = 1; } // Healing if (gBattleWeather & B_WEATHER_RAIN && holdEffect != HOLD_EFFECT_UTILITY_UMBRELLA) { if (ability == ABILITY_DRY_SKIN) { weatherImpact = -(maxHP / 8); if (weatherImpact == 0) weatherImpact = -1; } else if (ability == ABILITY_RAIN_DISH) { weatherImpact = -(maxHP / 16); if (weatherImpact == 0) weatherImpact = -1; } } if (((gBattleWeather & B_WEATHER_HAIL) || (gBattleWeather & B_WEATHER_SNOW)) && ability == ABILITY_ICE_BODY) { weatherImpact = -(maxHP / 16); if (weatherImpact == 0) weatherImpact = -1; } } return weatherImpact; } // Gets one turn of recurring healing static u32 GetSwitchinRecurringHealing(void) { u32 recurringHealing = 0, maxHP = gAiLogicData->switchinCandidate.battleMon.maxHP; enum Ability ability = gAiLogicData->switchinCandidate.battleMon.ability; enum HoldEffect holdEffect = GetItemHoldEffect(gAiLogicData->switchinCandidate.battleMon.item); // Items if (ability != ABILITY_KLUTZ) { if (holdEffect == HOLD_EFFECT_BLACK_SLUDGE && (gAiLogicData->switchinCandidate.battleMon.types[0] == TYPE_POISON || gAiLogicData->switchinCandidate.battleMon.types[1] == TYPE_POISON)) { recurringHealing = maxHP / 16; if (recurringHealing == 0) recurringHealing = 1; } else if (holdEffect == HOLD_EFFECT_LEFTOVERS) { recurringHealing = maxHP / 16; if (recurringHealing == 0) recurringHealing = 1; } } // Intentionally omitting Shell Bell for its inconsistency // Abilities if (ability == ABILITY_POISON_HEAL && (gAiLogicData->switchinCandidate.battleMon.status1 & STATUS1_POISON)) { u32 healing = maxHP / 8; if (healing == 0) healing = 1; recurringHealing += healing; } return recurringHealing; } // Gets one turn of recurring damage static u32 GetSwitchinRecurringDamage(void) { u32 passiveDamage = 0, maxHP = gAiLogicData->switchinCandidate.battleMon.maxHP; enum Ability ability = gAiLogicData->switchinCandidate.battleMon.ability; enum HoldEffect holdEffect = GetItemHoldEffect(gAiLogicData->switchinCandidate.battleMon.item); // Items if (ability != ABILITY_MAGIC_GUARD && ability != ABILITY_KLUTZ) { if (holdEffect == HOLD_EFFECT_BLACK_SLUDGE && gAiLogicData->switchinCandidate.battleMon.types[0] != TYPE_POISON && gAiLogicData->switchinCandidate.battleMon.types[1] != TYPE_POISON) { passiveDamage = maxHP / 8; if (passiveDamage == 0) passiveDamage = 1; } else if (holdEffect == HOLD_EFFECT_LIFE_ORB && ability != ABILITY_SHEER_FORCE) { passiveDamage = maxHP / 10; if (passiveDamage == 0) passiveDamage = 1; } else if (holdEffect == HOLD_EFFECT_STICKY_BARB) { passiveDamage = maxHP / 8; if (passiveDamage == 0) passiveDamage = 1; } } return passiveDamage; } // Gets one turn of status damage static u32 GetSwitchinStatusDamage(u32 battler) { enum Type defType1 = gAiLogicData->switchinCandidate.battleMon.types[0], defType2 = gAiLogicData->switchinCandidate.battleMon.types[1]; u8 tSpikesLayers = gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount; u16 heldItemEffect = GetItemHoldEffect(gAiLogicData->switchinCandidate.battleMon.item); u32 status = gAiLogicData->switchinCandidate.battleMon.status1; enum Ability ability = gAiLogicData->switchinCandidate.battleMon.ability, maxHP = gAiLogicData->switchinCandidate.battleMon.maxHP; u32 statusDamage = 0; // Status condition damage if ((status != 0) && gAiLogicData->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD) { if (status & STATUS1_BURN) { if (GetConfig(B_BURN_DAMAGE) >= GEN_7 || GetConfig(B_BURN_DAMAGE) == GEN_1) statusDamage = maxHP / 16; else statusDamage = maxHP / 8; if (ability == ABILITY_HEATPROOF) statusDamage = statusDamage / 2; if (statusDamage == 0) statusDamage = 1; } else if (status & STATUS1_FROSTBITE) { if (GetConfig(B_BURN_DAMAGE) >= GEN_7 || GetConfig(B_BURN_DAMAGE) == GEN_1) statusDamage = maxHP / 16; else statusDamage = maxHP / 8; if (statusDamage == 0) statusDamage = 1; } else if ((status & STATUS1_POISON) && ability != ABILITY_POISON_HEAL) { statusDamage = maxHP / 8; if (statusDamage == 0) statusDamage = 1; } else if ((status & STATUS1_TOXIC_POISON) && ability != ABILITY_POISON_HEAL) { if ((status & STATUS1_TOXIC_COUNTER) != STATUS1_TOXIC_TURN(15)) // not 16 turns gAiLogicData->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1); statusDamage = maxHP / 16; if (statusDamage == 0) statusDamage = 1; statusDamage *= gAiLogicData->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_COUNTER >> 8; } } // Apply hypothetical poisoning from Toxic Spikes, which means the first turn of damage already added in GetSwitchinHazardsDamage // Do this last to skip one iteration of Poison / Toxic damage, and start counting Toxic damage one turn later. if (tSpikesLayers != 0 && (defType1 != TYPE_POISON && defType2 != TYPE_POISON && ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL && status == 0 && !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS && (((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ))) && heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS && IsMonGrounded(heldItemEffect, ability, defType1, defType2))) { if (tSpikesLayers == 1) { gAiLogicData->switchinCandidate.battleMon.status1 = STATUS1_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes gAiLogicData->switchinCandidate.hypotheticalStatus = TRUE; } if (tSpikesLayers == 2) { gAiLogicData->switchinCandidate.battleMon.status1 = STATUS1_TOXIC_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes gAiLogicData->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1); gAiLogicData->switchinCandidate.hypotheticalStatus = TRUE; } } return statusDamage; } // Gets number of hits to KO factoring in hazards, healing held items, status, and weather static u32 GetSwitchinHitsToKO(s32 damageTaken, u32 battler) { u32 startingHP = gAiLogicData->switchinCandidate.battleMon.hp - GetSwitchinHazardsDamage(battler, &gAiLogicData->switchinCandidate.battleMon); s32 weatherImpact = GetSwitchinWeatherImpact(); // Signed to handle both damage and healing in the same value u32 recurringDamage = GetSwitchinRecurringDamage(); u32 recurringHealing = GetSwitchinRecurringHealing(); u32 statusDamage = GetSwitchinStatusDamage(battler); u32 hitsToKO = 0; u16 maxHP = gAiLogicData->switchinCandidate.battleMon.maxHP, item = gAiLogicData->switchinCandidate.battleMon.item, heldItemEffect = GetItemHoldEffect(item); u8 weatherDuration = gWishFutureKnock.weatherDuration, holdEffectParam = GetItemHoldEffectParam(item); u32 opposingBattler = GetOppositeBattler(battler); enum Ability opposingAbility = gAiLogicData->abilities[opposingBattler], ability = gAiLogicData->switchinCandidate.battleMon.ability; bool32 usedSingleUseHealingItem = FALSE, opponentCanBreakMold = IsMoldBreakerTypeAbility(opposingBattler, opposingAbility); s32 currentHP = startingHP, singleUseItemHeal = 0; // No damage being dealt if ((damageTaken + statusDamage + recurringDamage <= recurringHealing) || damageTaken + statusDamage + recurringDamage == 0) return hitsToKO; // Mon fainted to hazards if (startingHP == 0) return 1; // Find hits to KO while (currentHP > 0) { // Remove weather damage when it would run out if (weatherImpact != 0 && weatherDuration == 0) weatherImpact = 0; // Take attack damage for the turn currentHP = currentHP - damageTaken; // One shot prevention effects if (damageTaken >= maxHP && startingHP == maxHP && (heldItemEffect == HOLD_EFFECT_FOCUS_SASH || (!opponentCanBreakMold && GetConfig(B_STURDY) >= GEN_5 && ability == ABILITY_STURDY)) && hitsToKO < 1) currentHP = 1; // If mon is still alive, apply weather impact first, as it might KO the mon before it can heal with its item (order is weather -> item -> status) if (currentHP > 0) currentHP = currentHP - weatherImpact; // Check if we're at a single use healing item threshold if (currentHP > 0 && gAiLogicData->switchinCandidate.battleMon.ability != ABILITY_KLUTZ && usedSingleUseHealingItem == FALSE && !(opposingAbility == ABILITY_UNNERVE && GetItemPocket(item) == POCKET_BERRIES)) { switch (heldItemEffect) { case HOLD_EFFECT_RESTORE_HP: if (currentHP < maxHP / 2) singleUseItemHeal = holdEffectParam; break; case HOLD_EFFECT_RESTORE_PCT_HP: if (currentHP < maxHP / 2) { singleUseItemHeal = maxHP / holdEffectParam; if (singleUseItemHeal == 0) singleUseItemHeal = 1; } break; case HOLD_EFFECT_CONFUSE_SPICY: case HOLD_EFFECT_CONFUSE_DRY: case HOLD_EFFECT_CONFUSE_SWEET: case HOLD_EFFECT_CONFUSE_BITTER: case HOLD_EFFECT_CONFUSE_SOUR: if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION) { singleUseItemHeal = maxHP / holdEffectParam; if (singleUseItemHeal == 0) singleUseItemHeal = 1; } break; } // If we used one, apply it without overcapping our maxHP if (singleUseItemHeal > 0) { if ((currentHP + singleUseItemHeal) > maxHP) currentHP = maxHP; else currentHP = currentHP + singleUseItemHeal; usedSingleUseHealingItem = TRUE; } } // Healing from items occurs before status so we can do the rest in one line if (currentHP > 0) currentHP = currentHP + recurringHealing - recurringDamage - statusDamage; // Recalculate toxic damage if needed if (gAiLogicData->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_POISON) statusDamage = GetSwitchinStatusDamage(battler); // Reduce weather duration if (weatherDuration != 0) weatherDuration--; hitsToKO++; } // Disguise will always add an extra hit to KO if (!opponentCanBreakMold && gAiLogicData->switchinCandidate.battleMon.species == SPECIES_MIMIKYU_DISGUISED) hitsToKO++; // If mon had a hypothetical status from TSpikes, clear it if (gAiLogicData->switchinCandidate.hypotheticalStatus == TRUE) { gAiLogicData->switchinCandidate.battleMon.status1 = 0; gAiLogicData->switchinCandidate.hypotheticalStatus = FALSE; } return hitsToKO; } static u32 GetBattleMonTypeMatchup(struct BattlePokemon opposingBattleMon, struct BattlePokemon battleMon) { // Check type matchup u32 typeEffectiveness1 = UQ_4_12(1.0), typeEffectiveness2 = UQ_4_12(1.0); enum Type atkType1 = opposingBattleMon.types[0], atkType2 = opposingBattleMon.types[1]; enum Type defType1 = battleMon.types[0], defType2 = battleMon.types[1]; // Add each independent defensive type matchup together typeEffectiveness1 = uq4_12_multiply(typeEffectiveness1, (GetTypeModifier(atkType1, defType1))); if (defType2 != defType1) typeEffectiveness1 = uq4_12_multiply(typeEffectiveness1, (GetTypeModifier(atkType1, defType2))); if (typeEffectiveness1 == 0) // Immunity typeEffectiveness1 = UQ_4_12(0.1); if (atkType2 != atkType1) { typeEffectiveness2 = uq4_12_multiply(typeEffectiveness2, (GetTypeModifier(atkType2, defType1))); if (defType2 != defType1) typeEffectiveness2 = uq4_12_multiply(typeEffectiveness2, (GetTypeModifier(atkType2, defType2))); if (typeEffectiveness2 == 0) // Immunity typeEffectiveness2 = UQ_4_12(0.1); } else { typeEffectiveness2 = typeEffectiveness1; } return typeEffectiveness1 + typeEffectiveness2; } static int GetRandomSwitchinWithBatonPass(int aliveCount, int bits, int firstId, int lastId, int currentMonId) { // Breakout early if there aren't any Baton Pass mons to save computation time if (bits == 0) return PARTY_SIZE; // GetBestMonBatonPass randomly chooses between all mons that met Baton Pass check if ((aliveCount == 2 || (aliveCount > 2 && Random() % 3 == 0)) && bits) { do { return (Random() % (lastId - firstId)) + firstId; } while (!(bits & (1 << (currentMonId)))); } // Catch any other cases (such as only one mon alive and it has Baton Pass) else return PARTY_SIZE; } static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattler, struct BattlePokemon battleMon, u32 *bestPlayerMove) { int i = 0; u32 playerMove; u16 *playerMoves = GetMovesArray(opposingBattler); s32 damageTaken = 0, maxDamageTaken = 0; uq4_12_t effectiveness; for (i = 0; i < MAX_MON_MOVES; i++) { playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[i] : playerMoves[i]; if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH && gBattleMons[opposingBattler].pp[i] > 0) { damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, &effectiveness, AI_DEFENDING); if (playerMove == gBattleStruct->choicedMove[opposingBattler]) // If player is choiced, only care about the choice locked move { *bestPlayerMove = playerMove; return damageTaken; } if (damageTaken > maxDamageTaken) { maxDamageTaken = damageTaken; *bestPlayerMove = playerMove; } } } return maxDamageTaken; } static s32 GetMaxPriorityDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattler, struct BattlePokemon battleMon, u32 *bestPlayerPriorityMove) { int i = 0; u32 playerMove; u16 *playerMoves = GetMovesArray(opposingBattler); s32 damageTaken = 0, maxDamageTaken = 0; uq4_12_t effectiveness = UQ_4_12(1.0); for (i = 0; i < MAX_MON_MOVES; i++) { // If player is choiced into a non-priority move, AI understands that it can't deal priority damage if (gBattleStruct->choicedMove[opposingBattler] !=MOVE_NONE && GetMovePriority(gBattleStruct->choicedMove[opposingBattler]) < 1) break; playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[i] : playerMoves[i]; if (GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], playerMove) > 0 && playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH && gBattleMons[opposingBattler].pp[i] > 0) { damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, &effectiveness, AI_DEFENDING); if (playerMove == gBattleStruct->choicedMove[opposingBattler]) // If player is choiced, only care about the choice locked move { *bestPlayerPriorityMove = playerMove; return damageTaken; } if (damageTaken > maxDamageTaken) { maxDamageTaken = damageTaken; *bestPlayerPriorityMove = playerMove; } } } return maxDamageTaken; } static bool32 CanAbilityTrapOpponent(enum Ability ability, u32 opponent) { if ((GetConfig(B_GHOSTS_ESCAPE) >= GEN_6 && IS_BATTLER_OF_TYPE(opponent, TYPE_GHOST))) return FALSE; else if (ability == ABILITY_SHADOW_TAG) { if (B_SHADOW_TAG_ESCAPE >= GEN_4 && gAiLogicData->abilities[opponent] == ABILITY_SHADOW_TAG) // Check if ability exists in species return FALSE; else return TRUE; } else if (ability == ABILITY_ARENA_TRAP && IsBattlerGrounded(opponent, gAiLogicData->abilities[opponent], gAiLogicData->holdEffects[opponent])) return TRUE; else if (ability == ABILITY_MAGNET_PULL && IS_BATTLER_OF_TYPE(opponent, TYPE_STEEL)) return TRUE; else return FALSE; } static inline bool32 IsFreeSwitch(enum SwitchType switchType, u32 battlerSwitchingOut, u32 opposingBattler) { bool32 movedSecond = GetBattlerTurnOrderNum(battlerSwitchingOut) > GetBattlerTurnOrderNum(opposingBattler) ? TRUE : FALSE; // Switch out effects if (!IsDoubleBattle()) // Not handling doubles' additional complexity { if (IsSwitchOutEffect(GetMoveEffect(gCurrentMove)) && movedSecond) return TRUE; if (gAiLogicData->ejectButtonSwitch) return TRUE; if (gAiLogicData->ejectPackSwitch) { enum Ability opposingAbility = GetBattlerAbilityIgnoreMoldBreaker(opposingBattler); // If faster, not a free switch; likely lowered own stats if (!movedSecond && opposingAbility != ABILITY_INTIMIDATE && opposingAbility != ABILITY_SUPERSWEET_SYRUP) // Intimidate triggers switches before turn starts 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 (switchType == SWITCH_AFTER_KO) return TRUE; else return FALSE; } static inline bool32 CanSwitchinWin1v1(u32 hitsToKOAI, u32 hitsToKOPlayer, bool32 isSwitchinFirst, bool32 isFreeSwitch) { // Player's best move deals 0 damage if (hitsToKOAI == 0 && hitsToKOPlayer > 0) return TRUE; // AI's best move deals 0 damage if (hitsToKOPlayer == 0 && hitsToKOAI > 0) return FALSE; // Neither mon can damage the other if (hitsToKOPlayer == 0 && hitsToKOAI == 0) return FALSE; // 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, enum SwitchType switchType) { int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE, generic1v1MonId = 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, aceMonCount = 0; s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0, monMaxDamage = 0; u32 aiMove, hitsToKOAI, hitsToKOPlayer, hitsToKOAIPriority, bestPlayerMove = MOVE_NONE, bestPlayerPriorityMove = MOVE_NONE, maxHitsToKO = 0; u32 bestResist = UQ_4_12(2.0), bestResistEffective = UQ_4_12(2.0), typeMatchup; // 2.0 is the default "Neutral" matchup from GetBattleMonTypeMatchup bool32 isFreeSwitch = IsFreeSwitch(switchType, battlerIn1, opposingBattler), isSwitchinFirst, isSwitchinFirstPriority, canSwitchinWin1v1; u32 invalidMons = 0; uq4_12_t effectiveness = UQ_4_12(1.0); // Iterate through mons for (i = firstId; i < lastId; i++) { // Check mon validity if (!IsValidForBattle(&party[i]) || gBattlerPartyIndexes[battlerIn1] == i || gBattlerPartyIndexes[battlerIn2] == i || i == gBattleStruct->monToSwitchIntoId[battlerIn1] || i == gBattleStruct->monToSwitchIntoId[battlerIn2]) { invalidMons |= 1u << i; continue; } // Save Ace Pokemon for last else if (IsAceMon(battler, i)) { aceMonId = i; aceMonCount++; invalidMons |= 1u << i; continue; } else aliveCount++; InitializeSwitchinCandidate(&party[i]); // While not really invalid per se, not really wise to switch into this mon if (gAiLogicData->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler)) continue; // Get max number of hits for player to KO AI mon and type matchup for defensive switching hitsToKOAI = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, &bestPlayerMove), battler); hitsToKOAIPriority = GetSwitchinHitsToKO(GetMaxPriorityDamagePlayerCouldDealToSwitchin(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, &bestPlayerPriorityMove), battler); typeMatchup = GetBattleMonTypeMatchup(gBattleMons[opposingBattler], gAiLogicData->switchinCandidate.battleMon); monMaxDamage = 0; // Check through current mon's moves for (j = 0; j < MAX_MON_MOVES; j++) { // Check that move has PP remaining before running calcs if (gAiLogicData->switchinCandidate.battleMon.pp[j] < 1) continue; aiMove = gAiLogicData->switchinCandidate.battleMon.moves[j]; damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, &effectiveness, AI_ATTACKING); hitsToKOPlayer = GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler); // Offensive switchin decisions are based on which whether switchin moves first and whether it can win a 1v1 isSwitchinFirst = AI_IsPartyMonFaster(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, aiMove, bestPlayerMove, CONSIDER_PRIORITY); isSwitchinFirstPriority = AI_IsPartyMonFaster(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, aiMove, bestPlayerPriorityMove, CONSIDER_PRIORITY); canSwitchinWin1v1 = CanSwitchinWin1v1(hitsToKOAI, hitsToKOPlayer, isSwitchinFirst, isFreeSwitch) && CanSwitchinWin1v1(hitsToKOAIPriority, hitsToKOPlayer, isSwitchinFirstPriority, isFreeSwitch); // AI must successfully 1v1 with and without priority to be considered a good option // Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not if (GetMoveEffect(aiMove) == EFFECT_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; } } // Track max hits to KO and set defensive mon if (hitsToKOAI > maxHitsToKO && (canSwitchinWin1v1 || gAiThinkingStruct->aiFlags[battler] & AI_FLAG_STALL)) { maxHitsToKO = hitsToKOAI; if (maxHitsToKO > defensiveMonHitKOThreshold) defensiveMonId = i; } if (canSwitchinWin1v1) generic1v1MonId = i; // Check for mon with resistance and super effective move for best type matchup mon with effective move if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) { if (typeMatchup < bestResistEffective) { if (effectiveness >= UQ_4_12(2.0)) { if (canSwitchinWin1v1) { bestResistEffective = typeMatchup; typeMatchupEffectiveId = i; } } } // If a self destruction move doesn't OHKO, don't factor it into revenge killing enum BattleMoveEffects aiEffect = GetMoveEffect(aiMove); if ((aiEffect == EFFECT_EXPLOSION || aiEffect == EFFECT_MISTY_EXPLOSION) && damageDealt < playerMonHP) continue; if (damageDealt > monMaxDamage) monMaxDamage = damageDealt; // Check that mon isn't one shot and set best damage mon if (damageDealt > maxDamageDealt) { if ((isFreeSwitch && hitsToKOAI > 1) || hitsToKOAI > 2) // This is a "default", we have uniquely low standards { maxDamageDealt = damageDealt; damageMonId = i; } } // Check if current mon can revenge kill in some capacity // If AI mon can one shot if (damageDealt >= playerMonHP) { if (canSwitchinWin1v1) { if (isSwitchinFirst) revengeKillerId = i; else slowRevengeKillerId = i; } } // If AI mon can two shot if (damageDealt >= (playerMonHP / 2 + playerMonHP % 2)) // Modulo to handle odd numbers in non-decimal division { if (canSwitchinWin1v1) { if (isSwitchinFirst) fastThreatenId = i; else slowThreatenId = i; } } // If mon can trap if ((CanAbilityTrapOpponent(gAiLogicData->switchinCandidate.battleMon.ability, opposingBattler) || (CanAbilityTrapOpponent(gAiLogicData->abilities[opposingBattler], opposingBattler) && gAiLogicData->switchinCandidate.battleMon.ability == ABILITY_TRACE)) && CountUsablePartyMons(opposingBattler) > 0 && canSwitchinWin1v1) trapperId = i; } } if (monMaxDamage == 0) invalidMons |= 1u << i; } batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i); // 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; else if (revengeKillerId != PARTY_SIZE) return revengeKillerId; else if (slowRevengeKillerId != PARTY_SIZE) return slowRevengeKillerId; else if (fastThreatenId != PARTY_SIZE) return fastThreatenId; else if (slowThreatenId != PARTY_SIZE) return slowThreatenId; else if (typeMatchupEffectiveId != PARTY_SIZE) return typeMatchupEffectiveId; else if (typeMatchupId != PARTY_SIZE) return typeMatchupId; else if (batonPassId != PARTY_SIZE) return batonPassId; else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId; else if (damageMonId != PARTY_SIZE) return damageMonId; } else { // Return Trapper > Type Matchup > Best Defensive > Baton Pass if (trapperId != PARTY_SIZE) return trapperId; else if (typeMatchupEffectiveId != PARTY_SIZE) return typeMatchupEffectiveId; else if (typeMatchupId != PARTY_SIZE) return typeMatchupId; else if (defensiveMonId != PARTY_SIZE) return defensiveMonId; else if (batonPassId != PARTY_SIZE) return batonPassId; else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId; } if (switchType == SWITCH_MID_BATTLE_OPTIONAL) return PARTY_SIZE; // Fallback u32 bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons); if (bestMonId != PARTY_SIZE) return bestMonId; // If ace mon is the last available Pokemon and U-Turn/Volt Switch or Eject Pack/Button was used - switch to the mon. if (aceMonId != PARTY_SIZE && CountUsablePartyMons(battler) <= aceMonCount) return aceMonId; return PARTY_SIZE; } static u32 GetNextMonInParty(struct Pokemon *party, int firstId, int lastId, u32 battlerIn1, u32 battlerIn2) { u32 i; // Iterate through mons for (i = firstId; i < lastId; i++) { // Check mon validity if (!IsValidForBattle(&party[i]) || gBattlerPartyIndexes[battlerIn1] == i || gBattlerPartyIndexes[battlerIn2] == i || i == gBattleStruct->monToSwitchIntoId[battlerIn1] || i == gBattleStruct->monToSwitchIntoId[battlerIn2]) { continue; } return i; } return PARTY_SIZE; } u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType) { u32 opposingBattler = 0; u32 bestMonId = PARTY_SIZE; u32 battlerIn1 = 0, battlerIn2 = 0; s32 firstId = 0; s32 lastId = 0; // + 1 struct Pokemon *party; if (gBattleStruct->monToSwitchIntoId[battler] != PARTY_SIZE) return gBattleStruct->monToSwitchIntoId[battler]; if (gBattleTypeFlags & BATTLE_TYPE_ARENA) return gBattlerPartyIndexes[battler] + 1; if (IsDoubleBattle()) { battlerIn1 = battler; if (gAbsentBattlerFlags & (1u << GetPartnerBattler(battler))) battlerIn2 = battler; else battlerIn2 = GetPartnerBattler(battler); opposingBattler = BATTLE_OPPOSITE(battlerIn1); if (gAbsentBattlerFlags & (1u << opposingBattler)) opposingBattler ^= BIT_FLANK; } else { opposingBattler = GetOppositeBattler(battler); battlerIn1 = battler; battlerIn2 = battler; } GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) { bestMonId = GetNextMonInParty(party, firstId, lastId, battlerIn1, battlerIn2); return bestMonId; } // Only use better mon selection if AI_FLAG_SMART_MON_CHOICES is set for the trainer. if (gAiThinkingStruct->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 { bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchType); return bestMonId; } // This all handled by the GetBestMonIntegrated function if the AI_FLAG_SMART_MON_CHOICES flag is set else { s32 i, aliveCount = 0, aceMonCount = 0; u32 invalidMons = 0, aceMonId = PARTY_SIZE; // Get invalid slots ids. for (i = firstId; i < lastId; i++) { if (!IsValidForBattle(&party[i]) || gBattlerPartyIndexes[battlerIn1] == i || 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. { invalidMons |= 1u << i; } else if (IsAceMon(battler, i)) // Save Ace Pokemon for last. { aceMonId = i; aceMonCount++; invalidMons |= 1u << i; } else { aliveCount++; } } bestMonId = GetBestMonBatonPass(party, firstId, lastId, invalidMons, aliveCount, battler, opposingBattler); if (bestMonId != PARTY_SIZE) return bestMonId; if (aceMonId != PARTY_SIZE && aliveCount == 0) return aceMonId; bestMonId = GetBestMonTypeMatchup(party, firstId, lastId, invalidMons, battler, opposingBattler); if (bestMonId != PARTY_SIZE) return bestMonId; bestMonId = GetBestMonDmg(party, firstId, lastId, invalidMons, battler, opposingBattler); if (bestMonId != PARTY_SIZE) return bestMonId; if (switchType == SWITCH_MID_BATTLE_OPTIONAL) return PARTY_SIZE; // Fallback bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons); if (bestMonId != PARTY_SIZE) return bestMonId; if (aceMonId != PARTY_SIZE && CountUsablePartyMons(battler) <= aceMonCount) return aceMonId; return PARTY_SIZE; } } static bool32 AiExpectsToFaintPlayer(u32 battler) { u8 target = gAiBattleData->chosenTarget[battler]; if (gAiBattleData->actionFlee || gAiBattleData->choiceWatch) return FALSE; // AI not planning to use move if (!IsBattlerAlly(target, battler) && CanIndexMoveFaintTarget(battler, target, gAiBattleData->chosenMoveIndex[battler], AI_ATTACKING) && AI_IsFaster(battler, target, GetAIChosenMove(battler), GetIncomingMove(battler, target, gAiLogicData), CONSIDER_PRIORITY)) { // We expect to faint the target and move first -> dont use an item return TRUE; } return FALSE; } static bool32 ShouldUseItem(u32 battler) { struct Pokemon *party; s32 i; u8 validMons = 0; bool32 shouldUse = FALSE; u32 healAmount = 0; if (IsAiVsAiBattle()) return FALSE; // If teaming up with player and Pokemon is on the right, or Pokemon is currently held by Sky Drop if ((gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && GetBattlerPosition(battler) == B_POSITION_PLAYER_RIGHT) || gBattleMons[battler].volatiles.semiInvulnerable == STATE_SKY_DROP) return FALSE; if (gBattleMons[battler].volatiles.embargo) return FALSE; if (AiExpectsToFaintPlayer(battler)) return FALSE; party = GetBattlerParty(battler); for (i = 0; i < PARTY_SIZE; i++) { if (IsValidForBattle(&party[i])) { validMons++; } } for (i = 0; i < MAX_TRAINER_ITEMS; i++) { u16 item; const u8 *itemEffects; u8 battlerSide; item = gBattleHistory->trainerItems[i]; if (item == ITEM_NONE) continue; itemEffects = GetItemEffect(item); if (itemEffects == NULL) continue; switch (GetItemBattleUsage(item)) { case EFFECT_ITEM_HEAL_AND_CURE_STATUS: healAmount = GetHPHealAmount(itemEffects[GetItemEffectParamOffset(battler, item, 4, ITEM4_HEAL_HP)], GetBattlerMon(battler)); shouldUse = AI_ShouldHeal(battler, healAmount); break; case EFFECT_ITEM_RESTORE_HP: healAmount = GetHPHealAmount(itemEffects[GetItemEffectParamOffset(battler, item, 4, ITEM4_HEAL_HP)], GetBattlerMon(battler)); shouldUse = AI_ShouldHeal(battler, healAmount); break; case EFFECT_ITEM_CURE_STATUS: if ((itemEffects[3] & ITEM3_SLEEP && gBattleMons[battler].status1 & STATUS1_SLEEP) || (itemEffects[3] & ITEM3_POISON && gBattleMons[battler].status1 & STATUS1_PSN_ANY) || (itemEffects[3] & ITEM3_BURN && gBattleMons[battler].status1 & STATUS1_BURN) || (itemEffects[3] & ITEM3_FREEZE && gBattleMons[battler].status1 & STATUS1_ICY_ANY) || (itemEffects[3] & ITEM3_PARALYSIS && gBattleMons[battler].status1 & STATUS1_PARALYSIS) || (itemEffects[3] & ITEM3_CONFUSION && gBattleMons[battler].volatiles.confusionTurns > 0)) shouldUse = ShouldCureStatusWithItem(battler, battler, gAiLogicData); break; case EFFECT_ITEM_INCREASE_STAT: case EFFECT_ITEM_INCREASE_ALL_STATS: if (!gDisableStructs[battler].isFirstTurn || AI_OpponentCanFaintAiWithMod(battler, 0)) break; shouldUse = TRUE; break; case EFFECT_ITEM_SET_FOCUS_ENERGY: if (!gDisableStructs[battler].isFirstTurn || gBattleMons[battler].volatiles.dragonCheer || gBattleMons[battler].volatiles.focusEnergy || AI_OpponentCanFaintAiWithMod(battler, 0)) break; shouldUse = TRUE; break; case EFFECT_ITEM_SET_MIST: battlerSide = GetBattlerSide(battler); if (gDisableStructs[battler].isFirstTurn && !(gSideStatuses[battlerSide] & SIDE_STATUS_MIST)) shouldUse = TRUE; break; case EFFECT_ITEM_REVIVE: gBattleStruct->itemPartyIndex[battler] = GetFirstFaintedPartyIndex(battler); if (gBattleStruct->itemPartyIndex[battler] != PARTY_SIZE) // Revive if possible. shouldUse = TRUE; break; case EFFECT_ITEM_USE_POKE_FLUTE: if (gBattleMons[battler].status1 & STATUS1_SLEEP) shouldUse = TRUE; break; default: return FALSE; } if (shouldUse) { // Set selected party ID to current battler if none chosen. if (gBattleStruct->itemPartyIndex[battler] == PARTY_SIZE) gBattleStruct->itemPartyIndex[battler] = gBattlerPartyIndexes[battler]; BtlController_EmitTwoReturnValues(battler, B_COMM_TO_ENGINE, B_ACTION_USE_ITEM, 0); gBattleStruct->chosenItem[battler] = item; gBattleHistory->trainerItems[i] = 0; return shouldUse; } } return FALSE; } static bool32 AI_ShouldHeal(u32 battler, u32 healAmount) { bool32 shouldHeal = FALSE; u8 opponent; u32 maxDamage = 0; u32 dmg = 0; if (gBattleMons[battler].hp < gBattleMons[battler].maxHP / 4 || gBattleMons[battler].hp == 0 || (healAmount != 0 && gBattleMons[battler].maxHP - gBattleMons[battler].hp > healAmount)) { // We have low enough HP to consider healing shouldHeal = !AI_OpponentCanFaintAiWithMod(battler, healAmount); // if target can kill us even after we heal, why bother } //calculate max expected damage from the opponent for (opponent = 0; opponent < gBattlersCount; opponent++) { if (IsOnPlayerSide(opponent)) { dmg = GetBestDmgFromBattler(opponent, battler, AI_DEFENDING); if (dmg > maxDamage) maxDamage = dmg; } } // also heal if a 2HKO is outhealed if (AI_OpponentCanFaintAiWithMod(battler, 0) && !AI_OpponentCanFaintAiWithMod(battler, healAmount) && healAmount > 2*maxDamage) return TRUE; // also heal, if the expected damage is outhealed and it's the last remaining mon if (AI_OpponentCanFaintAiWithMod(battler, 0) && !AI_OpponentCanFaintAiWithMod(battler, healAmount) && CountUsablePartyMons(battler) == 0) return TRUE; return shouldHeal; } static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount) { u32 i; // Check special cases to NOT heal for (i = 0; i < gBattlersCount; i++) { if (IsOnPlayerSide(i) && CanTargetFaintAiWithMod(i, battler, healAmount, 0)) { // Target is expected to faint us return TRUE; } } return FALSE; } static u32 GetHPHealAmount(u8 itemEffectParam, struct Pokemon *mon) { switch (itemEffectParam) { case ITEM6_HEAL_HP_FULL: itemEffectParam = GetMonData(mon, MON_DATA_MAX_HP, NULL) - GetMonData(mon, MON_DATA_HP, NULL); break; case ITEM6_HEAL_HP_HALF: itemEffectParam = GetMonData(mon, MON_DATA_MAX_HP, NULL) / 2; if (itemEffectParam == 0) itemEffectParam = 1; break; case ITEM6_HEAL_HP_LVL_UP: itemEffectParam = gBattleScripting.levelUpHP; break; case ITEM6_HEAL_HP_QUARTER: itemEffectParam = GetMonData(mon, MON_DATA_MAX_HP, NULL) / 4; if (itemEffectParam == 0) itemEffectParam = 1; break; } return itemEffectParam; }