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

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

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.Plugin.Gen7.Libraries.Battling;
using PkmnLib.Plugin.Gen7.Scripts.Side;
using PkmnLib.Plugin.Gen7.Scripts.Status;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.AI;
@@ -157,6 +158,8 @@ public static class AIHelperFunctions
return true;
}
private static readonly StringKey FoulPlayAbilityName = "foul_play";
private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment,
AIMoveState move, float desireMult = 1)
{
@@ -200,7 +203,7 @@ public static class AIHelperFunctions
{
var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
x.MoveData.Category == MoveCategory.Physical &&
x.MoveData.SecondaryEffect?.Name != "foul_play");
x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
var inc = hasPhysicalMoves ? 8 : 12;
score += (int)(inc * incMult);
}
@@ -334,4 +337,55 @@ public static class AIHelperFunctions
private static bool Opposes(this IPokemon pokemon, IPokemon target) =>
pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide;
public static bool WantsStatusProblem(IPokemon pokemon, StringKey? status)
{
if (status is null)
return true;
if (pokemon.ActiveAbility != null)
{
if (pokemon.ActiveAbility.Name == "guts" && status != ScriptUtils.ResolveName<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.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
ExplicitAIFunctions.RegisterAIFunctions(registry.ExplicitAIHandlers);
ExplicitAIFunctionRegistration.RegisterAIFunctions(registry.ExplicitAIHandlers);
}
/// <inheritdoc />

View File

@@ -60,10 +60,10 @@ public class ChangeUserAttack : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Attack);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Attack);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Attack, ref score);
}
@@ -75,10 +75,10 @@ public class ChangeUserDefense : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Defense);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Defense);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Defense, ref score);
}
@@ -90,10 +90,11 @@ public class ChangeUserSpecialAttack : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialAttack);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) =>
WouldMoveFail(option, Statistic.SpecialAttack);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.SpecialAttack, ref score);
}
@@ -105,10 +106,11 @@ public class ChangeUserSpecialDefense : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.SpecialDefense);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) =>
WouldMoveFail(option, Statistic.SpecialDefense);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.SpecialDefense, ref score);
}
@@ -120,10 +122,10 @@ public class ChangeUserSpeed : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Speed);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Speed);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Speed, ref score);
}
@@ -135,10 +137,10 @@ public class ChangeUserAccuracy : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Accuracy);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Accuracy);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Accuracy, ref score);
}
@@ -150,9 +152,9 @@ public class ChangeUserEvasion : ChangeUserStats
}
[AIMoveFailureFunction]
public static bool AIMoveWillFail(MoveOption option) => WouldMoveFail(option, Statistic.Evasion);
public static bool AIMoveWillFail(IExplicitAI ai, MoveOption option) => WouldMoveFail(option, Statistic.Evasion);
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Evasion, ref score);
}

View File

