Move all battle state from IPokemon to an ephemeral IBattlePokemon wrapper

This commit is contained in:
2026-08-28 15:20:26 +02:00
parent 942be8eaeb
commit 8a2733a0a9
859 changed files with 4408 additions and 5331 deletions

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public class AIMoveState
{
/// <inheritdoc cref="AIMoveState" />
public AIMoveState(IPokemon user, IMoveData move)
public AIMoveState(IBattlePokemon user, IMoveData move)
{
User = user;
Move = move;
@@ -18,7 +18,7 @@ public class AIMoveState
/// <summary>
/// The user that's being wrapper
/// </summary>
public IPokemon User { get; }
public IBattlePokemon User { get; }
/// <summary>
/// The move that's being wrapper

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public partial class ExplicitAI
{
private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves,
private bool TryChooseToSwitchOut(IBattle battle, IBattlePokemon pokemon, bool terribleMoves,
[NotNullWhen(true)] out ITurnChoice? choice)
{
choice = null;
@@ -17,13 +17,13 @@ public partial class ExplicitAI
return false;
if (TrainerHighSkill)
{
var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide);
var opponentSide = battle.Sides.First(x => x != pokemon.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)));
new ResponsibleIndex(pokemon.SideIndex, pokemon.Position)));
if (party is null)
return false;
var usablePokemon = party.GetUsablePokemonNotInField().ToList();
@@ -44,20 +44,20 @@ public partial class ExplicitAI
if (!shouldSwitch)
return false;
}
var battleSide = pokemon.BattleData!.BattleSide;
var battleSide = pokemon.BattleSide;
var bestReplacement = ChooseBestReplacementPokemon(terribleMoves, usablePokemon, battleSide);
if (bestReplacement is null)
{
AILogging.LogInformation(
$"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}.");
$"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.Position}.");
return false;
}
choice = new SwitchChoice(pokemon, bestReplacement);
return true;
}
private IPokemon? ChooseBestReplacementPokemon(bool terribleMoves, IReadOnlyList<IPokemon> usablePokemon,
IBattleSide battleSide)
private IBattlePokemon? ChooseBestReplacementPokemon(bool terribleMoves,
IReadOnlyList<IBattlePokemon> usablePokemon, IBattleSide battleSide)
{
var options = usablePokemon.Where((_, index) =>
{
@@ -84,7 +84,7 @@ public partial class ExplicitAI
private static readonly StringKey ToxicSpikesName = "toxic_spikes";
private static readonly StringKey StickyWebName = "sticky_web";
private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide)
private int RateReplacementPokemon(IBattlePokemon pokemon, IBattleSide battleSide)
{
var score = 0;
var types = pokemon.Types;
@@ -107,7 +107,7 @@ public partial class ExplicitAI
var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide);
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
{
var lastMoveUsed = foe.BattleData?.LastMoveChoice;
var lastMoveUsed = foe.LastMoveChoice;
if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status)
continue;
var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType;
@@ -134,22 +134,19 @@ public partial class ExplicitAI
/// <summary>
/// Calculates the expected entry hazard damage for a given Pokémon on a given battle side.
/// </summary>
public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side)
public static uint CalculateEntryHazardDamage(IBattlePokemon pokemon, IBattleSide side)
{
var damage = 0u;
side.RunScriptHook<IAIInfoScriptExpectedEntryDamage>(x => x.ExpectedEntryDamage(pokemon, ref damage));
return damage;
}
private static bool CanSwitch(IPokemon pokemon)
private static bool CanSwitch(IBattlePokemon pokemon)
{
var battleData = pokemon.BattleData;
if (battleData == null)
if (pokemon.Battle.IsWildBattle)
return false;
if (battleData.Battle.IsWildBattle)
return false;
var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x =>
x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position)));
var partyForIndex = pokemon.Battle.Parties.FirstOrDefault(x =>
x.IsResponsibleForIndex(new ResponsibleIndex(pokemon.SideIndex, pokemon.Position)));
return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField();
}
}

View File

