391 lines
16 KiB
C#
391 lines
16 KiB
C#
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;
|
|
|
|
public static class AIHelperFunctions
|
|
{
|
|
public static int GetScoreForTargetStatRaise(int score, AIMoveState move, IPokemon target,
|
|
StatisticSet<sbyte> 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<IAIInfoScriptExpectedEndOfTurnDamage>(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<sbyte> statChanges,
|
|
bool fixedChange = false, bool ignoreContrary = false) =>
|
|
throw new NotImplementedException("This method is not implemented");
|
|
|
|
/// <summary>
|
|
/// Checks if a stat raise is worthwhile for the given Pokémon and stat.
|
|
/// </summary>
|
|
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 readonly StringKey FoulPlayAbilityName = "foul_play";
|
|
|
|
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 != FoulPlayAbilityName);
|
|
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<IAIInfoScriptExpectedEndOfTurnDamage>(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the score for the generic concept of raising a target's stats.
|
|
/// </summary>
|
|
private static int GetTargetStatRaiseScoreGeneric(int score, IPokemon target, StatisticSet<sbyte> 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<RainbowEffect>() == 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;
|
|
|
|
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;
|
|
}
|
|
} |