Getting started with implementing an explicit AI, based on the Essentials one.
All checks were successful
Build / Build (push) Successful in 1m2s

This commit is contained in:
2025-07-11 17:03:08 +02:00
parent 084ae84130
commit a3a4993407
56 changed files with 2687 additions and 1274 deletions

View File

@@ -1,6 +1,7 @@
using System.Buffers;
using System.Text.Json;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Plugins;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static.Moves;

View File

@@ -0,0 +1,337 @@
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<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 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<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;
}

View File

@@ -0,0 +1,8 @@
using JetBrains.Annotations;
namespace PkmnLib.Plugin.Gen7.AI;
[AttributeUsage(AttributeTargets.Method), MeansImplicitUse]
public class AIMoveScoreFunctionAttribute : Attribute
{
}

View File

@@ -0,0 +1,70 @@
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)
{
// Check if the move type has a static function in the following format:
// public static void AIMoveEffectScore(MoveOption option, ref int score);
var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
m.GetCustomAttribute<AIMoveScoreFunctionAttribute>() != null && m.ReturnType == typeof(void) &&
m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == typeof(MoveOption) &&
m.GetParameters()[1].ParameterType == typeof(int).MakeByRefType());
if (method != null)
{
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score");
var functionExpression = Expression.Lambda<AIScoreMoveHandler>(
Expression.Call(null, method, 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

@@ -438,7 +438,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -704,7 +705,8 @@
"flags": [
"protect",
"mirror",
"ballistics"
"ballistics",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -770,7 +772,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "beat_up"
@@ -1101,7 +1104,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -1413,7 +1417,8 @@
"flags": [
"protect",
"mirror",
"ballistics"
"ballistics",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -1747,7 +1752,8 @@
"contact",
"protect",
"mirror",
"punch"
"punch",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -2580,7 +2586,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_hit_move"
@@ -2616,7 +2623,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -3681,7 +3689,8 @@
"category": "special",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "fire_spin"
@@ -4263,7 +4272,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -4299,7 +4309,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -5726,7 +5737,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -6579,7 +6591,8 @@
"category": "special",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "magma_storm"
@@ -7895,7 +7908,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -9068,7 +9082,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -10663,7 +10678,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -11540,7 +11556,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"
@@ -12168,7 +12185,8 @@
"flags": [
"contact",
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "triple_kick"
@@ -12224,7 +12242,8 @@
"category": "physical",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "twineedle"
@@ -12550,7 +12569,8 @@
"category": "special",
"flags": [
"protect",
"mirror"
"mirror",
"multi_hit"
],
"effect": {
"name": "2_5_hit_move"

View File

@@ -1,3 +1,5 @@
using PkmnLib.Dynamic.Plugins;
using PkmnLib.Plugin.Gen7.AI;
using PkmnLib.Plugin.Gen7.Libraries.Battling;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Species;
@@ -47,6 +49,8 @@ public class Gen7Plugin : Plugin<Gen7PluginConfiguration>, IResourceProvider
registry.RegisterDamageCalculator(new Gen7DamageCalculator(Configuration));
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration));
ExplicitAIFunctions.RegisterAIFunctions(registry.ExplicitAIHandlers);
}
/// <inheritdoc />

View File

@@ -40,7 +40,7 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
public uint CalculateBoostedStat(IPokemon pokemon, Statistic stat)
{
var flatStat = CalculateFlatStat(pokemon, stat);
var boostModifier = GetStatBoostModifier(pokemon, stat);
var boostModifier = GetStatBoostModifier(pokemon.StatBoost.GetStatistic(stat));
var boostedStat = flatStat * boostModifier;
if (boostedStat > uint.MaxValue)
boostedStat = uint.MaxValue;
@@ -71,13 +71,7 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
if (ignoreEvasion)
targetEvasion = 0;
var userAccuracy = executingMove.User.StatBoost.Accuracy;
var difference = targetEvasion - userAccuracy;
var statModifier = difference switch
{
> 0 => 3.0f / (3.0f + Math.Min(difference, 6)),
< 0 => 3.0f + -Math.Max(difference, -6) / 3.0f,
_ => 1.0f,
};
var statModifier = GetAccuracyEvasionStatModifier(targetEvasion, userAccuracy);
modifiedAccuracy = (int)(modifiedAccuracy * statModifier);
modifiedAccuracy = modifiedAccuracy switch
{
@@ -116,10 +110,9 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
return (uint)modified;
}
private static float GetStatBoostModifier(IPokemon pokemon, Statistic statistic)
public static float GetStatBoostModifier(sbyte amount)
{
var boost = pokemon.StatBoost.GetStatistic(statistic);
return boost switch
return amount switch
{
-6 => 2.0f / 8.0f,
-5 => 2.0f / 7.0f,
@@ -134,7 +127,18 @@ public class Gen7BattleStatCalculator : IBattleStatCalculator
4 => 6.0f / 2.0f,
5 => 7.0f / 2.0f,
6 => 8.0f / 2.0f,
_ => throw new ArgumentException($"Stat boost was out of expected range of -6 to 6: {boost}"),
_ => throw new ArgumentException($"Stat boost was out of expected range of -6 to 6: {amount}"),
};
}
public static float GetAccuracyEvasionStatModifier(sbyte evasion, sbyte accuracy)
{
var difference = evasion - accuracy;
return difference switch
{
> 0 => 3.0f / (3.0f + Math.Min(difference, 6)),
< 0 => 3.0f + -Math.Max(difference, -6) / 3.0f,
_ => 1.0f,
};
}
}

View File

@@ -8,7 +8,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Dry_Skin_(Ability)">Bulbapedia - Dry Skin</see>
/// </summary>
[Script(ScriptCategory.Ability, "dry_skin")]
public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn
public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private IPokemon? _owningPokemon;
@@ -55,4 +55,13 @@ public class DrySkin : Script, IScriptChangeDamageModifier, IScriptOnEndTurn
_owningPokemon.Damage(_owningPokemon.MaxHealth / 8, DamageSource.Weather);
}
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
if (pokemon.BattleData?.Battle.WeatherName == ScriptUtils.ResolveName<Weather.HarshSunlight>())
{
damage += (int)(pokemon.MaxHealth / 8f);
}
}
}

View File

@@ -6,7 +6,8 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Solar_Power_(Ability)">Bulbapedia - Solar Power</see>
/// </summary>
[Script(ScriptCategory.Ability, "solar_power")]
public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndTurn
public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndTurn,
IAIInfoScriptExpectedEndOfTurnDamage
{
/// <inheritdoc />
public void ChangeOffensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint defensiveStat,
@@ -29,6 +30,26 @@ public class SolarPower : Script, IScriptChangeOffensiveStatValue, IScriptOnEndT
if (!pokemon.IsUsable)
return;
var weatherName = pokemon.BattleData?.Battle.WeatherName;
if (weatherName != ScriptUtils.ResolveName<Weather.HarshSunlight>() &&
weatherName != ScriptUtils.ResolveName<Weather.DesolateLands>())
{
return;
}
pokemon.Damage(pokemon.MaxHealth / 8, DamageSource.Weather);
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
var weatherName = pokemon.BattleData?.Battle.WeatherName;
if (weatherName != ScriptUtils.ResolveName<Weather.HarshSunlight>() &&
weatherName != ScriptUtils.ResolveName<Weather.DesolateLands>())
{
return;
}
damage += (int)(pokemon.MaxHealth / 8f);
}
}

