using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.AI.Explicit; public partial class ExplicitAI { private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves, [NotNullWhen(true)] out ITurnChoice? choice) { choice = null; if (battle.IsWildBattle) return false; if (TrainerHighSkill) { var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide); var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack); if (!foeCanAct) return false; } var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex( new ResponsibleIndex(pokemon.BattleData!.SideIndex, pokemon.BattleData.Position))); if (party is null) return false; var usablePokemon = party.GetUsablePokemonNotInField().ToList(); if (!terribleMoves) { if (!_skillFlags.ConsiderSwitching) return false; if (!usablePokemon.Any()) return false; var shouldSwitch = _handlers.ShouldSwitch(this, pokemon, battle, usablePokemon); if (shouldSwitch && TrainerMediumSkill) { if (_handlers.ShouldNotSwitch(this, pokemon, battle, usablePokemon)) { shouldSwitch = false; } } if (!shouldSwitch) return false; } var battleSide = pokemon.BattleData!.BattleSide; var bestReplacement = ChooseBestReplacementPokemon(pokemon.BattleData!.Position, terribleMoves, usablePokemon, battleSide); if (bestReplacement is null) { AILogging.LogInformation( $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}."); return false; } choice = new SwitchChoice(pokemon, bestReplacement); return true; } private IPokemon? ChooseBestReplacementPokemon(byte position, bool terribleMoves, IReadOnlyList usablePokemon, IBattleSide battleSide) { var options = usablePokemon.Where((pokemon, index) => { if (_skillFlags.ReserveLastPokemon && index == usablePokemon.Count - 1 && usablePokemon.Count > 1) return false; // Don't switch to the last Pokemon if there are others available. if (_skillFlags.UsePokemonInOrder && index != 0) return false; return true; }).ToList(); if (options.Count == 0) return null; var ratedOptions = options .Select(pokemon => new { Pokemon = pokemon, Score = RateReplacementPokemon(pokemon, battleSide) }) .OrderBy(x => x.Score).ToList(); if (TrainerHighSkill && !terribleMoves) { if (ratedOptions.First().Score < 100) return null; } return ratedOptions.First().Pokemon; } private static readonly StringKey HeavyDutyBootsName = "heavy_duty_boots"; private static readonly StringKey ToxicSpikesName = "toxic_spikes"; private static readonly StringKey StickyWebName = "sticky_web"; private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide) { var score = 0; var types = pokemon.Types; var entryDamage = CalculateEntryHazardDamage(pokemon, battleSide); if (entryDamage >= pokemon.CurrentHealth) score -= 50; else if (entryDamage > 0) score -= 50 * (int)Math.Round((double)entryDamage / pokemon.MaxHealth, MidpointRounding.AwayFromZero); if (!pokemon.HasHeldItem(HeavyDutyBootsName) && !pokemon.IsFloating) { if (battleSide.VolatileScripts.Contains(ToxicSpikesName) && CanBePoisoned(pokemon, battleSide.Battle)) { score -= 20; } if (battleSide.VolatileScripts.Contains(StickyWebName)) { score -= 15; } } var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide); foreach (var foe in opponentSide.Pokemon.WhereNotNull()) { var lastMoveUsed = foe.BattleData?.LastMoveChoice; if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status) continue; var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType; var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveType, types); score -= (int)(lastMoveUsed.ChosenMove.MoveData.BasePower * effectiveness / 5); } foreach (var learnedMove in pokemon.Moves.WhereNotNull()) { if (learnedMove.MoveData.BasePower == 0 || learnedMove is { CurrentPp: 0, MaxPp: > 0 }) continue; foreach (var foe in opponentSide.Pokemon.WhereNotNull()) { if (CanAbsorbMove(foe, learnedMove.MoveData, learnedMove.MoveData.MoveType, battleSide.Battle)) continue; var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(learnedMove.MoveData.MoveType, foe.Types); score += (int)(learnedMove.MoveData.BasePower * effectiveness / 10); } } return score; } public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side) { var damage = 0u; side.RunScriptHook(x => x.ExpectedEntryDamage(pokemon, ref damage)); return damage; } private static bool CanSwitch(IPokemon pokemon) { var battleData = pokemon.BattleData; if (battleData == null) return false; if (battleData.Battle.IsWildBattle) return false; var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position))); return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField(); } }