using PkmnLib.Dynamic.AI.Explicit; using PkmnLib.Plugin.Gen7.Libraries.Battling; using PkmnLib.Plugin.Gen7.Scripts.Side; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.AI; public static class AIHelperFunctions { public static int GetScoreForTargetStatRaise(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 GetScoreForTargetStatDrop(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, we don't want to // apply the score for the stat raise, as it will not be able to use it. if (expectedEndOfTurnDamage >= target.CurrentHealth) return wholeEffect ? ExplicitAI.MoveUselessScore : score; if (!move.User.HasMoveWithEffect("power_trip")) { var foeIsAware = target.BattleData?.BattleSide.Pokemon.Any(x => x?.ActiveAbility?.Name == "unaware") != true; if (!foeIsAware) { return wholeEffect ? ExplicitAI.MoveUselessScore : score; } } var realStatChanges = new StatBoostStatisticSet(); foreach (var (stat, i) in statChanges) { var increment = i; if (!IsStatRaiseWorthwhile(target, stat, increment, fixedChange)) { continue; } if (!fixedChange && target.ActiveAbility?.Name == "simple") { increment *= 2; } increment = (sbyte)Math.Max(increment, StatBoostStatisticSet.MaxStatBoost - target.StatBoost.GetStatistic(stat)); realStatChanges.SetStatistic(stat, increment); } if (realStatChanges.IsEmpty) { return wholeEffect ? ExplicitAI.MoveUselessScore : score; } score += addEffect; score = GetTargetStatRaiseScoreGeneric(score, target, realStatChanges, move, desireMult); foreach (var realStatChange in realStatChanges.Where(x => x.value > 0)) { GetTargetStatRaiseScoreOne(ref score, target, realStatChange.statistic, realStatChange.value, move, desireMult); } 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"); /// /// Checks if a stat raise is worthwhile for the given Pokémon and stat. /// private static bool IsStatRaiseWorthwhile(IPokemon pokemon, Statistic stat, sbyte amount, bool fixedChange = false) { if (!fixedChange && pokemon.StatBoost.GetStatistic(stat) == StatBoostStatisticSet.MaxStatBoost) return false; if (!pokemon.HasMoveWithEffect("power_trip", "baton_pass")) return true; switch (stat) { case Statistic.Attack: { if (!pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical && x.MoveData.SecondaryEffect?.Name != "foul_play")) { return false; } break; } 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: { if (pokemon.Moves.WhereNotNull().All(x => x.MoveData.Category != MoveCategory.Special)) { return false; } break; } 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", "power_trip")) { var targetSpeed = pokemon.BoostedStats.Speed; var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide); var meaningful = opponentSide.Pokemon.WhereNotNull().Select(opponent => opponent.BoostedStats.Speed) .Any(foeSpeed => targetSpeed < foeSpeed && targetSpeed * 2.5 > foeSpeed); if (!meaningful) return false; } break; } case Statistic.Accuracy: { var minAccuracy = pokemon.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy); if (minAccuracy >= 90 && pokemon.StatBoost.Accuracy >= 0) { var meaningful = false; var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide); if (opponentSide.Pokemon.WhereNotNull().Any(x => x.StatBoost.Evasion > 0)) { meaningful = true; } if (!meaningful) return false; } break; } } return true; } private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment, AIMoveState move, float desireMult = 1) { var oldStage = target.StatBoost.GetStatistic(stat); var newStage = (sbyte)(oldStage + increment); var incMult = Gen7BattleStatCalculator.GetStatBoostModifier(Math.Min(newStage, (sbyte)6)) / Gen7BattleStatCalculator.GetStatBoostModifier(oldStage); var actualIncrement = incMult; incMult -= 1; incMult *= desireMult; var opponentSide = target.BattleData!.Battle.Sides.First(x => x != target.BattleData.BattleSide); switch (stat) { case Statistic.Attack: { if (oldStage >= 2 && increment == 1) score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); else { var hasSpecialMoves = target.Moves.WhereNotNull() .Any(x => x.MoveData.Category == MoveCategory.Special); var inc = hasSpecialMoves ? 8 : 12; score += (int)(inc * incMult); } break; } case Statistic.Defense: { if (oldStage >= 2 && increment == 1) score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); else score += (int)(10 * incMult); break; } case Statistic.SpecialAttack: { if (oldStage >= 2 && increment == 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 != "foul_play"); var inc = hasPhysicalMoves ? 8 : 12; score += (int)(inc * incMult); } break; } case Statistic.SpecialDefense: { if (oldStage >= 2 && increment == 1) score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); else score += (int)(10 * incMult); break; } case Statistic.Speed: { var targetSpeed = target.BoostedStats.Speed; foreach (var opponent in opponentSide.Pokemon.WhereNotNull()) { var foeSpeed = opponent.BoostedStats.Speed; if (foeSpeed <= targetSpeed) continue; if (foeSpeed > targetSpeed * 2.5) continue; if (targetSpeed * actualIncrement > foeSpeed) score += (int)(15 * incMult); else score += (int)(8 * incMult); } if (target.HasMoveWithEffect("electro_ball", "power_trip")) { score += (int)(5 * incMult); } if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("gyro_ball"))) { score -= (int)(5 * incMult); } if (target.ActiveAbility?.Name == "speed_boost") { score -= (int)(15 * (target.Opposes(move.User) ? 1 : desireMult)); } break; } case Statistic.Accuracy: { if (oldStage >= 2 && increment == 1) { score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); } else { var minAccuracy = target.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy); var previousMinAccuracy = minAccuracy * Gen7BattleStatCalculator.GetAccuracyEvasionStatModifier(0, oldStage); if (previousMinAccuracy < 90) { score += (int)(10 * incMult); } } break; } case Statistic.Evasion: { foreach (var opponent in opponentSide.Pokemon.WhereNotNull()) { var endOfTurnDamage = 0; opponent.RunScriptHook(x => x.ExpectedEndOfTurnDamage(opponent, ref endOfTurnDamage)); if (endOfTurnDamage > 0) score += (int)(5 * incMult); } if (oldStage >= 2 && increment == 1) { score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult)); } else { score += (int)(10 * incMult); } break; } } if (target.HasMoveWithEffect("power_trip")) { score += (int)(5 * increment * desireMult); } if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("punishment"))) { score -= (int)(5 * increment * desireMult); } } /// /// Calculates the score for the generic concept of raising a target's stats. /// private static int GetTargetStatRaiseScoreGeneric(int score, IPokemon target, StatisticSet statChanges, AIMoveState move, float desireMult = 1) { var totalIncrement = statChanges.Sum(x => x.value); var turns = target.BattleData!.Battle.CurrentTurnNumber - target.BattleData!.SwitchInTurn; if (turns < 2 && move.Move.Category == MoveCategory.Status) score += (int)(totalIncrement * desireMult * 5); score += (int)(totalIncrement * 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) return 0; if (move.User.ActiveAbility?.Name == "sheer_force") return -999; if (target is not null && target.BattleData?.Position != move.User.BattleData?.Position && target.ActiveAbility?.Name == "shield_dust") return -999; if ((move.Move.SecondaryEffect.Chance < 100 && move.User.ActiveAbility?.Name == "serene_grace") || move.User.BattleData?.BattleSide.VolatileScripts.Contains() == true) { return 5; } return 0; } private static bool HasMoveWithEffect(this IPokemon pokemon, params StringKey[] effect) { return pokemon.Moves.WhereNotNull().Any(move => move.MoveData.SecondaryEffect?.Name is not null && effect.Contains(move.MoveData.SecondaryEffect.Name)); } private static bool Opposes(this IPokemon pokemon, IPokemon target) => pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide; }