View File

@@ -1,3 +1,6 @@
using PkmnLib.Dynamic.AI.Explicit;
using PkmnLib.Plugin.Gen7.AI;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
public abstract class ChangeUserStats : Script, IScriptOnInitialize, IScriptOnSecondaryEffect
@@ -31,6 +34,19 @@ public abstract class ChangeUserStats : Script, IScriptOnInitialize, IScriptOnSe
{
move.User.ChangeStatBoost(_stat, _amount, true, 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.GetScoreForTargetStatRaise(score, option.Move, option.Move.User, statisticSet);
}
}
[Script(ScriptCategory.Move, "change_user_attack")]
@@ -39,6 +55,10 @@ public class ChangeUserAttack : ChangeUserStats
public ChangeUserAttack() : base(Statistic.Attack)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Attack, ref score);
}
[Script(ScriptCategory.Move, "change_user_defense")]
@@ -47,6 +67,10 @@ public class ChangeUserDefense : ChangeUserStats
public ChangeUserDefense() : base(Statistic.Defense)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Defense, ref score);
}
[Script(ScriptCategory.Move, "change_user_special_attack")]
@@ -55,6 +79,10 @@ public class ChangeUserSpecialAttack : ChangeUserStats
public ChangeUserSpecialAttack() : base(Statistic.SpecialAttack)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.SpecialAttack, ref score);
}
[Script(ScriptCategory.Move, "change_user_special_defense")]
@@ -63,6 +91,10 @@ public class ChangeUserSpecialDefense : ChangeUserStats
public ChangeUserSpecialDefense() : base(Statistic.SpecialDefense)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.SpecialDefense, ref score);
}
[Script(ScriptCategory.Move, "change_user_speed")]
@@ -71,6 +103,10 @@ public class ChangeUserSpeed : ChangeUserStats
public ChangeUserSpeed() : base(Statistic.Speed)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Speed, ref score);
}
[Script(ScriptCategory.Move, "change_user_accuracy")]
@@ -79,6 +115,10 @@ public class ChangeUserAccuracy : ChangeUserStats
public ChangeUserAccuracy() : base(Statistic.Accuracy)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Accuracy, ref score);
}
[Script(ScriptCategory.Move, "change_user_evasion")]
@@ -87,4 +127,8 @@ public class ChangeUserEvasion : ChangeUserStats
public ChangeUserEvasion() : base(Statistic.Evasion)
{
}
[AIMoveScoreFunction]
public static void AIMoveEffectScore(MoveOption option, ref int score) =>
GetMoveEffectScore(option, Statistic.Evasion, ref score);
}

