Implements AI Switching
All checks were successful
Build / Build (push) Successful in 58s

This commit is contained in:
Deukhoofd 2025-07-12 13:03:00 +02:00
parent 364d4b9080
commit bf83b25238
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
34 changed files with 903 additions and 226 deletions

View File

@ -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 =>

View File

@ -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)
{ {

View 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();
}
}

View 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;
}
}

View File

@ -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;
}
} }

View File

@ -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;

View File

@ -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;

View File

@ -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()

View File

@ -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);
} }

View File

@ -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; }
} }

View File

@ -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>

View File

@ -27,3 +27,15 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage
/// </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);
}

View 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;
}
}
}
}

View File

@ -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;
}
} }

View 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;
}
}

View File

@ -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
}

View File

@ -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;
}
}
}
}

View File

@ -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 />

View File

@ -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);
} }

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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");

View File

@ -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");

View File

@ -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))
{ {

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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));
} }
} }

View File

@ -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);

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
} }
} }