From be5100df8ad126861cd738f1a2393c341ec342da Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Sat, 23 May 2026 12:57:15 +0200 Subject: [PATCH] Implement stat drop handling for AI, Fixes for Conversion2 --- PkmnLib.Static/Libraries/TypeLibrary.cs | 5 +- PkmnLib.Static/TypeIdentifier.cs | 3 + .../AI/AIHelperFunctions.cs | 238 +++++++++++++++++- .../Scripts/Moves/ChangeTargetStats.cs | 44 ++++ .../Scripts/Moves/Conversion2.cs | 4 +- 5 files changed, 286 insertions(+), 8 deletions(-) diff --git a/PkmnLib.Static/Libraries/TypeLibrary.cs b/PkmnLib.Static/Libraries/TypeLibrary.cs index f9fffcc..f00757e 100644 --- a/PkmnLib.Static/Libraries/TypeLibrary.cs +++ b/PkmnLib.Static/Libraries/TypeLibrary.cs @@ -84,7 +84,10 @@ public class TypeLibrary : IReadOnlyTypeLibrary public IEnumerable<(TypeIdentifier, float)> GetAllEffectivenessFromAttacking(TypeIdentifier attacking) { if (attacking.Value < 1 || attacking.Value > _effectiveness.Count) - throw new ArgumentOutOfRangeException(nameof(attacking)); + { + throw new ArgumentOutOfRangeException(nameof(attacking), attacking, + "Attacking type index is out of range."); + } for (var i = 0; i < _effectiveness.Count; i++) { var type = _types[i]; diff --git a/PkmnLib.Static/TypeIdentifier.cs b/PkmnLib.Static/TypeIdentifier.cs index d6b0843..6950cd3 100644 --- a/PkmnLib.Static/TypeIdentifier.cs +++ b/PkmnLib.Static/TypeIdentifier.cs @@ -36,4 +36,7 @@ public readonly struct TypeIdentifier : IEquatable public static bool operator ==(TypeIdentifier left, TypeIdentifier right) => left.Equals(right); public static bool operator !=(TypeIdentifier left, TypeIdentifier right) => !left.Equals(right); + + /// + public override string ToString() => Name.ToString(); } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs b/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs index f49f925..949f32d 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/AI/AIHelperFunctions.cs @@ -23,7 +23,7 @@ public static class AIHelperFunctions { return ExplicitAI.MoveUselessScore; } - return GetScoreForTargetStatDrop(move, target, statChanges, fixedChange, true); + return GetScoreForTargetStatDrop(score, move, target, statChanges, fixedChange, true); } var addEffect = move.GetScoreChangeForAdditionalEffect(target); @@ -80,9 +80,70 @@ public static class AIHelperFunctions return score; } - public static int GetScoreForTargetStatDrop(AIMoveState move, IPokemon target, StatisticSet statChanges, - bool fixedChange = false, bool ignoreContrary = false) => - throw new NotImplementedException("This method is not implemented"); + public static int GetScoreForTargetStatDrop(int score, AIMoveState move, IPokemon target, + StatisticSet statChanges, bool fixedChange = false, bool ignoreContrary = false) + { + var wholeEffect = move.Move.Category != MoveCategory.Status; + + var desireMult = -1; + if (move.User.BattleData?.SideIndex == target.BattleData?.SideIndex) + desireMult = 1; + if (!ignoreContrary && !fixedChange && target.ActiveAbility?.Name == "contrary") + { + if (desireMult > 0 && wholeEffect) + { + return ExplicitAI.MoveUselessScore; + } + return GetScoreForTargetStatRaise(score, move, target, statChanges, fixedChange, true); + } + var addEffect = move.GetScoreChangeForAdditionalEffect(target); + if (addEffect == -999) + { + return score; + } + var expectedEndOfTurnDamage = 0; + target.RunScriptHook(x => + x.ExpectedEndOfTurnDamage(target, ref expectedEndOfTurnDamage)); + // If the target is expected to faint from the end of turn damage, the stat drop is useless + if (expectedEndOfTurnDamage >= target.CurrentHealth) + return wholeEffect ? ExplicitAI.MoveUselessScore : score; + + var foeIsAware = false; + if (target.BattleData?.BattleSide.Pokemon.All(x => x?.ActiveAbility?.Name != "unaware") == true) + { + foeIsAware = true; + } + if (!foeIsAware) + { + return wholeEffect ? ExplicitAI.MoveUselessScore : score; + } + var realStatChanges = new StatBoostStatisticSet(); + foreach (var (stat, i) in statChanges) + { + var decrement = i; + if (!IsStatDropWorthwhile(target, stat, decrement)) + { + continue; + } + if (!fixedChange && target.ActiveAbility?.Name == "simple") + { + decrement *= 2; + } + decrement = (sbyte)Math.Min(decrement, + StatBoostStatisticSet.MinStatBoost - target.StatBoost.GetStatistic(stat)); + realStatChanges.SetStatistic(stat, (sbyte)-decrement); + } + if (realStatChanges.IsEmpty) + return wholeEffect ? ExplicitAI.MoveUselessScore : score; + score += addEffect; + score = GetTargetStatDropScoreGeneric(score, target, realStatChanges, move, desireMult); + foreach (var realStatChange in realStatChanges.Where(x => x.value > 0)) + { + GetTargetStatDropScoreOne(ref score, target, realStatChange.statistic, realStatChange.value, move, + desireMult); + } + return score; + } /// /// Checks if a stat raise is worthwhile for the given Pokémon and stat. @@ -158,6 +219,53 @@ public static class AIHelperFunctions return true; } + private static bool IsStatDropWorthwhile(IPokemon pokemon, Statistic stat, sbyte amount) + { + if (amount == 0) + return false; + switch (stat) + { + case Statistic.Attack: + { + return pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical && + x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName); + } + case Statistic.Defense: + { + var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide); + return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y => + y.MoveData.Category == MoveCategory.Physical || y.MoveData.SecondaryEffect?.Name == "psyshock")); + } + case Statistic.SpecialAttack: + { + return pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Special); + } + case Statistic.SpecialDefense: + { + var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide); + return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y => + y.MoveData.Category == MoveCategory.Special && y.MoveData.SecondaryEffect?.Name != "psyshock")); + } + case Statistic.Speed: + { + if (!pokemon.HasMoveWithEffect("electro_ball")) + { + var targetSpeed = pokemon.BoostedStats.Speed; + var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide); + return opponentSide.Pokemon.WhereNotNull().Select(opponent => opponent.BoostedStats.Speed) + .Any(foeSpeed => targetSpeed > foeSpeed && targetSpeed < foeSpeed * 2.5); + } + return true; + } + case Statistic.Accuracy: + { + var meaningful = pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Accuracy != 255); + return meaningful; + } + } + return true; + } + private static readonly StringKey FoulPlayAbilityName = "foul_play"; private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment, @@ -295,6 +403,115 @@ public static class AIHelperFunctions } } + private static void GetTargetStatDropScoreOne(ref int score, IPokemon target, Statistic stat, sbyte decrement, + AIMoveState move, float desireMult = 1) + { + var oldStage = target.StatBoost.GetStatistic(stat); + var newStage = (sbyte)(oldStage - decrement); + var decMult = Gen7BattleStatCalculator.GetStatBoostModifier(oldStage) / + Gen7BattleStatCalculator.GetStatBoostModifier(Math.Max(newStage, (sbyte)-6)); + decMult -= 1; + decMult *= desireMult; + var opponentSide = target.BattleData!.Battle.Sides.First(x => x != target.BattleData.BattleSide); + + switch (stat) + { + case Statistic.Attack: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + { + var hasSpecialMoves = target.Moves.WhereNotNull() + .Any(x => x.MoveData.Category == MoveCategory.Special); + var dec = hasSpecialMoves ? 8 : 12; + score += (int)(dec * decMult); + } + break; + } + case Statistic.Defense: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + score += (int)(10 * decMult); + break; + } + case Statistic.SpecialAttack: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + { + var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x => + x.MoveData.Category == MoveCategory.Physical && + x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName); + var dec = hasPhysicalMoves ? 8 : 12; + score += (int)(dec * decMult); + } + break; + } + case Statistic.SpecialDefense: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + score += (int)(10 * decMult); + break; + } + case Statistic.Speed: + { + var targetSpeed = target.BoostedStats.Speed; + foreach (var opponent in opponentSide.Pokemon.WhereNotNull()) + { + var foeSpeed = opponent.BoostedStats.Speed; + if (targetSpeed < foeSpeed) + continue; + if (targetSpeed > foeSpeed * 2.5) + continue; + if (targetSpeed < foeSpeed * 2 / (decrement + 2)) + score += (int)(15 * decMult); + else + score += (int)(8 * decMult); + break; + } + if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("electro_ball"))) + { + score += (int)(5 * decMult); + } + if (target.ActiveAbility?.Name == "speed_boost") + { + score -= (int)(15 * (target.Opposes(move.User) ? 1 : desireMult)); + } + break; + } + case Statistic.Accuracy: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + score += (int)(10 * decMult); + break; + } + case Statistic.Evasion: + { + if (oldStage <= -2 && decrement == 1) + score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); + else + score += (int)(10 * decMult); + break; + } + } + if (target.HasMoveWithEffect("power_trip")) + { + score += (int)(5 * decrement * desireMult); + } + if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("punishment"))) + { + score -= (int)(5 * decrement * desireMult); + } + } + /// /// Calculates the score for the generic concept of raising a target's stats. /// @@ -311,6 +528,19 @@ public static class AIHelperFunctions return score; } + private static int GetTargetStatDropScoreGeneric(int score, IPokemon target, StatisticSet statChanges, + AIMoveState move, float desireMult = 1) + { + var totalDecrement = statChanges.Sum(x => x.value); + var turns = target.BattleData!.Battle.CurrentTurnNumber - target.BattleData!.SwitchInTurn; + if (turns < 2 && move.Move.Category == MoveCategory.Status) + score += (int)(totalDecrement * desireMult * 5); + + score += + (int)(totalDecrement * desireMult * ((100 * (target.CurrentHealth / (float)target.MaxHealth) - 50) / 8)); + return score; + } + private static int GetScoreChangeForAdditionalEffect(this AIMoveState move, IPokemon? target) { if (move.Move.SecondaryEffect is null) diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeTargetStats.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeTargetStats.cs index f972ee4..3270f15 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeTargetStats.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeTargetStats.cs @@ -1,3 +1,6 @@ +using PkmnLib.Dynamic.AI.Explicit; +using PkmnLib.Plugin.Gen7.AI; + namespace PkmnLib.Plugin.Gen7.Scripts.Moves; public abstract class ChangeTargetStats : Script, IScriptOnInitialize, IScriptOnSecondaryEffect @@ -31,6 +34,19 @@ public abstract class ChangeTargetStats : Script, IScriptOnInitialize, IScriptOn { target.ChangeStatBoost(_stat, _amount, target == move.User, false); } + + protected static void GetMoveEffectScore(MoveOption option, Statistic stat, ref int score) + { + if (option.Move.Move.SecondaryEffect == null || + !option.Move.Move.SecondaryEffect.Parameters.TryGetValue("amount", out var amountObj) || + amountObj is not int amount) + { + return; + } + var statisticSet = new StatBoostStatisticSet(); + statisticSet.SetStatistic(stat, (sbyte)amount); + score = AIHelperFunctions.GetScoreForTargetStatDrop(score, option.Move, option.Move.User, statisticSet); + } } [Script(ScriptCategory.Move, "change_target_attack")] @@ -39,6 +55,10 @@ public class ChangeTargetAttack : ChangeTargetStats public ChangeTargetAttack() : base(Statistic.Attack) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.Attack, ref score); } [Script(ScriptCategory.Move, "change_target_defense")] @@ -47,6 +67,10 @@ public class ChangeTargetDefense : ChangeTargetStats public ChangeTargetDefense() : base(Statistic.Defense) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.Defense, ref score); } [Script(ScriptCategory.Move, "change_target_special_attack")] @@ -55,6 +79,10 @@ public class ChangeTargetSpecialAttack : ChangeTargetStats public ChangeTargetSpecialAttack() : base(Statistic.SpecialAttack) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.SpecialAttack, ref score); } [Script(ScriptCategory.Move, "change_target_special_defense")] @@ -63,6 +91,10 @@ public class ChangeTargetSpecialDefense : ChangeTargetStats public ChangeTargetSpecialDefense() : base(Statistic.SpecialDefense) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.SpecialDefense, ref score); } [Script(ScriptCategory.Move, "change_target_speed")] @@ -71,6 +103,10 @@ public class ChangeTargetSpeed : ChangeTargetStats public ChangeTargetSpeed() : base(Statistic.Speed) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.Speed, ref score); } [Script(ScriptCategory.Move, "change_target_accuracy")] @@ -79,6 +115,10 @@ public class ChangeTargetAccuracy : ChangeTargetStats public ChangeTargetAccuracy() : base(Statistic.Accuracy) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.Accuracy, ref score); } [Script(ScriptCategory.Move, "change_target_evasion")] @@ -87,4 +127,8 @@ public class ChangeTargetEvasion : ChangeTargetStats public ChangeTargetEvasion() : base(Statistic.Evasion) { } + + [AIMoveScoreFunction] + public static void AIMoveEffectScore(IExplicitAI ai, MoveOption option, ref int score) => + GetMoveEffectScore(option, Statistic.Evasion, ref score); } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs index 396ca7d..d5e1b7f 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs @@ -6,10 +6,8 @@ public class Conversion2 : Script, IScriptOnSecondaryEffect /// public void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) { - var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices; - var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek(); var lastMoveByTarget = target.BattleData?.LastMoveChoice; - if (lastMoveByTarget == null) + if (lastMoveByTarget == null || lastMoveByTarget.ChosenMove.MoveData.MoveType.Name == "none") { move.GetHitData(target, hit).Fail(); return;