Implement stat drop handling for AI, Fixes for Conversion2
All checks were successful
Build / Build (push) Successful in 39s

This commit is contained in:
2026-05-23 12:57:15 +02:00
parent a5ef757b01
commit be5100df8a
5 changed files with 286 additions and 8 deletions

View File

@@ -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<sbyte> 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<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 GetScoreForTargetStatRaise(score, 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, 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;
}
/// <summary>
/// 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);
}
}
/// <summary>
/// Calculates the score for the generic concept of raising a target's stats.
/// </summary>
@@ -311,6 +528,19 @@ public static class AIHelperFunctions
return score;
}
private static int GetTargetStatDropScoreGeneric(int score, IPokemon target, StatisticSet<sbyte> 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)