Getting started with implementing an explicit AI, based on the Essentials one.
All checks were successful
Build / Build (push) Successful in 1m2s
All checks were successful
Build / Build (push) Successful in 1m2s
This commit is contained in:
parent
084ae84130
commit
a3a4993407
@ -2,6 +2,8 @@
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Dynamic.AI;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Plugin.Gen7;
|
||||
using Serilog;
|
||||
|
||||
namespace AIRunner;
|
||||
@ -14,9 +16,11 @@ internal static class Program
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.Console().CreateLogger();
|
||||
Log.Information("Starting AI Runner...");
|
||||
_availableAIs = AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes())
|
||||
.Where(type => type.IsSubclassOf(typeof(PokemonAI)) && !type.IsAbstract).Select(Activator.CreateInstance)
|
||||
.Cast<PokemonAI>().ToList();
|
||||
AILogging.LogHandler = Log.Debug;
|
||||
var library = DynamicLibraryImpl.Create([
|
||||
new Gen7Plugin(),
|
||||
]);
|
||||
_availableAIs = PokemonAI.InstantiateAis(library);
|
||||
|
||||
var testCommand = new Command("test", "Run two AIs against each other")
|
||||
{
|
||||
@ -58,7 +62,7 @@ internal static class Program
|
||||
var ai2 = _availableAIs!.First(a =>
|
||||
string.Equals(a.Name, ai2Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return TestCommandRunner.RunTestCommand(ai1, ai2, result.GetRequiredValue<int>("--battles"));
|
||||
return TestCommandRunner.RunTestCommand(library, ai1, ai2, result.GetRequiredValue<int>("--battles"));
|
||||
});
|
||||
|
||||
var rootCommand = new RootCommand("PkmnLib.NET AI Runner")
|
||||
|
@ -13,12 +13,9 @@ namespace AIRunner;
|
||||
|
||||
public static class TestCommandRunner
|
||||
{
|
||||
internal static async Task RunTestCommand(PokemonAI ai1, PokemonAI ai2, int battles)
|
||||
internal static async Task RunTestCommand(IDynamicLibrary library, PokemonAI ai1, PokemonAI ai2, int battles)
|
||||
{
|
||||
var t1 = DateTime.UtcNow;
|
||||
var library = DynamicLibraryImpl.Create([
|
||||
new Gen7Plugin(),
|
||||
]);
|
||||
const int maxTasks = 10;
|
||||
|
||||
Log.Information("Running {Battles} battles between {AI1} and {AI2}", battles, ai1.Name, ai2.Name);
|
||||
|
59
PkmnLib.Dynamic/AI/AIHelpers.cs
Normal file
59
PkmnLib.Dynamic/AI/AIHelpers.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI;
|
||||
|
||||
public static class AIHelpers
|
||||
{
|
||||
public static uint CalculateDamageEstimation(IMoveData move, IPokemon user, IPokemon target,
|
||||
IDynamicLibrary library)
|
||||
{
|
||||
var hitData = new CustomHitData
|
||||
{
|
||||
BasePower = move.BasePower,
|
||||
Effectiveness = library.StaticLibrary.Types.GetEffectiveness(move.MoveType, target.Types),
|
||||
Type = move.MoveType,
|
||||
};
|
||||
return library.DamageCalculator.GetDamage(null, move.Category, user, target, 1, 0, hitData);
|
||||
}
|
||||
|
||||
private class CustomHitData : IHitData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsCritical => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort BasePower { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Effectiveness { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Damage => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeIdentifier? Type { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsContact => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFailed => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Fail()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetFlag(StringKey flag)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(StringKey flag) => false;
|
||||
}
|
||||
}
|
8
PkmnLib.Dynamic/AI/AILogging.cs
Normal file
8
PkmnLib.Dynamic/AI/AILogging.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace PkmnLib.Dynamic.AI;
|
||||
|
||||
public static class AILogging
|
||||
{
|
||||
public static Action<string> LogHandler { get; set; } = message => { };
|
||||
|
||||
public static void LogInformation(string message) => LogHandler(message);
|
||||
}
|
16
PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs
Normal file
16
PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public class AIMoveState
|
||||
{
|
||||
public AIMoveState(IPokemon user, IMoveData move)
|
||||
{
|
||||
User = user;
|
||||
Move = move;
|
||||
}
|
||||
|
||||
public IPokemon User { get; }
|
||||
public IMoveData Move { get; }
|
||||
}
|
346
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs
Normal file
346
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs
Normal file
@ -0,0 +1,346 @@
|
||||
using PkmnLib.Dynamic.BattleFlow;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// An explicit AI that has explicitly written logic for each Pokémon and move.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is heavily based on the AI used in <a href="https://github.com/Maruno17/pokemon-essentials">Pokémon Essentials</a>
|
||||
/// </remarks>
|
||||
public class ExplicitAI : PokemonAI
|
||||
{
|
||||
public const int MoveFailScore = 20;
|
||||
public const int MoveUselessScore = 60;
|
||||
public const int MoveBaseScore = 100;
|
||||
|
||||
private const float TrainerSkill = 100; // TODO: This should be configurable
|
||||
private bool CanPredictMoveFailure => true; // TODO: This should be configurable
|
||||
private bool ScoreMoves => true; // TODO: This should be configurable
|
||||
|
||||
private float MoveScoreThreshold => (float)(0.6f + 0.35f * Math.Sqrt(Math.Min(TrainerSkill, 100) / 100f));
|
||||
|
||||
private readonly IReadOnlyExplicitAIHandlers _handlers;
|
||||
|
||||
private readonly IRandom _random = new RandomImpl();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExplicitAI(IDynamicLibrary library) : base("explicit")
|
||||
{
|
||||
_handlers = library.ExplicitAIHandlers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
|
||||
{
|
||||
if (battle.HasForcedTurn(pokemon, out var choice))
|
||||
return choice;
|
||||
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 maxScore = moveChoices.Max(x => x.score);
|
||||
// TODO: Consider switching to a different pokémon if the score is too low
|
||||
|
||||
var threshold = (float)Math.Floor(maxScore * MoveScoreThreshold);
|
||||
var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray();
|
||||
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 initialRandomValue = _random.GetFloat(0, totalScore);
|
||||
var randomValue = initialRandomValue;
|
||||
for (var i = 0; i < considerChoices.Length; i++)
|
||||
{
|
||||
randomValue -= considerChoices[i].Item2;
|
||||
if (randomValue >= 0)
|
||||
continue;
|
||||
|
||||
var (index, _, targetIndex) = considerChoices[i].x;
|
||||
var learnedMove = pokemon.Moves[index];
|
||||
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
|
||||
if (targetIndex == -1)
|
||||
targetIndex = pokemon.BattleData.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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
var moveChoice = new MoveChoice(user, learnedMove!, 0, 0);
|
||||
if (!battle.CanUse(moveChoice))
|
||||
{
|
||||
if (learnedMove!.CurrentPp == 0 && learnedMove.MaxPp > 0)
|
||||
{
|
||||
AILogging.LogInformation($"{user} cannot use {learnedMove} because it has no PP left.");
|
||||
}
|
||||
else
|
||||
{
|
||||
AILogging.LogInformation($"{user} cannot use {learnedMove} because it is not a valid choice.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var moveData = learnedMove!.MoveData;
|
||||
var moveName = moveData.Name;
|
||||
moveChoice.RunScriptHook<IScriptChangeMove>(x => x.ChangeMove(moveChoice, ref moveName));
|
||||
if (moveName != learnedMove.MoveData.Name)
|
||||
{
|
||||
AILogging.LogInformation($"{user} changed {learnedMove} to {moveName}.");
|
||||
if (!battle.Library.StaticLibrary.Moves.TryGet(moveName, out moveData))
|
||||
throw new InvalidOperationException($"Move {moveName} not found in the move library.");
|
||||
var secondaryEffect = moveData.SecondaryEffect;
|
||||
if (secondaryEffect != null)
|
||||
{
|
||||
if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name,
|
||||
secondaryEffect.Parameters, out var script))
|
||||
{
|
||||
moveChoice.Script.Set(script);
|
||||
script.OnAddedToParent(moveChoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveChoice.Script.Clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
moveChoice.Script.Clear();
|
||||
}
|
||||
}
|
||||
var aiMove = new AIMoveState(user, moveData);
|
||||
if (CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove))
|
||||
{
|
||||
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail.");
|
||||
AddMoveToChoices(index, MoveFailScore);
|
||||
}
|
||||
|
||||
var target = moveData.Target;
|
||||
if (target is MoveTarget.All or MoveTarget.AllAlly or MoveTarget.AllOpponent or MoveTarget.SelfUse)
|
||||
{
|
||||
var score = GetMoveScore(user, aiMove, battle);
|
||||
AddMoveToChoices(index, score);
|
||||
}
|
||||
else if (target is MoveTarget.AdjacentOpponent or MoveTarget.Any or MoveTarget.Adjacent
|
||||
or MoveTarget.AdjacentAllySelf)
|
||||
{
|
||||
// TODO: get redirected target
|
||||
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
|
||||
{
|
||||
var battleData = pokemon.BattleData;
|
||||
if (battleData == null)
|
||||
continue;
|
||||
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
|
||||
continue;
|
||||
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
|
||||
AddMoveToChoices(index, score, battleData.Position);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var targets = new List<IPokemon>();
|
||||
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
|
||||
{
|
||||
var battleData = pokemon.BattleData;
|
||||
if (battleData == null)
|
||||
continue;
|
||||
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
|
||||
continue;
|
||||
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
targets.Add(pokemon);
|
||||
}
|
||||
var score = GetMoveScore(user, aiMove, battle, targets);
|
||||
AddMoveToChoices(index, score);
|
||||
}
|
||||
}
|
||||
return choices;
|
||||
|
||||
void AddMoveToChoices(int index, int score, int targetIndex = -1)
|
||||
{
|
||||
choices.Add((index, score, targetIndex));
|
||||
// If the user is a wild Pokémon, doubly prefer a random move.
|
||||
if (battle.IsWildBattle && user.PersonalityValue % user.Moves.Count == index)
|
||||
{
|
||||
choices.Add((index, score, targetIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove)
|
||||
{
|
||||
if (user.HasStatus("sleep"))
|
||||
{
|
||||
// User is asleep, and will not wake up, and the move is not usable while asleep.
|
||||
if (user.GetStatusTurnsLeft != 0 && !aiMove.Move.HasFlag("usable_while_asleep"))
|
||||
{
|
||||
AILogging.LogInformation($"{user} is asleep and cannot use {aiMove.Move.Name}.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// The move is only usable while asleep, but the user is not asleep
|
||||
else if (aiMove.Move.HasFlag("usable_while_asleep"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Primal weather
|
||||
if (battle.WeatherName == "primordial_sea" && aiMove.Move.MoveType.Name == "fire")
|
||||
return true;
|
||||
if (battle.WeatherName == "desolate_lands" && aiMove.Move.MoveType.Name == "water")
|
||||
return true;
|
||||
|
||||
// Check if the move will fail based on the handlers
|
||||
return aiMove.Move.SecondaryEffect != null &&
|
||||
_handlers.MoveWillFail(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null));
|
||||
}
|
||||
|
||||
private static readonly StringKey PsychicTerrainName = new("psychic_terrain");
|
||||
private static readonly StringKey DazzlingName = new("dazzling");
|
||||
private static readonly StringKey QueenlyMajestyName = new("queenly_majesty");
|
||||
private static readonly StringKey PranksterName = new("prankster");
|
||||
private static readonly StringKey DarkName = new("dark");
|
||||
private static readonly StringKey GroundName = new("ground");
|
||||
private static readonly StringKey PowderName = new("powder");
|
||||
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)
|
||||
{
|
||||
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(aiMove.Move.SecondaryEffect.Name,
|
||||
new MoveOption(aiMove, battle, target)))
|
||||
return true;
|
||||
if (aiMove.Move.Priority > 0)
|
||||
{
|
||||
if (target.BattleData?.SideIndex != user.BattleData?.SideIndex)
|
||||
{
|
||||
// Psychic Terrain makes all priority moves fail if the target is affected
|
||||
if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
|
||||
{
|
||||
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 =>
|
||||
x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Check immunity because of ability
|
||||
|
||||
var moveType = aiMove.Move.MoveType;
|
||||
var typeEffectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(moveType, target.Types);
|
||||
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)
|
||||
return true;
|
||||
if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
|
||||
return true;
|
||||
if (aiMove.Move.HasFlag(PowderName) && !AffectedByPowder(target))
|
||||
return true;
|
||||
if (target.Volatile.Contains(SubstituteName) && aiMove.Move.Category == MoveCategory.Status &&
|
||||
user.ActiveAbility?.Name != InfiltratorName)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList<IPokemon>? targets = null)
|
||||
{
|
||||
var score = MoveBaseScore;
|
||||
if (targets != null)
|
||||
{
|
||||
score = 0;
|
||||
var affectedTargets = 0;
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var targetScore = GetMoveScoreAgainstTarget(user, aiMove, target, battle);
|
||||
if (targetScore < 0)
|
||||
continue;
|
||||
score += targetScore;
|
||||
affectedTargets++;
|
||||
}
|
||||
if (affectedTargets == 0 && CanPredictMoveFailure)
|
||||
{
|
||||
return MoveFailScore;
|
||||
}
|
||||
if (affectedTargets > 0)
|
||||
score = (int)(score / (float)affectedTargets);
|
||||
}
|
||||
if (ScoreMoves)
|
||||
{
|
||||
if (aiMove.Move.SecondaryEffect != null)
|
||||
{
|
||||
_handlers.ApplyMoveEffectScore(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null),
|
||||
ref score);
|
||||
_handlers.ApplyGenerateMoveScoreModifiers(new MoveOption(aiMove, battle, null), ref score);
|
||||
}
|
||||
}
|
||||
if (score < 0)
|
||||
score = 0;
|
||||
return score;
|
||||
}
|
||||
|
||||
private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
|
||||
{
|
||||
if (CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
|
||||
{
|
||||
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} against {target} but it will fail.");
|
||||
return -1;
|
||||
}
|
||||
var score = MoveBaseScore;
|
||||
if (ScoreMoves && aiMove.Move.SecondaryEffect != null)
|
||||
{
|
||||
var estimatedDamage = AIHelpers.CalculateDamageEstimation(aiMove.Move, user, target, battle.Library);
|
||||
var moveOption = new MoveOption(aiMove, battle, target, estimatedDamage);
|
||||
_handlers.ApplyMoveEffectAgainstTargetScore(aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
|
||||
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(moveOption, ref score);
|
||||
}
|
||||
|
||||
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
|
||||
target.BattleData?.Position != user.BattleData?.Position)
|
||||
{
|
||||
if (score == MoveUselessScore)
|
||||
return -1;
|
||||
score = (int)(1.85f * MoveBaseScore - score);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static readonly StringKey GrassName = new("grass");
|
||||
private static readonly StringKey OvercoatName = new("overcoat");
|
||||
private static readonly StringKey SafetyGogglesName = new("safety_goggles");
|
||||
|
||||
private static bool AffectedByPowder(IPokemon pokemon)
|
||||
{
|
||||
if (pokemon.Types.Any(x => x.Name == GrassName))
|
||||
return false;
|
||||
if (pokemon.ActiveAbility?.Name == OvercoatName)
|
||||
return false;
|
||||
if (pokemon.HasHeldItem(SafetyGogglesName))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
199
PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs
Normal file
199
PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs
Normal file
@ -0,0 +1,199 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0);
|
||||
|
||||
public delegate bool AIBoolHandler(MoveOption option);
|
||||
|
||||
public delegate void AIMoveBasePowerHandler(MoveOption option, ref int score);
|
||||
|
||||
public delegate void AIScoreMoveHandler(MoveOption option, ref int score);
|
||||
|
||||
public interface IReadOnlyExplicitAIHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of checks to determine if a move will fail.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> MoveFailureCheck { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a move will fail based on the provided function code and options.
|
||||
/// </summary>
|
||||
bool MoveWillFail(StringKey functionCode, MoveOption option);
|
||||
|
||||
/// <summary>
|
||||
/// A list of checks to determine if a move will fail against a target.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> MoveFailureAgainstTargetCheck { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a move will fail against a target based on the provided function code and options.
|
||||
/// </summary>
|
||||
bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for move effects.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> MoveEffectScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the score for a move effect based on the provided name and options.
|
||||
/// </summary>
|
||||
void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for move effects against a target.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> MoveEffectAgainstTargetScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the score for a move effect against a target based on the provided name and options.
|
||||
/// </summary>
|
||||
void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to determine the base power of a move.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIMoveBasePowerHandler> MoveBasePower { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the base power for a move based on the provided name and options.
|
||||
/// </summary>
|
||||
void GetBasePower(StringKey name, MoveOption option, ref int power);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for general move effectiveness.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> GeneralMoveScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the score for a general move based on the provided option.
|
||||
/// </summary>
|
||||
/// <param name="option"></param>
|
||||
/// <param name="score"></param>
|
||||
/// <returns></returns>
|
||||
void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score);
|
||||
|
||||
/// <summary>
|
||||
/// A list of handlers to apply scores for general move effectiveness against a target.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> GeneralMoveAgainstTargetScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the score for a general move against a target based on the provided option.
|
||||
/// </summary>
|
||||
void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score);
|
||||
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldSwitch { get; }
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldNotSwitch { get; }
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> AbilityRanking { get; }
|
||||
}
|
||||
|
||||
public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.MoveFailureCheck => MoveFailureCheck;
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureCheck { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MoveWillFail(StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureAgainstTargetCheck { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.MoveFailureAgainstTargetCheck =>
|
||||
MoveFailureAgainstTargetCheck;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option) =>
|
||||
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectScore { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.MoveEffectScore =>
|
||||
MoveEffectScore;
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectAgainstTargetScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score)
|
||||
{
|
||||
if (MoveEffectScore.TryGetValue(name, out var handler))
|
||||
handler(option, ref score);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.MoveEffectAgainstTargetScore =>
|
||||
MoveEffectAgainstTargetScore;
|
||||
|
||||
public FunctionHandlerDictionary<AIMoveBasePowerHandler> MoveBasePower = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score)
|
||||
{
|
||||
if (MoveEffectAgainstTargetScore.TryGetValue(name, out var handler))
|
||||
handler(option, ref score);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIMoveBasePowerHandler> IReadOnlyExplicitAIHandlers.MoveBasePower =>
|
||||
MoveBasePower;
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GetBasePower(StringKey name, MoveOption option, ref int power)
|
||||
{
|
||||
if (MoveBasePower.TryGetValue(name, out var handler))
|
||||
handler(option, ref power);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveScore =>
|
||||
GeneralMoveScore;
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveAgainstTargetScore = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score)
|
||||
{
|
||||
foreach (var (_, handler) in GeneralMoveScore)
|
||||
{
|
||||
handler(option, ref score);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore =>
|
||||
GeneralMoveAgainstTargetScore;
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldSwitch = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score)
|
||||
{
|
||||
foreach (var (_, handler) in GeneralMoveAgainstTargetScore)
|
||||
{
|
||||
handler(option, ref score);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitch => ShouldSwitch;
|
||||
|
||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldNotSwitch = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitch => ShouldNotSwitch;
|
||||
|
||||
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.AbilityRanking =>
|
||||
AbilityRanking;
|
||||
}
|
117
PkmnLib.Dynamic/AI/Explicit/FunctionHandlerDictionary.cs
Normal file
117
PkmnLib.Dynamic/AI/Explicit/FunctionHandlerDictionary.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
public class FunctionHandlerDictionary<TValue> : IDictionary<StringKey, TValue>, IReadOnlyDictionary<StringKey, TValue>
|
||||
where TValue : class
|
||||
{
|
||||
private readonly OrderedDictionary _backingDictionary = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<KeyValuePair<StringKey, TValue>> GetEnumerator() => _backingDictionary.Cast<DictionaryEntry>()
|
||||
.Select(entry => new KeyValuePair<StringKey, TValue>((StringKey)entry.Key, (TValue)entry.Value))
|
||||
.GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(KeyValuePair<StringKey, TValue> item) => _backingDictionary.Add(item.Key, item.Value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _backingDictionary.Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(KeyValuePair<StringKey, TValue> item)
|
||||
{
|
||||
if (_backingDictionary.Contains(item.Key))
|
||||
{
|
||||
var value = _backingDictionary[item.Key];
|
||||
return EqualityComparer<TValue>.Default.Equals((TValue)value, item.Value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(KeyValuePair<StringKey, TValue>[] array, int arrayIndex)
|
||||
{
|
||||
if (array == null)
|
||||
throw new ArgumentNullException(nameof(array));
|
||||
if (arrayIndex < 0 || arrayIndex + Count > array.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
|
||||
|
||||
foreach (var item in this)
|
||||
{
|
||||
array[arrayIndex++] = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Remove(KeyValuePair<StringKey, TValue> item)
|
||||
{
|
||||
if (_backingDictionary.Contains(item.Key))
|
||||
{
|
||||
var value = _backingDictionary[item.Key];
|
||||
if (EqualityComparer<TValue>.Default.Equals((TValue)value, item.Value))
|
||||
{
|
||||
_backingDictionary.Remove(item.Key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{StringKey, TValue}.Count" />
|
||||
public int Count => _backingDictionary.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(StringKey key, TValue value) => _backingDictionary.Add(key, value);
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyDictionary{StringKey, TValue}.ContainsKey" />
|
||||
public bool ContainsKey(StringKey key) => _backingDictionary.Contains(key);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Remove(StringKey key)
|
||||
{
|
||||
if (!_backingDictionary.Contains(key))
|
||||
return false;
|
||||
_backingDictionary.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetValue(StringKey key, out TValue value)
|
||||
{
|
||||
if (_backingDictionary.Contains(key))
|
||||
{
|
||||
value = (TValue)_backingDictionary[key];
|
||||
return true;
|
||||
}
|
||||
value = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TValue this[StringKey key]
|
||||
{
|
||||
get => (TValue)_backingDictionary[key];
|
||||
set => _backingDictionary[key] = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerable<StringKey> IReadOnlyDictionary<StringKey, TValue>.Keys => Keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerable<TValue> IReadOnlyDictionary<StringKey, TValue>.Values => Values;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<StringKey> Keys => _backingDictionary.Keys.Cast<StringKey>().ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<TValue> Values => _backingDictionary.Values.Cast<TValue>().ToList();
|
||||
}
|
@ -30,21 +30,10 @@ public class HighestDamageAI : PokemonAI
|
||||
: battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, 0);
|
||||
}
|
||||
|
||||
var movesWithDamage = moves.Select(move =>
|
||||
var movesWithDamage = moves.Select(move => new
|
||||
{
|
||||
var hitData = new CustomHitData
|
||||
{
|
||||
BasePower = move.MoveData.BasePower,
|
||||
Effectiveness =
|
||||
battle.Library.StaticLibrary.Types.GetEffectiveness(move.MoveData.MoveType, opponent.Types),
|
||||
Type = move.MoveData.MoveType,
|
||||
};
|
||||
return new
|
||||
{
|
||||
Move = move,
|
||||
Damage = battle.Library.DamageCalculator.GetDamage(null, move.MoveData.Category, pokemon, opponent, 1,
|
||||
0, hitData),
|
||||
};
|
||||
Move = move,
|
||||
Damage = AIHelpers.CalculateDamageEstimation(move.MoveData, pokemon, opponent, battle.Library),
|
||||
}).OrderByDescending(x => x.Damage).FirstOrDefault();
|
||||
if (movesWithDamage is null)
|
||||
{
|
||||
@ -53,41 +42,4 @@ public class HighestDamageAI : PokemonAI
|
||||
var bestMove = movesWithDamage.Move;
|
||||
return new MoveChoice(pokemon, bestMove, opponentSide, 0);
|
||||
}
|
||||
|
||||
private class CustomHitData : IHitData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsCritical => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort BasePower { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Effectiveness { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Damage => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeIdentifier? Type { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsContact => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFailed => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Fail()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetFlag(StringKey flag)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(StringKey flag) => false;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Static.Moves;
|
||||
@ -96,4 +97,20 @@ public abstract class PokemonAI
|
||||
yield break;
|
||||
byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0;
|
||||
}
|
||||
|
||||
public static List<PokemonAI> InstantiateAis(IDynamicLibrary library)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes())
|
||||
.Where(type => type.IsSubclassOf(typeof(PokemonAI)) && !type.IsAbstract).Select(x =>
|
||||
{
|
||||
var ctorWithLibrary = x.GetConstructor([typeof(IDynamicLibrary)]);
|
||||
if (ctorWithLibrary != null)
|
||||
return Activator.CreateInstance(x, library);
|
||||
var defaultCtor = x.GetConstructor(Type.EmptyTypes);
|
||||
if (defaultCtor != null)
|
||||
return Activator.CreateInstance(x);
|
||||
throw new InvalidOperationException($"No suitable constructor found for {x.Name}. " +
|
||||
"Ensure it has a constructor with IDynamicLibrary parameter or a default constructor.");
|
||||
}).Cast<PokemonAI>().ToList();
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Libraries;
|
||||
@ -41,6 +43,12 @@ public interface IDynamicLibrary
|
||||
/// A holder of the script types that can be resolved by this library.
|
||||
/// </summary>
|
||||
ScriptResolver ScriptResolver { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The deterministic AI handlers provide a way to access the various handlers used by the
|
||||
/// <see cref="ExplicitAI"/> to make decisions.
|
||||
/// </summary>
|
||||
IReadOnlyExplicitAIHandlers ExplicitAIHandlers { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -55,18 +63,20 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
||||
var load = LibraryLoader.LoadPlugins(plugins);
|
||||
|
||||
return new DynamicLibraryImpl(load.StaticLibrary, load.Registry.BattleStatCalculator!,
|
||||
load.Registry.DamageCalculator!, load.Registry.MiscLibrary!, load.Registry.CaptureLibrary!, load.Resolver);
|
||||
load.Registry.DamageCalculator!, load.Registry.MiscLibrary!, load.Registry.CaptureLibrary!, load.Resolver,
|
||||
load.Registry.ExplicitAIHandlers);
|
||||
}
|
||||
|
||||
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
|
||||
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ICaptureLibrary captureLibrary,
|
||||
ScriptResolver scriptResolver)
|
||||
ScriptResolver scriptResolver, IReadOnlyExplicitAIHandlers explicitAIHandlers)
|
||||
{
|
||||
StaticLibrary = staticLibrary;
|
||||
StatCalculator = statCalculator;
|
||||
DamageCalculator = damageCalculator;
|
||||
MiscLibrary = miscLibrary;
|
||||
ScriptResolver = scriptResolver;
|
||||
ExplicitAIHandlers = explicitAIHandlers;
|
||||
CaptureLibrary = captureLibrary;
|
||||
}
|
||||
|
||||
@ -87,4 +97,7 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScriptResolver ScriptResolver { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyExplicitAIHandlers ExplicitAIHandlers { get; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Libraries;
|
||||
|
@ -220,6 +220,11 @@ public interface IPokemon : IScriptSource, IDeepCloneable
|
||||
/// </summary>
|
||||
ScriptContainer StatusScript { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of turns left for the current non-volatile status.
|
||||
/// </summary>
|
||||
int? GetStatusTurnsLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The volatile status scripts of the Pokemon.
|
||||
/// </summary>
|
||||
@ -782,6 +787,9 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
/// <inheritdoc />
|
||||
public ScriptContainer StatusScript { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScriptSet Volatile { get; }
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Dynamic.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// A plugin is a way to register scripts and other components to the script registry.
|
@ -1,7 +1,7 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders.Models;
|
||||
using PkmnLib.Static;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
namespace PkmnLib.Dynamic.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for plugins that can mutate data.
|
@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using PkmnLib.Static.Libraries;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
namespace PkmnLib.Dynamic.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for plugins that provide resources.
|
@ -1,5 +1,6 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Utils;
|
||||
@ -115,4 +116,5 @@ public class ScriptRegistry
|
||||
internal IDamageCalculator? DamageCalculator => _damageCalculator;
|
||||
internal IMiscLibrary? MiscLibrary => _miscLibrary;
|
||||
internal ICaptureLibrary? CaptureLibrary => _captureLibrary;
|
||||
public ExplicitAIHandlers ExplicitAIHandlers { get; } = new();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
// These interfaces are used for the AI to determine contextual information about the battle.
|
||||
|
||||
/// <summary>
|
||||
/// Script for getting the number of turns left on a script by the AI.
|
||||
/// </summary>
|
||||
public interface IAIInfoScriptNumberTurnsLeft
|
||||
{
|
||||
/// <summary>
|
||||
/// This function returns the number of turns left on the script. This is used for scripts that have a
|
||||
/// limited number of turns, such as Sleep or Confusion.
|
||||
/// </summary>
|
||||
int TurnsLeft();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script for getting the expected end of turn damage by the AI.
|
||||
/// </summary>
|
||||
public interface IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
/// <summary>
|
||||
/// This function returns the expected end of turn damage for the script. This is used for scripts that
|
||||
/// have an end of turn effect, such as Poison or Burn.
|
||||
/// </summary>
|
||||
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
|
||||
}
|
1159
PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs
Normal file
1159
PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -196,4 +196,12 @@ public class MoveDataImpl : IMoveData
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(StringKey key) => _flags.Contains(key);
|
||||
}
|
||||
|
||||
public static class MoveTargetHelpers
|
||||
{
|
||||
public static bool TargetsFoe(this MoveTarget target) =>
|
||||
target is MoveTarget.AdjacentOpponent or MoveTarget.RandomOpponent or MoveTarget.AllAdjacentOpponent
|
||||
or MoveTarget.AllOpponent or MoveTarget.Adjacent or MoveTarget.AllAdjacent or MoveTarget.Any
|
||||
or MoveTarget.All;
|
||||
}
|
@ -139,7 +139,8 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<(Statistic
|
||||
Speed = value;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid statistic.");
|
||||
SetUnknownStat(stat, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +238,9 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<(Statistic
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public bool IsEmpty => Hp.Equals(0) && Attack.Equals(0) && Defense.Equals(0) && SpecialAttack.Equals(0) &&
|
||||
SpecialDefense.Equals(0) && Speed.Equals(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -320,6 +324,9 @@ public abstract record ClampedStatisticSet<T> : StatisticSet<T> where T : struct
|
||||
/// </summary>
|
||||
public record StatBoostStatisticSet : ClampedStatisticSet<sbyte>
|
||||
{
|
||||
public const sbyte MaxStatBoost = 6;
|
||||
public const sbyte MinStatBoost = -6;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override sbyte Min => -6;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Tests.Dataloader;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Tests.Dataloader;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Tests.Dataloader;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Libraries;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Tests.Dataloader;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Libraries;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using PkmnLib.Dynamic.Libraries.DataLoaders;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
namespace PkmnLib.Tests.Dataloader;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Buffers;
|
||||
using System.Text.Json;
|
||||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
337
Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs
Normal file
337
Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs
Normal file
@ -0,0 +1,337 @@
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Plugin.Gen7.Libraries.Battling;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
public static class AIHelperFunctions
|
||||
{
|
||||
public static int GetScoreForTargetStatRaise(int score, AIMoveState move, IPokemon target,
|
||||
StatisticSet<sbyte> statChanges, bool fixedChange = false, bool ignoreContrary = false)
|
||||
{
|
||||
var wholeEffect = move.Move.Category != MoveCategory.Status;
|
||||
|
||||
var desireMult = 1;
|
||||
if (move.User.BattleData?.SideIndex != target.BattleData?.SideIndex)
|
||||
desireMult = -1;
|
||||
|
||||
if (!ignoreContrary && !fixedChange && target.ActiveAbility?.Name == "contrary")
|
||||
{
|
||||
if (desireMult > 0 && wholeEffect)
|
||||
{
|
||||
return ExplicitAI.MoveUselessScore;
|
||||
}
|
||||
return GetScoreForTargetStatDrop(move, target, statChanges, fixedChange, true);
|
||||
}
|
||||
|
||||
var addEffect = move.GetScoreChangeForAdditionalEffect(target);
|
||||
if (addEffect == -999)
|
||||
{
|
||||
return score;
|
||||
}
|
||||
var expectedEndOfTurnDamage = 0;
|
||||
target.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
|
||||
x.ExpectedEndOfTurnDamage(target, ref expectedEndOfTurnDamage));
|
||||
// If the target is expected to faint from the end of turn damage, we don't want to
|
||||
// apply the score for the stat raise, as it will not be able to use it.
|
||||
if (expectedEndOfTurnDamage >= target.CurrentHealth)
|
||||
return wholeEffect ? ExplicitAI.MoveUselessScore : score;
|
||||
|
||||
if (!move.User.HasMoveWithEffect("power_trip"))
|
||||
{
|
||||
var foeIsAware = target.BattleData?.BattleSide.Pokemon.Any(x => x?.ActiveAbility?.Name == "unaware") !=
|
||||
true;
|
||||
if (!foeIsAware)
|
||||
{
|
||||
return wholeEffect ? ExplicitAI.MoveUselessScore : score;
|
||||
}
|
||||
}
|
||||
var realStatChanges = new StatBoostStatisticSet();
|
||||
foreach (var (stat, i) in statChanges)
|
||||
{
|
||||
var increment = i;
|
||||
if (!IsStatRaiseWorthwhile(target, stat, increment, fixedChange))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!fixedChange && target.ActiveAbility?.Name == "simple")
|
||||
{
|
||||
increment *= 2;
|
||||
}
|
||||
increment = (sbyte)Math.Max(increment,
|
||||
StatBoostStatisticSet.MaxStatBoost - target.StatBoost.GetStatistic(stat));
|
||||
realStatChanges.SetStatistic(stat, increment);
|
||||
}
|
||||
if (realStatChanges.IsEmpty)
|
||||
{
|
||||
return wholeEffect ? ExplicitAI.MoveUselessScore : score;
|
||||
}
|
||||
|
||||
score += addEffect;
|
||||
score = GetTargetStatRaiseScoreGeneric(score, target, realStatChanges, move, desireMult);
|
||||
|
||||
foreach (var realStatChange in realStatChanges.Where(x => x.value > 0))
|
||||
{
|
||||
GetTargetStatRaiseScoreOne(ref score, target, realStatChange.statistic, realStatChange.value, move,
|
||||
desireMult);
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
public static int GetScoreForTargetStatDrop(AIMoveState move, IPokemon target, StatisticSet<sbyte> statChanges,
|
||||
bool fixedChange = false, bool ignoreContrary = false) =>
|
||||
throw new NotImplementedException("This method is not implemented");
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a stat raise is worthwhile for the given Pokémon and stat.
|
||||
/// </summary>
|
||||
private static bool IsStatRaiseWorthwhile(IPokemon pokemon, Statistic stat, sbyte amount, bool fixedChange = false)
|
||||
{
|
||||
if (!fixedChange && pokemon.StatBoost.GetStatistic(stat) == StatBoostStatisticSet.MaxStatBoost)
|
||||
return false;
|
||||
if (!pokemon.HasMoveWithEffect("power_trip", "baton_pass"))
|
||||
return true;
|
||||
|
||||
switch (stat)
|
||||
{
|
||||
case Statistic.Attack:
|
||||
{
|
||||
if (!pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical &&
|
||||
x.MoveData.SecondaryEffect?.Name != "foul_play"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.Defense:
|
||||
{
|
||||
var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
|
||||
return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
|
||||
y.MoveData.Category == MoveCategory.Physical || y.MoveData.SecondaryEffect?.Name == "psyshock"));
|
||||
}
|
||||
case Statistic.SpecialAttack:
|
||||
{
|
||||
if (pokemon.Moves.WhereNotNull().All(x => x.MoveData.Category != MoveCategory.Special))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.SpecialDefense:
|
||||
{
|
||||
var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
|
||||
return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
|
||||
y.MoveData.Category == MoveCategory.Special && y.MoveData.SecondaryEffect?.Name != "psyshock"));
|
||||
}
|
||||
case Statistic.Speed:
|
||||
{
|
||||
if (!pokemon.HasMoveWithEffect("electro_ball", "power_trip"))
|
||||
{
|
||||
var targetSpeed = pokemon.BoostedStats.Speed;
|
||||
var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
|
||||
var meaningful = opponentSide.Pokemon.WhereNotNull().Select(opponent => opponent.BoostedStats.Speed)
|
||||
.Any(foeSpeed => targetSpeed < foeSpeed && targetSpeed * 2.5 > foeSpeed);
|
||||
if (!meaningful)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.Accuracy:
|
||||
{
|
||||
var minAccuracy = pokemon.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy);
|
||||
if (minAccuracy >= 90 && pokemon.StatBoost.Accuracy >= 0)
|
||||
{
|
||||
var meaningful = false;
|
||||
var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
|
||||
if (opponentSide.Pokemon.WhereNotNull().Any(x => x.StatBoost.Evasion > 0))
|
||||
{
|
||||
meaningful = true;
|
||||
}
|
||||
if (!meaningful)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment,
|
||||
AIMoveState move, float desireMult = 1)
|
||||
{
|
||||
var oldStage = target.StatBoost.GetStatistic(stat);
|
||||
var newStage = (sbyte)(oldStage + increment);
|
||||
var incMult = Gen7BattleStatCalculator.GetStatBoostModifier(Math.Min(newStage, (sbyte)6)) /
|
||||
Gen7BattleStatCalculator.GetStatBoostModifier(oldStage);
|
||||
var actualIncrement = incMult;
|
||||
incMult -= 1;
|
||||
incMult *= desireMult;
|
||||
var opponentSide = target.BattleData!.Battle.Sides.First(x => x != target.BattleData.BattleSide);
|
||||
|
||||
switch (stat)
|
||||
{
|
||||
case Statistic.Attack:
|
||||
{
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
else
|
||||
{
|
||||
var hasSpecialMoves = target.Moves.WhereNotNull()
|
||||
.Any(x => x.MoveData.Category == MoveCategory.Special);
|
||||
var inc = hasSpecialMoves ? 8 : 12;
|
||||
score += (int)(inc * incMult);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.Defense:
|
||||
{
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
else
|
||||
score += (int)(10 * incMult);
|
||||
break;
|
||||
}
|
||||
case Statistic.SpecialAttack:
|
||||
{
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
else
|
||||
{
|
||||
var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
|
||||
x.MoveData.Category == MoveCategory.Physical &&
|
||||
x.MoveData.SecondaryEffect?.Name != "foul_play");
|
||||
var inc = hasPhysicalMoves ? 8 : 12;
|
||||
score += (int)(inc * incMult);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.SpecialDefense:
|
||||
{
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
else
|
||||
score += (int)(10 * incMult);
|
||||
break;
|
||||
}
|
||||
case Statistic.Speed:
|
||||
{
|
||||
var targetSpeed = target.BoostedStats.Speed;
|
||||
foreach (var opponent in opponentSide.Pokemon.WhereNotNull())
|
||||
{
|
||||
var foeSpeed = opponent.BoostedStats.Speed;
|
||||
if (foeSpeed <= targetSpeed)
|
||||
continue;
|
||||
if (foeSpeed > targetSpeed * 2.5)
|
||||
continue;
|
||||
if (targetSpeed * actualIncrement > foeSpeed)
|
||||
score += (int)(15 * incMult);
|
||||
else
|
||||
score += (int)(8 * incMult);
|
||||
}
|
||||
if (target.HasMoveWithEffect("electro_ball", "power_trip"))
|
||||
{
|
||||
score += (int)(5 * incMult);
|
||||
}
|
||||
if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("gyro_ball")))
|
||||
{
|
||||
score -= (int)(5 * incMult);
|
||||
}
|
||||
if (target.ActiveAbility?.Name == "speed_boost")
|
||||
{
|
||||
score -= (int)(15 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.Accuracy:
|
||||
{
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
{
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
}
|
||||
else
|
||||
{
|
||||
var minAccuracy = target.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy);
|
||||
var previousMinAccuracy =
|
||||
minAccuracy * Gen7BattleStatCalculator.GetAccuracyEvasionStatModifier(0, oldStage);
|
||||
if (previousMinAccuracy < 90)
|
||||
{
|
||||
score += (int)(10 * incMult);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Statistic.Evasion:
|
||||
{
|
||||
foreach (var opponent in opponentSide.Pokemon.WhereNotNull())
|
||||
{
|
||||
var endOfTurnDamage = 0;
|
||||
opponent.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
|
||||
x.ExpectedEndOfTurnDamage(opponent, ref endOfTurnDamage));
|
||||
if (endOfTurnDamage > 0)
|
||||
score += (int)(5 * incMult);
|
||||
}
|
||||
if (oldStage >= 2 && increment == 1)
|
||||
{
|
||||
score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
|
||||
}
|
||||
else
|
||||
{
|
||||
score += (int)(10 * incMult);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target.HasMoveWithEffect("power_trip"))
|
||||
{
|
||||
score += (int)(5 * increment * desireMult);
|
||||
}
|
||||
if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("punishment")))
|
||||
{
|
||||
score -= (int)(5 * increment * desireMult);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the score for the generic concept of raising a target's stats.
|
||||
/// </summary>
|
||||
private static int GetTargetStatRaiseScoreGeneric(int score, IPokemon target, StatisticSet<sbyte> statChanges,
|
||||
AIMoveState move, float desireMult = 1)
|
||||
{
|
||||
var totalIncrement = statChanges.Sum(x => x.value);
|
||||
var turns = target.BattleData!.Battle.CurrentTurnNumber - target.BattleData!.SwitchInTurn;
|
||||
if (turns < 2 && move.Move.Category == MoveCategory.Status)
|
||||
score += (int)(totalIncrement * desireMult * 5);
|
||||
|
||||
score +=
|
||||
(int)(totalIncrement * desireMult * ((100 * (target.CurrentHealth / (float)target.MaxHealth) - 50) / 8));
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int GetScoreChangeForAdditionalEffect(this AIMoveState move, IPokemon? target)
|
||||
{
|
||||
if (move.Move.SecondaryEffect is null)
|
||||
return 0;
|
||||
if (move.User.ActiveAbility?.Name == "sheer_force")
|
||||
return -999;
|
||||
if (target is not null && target.BattleData?.Position != move.User.BattleData?.Position &&
|
||||
target.ActiveAbility?.Name == "shield_dust")
|
||||
return -999;
|
||||
|
||||
if ((move.Move.SecondaryEffect.Chance < 100 && move.User.ActiveAbility?.Name == "serene_grace") ||
|
||||
move.User.BattleData?.BattleSide.VolatileScripts.Contains<RainbowEffect>() == true)
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool HasMoveWithEffect(this IPokemon pokemon, params StringKey[] effect)
|
||||
{
|
||||
return pokemon.Moves.WhereNotNull().Any(move => move.MoveData.SecondaryEffect?.Name is not null &&
|
||||
effect.Contains(move.MoveData.SecondaryEffect.Name));
|
||||
}
|
||||
|
||||
private static bool Opposes(this IPokemon pokemon, IPokemon target) =>
|
||||
pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method), MeansImplicitUse]
|
||||
public class AIMoveScoreFunctionAttribute : Attribute
|
||||
{
|
||||
}
|
70
Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctions.cs
Normal file
70
Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctions.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
public static class ExplicitAIFunctions
|
||||
{
|
||||
public static void RegisterAIFunctions(ExplicitAIHandlers handlers)
|
||||
{
|
||||
var baseType = typeof(Script);
|
||||
foreach (var type in typeof(ExplicitAIFunctions).Assembly.GetTypes().Where(t => baseType.IsAssignableFrom(t)))
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<ScriptAttribute>();
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
if (attribute.Category == ScriptCategory.Move)
|
||||
{
|
||||
// Check if the move type has a static function in the following format:
|
||||
// public static void AIMoveEffectScore(MoveOption option, ref int score);
|
||||
var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
||||
m.GetCustomAttribute<AIMoveScoreFunctionAttribute>() != null && m.ReturnType == typeof(void) &&
|
||||
m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == typeof(MoveOption) &&
|
||||
m.GetParameters()[1].ParameterType == typeof(int).MakeByRefType());
|
||||
if (method != null)
|
||||
{
|
||||
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
|
||||
var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score");
|
||||
var functionExpression = Expression.Lambda<AIScoreMoveHandler>(
|
||||
Expression.Call(null, method, optionParam, scoreParam), optionParam, scoreParam).Compile();
|
||||
handlers.MoveEffectScore.Add(attribute.Name, functionExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlers.GeneralMoveAgainstTargetScore.Add("predicated_damage", PredictedDamageScore);
|
||||
}
|
||||
|
||||
private static void PredictedDamageScore(MoveOption option, ref int score)
|
||||
{
|
||||
var target = option.Target;
|
||||
if (target == null)
|
||||
return;
|
||||
if (option.Move.Move.Category == MoveCategory.Status)
|
||||
return;
|
||||
var damage = option.EstimatedDamage;
|
||||
if (target.Volatile.TryGet<SubstituteEffect>(out var substitute))
|
||||
{
|
||||
var health = substitute.Health;
|
||||
score += (int)Math.Min(15.0f * damage / health, 20);
|
||||
return;
|
||||
}
|
||||
|
||||
score += (int)Math.Min(15.0f * damage / target.CurrentHealth, 30);
|
||||
|
||||
if (damage > target.CurrentHealth * 1.1f)
|
||||
{
|
||||
score += 10;
|
||||
if ((option.Move.Move.HasFlag("multi_hit") && target.CurrentHealth == target.MaxHealth &&
|
||||
target.ActiveAbility?.Name == "sturdy") || target.HasHeldItem("focus_sash"))
|
||||
{
|
||||
score += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -438,7 +438,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -704,7 +705,8 @@
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror",
|
||||
"ballistics"
|
||||
"ballistics",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -770,7 +772,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "beat_up"
|
||||
@ -1101,7 +1104,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -1413,7 +1417,8 @@
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror",
|
||||
"ballistics"
|
||||
"ballistics",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -1747,7 +1752,8 @@
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror",
|
||||
"punch"
|
||||
"punch",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -2580,7 +2586,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_hit_move"
|
||||
@ -2616,7 +2623,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -3681,7 +3689,8 @@
|
||||
"category": "special",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "fire_spin"
|
||||
@ -4263,7 +4272,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -4299,7 +4309,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -5726,7 +5737,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -6579,7 +6591,8 @@
|
||||
"category": "special",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "magma_storm"
|
||||
@ -7895,7 +7908,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -9068,7 +9082,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -10663,7 +10678,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -11540,7 +11556,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
@ -12168,7 +12185,8 @@
|
||||
"flags": [
|
||||
"contact",
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "triple_kick"
|
||||
@ -12224,7 +12242,8 @@
|
||||
"category": "physical",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "twineedle"
|
||||
@ -12550,7 +12569,8 @@
|
||||
"category": "special",
|
||||
"flags": [
|
||||
"protect",
|
||||
"mirror"
|
||||
"mirror",
|
||||
"multi_hit"
|
||||
],
|
||||
"effect": {
|
||||
"name": "2_5_hit_move"
|
||||
|
@ -1,3 +1,5 @@
|
||||
using PkmnLib.Dynamic.Plugins;
|
||||
using PkmnLib.Plugin.Gen7.AI;
|
||||
using PkmnLib.Plugin.Gen7.Libraries.Battling;
|
||||
using PkmnLib.Static.Libraries;
|
||||
using PkmnLib.Static.Species;
|
||||
@ -47,6 +49,8 @@ public class Gen7Plugin : Plugin<Gen7PluginConfiguration>, IResourceProvider
|
||||
registry.RegisterDamageCalculator(new Gen7DamageCalculator(Configuration));
|
||||
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
|
||||
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
|
||||
|
||||
ExplicitAIFunctions.RegisterAIFunctions(registry.ExplicitAIHandlers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -40,7 +40,7 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
|
||||
public uint CalculateBoostedStat(IPokemon pokemon, Statistic stat)
|
||||
{
|
||||
var flatStat = CalculateFlatStat(pokemon, stat);
|
||||
var boostModifier = GetStatBoostModifier(pokemon, stat);
|
||||
var boostModifier = GetStatBoostModifier(pokemon.StatBoost.GetStatistic(stat));
|
||||
var boostedStat = flatStat * boostModifier;
|
||||
if (boostedStat > uint.MaxValue)
|
||||
boostedStat = uint.MaxValue;
|
||||
@ -71,13 +71,7 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
|
||||
if (ignoreEvasion)
|
||||
targetEvasion = 0;
|
||||
var userAccuracy = executingMove.User.StatBoost.Accuracy;
|
||||
var difference = targetEvasion - userAccuracy;
|
||||
var statModifier = difference switch
|
||||
{
|
||||
> 0 => 3.0f / (3.0f + Math.Min(difference, 6)),
|
||||
< 0 => 3.0f + -Math.Max(difference, -6) / 3.0f,
|
||||
_ => 1.0f,
|
||||
};
|
||||
var statModifier = GetAccuracyEvasionStatModifier(targetEvasion, userAccuracy);
|
||||
modifiedAccuracy = (int)(modifiedAccuracy * statModifier);
|
||||
modifiedAccuracy = modifiedAccuracy switch
|
||||
{
|
||||
@ -116,10 +110,9 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
|
||||
return (uint)modified;
|
||||
}
|
||||
|
||||
private static float GetStatBoostModifier(IPokemon pokemon, Statistic statistic)
|
||||
public static float GetStatBoostModifier(sbyte amount)
|
||||
{
|
||||
var boost = pokemon.StatBoost.GetStatistic(statistic);
|
||||
return boost switch
|
||||
return amount switch
|
||||
{
|
||||
-6 => 2.0f / 8.0f,
|
||||
-5 => 2.0f / 7.0f,
|
||||
@ -134,7 +127,18 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
|
||||
4 => 6.0f / 2.0f,
|
||||
5 => 7.0f / 2.0f,
|
||||
6 => 8.0f / 2.0f,
|
||||
_ => throw new ArgumentException($"Stat boost was out of expected range of -6 to 6: {boost}"),
|
||||
_ => throw new ArgumentException($"Stat boost was out of expected range of -6 to 6: {amount}"),
|
||||
};
|
||||
}
|
||||
|
||||
public static float GetAccuracyEvasionStatModifier(sbyte evasion, sbyte accuracy)
|
||||
{
|
||||
var difference = evasion - accuracy;
|
||||
return difference switch
|
||||
{
|
||||
> 0 => 3.0f / (3.0f + Math.Min(difference, 6)),
|
||||
< 0 => 3.0f + -Math.Max(difference, -6) / 3.0f,
|
||||
_ => 1.0f,
|
||||
};
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
|
||||
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Dry_Skin_(Ability)">Bulbapedia - Dry Skin</see>
|
||||
/// </summary>
|
||||
[Script(ScriptCategory.Ability, "dry_skin")]
|
||||
public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn
|
||||
public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private IPokemon? _owningPokemon;
|
||||
|
||||
@ -55,4 +55,13 @@ public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn
|
||||
_owningPokemon.Damage(_owningPokemon.MaxHealth / 8, DamageSource.Weather);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
if (pokemon.BattleData?.Battle.WeatherName == ScriptUtils.ResolveName<Weather.HarshSunlight>())
|
||||
{
|
||||
damage += (int)(pokemon.MaxHealth / 8f);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,8 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
|
||||
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Solar_Power_(Ability)">Bulbapedia - Solar Power</see>
|
||||
/// </summary>
|
||||
[Script(ScriptCategory.Ability, "solar_power")]
|
||||
public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndTurn
|
||||
public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndTurn,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void ChangeOffensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint defensiveStat,
|
||||
@ -29,6 +30,26 @@ public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndT
|
||||
if (!pokemon.IsUsable)
|
||||
return;
|
||||
|
||||
var weatherName = pokemon.BattleData?.Battle.WeatherName;
|
||||
if (weatherName != ScriptUtils.ResolveName<Weather.HarshSunlight>() &&
|
||||
weatherName != ScriptUtils.ResolveName<Weather.DesolateLands>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pokemon.Damage(pokemon.MaxHealth / 8, DamageSource.Weather);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
var weatherName = pokemon.BattleData?.Battle.WeatherName;
|
||||
if (weatherName != ScriptUtils.ResolveName<Weather.HarshSunlight>() &&
|
||||
weatherName != ScriptUtils.ResolveName<Weather.DesolateLands>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
damage += (int)(pokemon.MaxHealth / 8f);
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
|
||||
public abstract class ChangeUserStats : Script, IScriptOnInitialize, IScriptOnSecondaryEffect
|
||||
@ -31,6 +34,19 @@ public abstract class ChangeUserStats : Script, IScriptOnInitialize, IScriptOnSe
|
||||
{
|
||||
move.User.ChangeStatBoost(_stat, _amount, true, false);
|
||||
}
|
||||
|
||||
protected static void GetMoveEffectScore(MoveOption option, Statistic stat, ref int score)
|
||||
{
|
||||
if (option.Move.Move.SecondaryEffect == null ||
|
||||
!option.Move.Move.SecondaryEffect.Parameters.TryGetValue("amount", out var amountObj) ||
|
||||
amountObj is not int amount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var statisticSet = new StatBoostStatisticSet();
|
||||
statisticSet.SetStatistic(stat, (sbyte)amount);
|
||||
score = AIHelperFunctions.GetScoreForTargetStatRaise(score, option.Move, option.Move.User, statisticSet);
|
||||
}
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_attack")]
|
||||
@ -39,6 +55,10 @@ public class ChangeUserAttack : ChangeUserStats
|
||||
public ChangeUserAttack() : base(Statistic.Attack)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.Attack, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_defense")]
|
||||
@ -47,6 +67,10 @@ public class ChangeUserDefense : ChangeUserStats
|
||||
public ChangeUserDefense() : base(Statistic.Defense)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.Defense, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_special_attack")]
|
||||
@ -55,6 +79,10 @@ public class ChangeUserSpecialAttack : ChangeUserStats
|
||||
public ChangeUserSpecialAttack() : base(Statistic.SpecialAttack)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.SpecialAttack, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_special_defense")]
|
||||
@ -63,6 +91,10 @@ public class ChangeUserSpecialDefense : ChangeUserStats
|
||||
public ChangeUserSpecialDefense() : base(Statistic.SpecialDefense)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.SpecialDefense, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_speed")]
|
||||
@ -71,6 +103,10 @@ public class ChangeUserSpeed : ChangeUserStats
|
||||
public ChangeUserSpeed() : base(Statistic.Speed)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.Speed, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_accuracy")]
|
||||
@ -79,6 +115,10 @@ public class ChangeUserAccuracy : ChangeUserStats
|
||||
public ChangeUserAccuracy() : base(Statistic.Accuracy)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.Accuracy, ref score);
|
||||
}
|
||||
|
||||
[Script(ScriptCategory.Move, "change_user_evasion")]
|
||||
@ -87,4 +127,8 @@ public class ChangeUserEvasion : ChangeUserStats
|
||||
public ChangeUserEvasion() : base(Statistic.Evasion)
|
||||
{
|
||||
}
|
||||
|
||||
[AIMoveScoreFunction]
|
||||
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
|
||||
GetMoveEffectScore(option, Statistic.Evasion, ref score);
|
||||
}
|
@ -13,11 +13,13 @@ public class MirrorMove : Script, IScriptChangeMove
|
||||
return;
|
||||
|
||||
var battle = battleData.Battle;
|
||||
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
|
||||
if (battle.ChoiceQueue == null)
|
||||
return;
|
||||
var currentTurn = battle.ChoiceQueue.LastRanChoice;
|
||||
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
|
||||
.TakeWhile(x => x != currentTurn).LastOrDefault(x => x.TargetPosition == choice.TargetPosition &&
|
||||
x.TargetSide == choice.TargetSide &&
|
||||
x.User.BattleData?.IsOnBattlefield == true);
|
||||
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.TargetPosition == choice.TargetPosition &&
|
||||
x.TargetSide == choice.TargetSide &&
|
||||
x.User.BattleData?.IsOnBattlefield == true);
|
||||
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
|
||||
{
|
||||
choice.Fail();
|
||||
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "bind")]
|
||||
public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway
|
||||
public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon? _owner;
|
||||
private int _turns;
|
||||
@ -33,4 +34,10 @@ public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IS
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent) => prevent = _turns > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_owner?.MaxHealth * _percentOfMaxHealth ?? 0);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "fire_spin")]
|
||||
public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch
|
||||
public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon _owner;
|
||||
|
||||
@ -21,4 +22,10 @@ public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAwa
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(pokemon.MaxHealth / 8f);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "ghostcurse")]
|
||||
public class GhostCurseEffect : Script, IScriptOnEndTurn
|
||||
public class GhostCurseEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private IPokemon _pokemon;
|
||||
|
||||
@ -15,4 +15,10 @@ public class GhostCurseEffect : Script, IScriptOnEndTurn
|
||||
{
|
||||
_pokemon.Damage(_pokemon.CurrentHealth / 4, DamageSource.Misc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_pokemon.CurrentHealth / 4f);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "infestation")]
|
||||
public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway
|
||||
public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon _owner;
|
||||
private int _turns;
|
||||
@ -30,4 +31,10 @@ public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwi
|
||||
RemoveSelf();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_owner.MaxHealth / 8f);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "leech_seed")]
|
||||
public class LeechSeedEffect : Script, IScriptOnEndTurn
|
||||
public class LeechSeedEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon _owner;
|
||||
private readonly IPokemon _placer;
|
||||
@ -15,7 +15,7 @@ public class LeechSeedEffect : Script, IScriptOnEndTurn
|
||||
/// <inheritdoc />
|
||||
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||
{
|
||||
var damage = _owner.BoostedStats.Hp / 8;
|
||||
var damage = _owner.MaxHealth / 8;
|
||||
if (_owner.CurrentHealth <= damage)
|
||||
damage = _owner.CurrentHealth;
|
||||
|
||||
@ -25,4 +25,10 @@ public class LeechSeedEffect : Script, IScriptOnEndTurn
|
||||
else
|
||||
_placer.Heal(damage);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_owner.MaxHealth / 8f);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "magma_storm")]
|
||||
public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch
|
||||
public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon _owner;
|
||||
|
||||
@ -21,4 +22,10 @@ public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunA
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(pokemon.MaxHealth / 16f);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "nightmare")]
|
||||
public class NightmareEffect : Script, IScriptOnEndTurn
|
||||
public class NightmareEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private readonly IPokemon _owner;
|
||||
|
||||
@ -23,4 +23,10 @@ public class NightmareEffect : Script, IScriptOnEndTurn
|
||||
var maxHp = _owner.MaxHealth;
|
||||
_owner.Damage(maxHp / 4, DamageSource.Misc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
damage += (int)(_owner.MaxHealth / 4f);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
[Script(ScriptCategory.Pokemon, "substitute")]
|
||||
public class SubstituteEffect(uint health) : Script, IScriptBlockIncomingHit
|
||||
{
|
||||
private uint _health = health;
|
||||
public uint Health { get; private set; } = health;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
|
||||
@ -19,12 +19,12 @@ public class SubstituteEffect(uint health) : Script, IScriptBlockIncomingHit
|
||||
|
||||
block = true;
|
||||
var damage = executingMove.GetHitData(target, hitIndex).Damage;
|
||||
if (damage >= _health)
|
||||
if (damage >= Health)
|
||||
{
|
||||
executingMove.Battle.EventHook.Invoke(new DialogEvent("substitute_broken"));
|
||||
RemoveSelf();
|
||||
return;
|
||||
}
|
||||
_health -= damage;
|
||||
Health -= damage;
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
|
||||
[Script(ScriptCategory.Pokemon, "whirlpool")]
|
||||
public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentRunAway, IScriptPreventOpponentSwitch
|
||||
public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentRunAway, IScriptPreventOpponentSwitch,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
public record PokemonTurn
|
||||
{
|
||||
@ -80,4 +81,14 @@ public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentR
|
||||
_targetedPokemon.Remove(turn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
var turn = _targetedPokemon.FirstOrDefault(x => x.Pokemon == pokemon);
|
||||
if (turn != null)
|
||||
{
|
||||
damage += (int)(pokemon.MaxHealth * turn.DamagePercent);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
|
||||
[Script(ScriptCategory.Status, "badly_poisoned")]
|
||||
public class BadlyPoisoned : Poisoned, IScriptOnEndTurn
|
||||
public class BadlyPoisoned : Poisoned
|
||||
{
|
||||
private int _turns = 1;
|
||||
|
||||
|
@ -3,7 +3,7 @@ using PkmnLib.Static.Moves;
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
|
||||
[Script(ScriptCategory.Status, "burned")]
|
||||
public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn
|
||||
public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private IPokemon? _target;
|
||||
|
||||
@ -41,4 +41,11 @@ public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn
|
||||
});
|
||||
_target.Damage(damage, DamageSource.Status, eventBatch);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
if (_target != null)
|
||||
damage += (int)(_target.MaxHealth / 16f);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
|
||||
[Script(ScriptCategory.Status, "poisoned")]
|
||||
public class Poisoned : Script, IScriptOnEndTurn
|
||||
public class Poisoned : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private IPokemon? _pokemon;
|
||||
|
||||
@ -46,4 +46,11 @@ public class Poisoned : Script, IScriptOnEndTurn
|
||||
else
|
||||
_pokemon.Damage(damage, DamageSource.Status, eventBatchId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
if (_pokemon != null)
|
||||
damage += (int)(_pokemon.MaxHealth * GetPoisonMultiplier());
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
|
||||
[Script(ScriptCategory.Status, "sleep")]
|
||||
public class Sleep : Script, IScriptPreventMove
|
||||
public class Sleep : Script, IScriptPreventMove, IAIInfoScriptNumberTurnsLeft
|
||||
{
|
||||
private IPokemon? _pokemon;
|
||||
public int Turns { get; set; }
|
||||
@ -54,4 +54,7 @@ public class Sleep : Script, IScriptPreventMove
|
||||
{ "pokemon", _pokemon },
|
||||
}));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TurnsLeft() => Turns;
|
||||
}
|
@ -27,7 +27,7 @@ public class PsychicTerrain : Script, IScriptIsInvulnerableToMove, IScriptChange
|
||||
if (!IsAffectedByTerrain(target))
|
||||
return;
|
||||
|
||||
// Psychic Terrain prevents priority moves from affecting affected Pokémon.
|
||||
// Psychic Terrain prevents priority moves from affected Pokémon.
|
||||
if (move.MoveChoice.Priority > 0)
|
||||
{
|
||||
invulnerable = true;
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Weather;
|
||||
|
||||
[Script(ScriptCategory.Weather, "hail")]
|
||||
public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn
|
||||
public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
private int? _duration;
|
||||
|
||||
@ -46,4 +46,15 @@ public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn
|
||||
battle.SetWeather(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
if (pokemon.Types.Any(x => x.Name == "ice"))
|
||||
return; // Ice types are immune to Hail damage.
|
||||
if (_duration.HasValue)
|
||||
{
|
||||
damage += (int)(pokemon.MaxHealth / 16f);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
namespace PkmnLib.Plugin.Gen7.Scripts.Weather;
|
||||
|
||||
[Script(ScriptCategory.Weather, "sandstorm")]
|
||||
public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveStatValue, IScriptOnEndTurn
|
||||
public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveStatValue, IScriptOnEndTurn,
|
||||
IAIInfoScriptExpectedEndOfTurnDamage
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||
@ -39,4 +40,12 @@ public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveS
|
||||
if (move.UseMove.Name == "solar_beam")
|
||||
basePower /= 2;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
|
||||
{
|
||||
if (pokemon.Types.Any(x => x.Name == "rock" || x.Name == "ground" || x.Name == "steel"))
|
||||
return; // Rock, Ground, and Steel types are immune to Sandstorm damage.
|
||||
damage += (int)(pokemon.MaxHealth / 16f);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user