@@ -37,7 +37,7 @@ public partial class ExplicitAI
private static readonly StringKey KomalaName = "komala";
private static readonly StringKey MiniorName = "minior";
private static bool CanBePoisoned(IPokemon pokemon, IBattle battle)
private static bool CanBePoisoned(IBattlePokemon pokemon, IBattle battle)
{
if (battle.TerrainName == MistyTerrainName)
return false;
@@ -60,7 +60,7 @@ public partial class ExplicitAI
return true;
}
private static bool CanAbsorbMove(IPokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle)
private static bool CanAbsorbMove(IBattlePokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle)
{
if (pokemon.ActiveAbility == null)
return false;

View File

@@ -118,7 +118,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
public IRandom Random => _random;
/// <inheritdoc />
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{
if (battle.HasForcedTurn(pokemon, out var choice))
return choice;
@@ -131,8 +131,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var moveChoices = GetMoveScores(pokemon, battle);
if (moveChoices.Count == 0)
{
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
}
var maxScore = moveChoices.Max(x => x.score);
if (TrainerHighSkill && CanSwitch(pokemon))
@@ -144,7 +144,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (!badMoves && _random.GetInt(100) < 25)
badMoves = true;
}
else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 &&
else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.TurnsOnField > 2 &&
_random.GetInt(100) < 80)
{
badMoves = true;
@@ -164,8 +164,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var totalScore = considerChoices.Sum(x => x.Item2);
if (totalScore == 0)
{
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
}
var initialRandomValue = _random.GetFloat(0, totalScore);
var randomValue = initialRandomValue;
@@ -177,15 +177,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var (index, _, targetIndex) = considerChoices[i].x;
var learnedMove = pokemon.Moves[index];
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
if (targetIndex == -1)
targetIndex = pokemon.BattleData.Position;
targetIndex = pokemon.Position;
return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex);
}
throw new InvalidOperationException("No valid move choice found. This should not happen.");
}
private List<(int index, int score, int targetIndex)> GetMoveScores(IPokemon user, IBattle battle)
private List<(int index, int score, int targetIndex)> GetMoveScores(IBattlePokemon user, IBattle battle)
{
var choices = new List<(int index, int score, int targetIndex)>();
foreach (var (learnedMove, index) in user.Moves.Select((x, i) => (x, i)).Where(x => x.x != null))
@@ -249,30 +249,24 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
// TODO: get redirected target
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
var battleData = pokemon.BattleData;
if (battleData == null)
if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
{
continue;
}
var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
AddMoveToChoices(index, score, battleData.Position);
AddMoveToChoices(index, score, pokemon.Position);
}
}
else
{
var targets = new List<IPokemon>();
var targets = new List<IBattlePokemon>();
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
var battleData = pokemon.BattleData;
if (battleData == null)
if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
{
continue;
}
@@ -295,7 +289,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
}
}
private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove)
private bool PredictMoveFailure(IBattlePokemon user, IBattle battle, AIMoveState aiMove)
{
if (user.HasStatus("sleep"))
{
@@ -333,14 +327,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey SubstituteName = new("substitute");
private static readonly StringKey InfiltratorName = new("infiltrator");
private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
private bool PredictMoveFailureAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target,
IBattle battle)
{
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
return true;
if (aiMove.Move.Priority > 0)
{
if (target.BattleData?.SideIndex != user.BattleData?.SideIndex)
if (target.SideIndex != user.SideIndex)
{
// Psychic Terrain makes all priority moves fail if the target is affected
if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
@@ -348,7 +343,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return true;
}
// Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities
if (target.BattleData?.BattleSide.Pokemon.WhereNotNull().Any(x =>
if (target.BattleSide.Pokemon.WhereNotNull().Any(x =>
x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
{
return true;
@@ -362,7 +357,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0)
return true;
if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status &&
target.Types.Any(x => x.Name == DarkName) && target.BattleData?.SideIndex != user.BattleData?.SideIndex)
target.Types.Any(x => x.Name == DarkName) && target.SideIndex != user.SideIndex)
return true;
if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
return true;
@@ -375,7 +370,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return false;
}
private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList<IPokemon>? targets = null)
private int GetMoveScore(IBattlePokemon user, AIMoveState aiMove, IBattle battle,
IReadOnlyList<IBattlePokemon>? targets = null)
{
var score = MoveBaseScore;
if (targets != null)
@@ -411,7 +407,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return score;
}
private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
private int GetMoveScoreAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target,
IBattle battle)
{
if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
{
@@ -427,8 +424,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
}
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
target.BattleData?.Position != user.BattleData?.Position)
if (aiMove.Move.Target.TargetsFoe() && target.SideIndex == user.SideIndex && target.Position != user.Position)
{
if (score == MoveUselessScore)
return -1;
@@ -442,7 +438,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey OvercoatName = new("overcoat");
private static readonly StringKey SafetyGogglesName = new("safety_goggles");
private static bool AffectedByPowder(IPokemon pokemon)
private static bool AffectedByPowder(IBattlePokemon pokemon)
{
if (pokemon.Types.Any(x => x.Name == GrassName))
return false;
@@ -456,7 +452,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey TruantName = "truant";
private static readonly StringKey TruantEffectName = "truant_effect";
private static bool CanAttack(IPokemon pokemon)
private static bool CanAttack(IBattlePokemon pokemon)
{
if (pokemon.Volatile.Contains("requires_recharge"))
return false;

View File

@@ -6,7 +6,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
/// <summary>
/// An option where a move is used against a target
/// </summary>
public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0);
public record struct MoveOption(AIMoveState Move, IBattle Battle, IBattlePokemon? Target, uint EstimatedDamage = 0);
/// <summary>
/// A function that takes an explicit AI and a move option and returns a boolean value.
@@ -16,8 +16,8 @@ public delegate bool AIBoolHandler(IExplicitAI ai, MoveOption option);
/// <summary>
/// A function for returning whether a Pokemon should switch.
/// </summary>
public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle,
IReadOnlyList<IPokemon> reserves);
public delegate bool AISwitchBoolHandler(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves);
/// <summary>
/// A function for returning the base power of a move.
@@ -112,7 +112,7 @@ public interface IReadOnlyExplicitAIHandlers
/// <summary>
/// Indicates whether a Pokemon should switch into another Pokemon
/// </summary>
bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, IReadOnlyList<IBattlePokemon> reserves);
/// <summary>
/// Functions that indicate whether a Pokemon should NOT switch
@@ -122,7 +122,8 @@ public interface IReadOnlyExplicitAIHandlers
/// <summary>
/// Indicates whether a Pokemon should NOT switch into another Pokemon
/// </summary>
bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves);
/// <summary>
/// Scores abilities
@@ -246,7 +247,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
ShouldSwitchFunctions;
/// <inheritdoc />
public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
public bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves)
{
var shouldSwitch = false;
foreach (var (_, handler) in ShouldSwitchFunctions)
@@ -272,7 +274,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
/// <inheritdoc />
public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
public bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves)
{
var shouldNotSwitch = false;
foreach (var (_, handler) in ShouldNotSwitchFunctions)