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;
}
}