Smarter SwitchAI Mon Choices | HasBadOdds Switch Check (#3253)

* SwitchAI makes much smarter mon choices

* Add HasHadOdds check to ShouldSwitch decision

* Remove early return

* Rework Baton Pass check as per discussion with Alex

* Forgot to adjust a comment

* Don't program before breakfast lol (if / else if fix)

* Switch AI_CalcDamage for AI_DATA->simulatedDmg in HasBadOdds

Thanks Alex! :D

* Typo in a hitToKO comparison

* Remove and replace AI_CalcPartyMonBestMoveDamage and IsAiPartyMonOHKOBy from https://github.com/rh-hideout/pokeemerald-expansion/pull/3146

See https://discord.com/channels/419213663107416084/1144447521960251472 for details

* Major refactor, new struct, switchin considers damage / healing from hazards / status / held item / weather

* Forgot Snow exists and heals Ice Body, haven't played Switch games lol

* (766a1a27a7) Compatibility, use new struct field instead of function call

* Fixing oversight from previous upstream merge

* Improve TSpikes handling to make GetSwitchinHazardDamage more applicable

Small fixes:
- EFFECT_EXPLOSION typo (!= to ==)
- Order of if statements near bestResistEffective
- Spacing of terms in big HasBadOdds if statements

* Forgot to uncomment blocks disabled for debugging what turned out to be vanilla behaviour lol

* Remove another holdover from debugging, sorry :/

* Lastly, undoing my debug trainer

* Type matchup based on species type rather than current type

Suggested by BLourenco on Discord, the idea is that a mon that's had its type affected by a move like Soak will still have moves as though it was its regular typing, and so prioritizing the temporary typing wouldn't be ideal.
https://discord.com/channels/419213663107416084/1144447521960251472/1146644578141736970

* gActiveBattler upcoming merge fixes

* Egg changes part 1

* Egg changes part 2, just need to address EWRAM still

* Move SwitchinCandidate struct to AiLogicData

* Consider Steel type when checking TSpikes

* Comment about CanBePoisoned compatibility

* Changes for Egg's 2nd review

* Put period back in comment, whoops lol

* Latest upcoming merge fixes

* Missed a few u32 updates

* Combine GetBestMonIntegrate functions / flags, some modularization

* Fix merge error

* Make modern fixes

* Two tests done, two to go

* Accidentally pushed reference test, removing it

* Type matchup switching tests

* Tests for defensive vs offense switches

---------

Co-authored-by: DizzyEggg <jajkodizzy@wp.pl>
This commit is contained in:
Pawkkie 2023-11-11 08:37:35 -05:00 committed by GitHub
parent 3fc47d137a
commit b93dfb9d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1033 additions and 136 deletions

View file

@ -290,6 +290,12 @@ struct AIPartyData // Opposing battlers - party mons.
u8 count[NUM_BATTLE_SIDES];
};
struct SwitchinCandidate
{
struct BattlePokemon battleMon;
bool8 hypotheticalStatus;
};
// Ai Data used when deciding which move to use, computed only once before each turn's start.
struct AiLogicData
{
@ -308,6 +314,8 @@ struct AiLogicData
bool8 shouldSwitchMon; // Because all available moves have no/little effect. Each bit per battler.
u8 monToSwitchId[MAX_BATTLERS_COUNT]; // ID of the mon to switch.
bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once.
u8 mostSuitableMonId; // 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
};
struct AI_ThinkingStruct

View file

@ -3,7 +3,7 @@
void GetAIPartyIndexes(u32 battlerId, s32 *firstId, s32 *lastId);
void AI_TrySwitchOrUseItem(u32 battler);
u8 GetMostSuitableMonToSwitchInto(u32 battler);
u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd);
bool32 ShouldSwitch(u32 battler);
#endif // GUARD_BATTLE_AI_SWITCH_ITEMS_H

View file

@ -174,7 +174,6 @@ bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move);
// party logic
struct BattlePokemon *AllocSaveBattleMons(void);
void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons);
s32 AI_CalcPartyMonBestMoveDamage(u32 battlerAtk, u32 battlerDef, struct Pokemon *attackerMon, struct Pokemon *targetMon);
s32 CountUsablePartyMons(u32 battlerId);
bool32 IsPartyFullyHealedExceptBattler(u32 battler);
bool32 PartyHasMoveSplit(u32 battlerId, u32 split);
@ -189,4 +188,6 @@ void IncreaseSleepScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score);
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, bool8 isPartyMonAttacker);
#endif //GUARD_BATTLE_AI_UTIL_H

View file

@ -45,6 +45,7 @@
#define AI_FLAG_SMART_SWITCHING (1 << 15) // AI includes a lot more switching checks
#define AI_FLAG_ACE_POKEMON (1 << 16) // AI has an Ace Pokemon. The last Pokemon in the party will not be used until it's the last one remaining.
#define AI_FLAG_OMNISCIENT (1 << 17) // AI has full knowledge of player moves, abilities, hold items
#define AI_FLAG_SMART_MON_CHOICES (1 << 18) // AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are separate decisions. Pairs very well with AI_FLAG_SMART_SWITCHING.
#define AI_FLAG_COUNT 18