View File

@@ -13,11 +13,13 @@ public class MirrorMove : Script, IScriptChangeMove
return;
var battle = battleData.Battle;
var currentTurn = battle.ChoiceQueue!.LastRanChoice;
if (battle.ChoiceQueue == null)
return;
var currentTurn = battle.ChoiceQueue.LastRanChoice;
var lastMove = battle.PreviousTurnChoices.SelectMany(x => x).OfType<IMoveChoice>()
.TakeWhile(x => x != currentTurn).LastOrDefault(x => x.TargetPosition == choice.TargetPosition &&
x.TargetSide == choice.TargetSide &&
x.User.BattleData?.IsOnBattlefield == true);
.TakeWhile(x => !Equals(x, currentTurn)).LastOrDefault(x => x.TargetPosition == choice.TargetPosition &&
x.TargetSide == choice.TargetSide &&
x.User.BattleData?.IsOnBattlefield == true);
if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
{
choice.Fail();

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "bind")]
public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway
public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway,
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon? _owner;
private int _turns;
@@ -33,4 +34,10 @@ public class BindEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IS
/// <inheritdoc />
public void PreventSelfRunAway(IFleeChoice choice, ref bool prevent) => prevent = _turns > 0;
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_owner?.MaxHealth * _percentOfMaxHealth ?? 0);
}
}

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "fire_spin")]
public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch
public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch,
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
@@ -21,4 +22,10 @@ public class FireSpinEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAwa
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(pokemon.MaxHealth / 8f);
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "ghostcurse")]
public class GhostCurseEffect : Script, IScriptOnEndTurn
public class GhostCurseEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private IPokemon _pokemon;
@@ -15,4 +15,10 @@ public class GhostCurseEffect : Script, IScriptOnEndTurn
{
_pokemon.Damage(_pokemon.CurrentHealth / 4, DamageSource.Misc);
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_pokemon.CurrentHealth / 4f);
}
}

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "infestation")]
public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway
public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwitch, IScriptPreventSelfRunAway,
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
private int _turns;
@@ -30,4 +31,10 @@ public class InfestationEffect : Script, IScriptOnEndTurn, IScriptPreventSelfSwi
RemoveSelf();
}
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_owner.MaxHealth / 8f);
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "leech_seed")]
public class LeechSeedEffect : Script, IScriptOnEndTurn
public class LeechSeedEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
private readonly IPokemon _placer;
@@ -15,7 +15,7 @@ public class LeechSeedEffect : Script, IScriptOnEndTurn
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
{
var damage = _owner.BoostedStats.Hp / 8;
var damage = _owner.MaxHealth / 8;
if (_owner.CurrentHealth <= damage)
damage = _owner.CurrentHealth;
@@ -25,4 +25,10 @@ public class LeechSeedEffect : Script, IScriptOnEndTurn
else
_placer.Heal(damage);
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_owner.MaxHealth / 8f);
}
}

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "magma_storm")]
public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch
public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunAway, IScriptPreventSelfSwitch,
IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
@@ -21,4 +22,10 @@ public class MagmaStormEffect : Script, IScriptOnEndTurn, IScriptPreventSelfRunA
/// <inheritdoc />
public void PreventSelfSwitch(ISwitchChoice choice, ref bool prevent) => prevent = true;
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(pokemon.MaxHealth / 16f);
}
}

View File

@@ -3,7 +3,7 @@ using PkmnLib.Plugin.Gen7.Scripts.Status;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "nightmare")]
public class NightmareEffect : Script, IScriptOnEndTurn
public class NightmareEffect : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private readonly IPokemon _owner;
@@ -23,4 +23,10 @@ public class NightmareEffect : Script, IScriptOnEndTurn
var maxHp = _owner.MaxHealth;
_owner.Damage(maxHp / 4, DamageSource.Misc);
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
damage += (int)(_owner.MaxHealth / 4f);
}
}

View File

