153 lines
6.3 KiB
C#
153 lines
6.3 KiB
C#
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<IPokemon> 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<IAIInfoScriptExpectedEntryDamage>(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();
|
|
}
|
|
} |