View file

@ -607,6 +607,20 @@
#define ITEM_UTILITY_UMBRELLA 513
// Berries
#if B_CONFUSE_BERRIES_HEAL >= GEN_8
#define CONFUSE_BERRY_HEAL_FRACTION 3
#elif B_CONFUSE_BERRIES_HEAL == GEN_7
#define CONFUSE_BERRY_HEAL_FRACTION 2
#else
#define CONFUSE_BERRY_HEAL_FRACTION 8
#endif
#if B_CONFUSE_BERRIES_HEAL >= GEN_7
#define CONFUSE_BERRY_HP_FRACTION 4
#else
#define CONFUSE_BERRY_HP_FRACTION 2
#endif
#define ITEM_CHERI_BERRY 514
#define ITEM_CHESTO_BERRY 515
#define ITEM_PECHA_BERRY 516

View file

@ -416,7 +416,7 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData)
static bool32 AI_SwitchMonIfSuitable(u32 battler)
{
u32 monToSwitchId = GetMostSuitableMonToSwitchInto(battler);
u32 monToSwitchId = AI_DATA->mostSuitableMonId;
if (monToSwitchId != PARTY_SIZE)
{
AI_DATA->shouldSwitchMon |= gBitTable[battler];

View file

@ -28,7 +28,12 @@ static bool8 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 bool32 IsAiPartyMonOHKOBy(u32 battlerAi, u32 battlerAtk, struct Pokemon *aiMon);
static void InitializeSwitchinCandidate(struct Pokemon *mon)
{
PokemonToBattleMon(mon, &AI_DATA->switchinCandidate.battleMon);
AI_DATA->switchinCandidate.hypotheticalStatus = FALSE;
}
static bool32 IsAceMon(u32 battler, u32 monPartyId)
{
@ -58,6 +63,156 @@ void GetAIPartyIndexes(u32 battler, s32 *firstId, s32 *lastId)
}
}
// 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 bool8 HasBadOdds(u32 battler)
{
//Variable initialization
u8 opposingPosition, atkType1, atkType2, defType1, defType2, effectiveness;
s32 i, damageDealt = 0, maxDamageDealt = 0, damageTaken = 0, maxDamageTaken = 0;
u32 aiMove, playerMove, aiBestMove = MOVE_NONE, aiAbility = GetBattlerAbility(battler), opposingBattler, weather = AI_GetWeather(AI_DATA);
bool8 getsOneShot = FALSE, hasStatusMove = FALSE, hasSuperEffectiveMove = FALSE;
u16 typeEffectiveness = UQ_4_12(1.0), aiMoveEffect; //baseline typing damage
// Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer
if (!(AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_SWITCHING))
return FALSE;
// Won't bother configuring this for double battles
if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE)
return FALSE;
opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler));
opposingBattler = GetBattlerAtPosition(opposingPosition);
// Gets types of player (opposingBattler) and computer (battler)
atkType1 = gBattleMons[opposingBattler].type1;
atkType2 = gBattleMons[opposingBattler].type2;
defType1 = gBattleMons[battler].type1;
defType2 = gBattleMons[battler].type2;
// Check AI moves for damage dealt
for (i = 0; i < MAX_MON_MOVES; i++)
{
aiMove = gBattleMons[battler].moves[i];
aiMoveEffect = gBattleMoves[aiMove].effect;
if (aiMove != MOVE_NONE)
{
// 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_SLEEP || aiMoveEffect == EFFECT_YAWN || aiMoveEffect == EFFECT_TOXIC || aiMoveEffect == EFFECT_WILL_O_WISP || aiMoveEffect == EFFECT_PARALYZE
|| aiMoveEffect == EFFECT_TRICK || aiMoveEffect == EFFECT_TRICK_ROOM || aiMoveEffect== EFFECT_WONDER_ROOM || aiMoveEffect == EFFECT_PSYCHO_SHIFT || aiMoveEffect == EFFECT_FAKE_OUT
)
{
hasStatusMove = TRUE;
}
// Only check damage if move has power
if (gBattleMoves[aiMove].power != 0)
{
// Check if mon has a super effective move
if (AI_GetTypeEffectiveness(aiMove, battler, opposingBattler) >= UQ_4_12(2.0))
hasSuperEffectiveMove = TRUE;
// Get maximum damage mon can deal
damageDealt = AI_DATA->simulatedDmg[battler][opposingBattler][i];
if(damageDealt > maxDamageDealt)
{
maxDamageDealt = damageDealt;
aiBestMove = aiMove;
}
}
}
}
// Calculate type advantage
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1)));
if (atkType2 != atkType1)
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1)));
if (defType2 != defType1)
{
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType2)));
if (atkType2 != atkType1)
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType2)));
}
// Get max damage mon could take
for (i = 0; i < MAX_MON_MOVES; i++)
{
playerMove = gBattleMons[opposingBattler].moves[i];
if (playerMove != MOVE_NONE && gBattleMoves[playerMove].power != 0)
{
damageTaken = AI_CalcDamage(playerMove, opposingBattler, battler, &effectiveness, FALSE, weather);
if (damageTaken > maxDamageTaken)
maxDamageTaken = damageTaken;
}
}
// Check if mon gets one shot
if(maxDamageTaken > gBattleMons[battler].hp)
{
getsOneShot = TRUE;
}
// Check if current mon can outspeed and KO in spite of bad matchup, and don't switch out if it can
if(damageDealt > gBattleMons[opposingBattler].hp)
{
if (AI_WhoStrikesFirst(battler, opposingBattler, aiBestMove) == AI_IS_FASTER)
return FALSE;
}
// If we don't have any other viable options, don't switch out
if (AI_DATA->mostSuitableMonId == PARTY_SIZE)
return FALSE;
// Start assessing whether or not mon has bad odds
// Jump straight to swtiching out in cases where mon gets OHKO'd
if (((getsOneShot && gBattleMons[opposingBattler].speed > gBattleMons[battler].speed) // If the player OHKOs and outspeeds OR OHKOs, doesn't outspeed but isn't 2HKO'd
|| (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2))
&& (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 (Random() % 2 == 0)
return FALSE;
// Switch mon out
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
return TRUE;
}
// General bad type matchups have more wiggle room
if (typeEffectiveness >= UQ_4_12(2.0)) // If the player has at least a 2x type advantage
{
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 (Random() % 2 == 0)
return FALSE;
// Switch mon out
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
return TRUE;
}
}
return FALSE;
}
static bool8 ShouldSwitchIfAllBadMoves(u32 battler)
{
if (AI_DATA->shouldSwitchMon & gBitTable[battler])
@ -437,12 +592,12 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler)
moduloChance = 4; //25%
//Attempt to cure bad ailment
if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON)
&& GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE)
&& AI_DATA->mostSuitableMonId != PARTY_SIZE)
break;
//Attempt to cure lesser ailment
if ((gBattleMons[battler].status1 & STATUS1_ANY)
&& (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2)
&& GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;
@ -454,7 +609,7 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler)
if (gBattleMons[battler].status1 & STATUS1_ANY)
return FALSE;
if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3))
&& GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;
@ -706,6 +861,8 @@ bool32 ShouldSwitch(u32 battler)
return TRUE;
if (ShouldSwitchIfAbilityBenefit(battler))
return TRUE;
if (HasBadOdds(battler))
return TRUE;
//Removing switch capabilites under specific conditions
//These Functions prevent the "FindMonWithFlagsAndSuperEffective" from getting out of hand.
@ -742,7 +899,7 @@ void AI_TrySwitchOrUseItem(u32 battler)
{
if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE)
{
s32 monToSwitchId = GetMostSuitableMonToSwitchInto(battler);
s32 monToSwitchId = AI_DATA->mostSuitableMonId;
if (monToSwitchId == PARTY_SIZE)
{
if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE))
@ -802,8 +959,6 @@ static u32 GetBestMonBatonPass(struct Pokemon *party, int firstId, int lastId, u
{
if (invalidMons & gBitTable[i])
continue;
if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i]))
continue;
for (j = 0; j < MAX_MON_MOVES; j++)
{
@ -848,9 +1003,6 @@ static u32 GetBestMonTypeMatchup(struct Pokemon *party, int firstId, int lastId,
u8 defType1 = gSpeciesInfo[species].types[0];
u8 defType2 = gSpeciesInfo[species].types[1];
if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i]))
continue;
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1)));
if (atkType2 != atkType1)
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1)));
@ -894,9 +1046,10 @@ static u32 GetBestMonTypeMatchup(struct Pokemon *party, int firstId, int lastId,
static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 invalidMons, u32 battler, u32 opposingBattler)
{
int i;
int i, j;
int dmg, bestDmg = 0;
int bestMonId = PARTY_SIZE;
u32 aiMove;
gMoveResultFlags = 0;
// If we couldn't find the best mon in terms of typing, find the one that deals most damage.
@ -904,21 +1057,683 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva
{
if (gBitTable[i] & invalidMons)
continue;
if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i]))
continue;
dmg = AI_CalcPartyMonBestMoveDamage(battler, opposingBattler, &party[i], NULL);
InitializeSwitchinCandidate(&party[i]);
for (j = 0; j < MAX_MON_MOVES; j++)
{
aiMove = AI_DATA->switchinCandidate.battleMon.moves[j];
if (aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0)
{
aiMove = GetMonData(&party[i], MON_DATA_MOVE1 + j);
dmg = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE);
if (bestDmg < dmg)
{
bestDmg = dmg;
bestMonId = i;
}
}
}
}
return bestMonId;
}
u8 GetMostSuitableMonToSwitchInto(u32 battler)
static bool32 IsMonGrounded(u16 heldItemEffect, u32 ability, u8 type1, u8 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))
{
// List that overrides being off the ground
if ((heldItemEffect == HOLD_EFFECT_IRON_BALL && ability != ABILITY_KLUTZ) || (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)
{
u8 defType1 = battleMon->type1, defType2 = battleMon->type2, tSpikesLayers;
u16 heldItemEffect = gItems[battleMon->item].holdEffect;
u32 maxHP = battleMon->maxHP, ability = battleMon->ability, status = battleMon->status1;
u32 spikesDamage = 0, tSpikesDamage = 0, hazardDamage = 0;
u32 hazardFlags = gSideStatuses[GetBattlerSide(battler)] & (SIDE_STATUS_SPIKES | SIDE_STATUS_STEALTH_ROCK | SIDE_STATUS_STICKY_WEB | SIDE_STATUS_TOXIC_SPIKES | SIDE_STATUS_SAFEGUARD);
// 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 ((hazardFlags & SIDE_STATUS_STEALTH_ROCK) && heldItemEffect != HOLD_EFFECT_HEAVY_DUTY_BOOTS)
hazardDamage += GetStealthHazardDamageByTypesAndHP(gBattleMoves[MOVE_STEALTH_ROCK].type, defType1, defType2, battleMon->hp);
// Spikes
if ((hazardFlags & SIDE_STATUS_SPIKES) && IsMonGrounded(heldItemEffect, ability, defType1, defType2))
{
spikesDamage = maxHP / ((5 - gSideTimers[GetBattlerSide(battler)].spikesAmount) * 2);
if (spikesDamage == 0)
spikesDamage = 1;
hazardDamage += spikesDamage;
}
// Toxic Spikes
// TODO: CanBePoisoned compatibility to avoid duplicate code
if ((hazardFlags & SIDE_STATUS_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
&& !(hazardFlags & SIDE_STATUS_SAFEGUARD)
&& !(IsAbilityOnSide(battler, ABILITY_PASTEL_VEIL))
&& !(IsBattlerTerrainAffected(battler, STATUS_FIELD_MISTY_TERRAIN))
&& !(IsAbilityStatusProtected(battler))
&& 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 = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability;
u16 item = AI_DATA->switchinCandidate.battleMon.item;
if (WEATHER_HAS_EFFECT)
{
// Damage
if (item != ITEM_SAFETY_GOGGLES)
{
if ((gBattleWeather & B_WEATHER_HAIL) && (AI_DATA->switchinCandidate.battleMon.type1 != TYPE_ICE || AI_DATA->switchinCandidate.battleMon.type2 != TYPE_ICE)
&& ability != ABILITY_OVERCOAT && ability != ABILITY_SNOW_CLOAK && ability != ABILITY_ICE_BODY)
{
weatherImpact = maxHP / 16;
if (weatherImpact == 0)
weatherImpact = 1;
}
else if ((gBattleWeather & B_WEATHER_SANDSTORM) && (AI_DATA->switchinCandidate.battleMon.type1 != TYPE_GROUND && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_GROUND
&& AI_DATA->switchinCandidate.battleMon.type1 != TYPE_ROCK && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_ROCK
&& AI_DATA->switchinCandidate.battleMon.type1 != TYPE_STEEL && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_STEEL
&& ability != ABILITY_OVERCOAT && 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) && (ability == ABILITY_SOLAR_POWER || ability == ABILITY_DRY_SKIN))
{
weatherImpact = maxHP / 8;
if (weatherImpact == 0)
weatherImpact = 1;
}
// Healing
if (gBattleWeather & B_WEATHER_RAIN)
{
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 = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability;
u16 item = AI_DATA->switchinCandidate.battleMon.item;
// Items
if (ability != ABILITY_KLUTZ)
{
if (item == ITEM_BLACK_SLUDGE && (AI_DATA->switchinCandidate.battleMon.type1 == TYPE_POISON || AI_DATA->switchinCandidate.battleMon.type2 == TYPE_POISON))
{
recurringHealing = maxHP / 16;
if (recurringHealing == 0)
recurringHealing = 1;
}
else if (item == ITEM_LEFTOVERS)
{
recurringHealing = maxHP / 16;
if (recurringHealing == 0)
recurringHealing = 1;
}
} // Intentionally omitting Shell Bell for its inconsistency
// Abilities
if (ability == ABILITY_POISON_HEAL && (AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_POISON))
{
recurringHealing = maxHP / 8;
if (recurringHealing == 0)
recurringHealing = 1;
}
return recurringHealing;
}
// Gets one turn of recurring damage
static u32 GetSwitchinRecurringDamage(void)
{
u32 passiveDamage = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability;
u16 item = AI_DATA->switchinCandidate.battleMon.item;
// Items
if (ability != ABILITY_MAGIC_GUARD && ability != ABILITY_KLUTZ)
{
if (item == ITEM_BLACK_SLUDGE && AI_DATA->switchinCandidate.battleMon.type1 != TYPE_POISON && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_POISON)
{
passiveDamage = maxHP / 8;
if (passiveDamage == 0)
passiveDamage = 1;
}
else if (item == ITEM_LIFE_ORB && ability != ABILITY_SHEER_FORCE)
{
passiveDamage = maxHP / 10;
if (passiveDamage == 0)
passiveDamage = 1;
}
else if (item == ITEM_STICKY_BARB)
{
passiveDamage = maxHP / 8;
if(passiveDamage == 0)
passiveDamage = 1;
}
}
return passiveDamage;
}
// Gets one turn of status damage
static u32 GetSwitchinStatusDamage(u32 battler)
{
u8 defType1 = AI_DATA->switchinCandidate.battleMon.type1, defType2 = AI_DATA->switchinCandidate.battleMon.type2;
u8 tSpikesLayers = gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount;
u16 heldItemEffect = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffect;
u32 status = AI_DATA->switchinCandidate.battleMon.status1, ability = AI_DATA->switchinCandidate.battleMon.ability, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP;
u32 statusDamage = 0;
// Status condition damage
if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD)
{
if (status & STATUS1_BURN)
{
#if B_BURN_DAMAGE >= GEN_7
statusDamage = maxHP / 16;
#else
statusDamage = maxHP / 8;
#endif
if(ability == ABILITY_HEATPROOF)
statusDamage = statusDamage / 2;
if (statusDamage == 0)
statusDamage = 1;
}
else if (status & STATUS1_FROSTBITE)
{
#if B_BURN_DAMAGE >= GEN_7
statusDamage = maxHP / 16;
#else
statusDamage = maxHP / 8;
#endif
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
AI_DATA->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1);
statusDamage *= AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_COUNTER >> 8;
if (statusDamage == 0)
statusDamage = 1;
}
}
// 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)
{
AI_DATA->switchinCandidate.battleMon.status1 = STATUS1_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes
AI_DATA->switchinCandidate.hypotheticalStatus = TRUE;
}
if (tSpikesLayers == 2)
{
AI_DATA->switchinCandidate.battleMon.status1 = STATUS1_TOXIC_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes
AI_DATA->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1);
AI_DATA->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 = AI_DATA->switchinCandidate.battleMon.hp - GetSwitchinHazardsDamage(battler, &AI_DATA->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, singleUseItemHeal = 0;
u16 maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, item = AI_DATA->switchinCandidate.battleMon.item, heldItemEffect = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffect;
u8 weatherDuration = gWishFutureKnock.weatherDuration, holdEffectParam = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffectParam;
u32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler)));
u32 opposingAbility = gBattleMons[opposingBattler].ability;
bool8 usedSingleUseHealingItem = FALSE;
s32 currentHP = startingHP;
// No damage being dealt
if (damageTaken + statusDamage + recurringDamage == 0)
return startingHP;
// 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;
// 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 (AI_DATA->switchinCandidate.battleMon.ability != ABILITY_KLUTZ && usedSingleUseHealingItem == FALSE)
{
if (currentHP < maxHP / 2)
{
if (item == ITEM_BERRY_JUICE)
{
singleUseItemHeal = holdEffectParam;
}
else if (opposingAbility != ABILITY_UNNERVE && heldItemEffect == HOLD_EFFECT_RESTORE_HP)
{
// By default, this should only encompass Oran Berry and Sitrus Berry.
singleUseItemHeal = holdEffectParam;
if (singleUseItemHeal == 0)
singleUseItemHeal = 1;
}
}
else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION
&& opposingAbility != ABILITY_UNNERVE
&& (item == ITEM_AGUAV_BERRY || item == ITEM_FIGY_BERRY || item == ITEM_IAPAPA_BERRY || item == ITEM_MAGO_BERRY || item == ITEM_WIKI_BERRY))
{
singleUseItemHeal = maxHP / CONFUSE_BERRY_HEAL_FRACTION;
if (singleUseItemHeal == 0)
singleUseItemHeal = 1;
}
// 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 (AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_POISON)
statusDamage = GetSwitchinStatusDamage(battler);
// Reduce weather duration
if (weatherDuration != 0)
weatherDuration--;
hitsToKO++;
}
// If mon had a hypothetical status from TSpikes, clear it
if (AI_DATA->switchinCandidate.hypotheticalStatus == TRUE)
{
AI_DATA->switchinCandidate.battleMon.status1 = 0;
AI_DATA->switchinCandidate.hypotheticalStatus = FALSE;
}
return hitsToKO;
}
static u16 GetSwitchinTypeMatchup(u32 opposingBattler, struct BattlePokemon battleMon)
{
// Check type matchup
u16 typeEffectiveness = UQ_4_12(1.0);
u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1],
defType1 = battleMon.type1, defType2 = battleMon.type2;
// Multiply type effectiveness by a factor depending on type matchup
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1)));
if (atkType2 != atkType1)
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1)));
if (defType2 != defType1)
{
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType2)));
if (atkType2 != atkType1)
typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType2)));
}
return typeEffectiveness;
}
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 & gBitTable[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)
{
int i = 0;
u32 playerMove;
s32 damageTaken = 0, maxDamageTaken = 0;
for (i = 0; i < MAX_MON_MOVES; i++)
{
playerMove = gBattleMons[opposingBattler].moves[i];
if (playerMove != MOVE_NONE && gBattleMoves[playerMove].power != 0)
{
damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, FALSE);
if (damageTaken > maxDamageTaken)
maxDamageTaken = damageTaken;
}
}
return maxDamageTaken;
}
// 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 u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u8 battlerIn1, u8 battlerIn2, bool8 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;
int i, j, aliveCount = 0, bits = 0;
s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed
u32 aiMove, hitsToKO, hitsToKOThreshold, maxHitsToKO = 0;
s32 playerMonSpeed = gBattleMons[opposingBattler].speed, playerMonHP = gBattleMons[opposingBattler].hp, aiMonSpeed, maxDamageDealt = 0, damageDealt = 0;
u16 bestResist = UQ_4_12(1.0), bestResistEffective = UQ_4_12(1.0), typeMatchup;
if (isSwitchAfterKO)
hitsToKOThreshold = 1; // After a KO, mons at minimum need to not be 1-shot, as they switch in for free
else
hitsToKOThreshold = 2; // When switching in otherwise need to not be 2-shot, as they do not switch in for free
// 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;
}
// Save Ace Pokemon for last
else if (IsAceMon(battler, i))
{
aceMonId = i;
continue;
}
else
aliveCount++;
InitializeSwitchinCandidate(&party[i]);
// While not really invalid per say, not really wise to switch into this mon
if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))
continue;
// Get max number of hits for player to KO AI mon
hitsToKO = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon), battler);
// Track max hits to KO and set GetBestMonDefensive if applicable
if(hitsToKO > maxHitsToKO)
{
maxHitsToKO = hitsToKO;
if(maxHitsToKO > defensiveMonHitKOThreshold)
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 ((hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed) || hitsToKO > hitsToKOThreshold + 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];
// Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn
if (isSwitchAfterKO && aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0)
damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE);
// 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 && ((hitsToKO > hitsToKOThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed)))
bits |= gBitTable[i];
// Check for mon with resistance and super effective move for GetBestMonTypeMatchup
if (aiMove != MOVE_NONE && gBattleMoves[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 (hitsToKO > hitsToKOThreshold)
{
bestResistEffective = typeMatchup;
typeMatchupEffectiveId = i;
}
}
}
// If a self destruction move doesn't OHKO, don't factor it into revenge killing
if (gBattleMoves[aiMove].effect == EFFECT_EXPLOSION && damageDealt < playerMonHP)
continue;
// Check that mon isn't one shot and set GetBestMonDmg if applicable
if (damageDealt > maxDamageDealt)
{
if(hitsToKO > hitsToKOThreshold)
{
maxDamageDealt = damageDealt;
damageMonId = i;
}
}
// Check if current mon can revenge kill in some capacity
// If AI mon can one shot
if (damageDealt > playerMonHP)
{
// If AI mon is faster and doesn't die to hazards
if ((aiMonSpeed > playerMonSpeed || gBattleMoves[aiMove].priority > 0) && AI_DATA->switchinCandidate.battleMon.hp > GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon))
{
// We have a revenge killer
revengeKillerId = i;
}
// If AI mon is slower
else
{
// If AI mon can't be OHKO'd
if (hitsToKO > hitsToKOThreshold)
{
// We have a slow revenge killer
slowRevengeKillerId = i;
}
}
}
// If AI mon can two shot
if (damageDealt > playerMonHP / 2)
{
// If AI mon is faster
if (aiMonSpeed > playerMonSpeed || gBattleMoves[aiMove].priority > 0)
{
// If AI mon can't be OHKO'd
if (hitsToKO > hitsToKOThreshold)
{
// We have a fast threaten
fastThreatenId = i;
}
}
// If AI mon is slower
else
{
// If AI mon can't be 2HKO'd
if (hitsToKO > hitsToKOThreshold + 1)
{
// We have a slow threaten
slowThreatenId = i;
}
}
}
}
}
}
batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i);
// Different switching priorities depending on switching mid battle vs switching after a KO
if (isSwitchAfterKO)
{
// Return GetBestMonRevengeKiller > GetBestMonTypeMatchup > GetBestMonBatonPass > GetBestMonDmg
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 (damageMonId != PARTY_SIZE)
return damageMonId;
}
else
{
// Return GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass
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;
// If ace mon is the last available Pokemon and U-Turn/Volt Switch was used - switch to the mon.
else if (aceMonId != PARTY_SIZE
&& (gBattleMoves[gLastUsedMove].effect == EFFECT_HIT_ESCAPE || gBattleMoves[gLastUsedMove].effect == EFFECT_PARTING_SHOT))
return aceMonId;
}
return PARTY_SIZE;
}
u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd)
{
u32 opposingBattler = 0;
u32 bestMonId = PARTY_SIZE;
@ -926,8 +1741,6 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler)
s32 firstId = 0;
s32 lastId = 0; // + 1
struct Pokemon *party;
s32 i, aliveCount = 0;
u32 invalidMons = 0, aceMonId = PARTY_SIZE;
if (*(gBattleStruct->monToSwitchIntoId + battler) != PARTY_SIZE)
return *(gBattleStruct->monToSwitchIntoId + battler);
@ -960,6 +1773,19 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler)
else
party = gEnemyParty;
// 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 & AI_FLAG_SMART_MON_CHOICES)
{
bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchAfterMonKOd);
return bestMonId;
}
// This all handled by the GetBestMonIntegrated function if the AI_FLAG_SMART_MON_CHOICES flag is set
else
{
s32 i, aliveCount = 0;
u32 invalidMons = 0, aceMonId = PARTY_SIZE;
// Get invalid slots ids.
for (i = firstId; i < lastId; i++)
{
@ -982,7 +1808,6 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler)
aliveCount++;
}
}
bestMonId = GetBestMonBatonPass(party, firstId, lastId, invalidMons, aliveCount, battler, opposingBattler);
if (bestMonId != PARTY_SIZE)
return bestMonId;
@ -1001,6 +1826,7 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler)
return PARTY_SIZE;
}
}
static bool32 AiExpectsToFaintPlayer(u32 battler)
{
@ -1159,29 +1985,3 @@ static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount)
}
return FALSE;
}
static bool32 IsAiPartyMonOHKOBy(u32 battlerAi, u32 battlerAtk, struct Pokemon *aiMon)
{
bool32 ret = FALSE;
struct BattlePokemon *savedBattleMons;
s32 hp = GetMonData(aiMon, MON_DATA_HP);
s32 bestDmg = AI_CalcPartyMonBestMoveDamage(battlerAtk, battlerAi, NULL, aiMon);
switch (GetNoOfHitsToKO(bestDmg, hp))
{
case 1:
ret = TRUE;
break;
case 2: // if AI mon is faster allow 2 turns
savedBattleMons = AllocSaveBattleMons();
PokemonToBattleMon(aiMon, &gBattleMons[battlerAi]);
if (AI_WhoStrikesFirst(battlerAi, battlerAtk, 0) == AI_IS_SLOWER)
ret = TRUE;
else
ret = FALSE;
FreeRestoreBattleMons(savedBattleMons);
break;
}
return ret;
}