@@ -3,7 +3,7 @@ namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "substitute")]
public class SubstituteEffect(uint health) : Script, IScriptBlockIncomingHit
{
private uint _health = health;
public uint Health { get; private set; } = health;
/// <inheritdoc />
public void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block)
@@ -19,12 +19,12 @@ public class SubstituteEffect(uint health) : Script, IScriptBlockIncomingHit
block = true;
var damage = executingMove.GetHitData(target, hitIndex).Damage;
if (damage >= _health)
if (damage >= Health)
{
executingMove.Battle.EventHook.Invoke(new DialogEvent("substitute_broken"));
RemoveSelf();
return;
}
_health -= damage;
Health -= damage;
}
}

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "whirlpool")]
public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentRunAway, IScriptPreventOpponentSwitch
public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentRunAway, IScriptPreventOpponentSwitch,
IAIInfoScriptExpectedEndOfTurnDamage
{
public record PokemonTurn
{
@@ -80,4 +81,14 @@ public class WhirlpoolEffect : Script, IScriptOnEndTurn, IScriptPreventOpponentR
_targetedPokemon.Remove(turn);
}
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
var turn = _targetedPokemon.FirstOrDefault(x => x.Pokemon == pokemon);
if (turn != null)
{
damage += (int)(pokemon.MaxHealth * turn.DamagePercent);
}
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
[Script(ScriptCategory.Status, "badly_poisoned")]
public class BadlyPoisoned : Poisoned, IScriptOnEndTurn
public class BadlyPoisoned : Poisoned
{
private int _turns = 1;

View File

@@ -3,7 +3,7 @@ using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
[Script(ScriptCategory.Status, "burned")]
public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn
public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private IPokemon? _target;
@@ -41,4 +41,11 @@ public class Burned : Script, IScriptChangeMoveDamage, IScriptOnEndTurn
});
_target.Damage(damage, DamageSource.Status, eventBatch);
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
if (_target != null)
damage += (int)(_target.MaxHealth / 16f);
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
[Script(ScriptCategory.Status, "poisoned")]
public class Poisoned : Script, IScriptOnEndTurn
public class Poisoned : Script, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private IPokemon? _pokemon;
@@ -46,4 +46,11 @@ public class Poisoned : Script, IScriptOnEndTurn
else
_pokemon.Damage(damage, DamageSource.Status, eventBatchId);
}
/// <inheritdoc />
public virtual void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
if (_pokemon != null)
damage += (int)(_pokemon.MaxHealth * GetPoisonMultiplier());
}
}

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Status;
[Script(ScriptCategory.Status, "sleep")]
public class Sleep : Script, IScriptPreventMove
public class Sleep : Script, IScriptPreventMove, IAIInfoScriptNumberTurnsLeft
{
private IPokemon? _pokemon;
public int Turns { get; set; }
@@ -54,4 +54,7 @@ public class Sleep : Script, IScriptPreventMove
{ "pokemon", _pokemon },
}));
}
/// <inheritdoc />
public int TurnsLeft() => Turns;
}

View File

@@ -27,7 +27,7 @@ public class PsychicTerrain : Script, IScriptIsInvulnerableToMove, IScriptChange
if (!IsAffectedByTerrain(target))
return;
// Psychic Terrain prevents priority moves from affecting affected Pokémon.
// Psychic Terrain prevents priority moves from affected Pokémon.
if (move.MoveChoice.Priority > 0)
{
invulnerable = true;

View File

@@ -1,7 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Weather;
[Script(ScriptCategory.Weather, "hail")]
public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn
public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn, IAIInfoScriptExpectedEndOfTurnDamage
{
private int? _duration;
@@ -46,4 +46,15 @@ public class Hail : Script, ILimitedTurnsScript, IScriptOnEndTurn
battle.SetWeather(null, 0);
}
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
if (pokemon.Types.Any(x => x.Name == "ice"))
return; // Ice types are immune to Hail damage.
if (_duration.HasValue)
{
damage += (int)(pokemon.MaxHealth / 16f);
}
}
}

View File

@@ -1,7 +1,8 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Weather;
[Script(ScriptCategory.Weather, "sandstorm")]
public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveStatValue, IScriptOnEndTurn
public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveStatValue, IScriptOnEndTurn,
IAIInfoScriptExpectedEndOfTurnDamage
{
/// <inheritdoc />
public void OnEndTurn(IScriptSource owner, IBattle battle)
@@ -39,4 +40,12 @@ public class Sandstorm : Script, IScriptChangeBasePower, IScriptChangeDefensiveS
if (move.UseMove.Name == "solar_beam")
basePower /= 2;
}
/// <inheritdoc />
public void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage)
{
if (pokemon.Types.Any(x => x.Name == "rock" || x.Name == "ground" || x.Name == "steel"))
return; // Rock, Ground, and Steel types are immune to Sandstorm damage.
damage += (int)(pokemon.MaxHealth / 16f);
}
}