diff --git a/AI/AIRunner/Program.cs b/AI/AIRunner/Program.cs index 5a7ded5..426ed3c 100644 --- a/AI/AIRunner/Program.cs +++ b/AI/AIRunner/Program.cs @@ -5,6 +5,8 @@ using PkmnLib.Dynamic.AI; using PkmnLib.Dynamic.Libraries; using PkmnLib.Plugin.Gen7; using Serilog; +using Serilog.Core; +using Serilog.Events; namespace AIRunner; @@ -12,9 +14,12 @@ internal static class Program { private static List? _availableAIs; + internal static LoggingLevelSwitch LogLevelSwitch { get; } = new(LogEventLevel.Information); + private static Task Main(string[] args) { - Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.Console().CreateLogger(); + Log.Logger = new LoggerConfiguration().MinimumLevel.ControlledBy(LogLevelSwitch).WriteTo.Console() + .CreateLogger(); Log.Information("Starting AI Runner..."); AILogging.LogHandler = Log.Debug; var library = DynamicLibraryImpl.Create([ @@ -24,6 +29,10 @@ internal static class Program var testCommand = new Command("test", "Run two AIs against each other") { + new Option("--debug") + { + Description = "Enable debug logging.", + }, new Option("--ai1") { Description = "The name of the first AI script to run against the second AI.", @@ -55,6 +64,9 @@ internal static class Program }; testCommand.SetAction(result => { + var debug = result.GetValue("--debug"); + LogLevelSwitch.MinimumLevel = debug ? LogEventLevel.Debug : LogEventLevel.Information; + var ai1Name = result.GetRequiredValue("--ai1"); var ai2Name = result.GetRequiredValue("--ai2"); var ai1 = _availableAIs!.First(a => diff --git a/AI/AIRunner/TestCommandRunner.cs b/AI/AIRunner/TestCommandRunner.cs index c66a01f..9e79010 100644 --- a/AI/AIRunner/TestCommandRunner.cs +++ b/AI/AIRunner/TestCommandRunner.cs @@ -32,6 +32,7 @@ public static class TestCommandRunner randoms[i] = new RandomImpl(rootRandom.GetInt()); battleTasks[i] = Task.CompletedTask; // Initialize tasks to avoid null references } + const int pokemonPerParty = 6; // Show a progress bar if debug logging is not enabled. // We disable this if debug logging is on, to prevent annoying console output where the progress bar is drawn in @@ -55,7 +56,7 @@ public static class TestCommandRunner { Log.Debug("Battle {BattleNumber}: {AI1} vs {AI2}", index + 1, ai1.Name, ai2.Name); var random = randoms[taskIndex]; - var battle = GenerateBattle(library, 3, random); + var battle = GenerateBattle(library, pokemonPerParty, random); var timePerTurn = new List(20); while (!battle.HasEnded) { diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs new file mode 100644 index 0000000..2cad868 --- /dev/null +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs @@ -0,0 +1,153 @@ +using System.Diagnostics.CodeAnalysis; +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Static.Moves; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.AI.Explicit; + +public partial class ExplicitAI +{ + private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves, + [NotNullWhen(true)] out ITurnChoice? choice) + { + choice = null; + if (battle.IsWildBattle) + return false; + if (TrainerHighSkill) + { + var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide); + var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack); + if (!foeCanAct) + return false; + } + var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex( + new ResponsibleIndex(pokemon.BattleData!.SideIndex, pokemon.BattleData.Position))); + if (party is null) + return false; + var usablePokemon = party.GetUsablePokemonNotInField().ToList(); + if (!terribleMoves) + { + if (!_skillFlags.ConsiderSwitching) + return false; + if (!usablePokemon.Any()) + return false; + var shouldSwitch = _handlers.ShouldSwitch(this, pokemon, battle, usablePokemon); + if (shouldSwitch && TrainerMediumSkill) + { + if (_handlers.ShouldNotSwitch(this, pokemon, battle, usablePokemon)) + { + shouldSwitch = false; + } + } + if (!shouldSwitch) + return false; + } + var battleSide = pokemon.BattleData!.BattleSide; + var bestReplacement = + ChooseBestReplacementPokemon(pokemon.BattleData!.Position, terribleMoves, usablePokemon, battleSide); + if (bestReplacement is null) + { + AILogging.LogInformation( + $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}."); + return false; + } + choice = new SwitchChoice(pokemon, bestReplacement); + return true; + } + + private IPokemon? ChooseBestReplacementPokemon(byte position, bool terribleMoves, + IReadOnlyList usablePokemon, IBattleSide battleSide) + { + var options = usablePokemon.Where((pokemon, index) => + { + if (_skillFlags.ReserveLastPokemon && index == usablePokemon.Count - 1 && usablePokemon.Count > 1) + return false; // Don't switch to the last Pokemon if there are others available. + if (_skillFlags.UsePokemonInOrder && index != 0) + return false; + return true; + }).ToList(); + if (options.Count == 0) + return null; + var ratedOptions = options + .Select(pokemon => new { Pokemon = pokemon, Score = RateReplacementPokemon(pokemon, battleSide) }) + .OrderBy(x => x.Score).ToList(); + if (TrainerHighSkill && !terribleMoves) + { + if (ratedOptions.First().Score < 100) + return null; + } + return ratedOptions.First().Pokemon; + } + + private static readonly StringKey HeavyDutyBootsName = "heavy_duty_boots"; + private static readonly StringKey ToxicSpikesName = "toxic_spikes"; + private static readonly StringKey StickyWebName = "sticky_web"; + + private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide) + { + var score = 0; + var types = pokemon.Types; + var entryDamage = CalculateEntryHazardDamage(pokemon, battleSide); + if (entryDamage >= pokemon.CurrentHealth) + score -= 50; + else if (entryDamage > 0) + score -= 50 * (int)Math.Round((double)entryDamage / pokemon.MaxHealth, MidpointRounding.AwayFromZero); + if (!pokemon.HasHeldItem(HeavyDutyBootsName) && !pokemon.IsFloating) + { + if (battleSide.VolatileScripts.Contains(ToxicSpikesName) && CanBePoisoned(pokemon, battleSide.Battle)) + { + score -= 20; + } + if (battleSide.VolatileScripts.Contains(StickyWebName)) + { + score -= 15; + } + } + var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide); + foreach (var foe in opponentSide.Pokemon.WhereNotNull()) + { + var lastMoveUsed = foe.BattleData?.LastMoveChoice; + if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status) + continue; + var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType; + var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveType, types); + score -= (int)(lastMoveUsed.ChosenMove.MoveData.BasePower * effectiveness / 5); + } + foreach (var learnedMove in pokemon.Moves.WhereNotNull()) + { + if (learnedMove.MoveData.BasePower == 0 || learnedMove is { CurrentPp: 0, MaxPp: > 0 }) + continue; + foreach (var foe in opponentSide.Pokemon.WhereNotNull()) + { + if (CanAbsorbMove(foe, learnedMove.MoveData, learnedMove.MoveData.MoveType, battleSide.Battle)) + continue; + var effectiveness = + pokemon.Library.StaticLibrary.Types.GetEffectiveness(learnedMove.MoveData.MoveType, foe.Types); + score += (int)(learnedMove.MoveData.BasePower * effectiveness / 10); + } + } + + return score; + } + + public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side) + { + var damage = 0u; + side.RunScriptHook(x => x.ExpectedEntryDamage(pokemon, ref damage)); + return damage; + } + + private static bool CanSwitch(IPokemon pokemon) + { + var battleData = pokemon.BattleData; + if (battleData == null) + return false; + if (battleData.Battle.IsWildBattle) + return false; + var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x => + x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position))); + return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField(); + } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs new file mode 100644 index 0000000..26e59aa --- /dev/null +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs @@ -0,0 +1,89 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Static; +using PkmnLib.Static.Moves; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.AI.Explicit; + +public partial class ExplicitAI +{ + private static readonly StringKey MistyTerrainName = "misty_terrain"; + private static readonly StringKey PoisonName = "poison"; + private static readonly StringKey SteelName = "steel"; + private static readonly StringKey ImmunityName = "immunity"; + private static readonly StringKey PastelVeilName = "pastel_veil"; + private static readonly StringKey FlowerVeilName = "flower_veil"; + private static readonly StringKey LeafGuardName = "leaf_guard"; + private static readonly StringKey ComatoseName = "comatose"; + private static readonly StringKey ShieldsDownName = "shields_down"; + private static readonly StringKey HarshSunlightName = "harsh_sunlight"; + private static readonly StringKey DesolateLandsName = "desolate_lands"; + private static readonly StringKey BulletproofName = "bulletproof"; + private static readonly StringKey FlashFireName = "flash_fire"; + private static readonly StringKey LightningRodName = "lightning_rod"; + private static readonly StringKey MotorDriveName = "motor_drive"; + private static readonly StringKey VoltAbsorbName = "volt_absorb"; + private static readonly StringKey SapSipperName = "sap_sipper"; + private static readonly StringKey SoundproofName = "soundproof"; + private static readonly StringKey StormDrainName = "storm_drain"; + private static readonly StringKey WaterAbsorbName = "water_absorb"; + private static readonly StringKey DrySkinName = "dry_skin"; + private static readonly StringKey TelepathyName = "telepathy"; + private static readonly StringKey WonderGuardName = "wonder_guard"; + private static readonly StringKey FireName = "fire"; + private static readonly StringKey ElectricName = "electric"; + private static readonly StringKey WaterName = "water"; + + private static bool CanBePoisoned(IPokemon pokemon, IBattle battle) + { + if (battle.TerrainName == MistyTerrainName) + return false; + if (pokemon.Types.Any(x => x.Name == PoisonName || x.Name == SteelName)) + return false; + if (pokemon.ActiveAbility?.Name == ImmunityName) + return false; + if (pokemon.ActiveAbility?.Name == PastelVeilName) + return false; + if (pokemon.ActiveAbility?.Name == FlowerVeilName && pokemon.Types.Any(x => x.Name == GrassName)) + return false; + if ((pokemon.ActiveAbility?.Name == LeafGuardName && battle.WeatherName == HarshSunlightName) || + battle.WeatherName == DesolateLandsName) + return false; + if (pokemon.ActiveAbility?.Name == ComatoseName && pokemon.Species.Name == "komala") + return false; + if (pokemon.ActiveAbility?.Name == ShieldsDownName && pokemon.Species.Name == "minior" && + pokemon.Form.Name.Contains("-meteor")) + return false; + return true; + } + + private static bool CanAbsorbMove(IPokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle) + { + if (pokemon.ActiveAbility == null) + return false; + + var abilityName = pokemon.ActiveAbility.Name; + + if (abilityName == BulletproofName) + return move.HasFlag("bomb"); + if (abilityName == FlashFireName) + return moveType.Name == FireName; + if (abilityName == LightningRodName || abilityName == MotorDriveName || abilityName == VoltAbsorbName) + return moveType.Name == ElectricName; + if (abilityName == SapSipperName) + return moveType.Name == GrassName; + if (abilityName == SoundproofName) + return move.HasFlag("sound"); + if (abilityName == StormDrainName || abilityName == WaterAbsorbName || abilityName == DrySkinName) + return moveType.Name == WaterName; + if (abilityName == TelepathyName) + return false; + if (abilityName == WonderGuardName) + { + var effectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(moveType, pokemon.Types); + return effectiveness <= 1.0f; + } + + return false; + } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs index 65fcf2e..8667cef 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.BattleFlow; using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models; @@ -8,21 +9,27 @@ using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.AI.Explicit; +public interface IExplicitAI +{ + public bool TrainerHighSkill { get; } + public bool TrainerMediumSkill { get; } + public IRandom Random { get; } +} + /// /// An explicit AI that has explicitly written logic for each Pokémon and move. /// /// /// This is heavily based on the AI used in Pokémon Essentials /// -public class ExplicitAI : PokemonAI +public partial class ExplicitAI : PokemonAI, IExplicitAI { 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 SkillFlags _skillFlags = new(); private float MoveScoreThreshold => (float)(0.6f + 0.35f * Math.Sqrt(Math.Min(TrainerSkill, 100) / 100f)); @@ -30,17 +37,39 @@ public class ExplicitAI : PokemonAI private readonly IRandom _random = new RandomImpl(); + public class SkillFlags + { + // TODO: Make these configurable + public bool CanPredictMoveFailure { get; set; } = true; + public bool ScoreMoves { get; set; } = true; + public bool ConsiderSwitching { get; set; } = true; + public bool ReserveLastPokemon { get; set; } = true; + public bool UsePokemonInOrder { get; set; } = true; + } + /// public ExplicitAI(IDynamicLibrary library) : base("explicit") { _handlers = library.ExplicitAIHandlers; } + public bool TrainerHighSkill => TrainerSkill >= 45; + public bool TrainerMediumSkill => TrainerSkill >= 32; + + /// + public IRandom Random => _random; + /// public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) { if (battle.HasForcedTurn(pokemon, out var choice)) return choice; + if (TryChooseToSwitchOut(battle, pokemon, false, out var turnChoice)) + { + AILogging.LogInformation($"{pokemon} is switching out."); + return turnChoice; + } + var moveChoices = GetMoveScores(pokemon, battle); if (moveChoices.Count == 0) { @@ -48,7 +77,29 @@ public class ExplicitAI : PokemonAI 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 + if (TrainerHighSkill && CanSwitch(pokemon)) + { + var badMoves = false; + if (maxScore <= MoveUselessScore) + { + badMoves = CanAttack(pokemon); + if (!badMoves && _random.GetInt(100) < 25) + badMoves = true; + } + else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 && + _random.GetInt(100) < 80) + { + badMoves = true; + } + if (badMoves) + { + AILogging.LogInformation($"{pokemon} has no good moves, considering switching."); + if (TryChooseToSwitchOut(battle, pokemon, badMoves, out var switchChoice)) + { + return switchChoice; + } + } + } var threshold = (float)Math.Floor(maxScore * MoveScoreThreshold); var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray(); @@ -122,7 +173,7 @@ public class ExplicitAI : PokemonAI } } var aiMove = new AIMoveState(user, moveData); - if (CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove)) + if (_skillFlags.CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove)) { AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail."); AddMoveToChoices(index, MoveFailScore); @@ -210,8 +261,8 @@ public class ExplicitAI : PokemonAI 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)); + return aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFail(this, aiMove.Move.SecondaryEffect.Name, + new MoveOption(aiMove, battle, null)); } private static readonly StringKey PsychicTerrainName = new("psychic_terrain"); @@ -226,8 +277,8 @@ public class ExplicitAI : PokemonAI 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))) + if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this, + aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target))) return true; if (aiMove.Move.Priority > 0) { @@ -281,20 +332,20 @@ public class ExplicitAI : PokemonAI score += targetScore; affectedTargets++; } - if (affectedTargets == 0 && CanPredictMoveFailure) + if (affectedTargets == 0 && _skillFlags.CanPredictMoveFailure) { return MoveFailScore; } if (affectedTargets > 0) score = (int)(score / (float)affectedTargets); } - if (ScoreMoves) + if (_skillFlags.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); + _handlers.ApplyMoveEffectScore(this, aiMove.Move.SecondaryEffect.Name, + new MoveOption(aiMove, battle, null), ref score); + _handlers.ApplyGenerateMoveScoreModifiers(this, new MoveOption(aiMove, battle, null), ref score); } } if (score < 0) @@ -304,18 +355,18 @@ public class ExplicitAI : PokemonAI private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle) { - if (CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle)) + if (_skillFlags.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) + if (_skillFlags.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); + _handlers.ApplyMoveEffectAgainstTargetScore(this, aiMove.Move.SecondaryEffect.Name, moveOption, ref score); + _handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score); } if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex && @@ -343,4 +394,17 @@ public class ExplicitAI : PokemonAI return false; return true; } + + private static bool CanAttack(IPokemon pokemon) + { + if (pokemon.Volatile.Contains("requires_recharge")) + return false; + if (pokemon.HasStatus("frozen") || pokemon.HasStatus("sleep")) + return false; + if (pokemon.ActiveAbility?.Name == "truant" && pokemon.Volatile.Contains("truant_effect")) + return false; + if (pokemon.Volatile.Contains("flinch_effect")) + return false; + return true; + } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs index 1e134f2..fe5ac56 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs @@ -5,11 +5,14 @@ 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 bool AIBoolHandler(IExplicitAI ai, MoveOption option); -public delegate void AIMoveBasePowerHandler(MoveOption option, ref int score); +public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle, + IReadOnlyList reserves); -public delegate void AIScoreMoveHandler(MoveOption option, ref int score); +public delegate void AIMoveBasePowerHandler(IExplicitAI ai, MoveOption option, ref int score); + +public delegate void AIScoreMoveHandler(IExplicitAI ai, MoveOption option, ref int score); public interface IReadOnlyExplicitAIHandlers { @@ -21,7 +24,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Checks if a move will fail based on the provided function code and options. /// - bool MoveWillFail(StringKey functionCode, MoveOption option); + bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option); /// /// A list of checks to determine if a move will fail against a target. @@ -31,7 +34,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Checks if a move will fail against a target based on the provided function code and options. /// - bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option); + bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option); /// /// A list of handlers to apply scores for move effects. @@ -41,7 +44,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Applies the score for a move effect based on the provided name and options. /// - void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score); + void ApplyMoveEffectScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score); /// /// A list of handlers to apply scores for move effects against a target. @@ -51,7 +54,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Applies the score for a move effect against a target based on the provided name and options. /// - void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score); + void ApplyMoveEffectAgainstTargetScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score); /// /// A list of handlers to determine the base power of a move. @@ -61,7 +64,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Applies the base power for a move based on the provided name and options. /// - void GetBasePower(StringKey name, MoveOption option, ref int power); + void GetBasePower(IExplicitAI ai, StringKey name, MoveOption option, ref int power); /// /// A list of handlers to apply scores for general move effectiveness. @@ -71,10 +74,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Applies the score for a general move based on the provided option. /// - /// - /// - /// - void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score); + void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score); /// /// A list of handlers to apply scores for general move effectiveness against a target. @@ -84,10 +84,16 @@ public interface IReadOnlyExplicitAIHandlers /// /// Applies the score for a general move against a target based on the provided option. /// - void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score); + void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score); + + IReadOnlyDictionary ShouldSwitchFunctions { get; } + + bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves); + + IReadOnlyDictionary ShouldNotSwitchFunctions { get; } + + bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves); - IReadOnlyDictionary ShouldSwitch { get; } - IReadOnlyDictionary ShouldNotSwitch { get; } IReadOnlyDictionary AbilityRanking { get; } } @@ -99,8 +105,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary MoveFailureCheck { get; } = new(); /// - public bool MoveWillFail(StringKey functionCode, MoveOption option) => - MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(option); + public bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option) => + MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(ai, option); public FunctionHandlerDictionary MoveFailureAgainstTargetCheck { get; } = new(); @@ -109,8 +115,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers MoveFailureAgainstTargetCheck; /// - public bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option) => - MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(option); + public bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option) => + MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(ai, option); public FunctionHandlerDictionary MoveEffectScore { get; } = []; @@ -121,10 +127,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary MoveEffectAgainstTargetScore = []; /// - public void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score) + public void ApplyMoveEffectScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score) { if (MoveEffectScore.TryGetValue(name, out var handler)) - handler(option, ref score); + handler(ai, option, ref score); } /// @@ -134,10 +140,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary MoveBasePower = []; /// - public void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score) + public void ApplyMoveEffectAgainstTargetScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score) { if (MoveEffectAgainstTargetScore.TryGetValue(name, out var handler)) - handler(option, ref score); + handler(ai, option, ref score); } /// @@ -147,10 +153,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary GeneralMoveScore = []; /// - public void GetBasePower(StringKey name, MoveOption option, ref int power) + public void GetBasePower(IExplicitAI ai, StringKey name, MoveOption option, ref int power) { if (MoveBasePower.TryGetValue(name, out var handler)) - handler(option, ref power); + handler(ai, option, ref power); } /// @@ -160,11 +166,11 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary GeneralMoveAgainstTargetScore = []; /// - public void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score) + public void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score) { foreach (var (_, handler) in GeneralMoveScore) { - handler(option, ref score); + handler(ai, option, ref score); } } @@ -172,27 +178,51 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers IReadOnlyDictionary IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore => GeneralMoveAgainstTargetScore; - public FunctionHandlerDictionary ShouldSwitch = []; - /// - public void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score) + public void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score) { foreach (var (_, handler) in GeneralMoveAgainstTargetScore) { - handler(option, ref score); + handler(ai, option, ref score); } } - /// - IReadOnlyDictionary IReadOnlyExplicitAIHandlers.ShouldSwitch => ShouldSwitch; - - public FunctionHandlerDictionary ShouldNotSwitch = []; + public FunctionHandlerDictionary ShouldSwitchFunctions = []; /// - IReadOnlyDictionary IReadOnlyExplicitAIHandlers.ShouldNotSwitch => ShouldNotSwitch; + IReadOnlyDictionary IReadOnlyExplicitAIHandlers.ShouldSwitchFunctions => + ShouldSwitchFunctions; + + public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) + { + var shouldSwitch = false; + foreach (var (_, handler) in ShouldSwitchFunctions) + { + shouldSwitch |= handler(ai, pokemon, battle, reserves); + } + return shouldSwitch; + } + + public FunctionHandlerDictionary ShouldNotSwitchFunctions = []; + + /// + /// + IReadOnlyDictionary IReadOnlyExplicitAIHandlers.ShouldNotSwitchFunctions => + ShouldNotSwitchFunctions; public FunctionHandlerDictionary AbilityRanking = []; + /// + public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) + { + var shouldNotSwitch = false; + foreach (var (_, handler) in ShouldNotSwitchFunctions) + { + shouldNotSwitch |= handler(ai, pokemon, battle, reserves); + } + return shouldNotSwitch; + } + /// IReadOnlyDictionary IReadOnlyExplicitAIHandlers.AbilityRanking => AbilityRanking; diff --git a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs index 3537536..a3b1c83 100644 --- a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs +++ b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs @@ -15,6 +15,7 @@ public static class MoveTurnExecutor { internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice) { + moveChoice.User.BattleData!.LastMoveChoice = moveChoice; var chosenMove = moveChoice.ChosenMove; var useMove = chosenMove.MoveData; diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index 534ab0b..2052abf 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -252,7 +252,7 @@ public class BattleImpl : ScriptSource, IBattle /// public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x => - x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasPokemonNotInField()); + x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField()); /// public void ValidateBattleState() diff --git a/PkmnLib.Dynamic/Models/BattleParty.cs b/PkmnLib.Dynamic/Models/BattleParty.cs index b7ee89b..3639770 100644 --- a/PkmnLib.Dynamic/Models/BattleParty.cs +++ b/PkmnLib.Dynamic/Models/BattleParty.cs @@ -21,7 +21,12 @@ public interface IBattleParty : IDeepCloneable /// /// Whether the party has a living Pokemon left that is not in the field. /// - bool HasPokemonNotInField(); + bool HasUsablePokemonNotInField(); + + /// + /// Gets all usable Pokemon that are not currently in the field. + /// + IEnumerable GetUsablePokemonNotInField(); } /// @@ -49,6 +54,10 @@ public class BattlePartyImpl : IBattleParty public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index); /// - public bool HasPokemonNotInField() => + public bool HasUsablePokemonNotInField() => Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); + + /// + public IEnumerable GetUsablePokemonNotInField() => + Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index fbebf54..51ed2e6 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Libraries; +using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.Models.Serialized; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; @@ -489,6 +490,8 @@ public interface IPokemonBattleData : IDeepCloneable /// uint SwitchInTurn { get; internal set; } + uint TurnsOnField { get; } + /// /// The side the Pokémon is on. /// @@ -503,6 +506,11 @@ public interface IPokemonBattleData : IDeepCloneable /// The form of the Pokémon at the time it was sent out. /// IForm OriginalForm { get; } + + /// + /// The last move choice executed by the Pokémon. + /// + IMoveChoice? LastMoveChoice { get; internal set; } } /// @@ -1490,6 +1498,9 @@ public class PokemonBattleDataImpl : IPokemonBattleData /// public uint SwitchInTurn { get; set; } + /// + public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn; + /// public IBattleSide BattleSide => Battle.Sides[SideIndex]; @@ -1498,4 +1509,7 @@ public class PokemonBattleDataImpl : IPokemonBattleData /// public IForm OriginalForm { get; } + + /// + public IMoveChoice? LastMoveChoice { get; set; } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/PkmnLib.Dynamic.csproj b/PkmnLib.Dynamic/PkmnLib.Dynamic.csproj index d2d2f99..a3e0fbf 100644 --- a/PkmnLib.Dynamic/PkmnLib.Dynamic.csproj +++ b/PkmnLib.Dynamic/PkmnLib.Dynamic.csproj @@ -12,5 +12,10 @@ + + + ExplicitAI.cs + + diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs index 41ab263..8dcdaa8 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs @@ -26,4 +26,16 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage /// have an end of turn effect, such as Poison or Burn. /// void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage); +} + +/// +/// Script for getting the expected entry damage by the AI. +/// +public interface IAIInfoScriptExpectedEntryDamage +{ + /// + /// This function returns the expected entry damage for the script. This is used for scripts that have + /// an entry hazard effect, such as Spikes or Stealth Rock. + /// + void ExpectedEntryDamage(IPokemon pokemon, ref uint damage); } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs new file mode 100644 index 0000000..6c33659 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs @@ -0,0 +1,36 @@ +using PkmnLib.Dynamic.AI.Explicit; +using PkmnLib.Plugin.Gen7.Scripts.Pokemon; +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.AI; + +public static class AIDamageFunctions +{ + internal static void PredictedDamageScore(IExplicitAI ai, 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(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; + } + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs index 925b531..f49f925 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs @@ -1,6 +1,7 @@ using PkmnLib.Dynamic.AI.Explicit; using PkmnLib.Plugin.Gen7.Libraries.Battling; using PkmnLib.Plugin.Gen7.Scripts.Side; +using PkmnLib.Plugin.Gen7.Scripts.Status; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.AI; @@ -157,6 +158,8 @@ public static class AIHelperFunctions return true; } + private static readonly StringKey FoulPlayAbilityName = "foul_play"; + private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment, AIMoveState move, float desireMult = 1) { @@ -200,7 +203,7 @@ public static class AIHelperFunctions { var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical && - x.MoveData.SecondaryEffect?.Name != "foul_play"); + x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName); var inc = hasPhysicalMoves ? 8 : 12; score += (int)(inc * incMult); } @@ -334,4 +337,55 @@ public static class AIHelperFunctions private static bool Opposes(this IPokemon pokemon, IPokemon target) => pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide; + + public static bool WantsStatusProblem(IPokemon pokemon, StringKey? status) + { + if (status is null) + return true; + if (pokemon.ActiveAbility != null) + { + if (pokemon.ActiveAbility.Name == "guts" && status != ScriptUtils.ResolveName() && + status != ScriptUtils.ResolveName() && + IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true)) + { + return true; + } + if (pokemon.ActiveAbility.Name == "marvel_scale" && + IsStatRaiseWorthwhile(pokemon, Statistic.Defense, 1, true)) + { + return true; + } + if (pokemon.ActiveAbility.Name == "quick_feet" && status != ScriptUtils.ResolveName() && + status != ScriptUtils.ResolveName() && IsStatRaiseWorthwhile(pokemon, Statistic.Speed, 1, true)) + { + return true; + } + if (pokemon.ActiveAbility.Name == "flare_boost" && status == ScriptUtils.ResolveName() && + IsStatRaiseWorthwhile(pokemon, Statistic.SpecialAttack, 1, true)) + { + return true; + } + if (pokemon.ActiveAbility.Name == "toxic_boost" && + (status == ScriptUtils.ResolveName() || status == ScriptUtils.ResolveName()) && + IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true)) + { + return true; + } + if (pokemon.ActiveAbility.Name == "poison_heal" && status == ScriptUtils.ResolveName()) + { + return true; + } + if (pokemon.ActiveAbility.Name == "magic_guard") + { + if (status != ScriptUtils.ResolveName() && + status != ScriptUtils.ResolveName() && status != ScriptUtils.ResolveName()) + return false; + if (IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true)) + { + return true; + } + } + } + return false; + } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs new file mode 100644 index 0000000..bd71d0f --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs @@ -0,0 +1,208 @@ +using PkmnLib.Dynamic.AI.Explicit; +using PkmnLib.Plugin.Gen7.Scripts.Moves; +using PkmnLib.Plugin.Gen7.Scripts.Pokemon; +using PkmnLib.Plugin.Gen7.Scripts.Side; +using PkmnLib.Plugin.Gen7.Scripts.Status; +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.AI; + +public static class AISwitchFunctions +{ + internal static void RegisterAISwitchFunctions(ExplicitAIHandlers handlers) + { + handlers.ShouldSwitchFunctions.Add("perish_song", PerishSong); + handlers.ShouldSwitchFunctions.Add("significant_end_of_turn_damage", SignificantEndOfTurnDamage); + handlers.ShouldSwitchFunctions.Add("high_damage_from_foe", HighDamageFromFoe); + handlers.ShouldSwitchFunctions.Add("cure_status_problem_by_switching_out", CureStatusProblemBySwitchingOut); + } + + /// + /// Switch out if the Perish Song effect is about to cause the Pokémon to faint. + /// + private static bool PerishSong(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) + { + if (!pokemon.Volatile.TryGet(out var effect)) + return false; + return effect.Turns <= 1; + } + + private static readonly StringKey PoisonHealAbilityName = "poison_heal"; + + /// + /// Switch out if the Pokémon is expected to take significant end-of-turn damage. + /// + private static bool SignificantEndOfTurnDamage(IExplicitAI ai, IPokemon pokemon, IBattle battle, + IReadOnlyList reserves) + { + var eorDamage = 0; + pokemon.RunScriptHook(x => + x.ExpectedEndOfTurnDamage(pokemon, ref eorDamage)); + if (eorDamage >= pokemon.CurrentHealth / 2 || eorDamage >= pokemon.MaxHealth / 4) + return true; + if (!ai.TrainerHighSkill || eorDamage <= 0) + return false; + + if (pokemon.Volatile.Contains() && ai.Random.GetBool()) + return true; + if (pokemon.Volatile.Contains()) + return true; + if (pokemon.Volatile.Contains()) + return true; + var statusScript = pokemon.StatusScript.Script; + if (statusScript is BadlyPoisoned { Turns: > 0 } badlyPoisoned && + pokemon.ActiveAbility?.Name != PoisonHealAbilityName) + { + var poisonDamage = pokemon.MaxHealth / 8; + var nextToxicDamage = pokemon.MaxHealth * badlyPoisoned.GetPoisonMultiplier(); + if ((pokemon.CurrentHealth <= nextToxicDamage && pokemon.CurrentHealth > poisonDamage) || + nextToxicDamage > poisonDamage * 2) + { + return true; + } + } + return false; + } + + private static bool HighDamageFromFoe(IExplicitAI ai, IPokemon pokemon, IBattle battle, + IReadOnlyList reserves) + { + if (!ai.TrainerHighSkill) + return false; + if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2) + return false; + var bigThreat = false; + var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide) + .SelectMany(x => x.Pokemon.WhereNotNull()); + foreach (var opponent in opponents) + { + if (Math.Abs(opponent.Level - pokemon.Level) > 5) + continue; + var lastMoveUsed = opponent.BattleData?.LastMoveChoice; + if (lastMoveUsed is null) + continue; + var moveData = lastMoveUsed.ChosenMove.MoveData; + if (moveData.Category == MoveCategory.Status) + continue; + var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveData.MoveType, pokemon.Types); + if (effectiveness <= 1 || moveData.BasePower < 70) + continue; + var switchChange = moveData.BasePower > 90 ? 50 : 25; + bigThreat = ai.Random.GetInt(100) < switchChange; + } + return bigThreat; + } + + private static readonly StringKey ImmunityAbilityName = "immunity"; + private static readonly StringKey InsomniaAbilityName = "insomnia"; + private static readonly StringKey LimberAbilityName = "limber"; + private static readonly StringKey MagmaArmorAbilityName = "magma_armor"; + private static readonly StringKey VitalSpiritAbilityName = "vital_spirit"; + private static readonly StringKey WaterBubbleAbilityName = "water_bubble"; + private static readonly StringKey WaterVeilAbilityName = "water_veil"; + private static readonly StringKey NaturalCureAbilityName = "natural_cure"; + private static readonly StringKey RegeneratorAbilityName = "regenerator"; + + private static readonly Dictionary> StatusCureAbilities = new() + { + { + ImmunityAbilityName, + [ScriptUtils.ResolveName(), ScriptUtils.ResolveName()] + }, + { InsomniaAbilityName, [ScriptUtils.ResolveName()] }, + { LimberAbilityName, [ScriptUtils.ResolveName()] }, + { MagmaArmorAbilityName, [ScriptUtils.ResolveName()] }, + { VitalSpiritAbilityName, [ScriptUtils.ResolveName()] }, + { WaterBubbleAbilityName, [ScriptUtils.ResolveName()] }, + { WaterVeilAbilityName, [ScriptUtils.ResolveName()] }, + }; + + /// + /// Switch out to cure a status problem or heal HP with abilities like Natural Cure or Regenerator. + /// + private static bool CureStatusProblemBySwitchingOut(IExplicitAI ai, IPokemon pokemon, IBattle battle, + IReadOnlyList reserves) + { + if (pokemon.ActiveAbility == null) + return false; + + // Don't try to cure a status problem/heal a bit of HP if entry hazards will + // KO the battler if it switches back in + var entryHazardDamage = ExplicitAI.CalculateEntryHazardDamage(pokemon, pokemon.BattleData!.BattleSide); + if (entryHazardDamage >= pokemon.CurrentHealth) + return false; + if (pokemon.StatusScript.Script is null) + return false; + + var abilityName = pokemon.ActiveAbility.Name; + var statusName = pokemon.StatusScript.Script.Name; + + // Check abilities that cure specific status conditions + var canCureStatus = false; + if (abilityName == NaturalCureAbilityName) + { + canCureStatus = true; + } + else if (StatusCureAbilities.TryGetValue(abilityName, out var statusList)) + { + canCureStatus = statusList.Any(status => status == statusName); + } + + if (canCureStatus) + { + if (AIHelperFunctions.WantsStatusProblem(pokemon, statusName)) + return false; + + // Don't bother if the status will cure itself soon + if (pokemon.StatusScript.Script is Sleep { Turns: 1 }) + return false; + + if (entryHazardDamage >= pokemon.MaxHealth / 4) + return false; + + // Don't bother curing a poisoning if Toxic Spikes will just re-poison + if (pokemon.StatusScript.Script is Poisoned or BadlyPoisoned && + !reserves.Any(p => p.Types.Any(t => t.Name == "poison"))) + { + if (pokemon.BattleData!.BattleSide.VolatileScripts.TryGet(out _)) + { + return false; + } + } + + // Not worth curing status problems that still allow actions if at high HP + var isImmobilizing = pokemon.StatusScript.Script is Sleep or Frozen; + if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2 && !isImmobilizing) + return false; + + if (ai.Random.GetInt(100) < 70) + return true; + } + else if (abilityName == RegeneratorAbilityName) + { + // Not worth healing if battler would lose more HP from switching back in later + if (entryHazardDamage >= pokemon.MaxHealth / 3) + return false; + + // Not worth healing HP if already at high HP + if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2) + return false; + + // Don't bother if a foe is at low HP and could be knocked out instead + var hasDamagingMove = pokemon.Moves.Any(m => m?.MoveData.Category != MoveCategory.Status); + if (hasDamagingMove) + { + var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide) + .SelectMany(x => x.Pokemon.WhereNotNull()); + var weakFoe = opponents.Any(opponent => opponent.CurrentHealth < opponent.MaxHealth / 3); + if (weakFoe) + return false; + } + + if (ai.Random.GetInt(100) < 70) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctionRegistration.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctionRegistration.cs new file mode 100644 index 0000000..283a0bf --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctionRegistration.cs @@ -0,0 +1,77 @@ +using System.Linq.Expressions; +using System.Reflection; +using PkmnLib.Dynamic.AI.Explicit; + +namespace PkmnLib.Plugin.Gen7.AI; + +public static class ExplicitAIFunctionRegistration +{ + public static void RegisterAIFunctions(ExplicitAIHandlers handlers) + { + var baseType = typeof(Script); + foreach (var type in typeof(ExplicitAIFunctionRegistration).Assembly.GetTypes() + .Where(t => baseType.IsAssignableFrom(t))) + { + var attribute = type.GetCustomAttribute(); + if (attribute == null) + continue; + + if (attribute.Category == ScriptCategory.Move) + { + InitializeMoveFailFunction(handlers, type, attribute); + InitializeMoveScoreFunction(handlers, type, attribute); + } + } + + handlers.GeneralMoveAgainstTargetScore.Add("predicted_damage", AIDamageFunctions.PredictedDamageScore); + + AISwitchFunctions.RegisterAISwitchFunctions(handlers); + } + + #region Reflection based function initialization + + private static void InitializeMoveFailFunction(ExplicitAIHandlers handlers, Type type, ScriptAttribute attribute) + { + var failureMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m => + m.GetCustomAttribute() != null); + if (failureMethod == null) + return; + if (failureMethod.ReturnType != typeof(bool) || failureMethod.GetParameters().Length != 2 || + failureMethod.GetParameters()[0].ParameterType != typeof(IExplicitAI) || + failureMethod.GetParameters()[1].ParameterType != typeof(MoveOption)) + { + throw new InvalidOperationException( + $"Method {failureMethod.Name} in {type.Name} must return bool and take an IExplicitAI and a MoveOption as parameters."); + } + var aiParam = Expression.Parameter(typeof(IExplicitAI), "ai"); + var optionParam = Expression.Parameter(typeof(MoveOption), "option"); + var functionExpression = Expression.Lambda( + Expression.Call(null, failureMethod, aiParam, optionParam), aiParam, optionParam).Compile(); + handlers.MoveFailureCheck.Add(attribute.Name, functionExpression); + } + + private static void InitializeMoveScoreFunction(ExplicitAIHandlers handlers, Type type, ScriptAttribute attribute) + { + var scoreMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m => + m.GetCustomAttribute() != null); + if (scoreMethod == null) + return; + if (scoreMethod.ReturnType != typeof(void) || scoreMethod.GetParameters().Length != 3 || + scoreMethod.GetParameters()[0].ParameterType != typeof(IExplicitAI) || + scoreMethod.GetParameters()[1].ParameterType != typeof(MoveOption) || + scoreMethod.GetParameters()[2].ParameterType != typeof(int).MakeByRefType()) + { + throw new InvalidOperationException( + $"Method {scoreMethod.Name} in {type.Name} must return void and take an IExplicitAI, a MoveOption, and a ref int as parameters."); + } + var aiParam = Expression.Parameter(typeof(IExplicitAI), "ai"); + var optionParam = Expression.Parameter(typeof(MoveOption), "option"); + var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score"); + var functionExpression = Expression.Lambda( + Expression.Call(null, scoreMethod, aiParam, optionParam, scoreParam), aiParam, optionParam, scoreParam) + .Compile(); + handlers.MoveEffectScore.Add(attribute.Name, functionExpression); + } + + #endregion +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctions.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctions.cs deleted file mode 100644 index bd76dd9..0000000 --- a/Plugins/PkmnLib.Plugin.Gen7/AI/ExplicitAIFunctions.cs +++ /dev/null @@ -1,89 +0,0 @@ -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(); - if (attribute == null) - continue; - - if (attribute.Category == ScriptCategory.Move) - { - var failureMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m => - m.GetCustomAttribute() != null); - if (failureMethod != null) - { - if (failureMethod.ReturnType != typeof(bool) || failureMethod.GetParameters().Length != 1 || - failureMethod.GetParameters()[0].ParameterType != typeof(MoveOption)) - { - throw new InvalidOperationException( - $"Method {failureMethod.Name} in {type.Name} must return bool and take a single MoveOption parameter."); - } - var optionParam = Expression.Parameter(typeof(MoveOption), "option"); - var functionExpression = Expression.Lambda( - Expression.Call(null, failureMethod, optionParam), optionParam).Compile(); - handlers.MoveFailureCheck.Add(attribute.Name, functionExpression); - } - - var scoreMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m => - m.GetCustomAttribute() != null); - if (scoreMethod != null) - { - if (scoreMethod.ReturnType != typeof(void) || scoreMethod.GetParameters().Length != 2 || - scoreMethod.GetParameters()[0].ParameterType != typeof(MoveOption) || - scoreMethod.GetParameters()[1].ParameterType != typeof(int).MakeByRefType()) - { - throw new InvalidOperationException( - $"Method {scoreMethod.Name} in {type.Name} must return void and take a MoveOption and an int by reference parameter."); - } - var optionParam = Expression.Parameter(typeof(MoveOption), "option"); - var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score"); - var functionExpression = Expression.Lambda( - Expression.Call(null, scoreMethod, 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(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; - } - } - } -} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs b/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs index 713518e..335262b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs @@ -50,7 +50,7 @@ public class Gen7Plugin : Plugin, IResourceProvider registry.RegisterMiscLibrary(new Gen7MiscLibrary()); registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration)); - ExplicitAIFunctions.RegisterAIFunctions(registry.ExplicitAIHandlers); + ExplicitAIFunctionRegistration.RegisterAIFunctions(registry.ExplicitAIHandlers); } /// diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs index e057822..295a649 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs @@ -60,10 +60,10 @@ public class ChangeUserAttack : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Attack); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Attack); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.Attack, ref score); } @@ -75,10 +75,10 @@ public class ChangeUserDefense : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Defense); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Defense); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.Defense, ref score); } @@ -90,10 +90,11 @@ public class ChangeUserSpecialAttack : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialAttack); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => + WouldMoveFail(option, Statistic.SpecialAttack); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.SpecialAttack, ref score); } @@ -105,10 +106,11 @@ public class ChangeUserSpecialDefense : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialDefense); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => + WouldMoveFail(option, Statistic.SpecialDefense); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.SpecialDefense, ref score); } @@ -120,10 +122,10 @@ public class ChangeUserSpeed : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Speed); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Speed); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.Speed, ref score); } @@ -135,10 +137,10 @@ public class ChangeUserAccuracy : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Accuracy); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Accuracy); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.Accuracy, ref score); } @@ -150,9 +152,9 @@ public class ChangeUserEvasion : ChangeUserStats } [AIMoveFailureFunction] - public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Evasion); + public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Evasion); [AIMoveScoreFunction] - public static void AIMoveEffectScore(MoveOption option, ref int score) => + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => GetMoveEffectScore(option, Statistic.Evasion, ref score); } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs index 47df89f..396ca7d 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs @@ -8,25 +8,7 @@ public class Conversion2 : Script, IScriptOnSecondaryEffect { var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices; var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek(); - var lastMoveByTarget = previousTurnChoices? - // The previous turn choices include the choices of the current turn, so we need to have special handling for - // the current turn - .Select((x, index) => - { - // All choices before the current turn are valid - if (index < previousTurnChoices.Count - 1) - return x; - // If there is no next choice, we're at the end of the list, so we can just return the whole list - if (nextExecutingChoice == null) - return x; - // Otherwise we determine where the next choice is and return everything before that - var indexOfNext = x.IndexOf(nextExecutingChoice); - if (indexOfNext == -1) - return x; - return x.Take(indexOfNext); - }).SelectMany(x => x) - // We only want the last move choice by the target - .OfType().FirstOrDefault(x => x.User == target); + var lastMoveByTarget = target.BattleData?.LastMoveChoice; if (lastMoveByTarget == null) { move.GetHitData(target, hit).Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs index 446d8f0..9dd8ce0 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs @@ -8,8 +8,7 @@ public class Copycat : Script, IScriptChangeMove /// public void ChangeMove(IMoveChoice choice, ref StringKey moveName) { - var lastMove = choice.User.BattleData?.Battle.PreviousTurnChoices.SelectMany(x => x).OfType() - .LastOrDefault(); + var lastMove = choice.User.BattleData?.LastMoveChoice; if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove()) { choice.Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Disable.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Disable.cs index 5451ea8..cc50e84 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Disable.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Disable.cs @@ -11,8 +11,7 @@ public class Disable : Script, IScriptOnSecondaryEffect var battleData = move.User.BattleData; if (battleData == null) return; - var choiceQueue = battleData.Battle.PreviousTurnChoices; - var lastMove = choiceQueue.SelectMany(x => x).OfType().LastOrDefault(x => x.User == target); + var lastMove = target.BattleData?.LastMoveChoice; if (lastMove == null) { move.GetHitData(target, hit).Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs index dff7faa..57af690 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Encore.cs @@ -12,9 +12,7 @@ public class Encore : Script, IScriptOnSecondaryEffect if (battle == null) return; - var currentTurn = battle.ChoiceQueue!.LastRanChoice; - var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType() - .TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.User == target); + var lastMove = target.BattleData?.LastMoveChoice; if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove)) { move.GetHitData(target, hit).Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionBolt.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionBolt.cs index e91c39c..c878333 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionBolt.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionBolt.cs @@ -11,7 +11,7 @@ public class FusionBolt : Script, IScriptChangeDamageModifier return; // Grab the choices for the current turn, that have been executed before this move. - var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => x != move.MoveChoice) + var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice)) // Of these, find the move choice that used Fusion Flare. .OfType().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_flare"); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionFlare.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionFlare.cs index 79cd0d4..f6cee80 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionFlare.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/FusionFlare.cs @@ -11,7 +11,7 @@ public class FusionFlare : Script, IScriptChangeDamageModifier return; // Grab the choices for the current turn, that have been executed before this move. - var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => x != move.MoveChoice) + var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice)) // Of these, find the move choice that used Fusion Bolt. .OfType().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_bolt"); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Instruct.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Instruct.cs index ffbcda7..e36e4c0 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Instruct.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Instruct.cs @@ -14,8 +14,7 @@ public class Instruct : Script, IScriptOnSecondaryEffect if (battleData == null) return; - var lastMoveChoiceByTarget = battleData.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse() - .SkipWhile(x => x != move.MoveChoice).OfType().FirstOrDefault(x => x.User == target); + var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice; if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget)) { diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Sketch.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Sketch.cs index 2feb0f6..6a7ffea 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Sketch.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Sketch.cs @@ -21,8 +21,7 @@ public class Sketch : Script, IScriptOnSecondaryEffect return; } - var choiceQueue = move.Battle.PreviousTurnChoices; - var lastMove = choiceQueue.SelectMany(x => x).OfType().LastOrDefault(x => x.User == target); + var lastMove = target.BattleData?.LastMoveChoice; if (lastMove == null || lastMove.ChosenMove.MoveData.HasFlag("not_sketchable")) { move.GetHitData(target, hit).Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Spite.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Spite.cs index b66978f..3c79147 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Spite.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Spite.cs @@ -6,8 +6,7 @@ public class Spite : Script, IScriptOnSecondaryEffect /// public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) { - var lastMoveChoiceByTarget = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse() - .SkipWhile(x => x != move.MoveChoice).OfType().FirstOrDefault(x => x.User == target); + var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice; if (lastMoveChoiceByTarget == null || lastMoveChoiceByTarget.HasFailed) { move.GetHitData(target, hit).Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/StompingTantrum.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/StompingTantrum.cs index 3a8183b..9bacd31 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/StompingTantrum.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/StompingTantrum.cs @@ -6,8 +6,7 @@ public class StompingTantrum : Script, IScriptChangeBasePower /// public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower) { - var lastMoveChoice = move.Battle.PreviousTurnChoices.Reverse().Skip(1).SelectMany(x => x.Reverse()) - .OfType().FirstOrDefault(x => x.User == move.User); + var lastMoveChoice = move.User.BattleData?.LastMoveChoice; if (lastMoveChoice is { HasFailed: true }) { basePower = basePower.MultiplyOrMax(2); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Torment.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Torment.cs index 39be51d..1aeccbf 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Torment.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Torment.cs @@ -6,8 +6,7 @@ public class Torment : Script, IScriptOnSecondaryEffect /// public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) { - var lastTargetChoice = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse().OfType() - .FirstOrDefault(x => x.User == target); + var lastTargetChoice = target.BattleData?.LastMoveChoice; target.Volatile.Add(new Pokemon.TormentEffect(lastTargetChoice)); } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/PerishSongEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/PerishSongEffect.cs index e5731ae..0bcc1dd 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/PerishSongEffect.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/PerishSongEffect.cs @@ -3,20 +3,20 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; [Script(ScriptCategory.Pokemon, "perish_song")] public class PerishSongEffect : Script, IScriptOnEndTurn { - private int _turns; + internal int Turns { get; private set; } private IPokemon _owner; public PerishSongEffect(IPokemon owner, int turns = 3) { _owner = owner; - _turns = turns; + Turns = turns; } /// public void OnEndTurn(IScriptSource owner, IBattle battle) { - _turns--; - if (_turns <= 0) + Turns--; + if (Turns <= 0) { RemoveSelf(); _owner.Faint(DamageSource.Misc); diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/SpikesEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/SpikesEffect.cs index ceb853d..dd6f1cb 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/SpikesEffect.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/SpikesEffect.cs @@ -1,7 +1,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Side; [Script(ScriptCategory.Side, "spikes")] -public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack +public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack, IAIInfoScriptExpectedEntryDamage { private int _layers = 1; @@ -18,14 +18,7 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack if (pokemon.IsFloating) return; - var modifier = _layers switch - { - 1 => 1 / 16f, - 2 => 3 / 16f, - 3 => 1 / 4f, - _ => throw new ArgumentOutOfRangeException(), - }; - var damage = (uint)(pokemon.MaxHealth * modifier); + var damage = CalculateDamage(pokemon); EventBatchId eventBatch = new(); pokemon.Damage(damage, DamageSource.Misc, eventBatch); @@ -37,4 +30,26 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack BatchId = eventBatch, }); } + + private uint CalculateDamage(IPokemon pokemon) + { + var modifier = _layers switch + { + 1 => 1 / 16f, + 2 => 3 / 16f, + 3 => 1 / 4f, + _ => throw new ArgumentOutOfRangeException(), + }; + var damage = (uint)(pokemon.MaxHealth * modifier); + return damage; + } + + /// + public void ExpectedEntryDamage(IPokemon pokemon, ref uint damage) + { + if (pokemon.IsFloating) + return; + + damage += CalculateDamage(pokemon); + } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/StealthRockEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/StealthRockEffect.cs index dbcdbcf..e81b9be 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/StealthRockEffect.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/StealthRockEffect.cs @@ -1,18 +1,12 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Side; [Script(ScriptCategory.Side, "stealth_rock")] -public class StealthRockEffect : Script, IScriptOnSwitchIn +public class StealthRockEffect : Script, IScriptOnSwitchIn, IAIInfoScriptExpectedEntryDamage { /// public void OnSwitchIn(IPokemon pokemon, byte position) { - var typeLibrary = pokemon.Library.StaticLibrary.Types; - var effectiveness = 1.0f; - if (typeLibrary.TryGetTypeIdentifier("rock", out var rockType)) - { - effectiveness = typeLibrary.GetEffectiveness(rockType, pokemon.Types); - } - var damage = (uint)(pokemon.MaxHealth / 8f * effectiveness); + var damage = CalculateStealthRockDamage(pokemon); EventBatchId batchId = new(); pokemon.Damage(damage, DamageSource.Misc, batchId); pokemon.BattleData?.Battle.EventHook.Invoke(new DialogEvent("stealth_rock_damage", @@ -21,4 +15,21 @@ public class StealthRockEffect : Script, IScriptOnSwitchIn { "pokemon", pokemon }, })); } + + private static uint CalculateStealthRockDamage(IPokemon pokemon) + { + var typeLibrary = pokemon.Library.StaticLibrary.Types; + var effectiveness = 1.0f; + if (typeLibrary.TryGetTypeIdentifier("rock", out var rockType)) + { + effectiveness = typeLibrary.GetEffectiveness(rockType, pokemon.Types); + } + return (uint)(pokemon.MaxHealth / 8f * effectiveness); + } + + /// + public void ExpectedEntryDamage(IPokemon pokemon, ref uint damage) + { + damage += CalculateStealthRockDamage(pokemon); + } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/BadlyPoisoned.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/BadlyPoisoned.cs index ea308c7..e398ac6 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/BadlyPoisoned.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/BadlyPoisoned.cs @@ -3,15 +3,15 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Status; [Script(ScriptCategory.Status, "badly_poisoned")] public class BadlyPoisoned : Poisoned { - private int _turns = 1; + internal int Turns { get; private set; } = 1; /// - public override float GetPoisonMultiplier() => 1f / (16f * _turns); + public override float GetPoisonMultiplier() => 1f / (16f * Turns); /// public override void OnEndTurn(IScriptSource owner, IBattle battle) { base.OnEndTurn(owner, battle); - _turns = Math.Min(_turns + 1, 15); + Turns = Math.Min(Turns + 1, 15); } } \ No newline at end of file