View file

@ -3366,32 +3366,16 @@ void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons)
}
// party logic
s32 AI_CalcPartyMonBestMoveDamage(u32 battlerAtk, u32 battlerDef, struct Pokemon *attackerMon, struct Pokemon *targetMon)
s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool8 isPartyMonAttacker)
{
s32 i, move, bestDmg, dmg = 0;
s32 dmg;
u8 effectiveness;
struct BattlePokemon *savedBattleMons = AllocSaveBattleMons();
if (attackerMon != NULL)
PokemonToBattleMon(attackerMon, &gBattleMons[battlerAtk]);
if (targetMon != NULL)
PokemonToBattleMon(targetMon, &gBattleMons[battlerDef]);
for (bestDmg = 0, i = 0; i < MAX_MON_MOVES; i++)
{
if (BattlerHasAi(battlerAtk))
move = GetMonData(attackerMon, MON_DATA_MOVE1 + i);
if(isPartyMonAttacker)
gBattleMons[battlerAtk] = switchinCandidate;
else
move = AI_PARTY->mons[GetBattlerSide(battlerAtk)][gBattlerPartyIndexes[battlerAtk]].moves[i];
if (move != MOVE_NONE && gBattleMoves[move].power != 0)
{
dmg = AI_CalcDamageSaveBattlers(move, battlerAtk, battlerDef, &effectiveness, FALSE);
if (dmg > bestDmg)
bestDmg = dmg;
}
}
gBattleMons[battlerDef] = switchinCandidate;
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, FALSE, AI_GetWeather(AI_DATA));
FreeRestoreBattleMons(savedBattleMons);
return dmg;
}