@@ -8,25 +8,7 @@ public class Conversion2 : Script, IScriptOnSecondaryEffect
{
var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices;
var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek();
var lastMoveByTarget = previousTurnChoices?
// The previous turn choices include the choices of the current turn, so we need to have special handling for
// the current turn
.Select((x, index) =>
{
// All choices before the current turn are valid
if (index < previousTurnChoices.Count - 1)
return x;
// If there is no next choice, we're at the end of the list, so we can just return the whole list
if (nextExecutingChoice == null)
return x;
// Otherwise we determine where the next choice is and return everything before that
var indexOfNext = x.IndexOf(nextExecutingChoice);
if (indexOfNext == -1)
return x;
return x.Take(indexOfNext);
}).SelectMany(x => x)
// We only want the last move choice by the target
.OfType<IMoveChoice>().FirstOrDefault(x => x.User == target);
var lastMoveByTarget = target.BattleData?.LastMoveChoice;
if (lastMoveByTarget == null)
{
move.GetHitData(target, hit).Fail();

View File

@@ -8,8 +8,7 @@ public class Copycat : Script, IScriptChangeMove
/// <inheritdoc />
public void ChangeMove(IMoveChoice choice, ref StringKey moveName)
{
var lastMove = choice.User.BattleData?.Battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
.LastOrDefault();
var lastMove = choice.User.BattleData?.LastMoveChoice;
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
{
choice.Fail();

View File

@@ -11,8 +11,7 @@ public class Disable : Script, IScriptOnSecondaryEffect
var battleData = move.User.BattleData;
if (battleData == null)
return;
var choiceQueue = battleData.Battle.PreviousTurnChoices;
var lastMove = choiceQueue.SelectMany(x => x).OfType<IMoveChoice>().LastOrDefault(x => x.User == target);
var lastMove = target.BattleData?.LastMoveChoice;
if (lastMove == null)
{
move.GetHitData(target, hit).Fail();

View File

@@ -12,9 +12,7 @@ public class Encore : Script, IScriptOnSecondaryEffect
if (battle == null)
return;
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.User == target);
var lastMove = target.BattleData?.LastMoveChoice;
if (lastMove == null || battle.Library.MiscLibrary.IsReplacementChoice(lastMove))
{
move.GetHitData(target, hit).Fail();

View File

@@ -11,7 +11,7 @@ public class FusionBolt : Script, IScriptChangeDamageModifier
return;
// Grab the choices for the current turn, that have been executed before this move.
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => x != move.MoveChoice)
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice))
// Of these, find the move choice that used Fusion Flare.
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_flare");

View File

@@ -11,7 +11,7 @@ public class FusionFlare : Script, IScriptChangeDamageModifier
return;
// Grab the choices for the current turn, that have been executed before this move.
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => x != move.MoveChoice)
var choice = battleData.Battle.PreviousTurnChoices.Last().TakeWhile(x => !Equals(x, move.MoveChoice))
// Of these, find the move choice that used Fusion Bolt.
.OfType<MoveChoice>().FirstOrDefault(x => x.ChosenMove.MoveData.Name == "fusion_bolt");

View File

@@ -14,8 +14,7 @@ public class Instruct : Script, IScriptOnSecondaryEffect
if (battleData == null)
return;
var lastMoveChoiceByTarget = battleData.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse()
.SkipWhile(x => x != move.MoveChoice).OfType<MoveChoice>().FirstOrDefault(x => x.User == target);
var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice;
if (lastMoveChoiceByTarget == null || !battleData.Battle.CanUse(lastMoveChoiceByTarget))
{

View File

@@ -21,8 +21,7 @@ public class Sketch : Script, IScriptOnSecondaryEffect
return;
}
var choiceQueue = move.Battle.PreviousTurnChoices;
var lastMove = choiceQueue.SelectMany(x => x).OfType<IMoveChoice>().LastOrDefault(x => x.User == target);
var lastMove = target.BattleData?.LastMoveChoice;
if (lastMove == null || lastMove.ChosenMove.MoveData.HasFlag("not_sketchable"))
{
move.GetHitData(target, hit).Fail();

View File

@@ -6,8 +6,7 @@ public class Spite : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var lastMoveChoiceByTarget = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse()
.SkipWhile(x => x != move.MoveChoice).OfType<MoveChoice>().FirstOrDefault(x => x.User == target);
var lastMoveChoiceByTarget = target.BattleData?.LastMoveChoice;
if (lastMoveChoiceByTarget == null || lastMoveChoiceByTarget.HasFailed)
{
move.GetHitData(target, hit).Fail();

View File

@@ -6,8 +6,7 @@ public class StompingTantrum : Script, IScriptChangeBasePower
/// <inheritdoc />
public void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
var lastMoveChoice = move.Battle.PreviousTurnChoices.Reverse().Skip(1).SelectMany(x => x.Reverse())
.OfType<IMoveChoice>().FirstOrDefault(x => x.User == move.User);
var lastMoveChoice = move.User.BattleData?.LastMoveChoice;
if (lastMoveChoice is { HasFailed: true })
{
basePower = basePower.MultiplyOrMax(2);

View File

@@ -6,8 +6,7 @@ public class Torment : Script, IScriptOnSecondaryEffect
/// <inheritdoc />
public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var lastTargetChoice = move.Battle.PreviousTurnChoices.SelectMany(x => x).Reverse().OfType<IMoveChoice>()
.FirstOrDefault(x => x.User == target);
var lastTargetChoice = target.BattleData?.LastMoveChoice;
target.Volatile.Add(new Pokemon.TormentEffect(lastTargetChoice));
}
}

View File

@@ -3,20 +3,20 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "perish_song")]
public class PerishSongEffect : Script, IScriptOnEndTurn
{
private int _turns;
internal int Turns { get; private set; }
private IPokemon _owner;
public PerishSongEffect(IPokemon owner, int turns = 3)
{
_owner = owner;
_turns = turns;
Turns = turns;
}
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
_turns--;
if (_turns <= 0)
Turns--;
if (Turns <= 0)
{
RemoveSelf();
_owner.Faint(DamageSource.Misc);

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "spikes")]
public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack, IAIInfoScriptExpectedEntryDamage
{
private int _layers = 1;
@@ -18,14 +18,7 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
if (pokemon.IsFloating)
return;
var modifier = _layers switch
{
1 => 1 / 16f,
2 => 3 / 16f,
3 => 1 / 4f,
_ => throw new ArgumentOutOfRangeException(),
};
var damage = (uint)(pokemon.MaxHealth * modifier);
var damage = CalculateDamage(pokemon);
EventBatchId eventBatch = new();
pokemon.Damage(damage, DamageSource.Misc, eventBatch);
@@ -37,4 +30,26 @@ public class SpikesEffect : Script, IScriptOnSwitchIn, IScriptStack
BatchId = eventBatch,
});
}
private uint CalculateDamage(IPokemon pokemon)
{
var modifier = _layers switch
{
1 => 1 / 16f,
2 => 3 / 16f,
3 => 1 / 4f,
_ => throw new ArgumentOutOfRangeException(),
};
var damage = (uint)(pokemon.MaxHealth * modifier);
return damage;
}
/// <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;
[Script(ScriptCategory.Side, "stealth_rock")]
public class StealthRockEffect : Script, IScriptOnSwitchIn
public class StealthRockEffect : Script, IScriptOnSwitchIn, IAIInfoScriptExpectedEntryDamage
{
/// <inheritdoc />
public void OnSwitchIn(IPokemon pokemon, byte position)
{
var typeLibrary = pokemon.Library.StaticLibrary.Types;
var effectiveness = 1.0f;
if (typeLibrary.TryGetTypeIdentifier("rock", out var rockType))
{
effectiveness = typeLibrary.GetEffectiveness(rockType, pokemon.Types);
}
var damage = (uint)(pokemon.MaxHealth / 8f * effectiveness);
var damage = CalculateStealthRockDamage(pokemon);
EventBatchId batchId = new();
pokemon.Damage(damage, DamageSource.Misc, batchId);
pokemon.BattleData?.Battle.EventHook.Invoke(new DialogEvent("stealth_rock_damage",
@@ -21,4 +15,21 @@ public class StealthRockEffect : Script, IScriptOnSwitchIn
{ "pokemon", pokemon },
}));
}
private static uint CalculateStealthRockDamage(IPokemon pokemon)
{
var typeLibrary = pokemon.Library.StaticLibrary.Types;
var effectiveness = 1.0f;
if (typeLibrary.TryGetTypeIdentifier("rock", out var rockType))
{
effectiveness = typeLibrary.GetEffectiveness(rockType, pokemon.Types);
}
return (uint)(pokemon.MaxHealth / 8f * effectiveness);
}
/// <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")]
public class BadlyPoisoned : Poisoned
{
private int _turns = 1;
internal int Turns { get; private set; } = 1;
/// <inheritdoc />
public override float GetPoisonMultiplier() => 1f / (16f * _turns);
public override float GetPoisonMultiplier() => 1f / (16f * Turns);
/// <inheritdoc />
public override void OnEndTurn(IScriptSource owner, IBattle battle)
{
base.OnEndTurn(owner, battle);
_turns = Math.Min(_turns + 1, 15);
Turns = Math.Min(Turns + 1, 15);
}
}