using PkmnLib.Dynamic.AI.Explicit; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Pokemon; using PkmnLib.Plugin.Gen7.Scripts.Side; using PkmnLib.Plugin.Gen7.Scripts.Status; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.AI; public static class AISwitchFunctions { internal static void RegisterAISwitchFunctions(ExplicitAIHandlers handlers) { handlers.ShouldSwitchFunctions.Add("perish_song", PerishSong); handlers.ShouldSwitchFunctions.Add("significant_end_of_turn_damage", SignificantEndOfTurnDamage); handlers.ShouldSwitchFunctions.Add("high_damage_from_foe", HighDamageFromFoe); handlers.ShouldSwitchFunctions.Add("cure_status_problem_by_switching_out", CureStatusProblemBySwitchingOut); } /// /// Switch out if the Perish Song effect is about to cause the Pokémon to faint. /// private static bool PerishSong(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) { if (!pokemon.Volatile.TryGet(out var effect)) return false; return effect.Turns <= 1; } private static readonly StringKey PoisonHealAbilityName = "poison_heal"; /// /// Switch out if the Pokémon is expected to take significant end-of-turn damage. /// private static bool SignificantEndOfTurnDamage(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) { var eorDamage = 0; pokemon.RunScriptHook(x => x.ExpectedEndOfTurnDamage(pokemon, ref eorDamage)); if (eorDamage >= pokemon.CurrentHealth / 2 || eorDamage >= pokemon.MaxHealth / 4) return true; if (!ai.TrainerHighSkill || eorDamage <= 0) return false; if (pokemon.Volatile.Contains() && ai.Random.GetBool()) return true; if (pokemon.Volatile.Contains()) return true; if (pokemon.Volatile.Contains()) return true; var statusScript = pokemon.StatusScript.Script; if (statusScript is BadlyPoisoned { Turns: > 0 } badlyPoisoned && pokemon.ActiveAbility?.Name != PoisonHealAbilityName) { var poisonDamage = pokemon.MaxHealth / 8; var nextToxicDamage = pokemon.MaxHealth * badlyPoisoned.GetPoisonMultiplier(); if ((pokemon.CurrentHealth <= nextToxicDamage && pokemon.CurrentHealth > poisonDamage) || nextToxicDamage > poisonDamage * 2) { return true; } } return false; } private static bool HighDamageFromFoe(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) { if (!ai.TrainerHighSkill) return false; if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2) return false; var bigThreat = false; var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide) .SelectMany(x => x.Pokemon.WhereNotNull()); foreach (var opponent in opponents) { if (Math.Abs(opponent.Level - pokemon.Level) > 5) continue; var lastMoveUsed = opponent.BattleData?.LastMoveChoice; if (lastMoveUsed is null) continue; var moveData = lastMoveUsed.ChosenMove.MoveData; if (moveData.Category == MoveCategory.Status) continue; var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveData.MoveType, pokemon.Types); if (effectiveness <= 1 || moveData.BasePower < 70) continue; var switchChange = moveData.BasePower > 90 ? 50 : 25; bigThreat = ai.Random.GetInt(100) < switchChange; } return bigThreat; } private static readonly StringKey ImmunityAbilityName = "immunity"; private static readonly StringKey InsomniaAbilityName = "insomnia"; private static readonly StringKey LimberAbilityName = "limber"; private static readonly StringKey MagmaArmorAbilityName = "magma_armor"; private static readonly StringKey VitalSpiritAbilityName = "vital_spirit"; private static readonly StringKey WaterBubbleAbilityName = "water_bubble"; private static readonly StringKey WaterVeilAbilityName = "water_veil"; private static readonly StringKey NaturalCureAbilityName = "natural_cure"; private static readonly StringKey RegeneratorAbilityName = "regenerator"; private static readonly Dictionary> StatusCureAbilities = new() { { ImmunityAbilityName, [ScriptUtils.ResolveName(), ScriptUtils.ResolveName()] }, { InsomniaAbilityName, [ScriptUtils.ResolveName()] }, { LimberAbilityName, [ScriptUtils.ResolveName()] }, { MagmaArmorAbilityName, [ScriptUtils.ResolveName()] }, { VitalSpiritAbilityName, [ScriptUtils.ResolveName()] }, { WaterBubbleAbilityName, [ScriptUtils.ResolveName()] }, { WaterVeilAbilityName, [ScriptUtils.ResolveName()] }, }; /// /// Switch out to cure a status problem or heal HP with abilities like Natural Cure or Regenerator. /// private static bool CureStatusProblemBySwitchingOut(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) { if (pokemon.ActiveAbility == null) return false; // Don't try to cure a status problem/heal a bit of HP if entry hazards will // KO the battler if it switches back in var entryHazardDamage = ExplicitAI.CalculateEntryHazardDamage(pokemon, pokemon.BattleData!.BattleSide); if (entryHazardDamage >= pokemon.CurrentHealth) return false; if (pokemon.StatusScript.Script is null) return false; var abilityName = pokemon.ActiveAbility.Name; var statusName = pokemon.StatusScript.Script.Name; // Check abilities that cure specific status conditions var canCureStatus = false; if (abilityName == NaturalCureAbilityName) { canCureStatus = true; } else if (StatusCureAbilities.TryGetValue(abilityName, out var statusList)) { canCureStatus = statusList.Any(status => status == statusName); } if (canCureStatus) { if (AIHelperFunctions.WantsStatusProblem(pokemon, statusName)) return false; // Don't bother if the status will cure itself soon if (pokemon.StatusScript.Script is Sleep { Turns: 1 }) return false; if (entryHazardDamage >= pokemon.MaxHealth / 4) return false; // Don't bother curing a poisoning if Toxic Spikes will just re-poison if (pokemon.StatusScript.Script is Poisoned or BadlyPoisoned && !reserves.Any(p => p.Types.Any(t => t.Name == "poison"))) { if (pokemon.BattleData!.BattleSide.VolatileScripts.TryGet(out _)) { return false; } } // Not worth curing status problems that still allow actions if at high HP var isImmobilizing = pokemon.StatusScript.Script is Sleep or Frozen; if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2 && !isImmobilizing) return false; if (ai.Random.GetInt(100) < 70) return true; } else if (abilityName == RegeneratorAbilityName) { // Not worth healing if battler would lose more HP from switching back in later if (entryHazardDamage >= pokemon.MaxHealth / 3) return false; // Not worth healing HP if already at high HP if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2) return false; // Don't bother if a foe is at low HP and could be knocked out instead var hasDamagingMove = pokemon.Moves.Any(m => m?.MoveData.Category != MoveCategory.Status); if (hasDamagingMove) { var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide) .SelectMany(x => x.Pokemon.WhereNotNull()); var weakFoe = opponents.Any(opponent => opponent.CurrentHealth < opponent.MaxHealth / 3); if (weakFoe) return false; } if (ai.Random.GetInt(100) < 70) return true; } return false; } }