View file

@ -656,7 +656,7 @@ static void OpponentHandleChoosePokemon(u32 battler)
// Switching out
else if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE)
{
chosenMonId = GetMostSuitableMonToSwitchInto(battler);
chosenMonId = GetMostSuitableMonToSwitchInto(battler, TRUE);
if (chosenMonId == PARTY_SIZE)
{
s32 battler1, battler2, firstId, lastId;

View file

@ -399,7 +399,7 @@ static void PlayerPartnerHandleChoosePokemon(u32 battler)
// Switching out
else if (gBattleStruct->monToSwitchIntoId[battler] >= PARTY_SIZE || !IsValidForBattle(&gPlayerParty[gBattleStruct->monToSwitchIntoId[battler]]))
{
chosenMonId = GetMostSuitableMonToSwitchInto(battler);
chosenMonId = GetMostSuitableMonToSwitchInto(battler, TRUE);
if (chosenMonId == PARTY_SIZE || !IsValidForBattle(&gPlayerParty[chosenMonId])) // just switch to the next mon
{

View file

@ -4052,6 +4052,7 @@ static void HandleTurnActionSelectionState(void)
if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart())
&& (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE)))
{
AI_DATA->mostSuitableMonId = GetMostSuitableMonToSwitchInto(battler, FALSE);
gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler);
}
// fallthrough

