This commit is contained in:
parent
364d4b9080
commit
bf83b25238
@ -5,6 +5,8 @@ using PkmnLib.Dynamic.AI;
|
|||||||
using PkmnLib.Dynamic.Libraries;
|
using PkmnLib.Dynamic.Libraries;
|
||||||
using PkmnLib.Plugin.Gen7;
|
using PkmnLib.Plugin.Gen7;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
namespace AIRunner;
|
namespace AIRunner;
|
||||||
|
|
||||||
@ -12,9 +14,12 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
private static List<PokemonAI>? _availableAIs;
|
private static List<PokemonAI>? _availableAIs;
|
||||||
|
|
||||||
|
internal static LoggingLevelSwitch LogLevelSwitch { get; } = new(LogEventLevel.Information);
|
||||||
|
|
||||||
private static Task<int> Main(string[] args)
|
private static Task<int> 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...");
|
Log.Information("Starting AI Runner...");
|
||||||
AILogging.LogHandler = Log.Debug;
|
AILogging.LogHandler = Log.Debug;
|
||||||
var library = DynamicLibraryImpl.Create([
|
var library = DynamicLibraryImpl.Create([
|
||||||
@ -24,6 +29,10 @@ internal static class Program
|
|||||||
|
|
||||||
var testCommand = new Command("test", "Run two AIs against each other")
|
var testCommand = new Command("test", "Run two AIs against each other")
|
||||||
{
|
{
|
||||||
|
new Option<bool>("--debug")
|
||||||
|
{
|
||||||
|
Description = "Enable debug logging.",
|
||||||
|
},
|
||||||
new Option<string>("--ai1")
|
new Option<string>("--ai1")
|
||||||
{
|
{
|
||||||
Description = "The name of the first AI script to run against the second AI.",
|
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 =>
|
testCommand.SetAction(result =>
|
||||||
{
|
{
|
||||||
|
var debug = result.GetValue<bool>("--debug");
|
||||||
|
LogLevelSwitch.MinimumLevel = debug ? LogEventLevel.Debug : LogEventLevel.Information;
|
||||||
|
|
||||||
var ai1Name = result.GetRequiredValue<string>("--ai1");
|
var ai1Name = result.GetRequiredValue<string>("--ai1");
|
||||||
var ai2Name = result.GetRequiredValue<string>("--ai2");
|
var ai2Name = result.GetRequiredValue<string>("--ai2");
|
||||||
var ai1 = _availableAIs!.First(a =>
|
var ai1 = _availableAIs!.First(a =>
|
||||||
|
@ -32,6 +32,7 @@ public static class TestCommandRunner
|
|||||||
randoms[i] = new RandomImpl(rootRandom.GetInt());
|
randoms[i] = new RandomImpl(rootRandom.GetInt());
|
||||||
battleTasks[i] = Task.CompletedTask; // Initialize tasks to avoid null references
|
battleTasks[i] = Task.CompletedTask; // Initialize tasks to avoid null references
|
||||||
}
|
}
|
||||||
|
const int pokemonPerParty = 6;
|
||||||
|
|
||||||
// Show a progress bar if debug logging is not enabled.
|
// 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
|
// 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);
|
Log.Debug("Battle {BattleNumber}: {AI1} vs {AI2}", index + 1, ai1.Name, ai2.Name);
|
||||||
var random = randoms[taskIndex];
|
var random = randoms[taskIndex];
|
||||||
var battle = GenerateBattle(library, 3, random);
|
var battle = GenerateBattle(library, pokemonPerParty, random);
|
||||||
var timePerTurn = new List<double>(20);
|
var timePerTurn = new List<double>(20);
|
||||||
while (!battle.HasEnded)
|
while (!battle.HasEnded)
|
||||||
{
|
{
|
||||||
|
153
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
Normal file
153
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
Normal file
@ -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<IPokemon> usablePokemon, IBattleSide battleSide)
|
||||||
|
{
|
||||||
|
var options = usablePokemon.Where((pokemon, index) =>
|
||||||
|
{
|
||||||
|
if (_skillFlags.ReserveLastPokemon && index == usablePokemon.Count - 1 && usablePokemon.Count > 1)
|
||||||
|
return false; // Don't switch to the last Pokemon if there are others available.
|
||||||
|
if (_skillFlags.UsePokemonInOrder && index != 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}).ToList();
|
||||||
|
if (options.Count == 0)
|
||||||
|
return null;
|
||||||
|
var ratedOptions = options
|
||||||
|
.Select(pokemon => new { Pokemon = pokemon, Score = RateReplacementPokemon(pokemon, battleSide) })
|
||||||
|
.OrderBy(x => x.Score).ToList();
|
||||||
|
if (TrainerHighSkill && !terribleMoves)
|
||||||
|
{
|
||||||
|
if (ratedOptions.First().Score < 100)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ratedOptions.First().Pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly StringKey HeavyDutyBootsName = "heavy_duty_boots";
|
||||||
|
private static readonly StringKey ToxicSpikesName = "toxic_spikes";
|
||||||
|
private static readonly StringKey StickyWebName = "sticky_web";
|
||||||
|
|
||||||
|
private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide)
|
||||||
|
{
|
||||||
|
var score = 0;
|
||||||
|
var types = pokemon.Types;
|
||||||
|
var entryDamage = CalculateEntryHazardDamage(pokemon, battleSide);
|
||||||
|
if (entryDamage >= pokemon.CurrentHealth)
|
||||||
|
score -= 50;
|
||||||
|
else if (entryDamage > 0)
|
||||||
|
score -= 50 * (int)Math.Round((double)entryDamage / pokemon.MaxHealth, MidpointRounding.AwayFromZero);
|
||||||
|
if (!pokemon.HasHeldItem(HeavyDutyBootsName) && !pokemon.IsFloating)
|
||||||
|
{
|
||||||
|
if (battleSide.VolatileScripts.Contains(ToxicSpikesName) && CanBePoisoned(pokemon, battleSide.Battle))
|
||||||
|
{
|
||||||
|
score -= 20;
|
||||||
|
}
|
||||||
|
if (battleSide.VolatileScripts.Contains(StickyWebName))
|
||||||
|
{
|
||||||
|
score -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide);
|
||||||
|
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
|
||||||
|
{
|
||||||
|
var lastMoveUsed = foe.BattleData?.LastMoveChoice;
|
||||||
|
if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status)
|
||||||
|
continue;
|
||||||
|
var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType;
|
||||||
|
var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveType, types);
|
||||||
|
score -= (int)(lastMoveUsed.ChosenMove.MoveData.BasePower * effectiveness / 5);
|
||||||
|
}
|
||||||
|
foreach (var learnedMove in pokemon.Moves.WhereNotNull())
|
||||||
|
{
|
||||||
|
if (learnedMove.MoveData.BasePower == 0 || learnedMove is { CurrentPp: 0, MaxPp: > 0 })
|
||||||
|
continue;
|
||||||
|
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
|
||||||
|
{
|
||||||
|
if (CanAbsorbMove(foe, learnedMove.MoveData, learnedMove.MoveData.MoveType, battleSide.Battle))
|
||||||
|
continue;
|
||||||
|
var effectiveness =
|
||||||
|
pokemon.Library.StaticLibrary.Types.GetEffectiveness(learnedMove.MoveData.MoveType, foe.Types);
|
||||||
|
score += (int)(learnedMove.MoveData.BasePower * effectiveness / 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side)
|
||||||
|
{
|
||||||
|
var damage = 0u;
|
||||||
|
side.RunScriptHook<IAIInfoScriptExpectedEntryDamage>(x => x.ExpectedEntryDamage(pokemon, ref damage));
|
||||||
|
return damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanSwitch(IPokemon pokemon)
|
||||||
|
{
|
||||||
|
var battleData = pokemon.BattleData;
|
||||||
|
if (battleData == null)
|
||||||
|
return false;
|
||||||
|
if (battleData.Battle.IsWildBattle)
|
||||||
|
return false;
|
||||||
|
var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x =>
|
||||||
|
x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position)));
|
||||||
|
return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField();
|
||||||
|
}
|
||||||
|
}
|
89
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
Normal file
89
PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using PkmnLib.Dynamic.BattleFlow;
|
using PkmnLib.Dynamic.BattleFlow;
|
||||||
using PkmnLib.Dynamic.Libraries;
|
using PkmnLib.Dynamic.Libraries;
|
||||||
using PkmnLib.Dynamic.Models;
|
using PkmnLib.Dynamic.Models;
|
||||||
@ -8,21 +9,27 @@ using PkmnLib.Static.Utils;
|
|||||||
|
|
||||||
namespace PkmnLib.Dynamic.AI.Explicit;
|
namespace PkmnLib.Dynamic.AI.Explicit;
|
||||||
|
|
||||||
|
public interface IExplicitAI
|
||||||
|
{
|
||||||
|
public bool TrainerHighSkill { get; }
|
||||||
|
public bool TrainerMediumSkill { get; }
|
||||||
|
public IRandom Random { get; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An explicit AI that has explicitly written logic for each Pokémon and move.
|
/// An explicit AI that has explicitly written logic for each Pokémon and move.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This is heavily based on the AI used in <a href="https://github.com/Maruno17/pokemon-essentials">Pokémon Essentials</a>
|
/// This is heavily based on the AI used in <a href="https://github.com/Maruno17/pokemon-essentials">Pokémon Essentials</a>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class ExplicitAI : PokemonAI
|
public partial class ExplicitAI : PokemonAI, IExplicitAI
|
||||||
{
|
{
|
||||||
public const int MoveFailScore = 20;
|
public const int MoveFailScore = 20;
|
||||||
public const int MoveUselessScore = 60;
|
public const int MoveUselessScore = 60;
|
||||||
public const int MoveBaseScore = 100;
|
public const int MoveBaseScore = 100;
|
||||||
|
|
||||||
private const float TrainerSkill = 100; // TODO: This should be configurable
|
private const float TrainerSkill = 100; // TODO: This should be configurable
|
||||||
private bool CanPredictMoveFailure => true; // TODO: This should be configurable
|
private SkillFlags _skillFlags = new();
|
||||||
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 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();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExplicitAI(IDynamicLibrary library) : base("explicit")
|
public ExplicitAI(IDynamicLibrary library) : base("explicit")
|
||||||
{
|
{
|
||||||
_handlers = library.ExplicitAIHandlers;
|
_handlers = library.ExplicitAIHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TrainerHighSkill => TrainerSkill >= 45;
|
||||||
|
public bool TrainerMediumSkill => TrainerSkill >= 32;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRandom Random => _random;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
|
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
|
||||||
{
|
{
|
||||||
if (battle.HasForcedTurn(pokemon, out var choice))
|
if (battle.HasForcedTurn(pokemon, out var choice))
|
||||||
return choice;
|
return choice;
|
||||||
|
if (TryChooseToSwitchOut(battle, pokemon, false, out var turnChoice))
|
||||||
|
{
|
||||||
|
AILogging.LogInformation($"{pokemon} is switching out.");
|
||||||
|
return turnChoice;
|
||||||
|
}
|
||||||
|
|
||||||
var moveChoices = GetMoveScores(pokemon, battle);
|
var moveChoices = GetMoveScores(pokemon, battle);
|
||||||
if (moveChoices.Count == 0)
|
if (moveChoices.Count == 0)
|
||||||
{
|
{
|
||||||
@ -48,7 +77,29 @@ public class ExplicitAI : PokemonAI
|
|||||||
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
|
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
|
||||||
}
|
}
|
||||||
var maxScore = moveChoices.Max(x => x.score);
|
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 threshold = (float)Math.Floor(maxScore * MoveScoreThreshold);
|
||||||
var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray();
|
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);
|
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.");
|
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail.");
|
||||||
AddMoveToChoices(index, MoveFailScore);
|
AddMoveToChoices(index, MoveFailScore);
|
||||||
@ -210,8 +261,8 @@ public class ExplicitAI : PokemonAI
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Check if the move will fail based on the handlers
|
// Check if the move will fail based on the handlers
|
||||||
return aiMove.Move.SecondaryEffect != null &&
|
return aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFail(this, aiMove.Move.SecondaryEffect.Name,
|
||||||
_handlers.MoveWillFail(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null));
|
new MoveOption(aiMove, battle, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly StringKey PsychicTerrainName = new("psychic_terrain");
|
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)
|
private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
|
||||||
{
|
{
|
||||||
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(aiMove.Move.SecondaryEffect.Name,
|
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
|
||||||
new MoveOption(aiMove, battle, target)))
|
aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
|
||||||
return true;
|
return true;
|
||||||
if (aiMove.Move.Priority > 0)
|
if (aiMove.Move.Priority > 0)
|
||||||
{
|
{
|
||||||
@ -281,20 +332,20 @@ public class ExplicitAI : PokemonAI
|
|||||||
score += targetScore;
|
score += targetScore;
|
||||||
affectedTargets++;
|
affectedTargets++;
|
||||||
}
|
}
|
||||||
if (affectedTargets == 0 && CanPredictMoveFailure)
|
if (affectedTargets == 0 && _skillFlags.CanPredictMoveFailure)
|
||||||
{
|
{
|
||||||
return MoveFailScore;
|
return MoveFailScore;
|
||||||
}
|
}
|
||||||
if (affectedTargets > 0)
|
if (affectedTargets > 0)
|
||||||
score = (int)(score / (float)affectedTargets);
|
score = (int)(score / (float)affectedTargets);
|
||||||
}
|
}
|
||||||
if (ScoreMoves)
|
if (_skillFlags.ScoreMoves)
|
||||||
{
|
{
|
||||||
if (aiMove.Move.SecondaryEffect != null)
|
if (aiMove.Move.SecondaryEffect != null)
|
||||||
{
|
{
|
||||||
_handlers.ApplyMoveEffectScore(aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, null),
|
_handlers.ApplyMoveEffectScore(this, aiMove.Move.SecondaryEffect.Name,
|
||||||
ref score);
|
new MoveOption(aiMove, battle, null), ref score);
|
||||||
_handlers.ApplyGenerateMoveScoreModifiers(new MoveOption(aiMove, battle, null), ref score);
|
_handlers.ApplyGenerateMoveScoreModifiers(this, new MoveOption(aiMove, battle, null), ref score);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (score < 0)
|
if (score < 0)
|
||||||
@ -304,18 +355,18 @@ public class ExplicitAI : PokemonAI
|
|||||||
|
|
||||||
private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
|
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.");
|
AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} against {target} but it will fail.");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
var score = MoveBaseScore;
|
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 estimatedDamage = AIHelpers.CalculateDamageEstimation(aiMove.Move, user, target, battle.Library);
|
||||||
var moveOption = new MoveOption(aiMove, battle, target, estimatedDamage);
|
var moveOption = new MoveOption(aiMove, battle, target, estimatedDamage);
|
||||||
_handlers.ApplyMoveEffectAgainstTargetScore(aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
|
_handlers.ApplyMoveEffectAgainstTargetScore(this, aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
|
||||||
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(moveOption, ref score);
|
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
|
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
|
||||||
@ -343,4 +394,17 @@ public class ExplicitAI : PokemonAI
|
|||||||
return false;
|
return false;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,11 +5,14 @@ namespace PkmnLib.Dynamic.AI.Explicit;
|
|||||||
|
|
||||||
public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0);
|
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<IPokemon> 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
|
public interface IReadOnlyExplicitAIHandlers
|
||||||
{
|
{
|
||||||
@ -21,7 +24,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a move will fail based on the provided function code and options.
|
/// Checks if a move will fail based on the provided function code and options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool MoveWillFail(StringKey functionCode, MoveOption option);
|
bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of checks to determine if a move will fail against a target.
|
/// A list of checks to determine if a move will fail against a target.
|
||||||
@ -31,7 +34,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a move will fail against a target based on the provided function code and options.
|
/// Checks if a move will fail against a target based on the provided function code and options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option);
|
bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of handlers to apply scores for move effects.
|
/// A list of handlers to apply scores for move effects.
|
||||||
@ -41,7 +44,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the score for a move effect based on the provided name and options.
|
/// Applies the score for a move effect based on the provided name and options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ApplyMoveEffectScore(StringKey name, MoveOption option, ref int score);
|
void ApplyMoveEffectScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of handlers to apply scores for move effects against a target.
|
/// A list of handlers to apply scores for move effects against a target.
|
||||||
@ -51,7 +54,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the score for a move effect against a target based on the provided name and options.
|
/// Applies the score for a move effect against a target based on the provided name and options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ApplyMoveEffectAgainstTargetScore(StringKey name, MoveOption option, ref int score);
|
void ApplyMoveEffectAgainstTargetScore(IExplicitAI ai, StringKey name, MoveOption option, ref int score);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of handlers to determine the base power of a move.
|
/// A list of handlers to determine the base power of a move.
|
||||||
@ -61,7 +64,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the base power for a move based on the provided name and options.
|
/// Applies the base power for a move based on the provided name and options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void GetBasePower(StringKey name, MoveOption option, ref int power);
|
void GetBasePower(IExplicitAI ai, StringKey name, MoveOption option, ref int power);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of handlers to apply scores for general move effectiveness.
|
/// A list of handlers to apply scores for general move effectiveness.
|
||||||
@ -71,10 +74,7 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the score for a general move based on the provided option.
|
/// Applies the score for a general move based on the provided option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="option"></param>
|
void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score);
|
||||||
/// <param name="score"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of handlers to apply scores for general move effectiveness against a target.
|
/// A list of handlers to apply scores for general move effectiveness against a target.
|
||||||
@ -84,10 +84,16 @@ public interface IReadOnlyExplicitAIHandlers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the score for a general move against a target based on the provided option.
|
/// Applies the score for a general move against a target based on the provided option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score);
|
void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score);
|
||||||
|
|
||||||
|
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> ShouldSwitchFunctions { get; }
|
||||||
|
|
||||||
|
bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
|
||||||
|
|
||||||
|
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> ShouldNotSwitchFunctions { get; }
|
||||||
|
|
||||||
|
bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves);
|
||||||
|
|
||||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldSwitch { get; }
|
|
||||||
IReadOnlyDictionary<StringKey, AIBoolHandler> ShouldNotSwitch { get; }
|
|
||||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> AbilityRanking { get; }
|
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> AbilityRanking { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,8 +105,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureCheck { get; } = new();
|
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureCheck { get; } = new();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool MoveWillFail(StringKey functionCode, MoveOption option) =>
|
public bool MoveWillFail(IExplicitAI ai, StringKey functionCode, MoveOption option) =>
|
||||||
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
MoveFailureCheck.TryGetValue(functionCode, out var handler) && handler(ai, option);
|
||||||
|
|
||||||
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureAgainstTargetCheck { get; } = new();
|
public FunctionHandlerDictionary<AIBoolHandler> MoveFailureAgainstTargetCheck { get; } = new();
|
||||||
|
|
||||||
@ -109,8 +115,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
MoveFailureAgainstTargetCheck;
|
MoveFailureAgainstTargetCheck;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool MoveWillFailAgainstTarget(StringKey functionCode, MoveOption option) =>
|
public bool MoveWillFailAgainstTarget(IExplicitAI ai, StringKey functionCode, MoveOption option) =>
|
||||||
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(option);
|
MoveFailureAgainstTargetCheck.TryGetValue(functionCode, out var handler) && handler(ai, option);
|
||||||
|
|
||||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectScore { get; } = [];
|
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectScore { get; } = [];
|
||||||
|
|
||||||
@ -121,10 +127,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectAgainstTargetScore = [];
|
public FunctionHandlerDictionary<AIScoreMoveHandler> MoveEffectAgainstTargetScore = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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))
|
if (MoveEffectScore.TryGetValue(name, out var handler))
|
||||||
handler(option, ref score);
|
handler(ai, option, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -134,10 +140,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
public FunctionHandlerDictionary<AIMoveBasePowerHandler> MoveBasePower = [];
|
public FunctionHandlerDictionary<AIMoveBasePowerHandler> MoveBasePower = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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))
|
if (MoveEffectAgainstTargetScore.TryGetValue(name, out var handler))
|
||||||
handler(option, ref score);
|
handler(ai, option, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -147,10 +153,10 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveScore = [];
|
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveScore = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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))
|
if (MoveBasePower.TryGetValue(name, out var handler))
|
||||||
handler(option, ref power);
|
handler(ai, option, ref power);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -160,11 +166,11 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveAgainstTargetScore = [];
|
public FunctionHandlerDictionary<AIScoreMoveHandler> GeneralMoveAgainstTargetScore = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ApplyGenerateMoveScoreModifiers(MoveOption option, ref int score)
|
public void ApplyGenerateMoveScoreModifiers(IExplicitAI ai, MoveOption option, ref int score)
|
||||||
{
|
{
|
||||||
foreach (var (_, handler) in GeneralMoveScore)
|
foreach (var (_, handler) in GeneralMoveScore)
|
||||||
{
|
{
|
||||||
handler(option, ref score);
|
handler(ai, option, ref score);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,27 +178,51 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
|
|||||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore =>
|
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.GeneralMoveAgainstTargetScore =>
|
||||||
GeneralMoveAgainstTargetScore;
|
GeneralMoveAgainstTargetScore;
|
||||||
|
|
||||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldSwitch = [];
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ApplyGenerateMoveAgainstTargetScoreModifiers(MoveOption option, ref int score)
|
public void ApplyGenerateMoveAgainstTargetScoreModifiers(IExplicitAI ai, MoveOption option, ref int score)
|
||||||
{
|
{
|
||||||
foreach (var (_, handler) in GeneralMoveAgainstTargetScore)
|
foreach (var (_, handler) in GeneralMoveAgainstTargetScore)
|
||||||
{
|
{
|
||||||
handler(option, ref score);
|
handler(ai, option, ref score);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public FunctionHandlerDictionary<AISwitchBoolHandler> ShouldSwitchFunctions = [];
|
||||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitch => ShouldSwitch;
|
|
||||||
|
|
||||||
public FunctionHandlerDictionary<AIBoolHandler> ShouldNotSwitch = [];
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
IReadOnlyDictionary<StringKey, AIBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitch => ShouldNotSwitch;
|
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> IReadOnlyExplicitAIHandlers.ShouldSwitchFunctions =>
|
||||||
|
ShouldSwitchFunctions;
|
||||||
|
|
||||||
|
public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||||
|
{
|
||||||
|
var shouldSwitch = false;
|
||||||
|
foreach (var (_, handler) in ShouldSwitchFunctions)
|
||||||
|
{
|
||||||
|
shouldSwitch |= handler(ai, pokemon, battle, reserves);
|
||||||
|
}
|
||||||
|
return shouldSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FunctionHandlerDictionary<AISwitchBoolHandler> ShouldNotSwitchFunctions = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <inheritdoc />
|
||||||
|
IReadOnlyDictionary<StringKey, AISwitchBoolHandler> IReadOnlyExplicitAIHandlers.ShouldNotSwitchFunctions =>
|
||||||
|
ShouldNotSwitchFunctions;
|
||||||
|
|
||||||
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
|
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||||
|
{
|
||||||
|
var shouldNotSwitch = false;
|
||||||
|
foreach (var (_, handler) in ShouldNotSwitchFunctions)
|
||||||
|
{
|
||||||
|
shouldNotSwitch |= handler(ai, pokemon, battle, reserves);
|
||||||
|
}
|
||||||
|
return shouldNotSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.AbilityRanking =>
|
IReadOnlyDictionary<StringKey, AIScoreMoveHandler> IReadOnlyExplicitAIHandlers.AbilityRanking =>
|
||||||
AbilityRanking;
|
AbilityRanking;
|
||||||
|
@ -15,6 +15,7 @@ public static class MoveTurnExecutor
|
|||||||
{
|
{
|
||||||
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
|
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
|
||||||
{
|
{
|
||||||
|
moveChoice.User.BattleData!.LastMoveChoice = moveChoice;
|
||||||
var chosenMove = moveChoice.ChosenMove;
|
var chosenMove = moveChoice.ChosenMove;
|
||||||
var useMove = chosenMove.MoveData;
|
var useMove = chosenMove.MoveData;
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ public class BattleImpl : ScriptSource, IBattle
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x =>
|
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());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ValidateBattleState()
|
public void ValidateBattleState()
|
||||||
|
@ -21,7 +21,12 @@ public interface IBattleParty : IDeepCloneable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the party has a living Pokemon left that is not in the field.
|
/// Whether the party has a living Pokemon left that is not in the field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool HasPokemonNotInField();
|
bool HasUsablePokemonNotInField();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all usable Pokemon that are not currently in the field.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<IPokemon> GetUsablePokemonNotInField();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -49,6 +54,10 @@ public class BattlePartyImpl : IBattleParty
|
|||||||
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
|
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool HasPokemonNotInField() =>
|
public bool HasUsablePokemonNotInField() =>
|
||||||
Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
|
Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IPokemon> GetUsablePokemonNotInField() =>
|
||||||
|
Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using PkmnLib.Dynamic.Events;
|
using PkmnLib.Dynamic.Events;
|
||||||
using PkmnLib.Dynamic.Libraries;
|
using PkmnLib.Dynamic.Libraries;
|
||||||
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
using PkmnLib.Dynamic.Models.Serialized;
|
using PkmnLib.Dynamic.Models.Serialized;
|
||||||
using PkmnLib.Dynamic.ScriptHandling;
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
using PkmnLib.Static;
|
using PkmnLib.Static;
|
||||||
@ -489,6 +490,8 @@ public interface IPokemonBattleData : IDeepCloneable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
uint SwitchInTurn { get; internal set; }
|
uint SwitchInTurn { get; internal set; }
|
||||||
|
|
||||||
|
uint TurnsOnField { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The side the Pokémon is on.
|
/// The side the Pokémon is on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -503,6 +506,11 @@ public interface IPokemonBattleData : IDeepCloneable
|
|||||||
/// The form of the Pokémon at the time it was sent out.
|
/// The form of the Pokémon at the time it was sent out.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IForm OriginalForm { get; }
|
IForm OriginalForm { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last move choice executed by the Pokémon.
|
||||||
|
/// </summary>
|
||||||
|
IMoveChoice? LastMoveChoice { get; internal set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IPokemon"/>
|
/// <inheritdoc cref="IPokemon"/>
|
||||||
@ -1490,6 +1498,9 @@ public class PokemonBattleDataImpl : IPokemonBattleData
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public uint SwitchInTurn { get; set; }
|
public uint SwitchInTurn { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IBattleSide BattleSide => Battle.Sides[SideIndex];
|
public IBattleSide BattleSide => Battle.Sides[SideIndex];
|
||||||
|
|
||||||
@ -1498,4 +1509,7 @@ public class PokemonBattleDataImpl : IPokemonBattleData
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IForm OriginalForm { get; }
|
public IForm OriginalForm { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IMoveChoice? LastMoveChoice { get; set; }
|
||||||
}
|
}
|
@ -12,5 +12,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/>
|
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="AI\Explicit\ExplicitAI.*.cs">
|
||||||
|
<DependentUpon>ExplicitAI.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -26,4 +26,16 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage
|
|||||||
/// have an end of turn effect, such as Poison or Burn.
|
/// have an end of turn effect, such as Poison or Burn.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
|
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Script for getting the expected entry damage by the AI.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAIInfoScriptExpectedEntryDamage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
void ExpectedEntryDamage(IPokemon pokemon, ref uint damage);
|
||||||
}
|
}
|
36
Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs
Normal file
36
Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs
Normal file
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using PkmnLib.Dynamic.AI.Explicit;
|
using PkmnLib.Dynamic.AI.Explicit;
|
||||||
using PkmnLib.Plugin.Gen7.Libraries.Battling;
|
using PkmnLib.Plugin.Gen7.Libraries.Battling;
|
||||||
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
using PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||||
using PkmnLib.Static.Moves;
|
using PkmnLib.Static.Moves;
|
||||||
|
|
||||||
namespace PkmnLib.Plugin.Gen7.AI;
|
namespace PkmnLib.Plugin.Gen7.AI;
|
||||||
@ -157,6 +158,8 @@ public static class AIHelperFunctions
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly StringKey FoulPlayAbilityName = "foul_play";
|
||||||
|
|
||||||
private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment,
|
private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment,
|
||||||
AIMoveState move, float desireMult = 1)
|
AIMoveState move, float desireMult = 1)
|
||||||
{
|
{
|
||||||
@ -200,7 +203,7 @@ public static class AIHelperFunctions
|
|||||||
{
|
{
|
||||||
var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
|
var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
|
||||||
x.MoveData.Category == MoveCategory.Physical &&
|
x.MoveData.Category == MoveCategory.Physical &&
|
||||||
x.MoveData.SecondaryEffect?.Name != "foul_play");
|
x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
|
||||||
var inc = hasPhysicalMoves ? 8 : 12;
|
var inc = hasPhysicalMoves ? 8 : 12;
|
||||||
score += (int)(inc * incMult);
|
score += (int)(inc * incMult);
|
||||||
}
|
}
|
||||||
@ -334,4 +337,55 @@ public static class AIHelperFunctions
|
|||||||
|
|
||||||
private static bool Opposes(this IPokemon pokemon, IPokemon target) =>
|
private static bool Opposes(this IPokemon pokemon, IPokemon target) =>
|
||||||
pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide;
|
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<Sleep>() &&
|
||||||
|
status != ScriptUtils.ResolveName<Frozen>() &&
|
||||||
|
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<Sleep>() &&
|
||||||
|
status != ScriptUtils.ResolveName<Frozen>() && IsStatRaiseWorthwhile(pokemon, Statistic.Speed, 1, true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pokemon.ActiveAbility.Name == "flare_boost" && status == ScriptUtils.ResolveName<Burned>() &&
|
||||||
|
IsStatRaiseWorthwhile(pokemon, Statistic.SpecialAttack, 1, true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pokemon.ActiveAbility.Name == "toxic_boost" &&
|
||||||
|
(status == ScriptUtils.ResolveName<Poisoned>() || status == ScriptUtils.ResolveName<BadlyPoisoned>()) &&
|
||||||
|
IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pokemon.ActiveAbility.Name == "poison_heal" && status == ScriptUtils.ResolveName<Poisoned>())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pokemon.ActiveAbility.Name == "magic_guard")
|
||||||
|
{
|
||||||
|
if (status != ScriptUtils.ResolveName<Poisoned>() &&
|
||||||
|
status != ScriptUtils.ResolveName<BadlyPoisoned>() && status != ScriptUtils.ResolveName<Burned>())
|
||||||
|
return false;
|
||||||
|
if (IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
208
Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs
Normal file
208
Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switch out if the Perish Song effect is about to cause the Pokémon to faint.
|
||||||
|
/// </summary>
|
||||||
|
private static bool PerishSong(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||||
|
{
|
||||||
|
if (!pokemon.Volatile.TryGet<PerishSongEffect>(out var effect))
|
||||||
|
return false;
|
||||||
|
return effect.Turns <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly StringKey PoisonHealAbilityName = "poison_heal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switch out if the Pokémon is expected to take significant end-of-turn damage.
|
||||||
|
/// </summary>
|
||||||
|
private static bool SignificantEndOfTurnDamage(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||||
|
IReadOnlyList<IPokemon> reserves)
|
||||||
|
{
|
||||||
|
var eorDamage = 0;
|
||||||
|
pokemon.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(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<LeechSeedEffect>() && ai.Random.GetBool())
|
||||||
|
return true;
|
||||||
|
if (pokemon.Volatile.Contains<NightmareEffect>())
|
||||||
|
return true;
|
||||||
|
if (pokemon.Volatile.Contains<GhostCurseEffect>())
|
||||||
|
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<IPokemon> 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<StringKey, List<StringKey>> StatusCureAbilities = new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
ImmunityAbilityName,
|
||||||
|
[ScriptUtils.ResolveName<Poisoned>(), ScriptUtils.ResolveName<BadlyPoisoned>()]
|
||||||
|
},
|
||||||
|
{ InsomniaAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
|
||||||
|
{ LimberAbilityName, [ScriptUtils.ResolveName<Paralyzed>()] },
|
||||||
|
{ MagmaArmorAbilityName, [ScriptUtils.ResolveName<Frozen>()] },
|
||||||
|
{ VitalSpiritAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
|
||||||
|
{ WaterBubbleAbilityName, [ScriptUtils.ResolveName<Burned>()] },
|
||||||
|
{ WaterVeilAbilityName, [ScriptUtils.ResolveName<Burned>()] },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switch out to cure a status problem or heal HP with abilities like Natural Cure or Regenerator.
|
||||||
|
/// </summary>
|
||||||
|
private static bool CureStatusProblemBySwitchingOut(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||||
|
IReadOnlyList<IPokemon> 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<ToxicSpikesEffect>(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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<ScriptAttribute>();
|
||||||
|
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<AIMoveFailureFunctionAttribute>() != 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<AIBoolHandler>(
|
||||||
|
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<AIMoveScoreFunctionAttribute>() != 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<AIScoreMoveHandler>(
|
||||||
|
Expression.Call(null, scoreMethod, aiParam, optionParam, scoreParam), aiParam, optionParam, scoreParam)
|
||||||
|
.Compile();
|
||||||
|
handlers.MoveEffectScore.Add(attribute.Name, functionExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
@ -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<ScriptAttribute>();
|
|
||||||
if (attribute == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (attribute.Category == ScriptCategory.Move)
|
|
||||||
{
|
|
||||||
var failureMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
|
||||||
m.GetCustomAttribute<AIMoveFailureFunctionAttribute>() != 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<AIBoolHandler>(
|
|
||||||
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<AIMoveScoreFunctionAttribute>() != 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<AIScoreMoveHandler>(
|
|
||||||
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<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -50,7 +50,7 @@ public class Gen7Plugin : Plugin<Gen7PluginConfiguration>, IResourceProvider
|
|||||||
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
|
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
|
||||||
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
|
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
|
||||||
|
|
||||||
ExplicitAIFunctions.RegisterAIFunctions(registry.ExplicitAIHandlers);
|
ExplicitAIFunctionRegistration.RegisterAIFunctions(registry.ExplicitAIHandlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -60,10 +60,10 @@ public class ChangeUserAttack : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Attack);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Attack);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.Attack, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,10 +75,10 @@ public class ChangeUserDefense : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Defense);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Defense);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.Defense, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,10 +90,11 @@ public class ChangeUserSpecialAttack : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialAttack);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) =>
|
||||||
|
WouldMoveFail(option, Statistic.SpecialAttack);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.SpecialAttack, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,10 +106,11 @@ public class ChangeUserSpecialDefense : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialDefense);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) =>
|
||||||
|
WouldMoveFail(option, Statistic.SpecialDefense);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.SpecialDefense, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,10 +122,10 @@ public class ChangeUserSpeed : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Speed);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Speed);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.Speed, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +137,10 @@ public class ChangeUserAccuracy : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Accuracy);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Accuracy);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.Accuracy, ref score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,9 +152,9 @@ public class ChangeUserEvasion : ChangeUserStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AIMoveFailureFunction]
|
[AIMoveFailureFunction]
|
||||||
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Evasion);
|
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Evasion);
|
||||||
|
|
||||||
[AIMoveScoreFunction]
|
[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);
|
GetMoveEffectScore(option, Statistic.Evasion, ref score);
|
||||||
}
|
}
|
@ -8,25 +8,7 @@ public class Conversion2 : Script, IScriptOnSecondaryEffect
|
|||||||
{
|
{
|
||||||
var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices;
|
var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices;
|
||||||
var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek();
|
var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek();
|
||||||
var lastMoveByTarget = previousTurnChoices?
|
var lastMoveByTarget = target.BattleData?.LastMoveChoice;
|
||||||
// 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<IMoveChoice>().FirstOrDefault(x => x.User == target);
|
|
||||||
if (lastMoveByTarget == null)
|
if (lastMoveByTarget == null)
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
|
@ -8,8 +8,7 @@ public class Copycat : Script, IScriptChangeMove
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ChangeMove(IMoveChoice choice, ref StringKey moveName)
|
public void ChangeMove(IMoveChoice choice, ref StringKey moveName)
|
||||||
{
|
{
|
||||||
var lastMove = choice.User.BattleData?.Battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
|
var lastMove = choice.User.BattleData?.LastMoveChoice;
|
||||||
.LastOrDefault();
|
|
||||||
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
|
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
|
||||||
{
|
{
|
||||||
choice.Fail();
|
choice.Fail();
|
||||||
|
@ -11,8 +11,7 @@ public class Disable : Script, IScriptOnSecondaryEffect
|
|||||||
var battleData = move.User.BattleData;
|
var battleData = move.User.BattleData;
|
||||||
if (battleData == null)
|
if (battleData == null)
|
||||||
return;
|
return;
|
||||||
var choiceQueue = battleData.Battle.PreviousTurnChoices;
|
var lastMove = target.BattleData?.LastMoveChoice;
|
||||||
var lastMove = choiceQueue.SelectMany(x => x).OfType<IMoveChoice>().LastOrDefault(x => x.User == target);
|
|
||||||
if (lastMove == null)
|
if (lastMove == null)
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
|
@ -12,9 +12,7 @@ public class Encore : Script, IScriptOnSecondaryEffect
|
|||||||
if (battle == null)
|
if (battle == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
|
var lastMove = target.BattleData?.LastMoveChoice;
|
||||||
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
|
|
||||||
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.User == target);
|
|
||||||
if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove))
|
if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove))
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
|
@ -11,7 +11,7 @@ public class FusionBolt : Script, IScriptChangeDamageModifier
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Grab the choices for the current turn, that have been executed before this move.
|
// 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.
|
// Of these, find the move choice that used Fusion Flare.
|
||||||
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_flare");
|
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_flare");
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ public class FusionFlare : Script, IScriptChangeDamageModifier
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Grab the choices for the current turn, that have been executed before this move.
|
// 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.
|
// Of these, find the move choice that used Fusion Bolt.
|
||||||
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_bolt");
|
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_bolt");
|
||||||
|
|
||||||
|
@ -14,8 +14,7 @@ public class Instruct : Script, IScriptOnSecondaryEffect
|
|||||||
if (battleData == null)
|
if (battleData == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var lastMoveChoiceByTarget = battleData.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse()
|
var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice;
|
||||||
.SkipWhile(x => x != move.MoveChoice).OfType<MoveChoice>().FirstOrDefault(x => x.User == target);
|
|
||||||
|
|
||||||
if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget))
|
if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget))
|
||||||
{
|
{
|
||||||
|
@ -21,8 +21,7 @@ public class Sketch : Script, IScriptOnSecondaryEffect
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var choiceQueue = move.Battle.PreviousTurnChoices;
|
var lastMove = target.BattleData?.LastMoveChoice;
|
||||||
var lastMove = choiceQueue.SelectMany(x => x).OfType<IMoveChoice>().LastOrDefault(x => x.User == target);
|
|
||||||
if (lastMove == null || lastMove.ChosenMove.MoveData.HasFlag("not_sketchable"))
|
if (lastMove == null || lastMove.ChosenMove.MoveData.HasFlag("not_sketchable"))
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
|
@ -6,8 +6,7 @@ public class Spite : Script, IScriptOnSecondaryEffect
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
||||||
{
|
{
|
||||||
var lastMoveChoiceByTarget = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse()
|
var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice;
|
||||||
.SkipWhile(x => x != move.MoveChoice).OfType<MoveChoice>().FirstOrDefault(x => x.User == target);
|
|
||||||
if (lastMoveChoiceByTarget == null || lastMoveChoiceByTarget.HasFailed)
|
if (lastMoveChoiceByTarget == null || lastMoveChoiceByTarget.HasFailed)
|
||||||
{
|
{
|
||||||
move.GetHitData(target, hit).Fail();
|
move.GetHitData(target, hit).Fail();
|
||||||
|
@ -6,8 +6,7 @@ public class StompingTantrum : Script, IScriptChangeBasePower
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
|
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
|
||||||
{
|
{
|
||||||
var lastMoveChoice = move.Battle.PreviousTurnChoices.Reverse().Skip(1).SelectMany(x => x.Reverse())
|
var lastMoveChoice = move.User.BattleData?.LastMoveChoice;
|
||||||
.OfType<IMoveChoice>().FirstOrDefault(x => x.User == move.User);
|
|
||||||
if (lastMoveChoice is { HasFailed: true })
|
if (lastMoveChoice is { HasFailed: true })
|
||||||
{
|
{
|
||||||
basePower = basePower.MultiplyOrMax(2);
|
basePower = basePower.MultiplyOrMax(2);
|
||||||
|
@ -6,8 +6,7 @@ public class Torment : Script, IScriptOnSecondaryEffect
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
|
||||||
{
|
{
|
||||||
var lastTargetChoice = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse().OfType<IMoveChoice>()
|
var lastTargetChoice = target.BattleData?.LastMoveChoice;
|
||||||
.FirstOrDefault(x => x.User == target);
|
|
||||||
target.Volatile.Add(new Pokemon.TormentEffect(lastTargetChoice));
|
target.Volatile.Add(new Pokemon.TormentEffect(lastTargetChoice));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,20 +3,20 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
|||||||
[Script(ScriptCategory.Pokemon, "perish_song")]
|
[Script(ScriptCategory.Pokemon, "perish_song")]
|
||||||
public class PerishSongEffect : Script, IScriptOnEndTurn
|
public class PerishSongEffect : Script, IScriptOnEndTurn
|
||||||
{
|
{
|
||||||
private int _turns;
|
internal int Turns { get; private set; }
|
||||||
private IPokemon _owner;
|
private IPokemon _owner;
|
||||||
|
|
||||||
public PerishSongEffect(IPokemon owner, int turns = 3)
|
public PerishSongEffect(IPokemon owner, int turns = 3)
|
||||||
{
|
{
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
_turns = turns;
|
Turns = turns;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
public void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||||
{
|
{
|
||||||
_turns--;
|
Turns--;
|
||||||
if (_turns <= 0)
|
if (Turns <= 0)
|
||||||
{
|
{
|
||||||
RemoveSelf();
|
RemoveSelf();
|
||||||
_owner.Faint(DamageSource.Misc);
|
_owner.Faint(DamageSource.Misc);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
|
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
|
||||||
[Script(ScriptCategory.Side, "spikes")]
|
[Script(ScriptCategory.Side, "spikes")]
|
||||||
public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
|
public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack, IAIInfoScriptExpectedEntryDamage
|
||||||
{
|
{
|
||||||
private int _layers = 1;
|
private int _layers = 1;
|
||||||
|
|
||||||
@ -18,14 +18,7 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
|
|||||||
if (pokemon.IsFloating)
|
if (pokemon.IsFloating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var modifier = _layers switch
|
var damage = CalculateDamage(pokemon);
|
||||||
{
|
|
||||||
1 => 1 / 16f,
|
|
||||||
2 => 3 / 16f,
|
|
||||||
3 => 1 / 4f,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(),
|
|
||||||
};
|
|
||||||
var damage = (uint)(pokemon.MaxHealth * modifier);
|
|
||||||
|
|
||||||
EventBatchId eventBatch = new();
|
EventBatchId eventBatch = new();
|
||||||
pokemon.Damage(damage, DamageSource.Misc, eventBatch);
|
pokemon.Damage(damage, DamageSource.Misc, eventBatch);
|
||||||
@ -37,4 +30,26 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
|
|||||||
BatchId = eventBatch,
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ExpectedEntryDamage(IPokemon pokemon, ref uint damage)
|
||||||
|
{
|
||||||
|
if (pokemon.IsFloating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
damage += CalculateDamage(pokemon);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,18 +1,12 @@
|
|||||||
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
|
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||||
|
|
||||||
[Script(ScriptCategory.Side, "stealth_rock")]
|
[Script(ScriptCategory.Side, "stealth_rock")]
|
||||||
public class StealthRockEffect : Script, IScriptOnSwitchIn
|
public class StealthRockEffect : Script, IScriptOnSwitchIn, IAIInfoScriptExpectedEntryDamage
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OnSwitchIn(IPokemon pokemon, byte position)
|
public void OnSwitchIn(IPokemon pokemon, byte position)
|
||||||
{
|
{
|
||||||
var typeLibrary = pokemon.Library.StaticLibrary.Types;
|
var damage = CalculateStealthRockDamage(pokemon);
|
||||||
var effectiveness = 1.0f;
|
|
||||||
if (typeLibrary.TryGetTypeIdentifier("rock", out var rockType))
|
|
||||||
{
|
|
||||||
effectiveness = typeLibrary.GetEffectiveness(rockType, pokemon.Types);
|
|
||||||
}
|
|
||||||
var damage = (uint)(pokemon.MaxHealth / 8f * effectiveness);
|
|
||||||
EventBatchId batchId = new();
|
EventBatchId batchId = new();
|
||||||
pokemon.Damage(damage, DamageSource.Misc, batchId);
|
pokemon.Damage(damage, DamageSource.Misc, batchId);
|
||||||
pokemon.BattleData?.Battle.EventHook.Invoke(new DialogEvent("stealth_rock_damage",
|
pokemon.BattleData?.Battle.EventHook.Invoke(new DialogEvent("stealth_rock_damage",
|
||||||
@ -21,4 +15,21 @@ public class StealthRockEffect : Script, IScriptOnSwitchIn
|
|||||||
{ "pokemon", pokemon },
|
{ "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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ExpectedEntryDamage(IPokemon pokemon, ref uint damage)
|
||||||
|
{
|
||||||
|
damage += CalculateStealthRockDamage(pokemon);
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,15 +3,15 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Status;
|
|||||||
[Script(ScriptCategory.Status, "badly_poisoned")]
|
[Script(ScriptCategory.Status, "badly_poisoned")]
|
||||||
public class BadlyPoisoned : Poisoned
|
public class BadlyPoisoned : Poisoned
|
||||||
{
|
{
|
||||||
private int _turns = 1;
|
internal int Turns { get; private set; } = 1;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override float GetPoisonMultiplier() => 1f / (16f * _turns);
|
public override float GetPoisonMultiplier() => 1f / (16f * Turns);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void OnEndTurn(IScriptSource owner, IBattle battle)
|
public override void OnEndTurn(IScriptSource owner, IBattle battle)
|
||||||
{
|
{
|
||||||
base.OnEndTurn(owner, battle);
|
base.OnEndTurn(owner, battle);
|
||||||
_turns = Math.Min(_turns + 1, 15);
|
Turns = Math.Min(Turns + 1, 15);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user