View file

@ -6434,14 +6434,6 @@ const struct Item gItems[] =
.flingPower = 10,
},
#if B_CONFUSE_BERRIES_HEAL >= GEN_8
#define CONFUSE_BERRY_HEAL_FRACTION 3
#elif B_CONFUSE_BERRIES_HEAL == GEN_7
#define CONFUSE_BERRY_HEAL_FRACTION 2
#else
#define CONFUSE_BERRY_HEAL_FRACTION 8
#endif
[ITEM_FIGY_BERRY] =
{
.name = _("Figy Berry"),
@ -6507,8 +6499,6 @@ const struct Item gItems[] =
.flingPower = 10,
},
#undef CONFUSE_BERRY_HEAL_FRACTION
[ITEM_RAZZ_BERRY] =
{
.name = _("Razz Berry"),

View file

@ -373,23 +373,6 @@ AI_SINGLE_BATTLE_TEST("AI will not switch in a Pokemon which is slower and gets
}
}
AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(player, MOVE_PERISH_SONG); }
TURN { ; }
TURN { ; }
TURN { EXPECT_SWITCH(opponent, 1); }
} SCENE {
MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!");
}
}
AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already chose such move")
{
u32 j, k;
@ -511,3 +494,118 @@ AI_DOUBLE_BATTLE_TEST("AI without any flags chooses moves at random - doubles")
}
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting")
{
bool32 alakazamFirst;
u32 speedAlakazm;
u32 aiSmartSwitchFlags = 0;
PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = TRUE; } // AI will always send out Alakazan as it sees a KO with Focus Blast, even if Alakazam dies before it can get it off
PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = FALSE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES lets AI see that Alakazam would be KO'd before it can KO, and won't switch it in
PARAMETRIZE{ speedAlakazm = 400; alakazamFirst = TRUE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES recognizes that Alakazam is faster and can KO, and will switch it in
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags);
PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting.
OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); }
OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first.
OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash.
} WHEN {
TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFirst ? 1 : 2); } // AI doesn't send out Alakazam if it gets outsped
} SCENE {
MESSAGE("Foe Kadabra fainted!");
if (alakazamFirst) {
MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!");
} else {
MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!");
}
}
}
AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(player, MOVE_PERISH_SONG); }
TURN { ; }
TURN { ; }
TURN { EXPECT_SWITCH(opponent, 1); }
} SCENE {
MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!");
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI considers hazard damage when choosing which Pokemon to switch in")
{
u32 aiIsSmart = 0;
u32 aiSmartSwitchFlags = 0;
PARAMETRIZE{ aiIsSmart = 0; aiSmartSwitchFlags = 0; } // AI doesn't care about hazard damage resulting in Pokemon being KO'd
PARAMETRIZE{ aiIsSmart = 1; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES avoids being KO'd as a result of hazards damage
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags);
PLAYER(SPECIES_MEGANIUM) { Speed(100); SpDefense(328); SpAttack(265); Moves(MOVE_STEALTH_ROCK, MOVE_SURF); } // Meganium does ~56% minimum ~66% maximum, enough to KO Charizard after rocks and never KO Typhlosion after rocks
OPPONENT(SPECIES_PONYTA) { Level(5); Speed(5); Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_CHARIZARD) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium
OPPONENT(SPECIES_TYPHLOSION) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium
} WHEN {
TURN { MOVE(player, MOVE_STEALTH_ROCK) ;}
TURN { MOVE(player, MOVE_SURF) ; EXPECT_SEND_OUT(opponent, aiIsSmart ? 2 : 1); } // AI sends out Typhlosion to get the KO with the flag rather than Charizard
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize type matchup + SE move, then type matchup")
{
u32 aiSmartSwitchFlags = 0;
u32 move1;
u32 move2;
u32 expectedIndex;
PARAMETRIZE{ expectedIndex = 3; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } // When not smart, AI will only switch in a defensive mon if it has a SE move, otherwise will just default to damage
PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; }
PARAMETRIZE{ expectedIndex = 2; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // When smart, AI will prioritize SE move, but still switch in good type matchup without SE move
PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags);
PLAYER(SPECIES_MARSHTOMP) { Level(30); Moves(MOVE_MUD_BOMB, MOVE_WATER_GUN, MOVE_GROWL, MOVE_MUD_SHOT); Speed(5); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(6); } // Forces switchout
OPPONENT(SPECIES_TANGELA) { Level(30); Moves(move1); Speed(4); }
OPPONENT(SPECIES_LOMBRE) { Level(30); Moves(move2); Speed(4); }
OPPONENT(SPECIES_HARIYAMA) { Level(30); Moves(MOVE_VITAL_THROW); Speed(4); }
} WHEN {
TURN { MOVE(player, MOVE_GROWL) ; EXPECT_SWITCH(opponent, expectedIndex); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive 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_NONE); Speed(4); } // Forces switchout
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); }
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK) ; EXPECT_SWITCH(opponent, 1); }
}
}
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); }
} WHEN {
TURN { MOVE(player, MOVE_WING_ATTACK) ; EXPECT_SEND_OUT(opponent, 2); }
}
}