This commit is contained in:
36
Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs
Normal file
36
Plugins/PkmnLib.Plugin.Gen7/AI/AIDamageFunctions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
public static class AIDamageFunctions
|
||||
{
|
||||
internal static void PredictedDamageScore(IExplicitAI ai, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -157,6 +158,8 @@ public static class AIHelperFunctions
|
||||
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)
|
||||
{
|
||||
@@ -200,7 +203,7 @@ public static class AIHelperFunctions
|
||||
{
|
||||
var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
|
||||
x.MoveData.Category == MoveCategory.Physical &&
|
||||
x.MoveData.SecondaryEffect?.Name != "foul_play");
|
||||
x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
|
||||
var inc = hasPhysicalMoves ? 8 : 12;
|
||||
score += (int)(inc * incMult);
|
||||
}
|
||||
@@ -334,4 +337,55 @@ public static class AIHelperFunctions
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
208
Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs
Normal file
208
Plugins/PkmnLib.Plugin.Gen7/AI/AISwitchFunctions.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Moves;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Side;
|
||||
using PkmnLib.Plugin.Gen7.Scripts.Status;
|
||||
using PkmnLib.Static.Moves;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
public static class AISwitchFunctions
|
||||
{
|
||||
internal static void RegisterAISwitchFunctions(ExplicitAIHandlers handlers)
|
||||
{
|
||||
handlers.ShouldSwitchFunctions.Add("perish_song", PerishSong);
|
||||
handlers.ShouldSwitchFunctions.Add("significant_end_of_turn_damage", SignificantEndOfTurnDamage);
|
||||
handlers.ShouldSwitchFunctions.Add("high_damage_from_foe", HighDamageFromFoe);
|
||||
handlers.ShouldSwitchFunctions.Add("cure_status_problem_by_switching_out", CureStatusProblemBySwitchingOut);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out if the Perish Song effect is about to cause the Pokémon to faint.
|
||||
/// </summary>
|
||||
private static bool PerishSong(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
if (!pokemon.Volatile.TryGet<PerishSongEffect>(out var effect))
|
||||
return false;
|
||||
return effect.Turns <= 1;
|
||||
}
|
||||
|
||||
private static readonly StringKey PoisonHealAbilityName = "poison_heal";
|
||||
|
||||
/// <summary>
|
||||
/// Switch out if the Pokémon is expected to take significant end-of-turn damage.
|
||||
/// </summary>
|
||||
private static bool SignificantEndOfTurnDamage(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||
IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
var eorDamage = 0;
|
||||
pokemon.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
|
||||
x.ExpectedEndOfTurnDamage(pokemon, ref eorDamage));
|
||||
if (eorDamage >= pokemon.CurrentHealth / 2 || eorDamage >= pokemon.MaxHealth / 4)
|
||||
return true;
|
||||
if (!ai.TrainerHighSkill || eorDamage <= 0)
|
||||
return false;
|
||||
|
||||
if (pokemon.Volatile.Contains<LeechSeedEffect>() && ai.Random.GetBool())
|
||||
return true;
|
||||
if (pokemon.Volatile.Contains<NightmareEffect>())
|
||||
return true;
|
||||
if (pokemon.Volatile.Contains<GhostCurseEffect>())
|
||||
return true;
|
||||
var statusScript = pokemon.StatusScript.Script;
|
||||
if (statusScript is BadlyPoisoned { Turns: > 0 } badlyPoisoned &&
|
||||
pokemon.ActiveAbility?.Name != PoisonHealAbilityName)
|
||||
{
|
||||
var poisonDamage = pokemon.MaxHealth / 8;
|
||||
var nextToxicDamage = pokemon.MaxHealth * badlyPoisoned.GetPoisonMultiplier();
|
||||
if ((pokemon.CurrentHealth <= nextToxicDamage && pokemon.CurrentHealth > poisonDamage) ||
|
||||
nextToxicDamage > poisonDamage * 2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HighDamageFromFoe(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||
IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
if (!ai.TrainerHighSkill)
|
||||
return false;
|
||||
if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2)
|
||||
return false;
|
||||
var bigThreat = false;
|
||||
var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide)
|
||||
.SelectMany(x => x.Pokemon.WhereNotNull());
|
||||
foreach (var opponent in opponents)
|
||||
{
|
||||
if (Math.Abs(opponent.Level - pokemon.Level) > 5)
|
||||
continue;
|
||||
var lastMoveUsed = opponent.BattleData?.LastMoveChoice;
|
||||
if (lastMoveUsed is null)
|
||||
continue;
|
||||
var moveData = lastMoveUsed.ChosenMove.MoveData;
|
||||
if (moveData.Category == MoveCategory.Status)
|
||||
continue;
|
||||
var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveData.MoveType, pokemon.Types);
|
||||
if (effectiveness <= 1 || moveData.BasePower < 70)
|
||||
continue;
|
||||
var switchChange = moveData.BasePower > 90 ? 50 : 25;
|
||||
bigThreat = ai.Random.GetInt(100) < switchChange;
|
||||
}
|
||||
return bigThreat;
|
||||
}
|
||||
|
||||
private static readonly StringKey ImmunityAbilityName = "immunity";
|
||||
private static readonly StringKey InsomniaAbilityName = "insomnia";
|
||||
private static readonly StringKey LimberAbilityName = "limber";
|
||||
private static readonly StringKey MagmaArmorAbilityName = "magma_armor";
|
||||
private static readonly StringKey VitalSpiritAbilityName = "vital_spirit";
|
||||
private static readonly StringKey WaterBubbleAbilityName = "water_bubble";
|
||||
private static readonly StringKey WaterVeilAbilityName = "water_veil";
|
||||
private static readonly StringKey NaturalCureAbilityName = "natural_cure";
|
||||
private static readonly StringKey RegeneratorAbilityName = "regenerator";
|
||||
|
||||
private static readonly Dictionary<StringKey, List<StringKey>> StatusCureAbilities = new()
|
||||
{
|
||||
{
|
||||
ImmunityAbilityName,
|
||||
[ScriptUtils.ResolveName<Poisoned>(), ScriptUtils.ResolveName<BadlyPoisoned>()]
|
||||
},
|
||||
{ InsomniaAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
|
||||
{ LimberAbilityName, [ScriptUtils.ResolveName<Paralyzed>()] },
|
||||
{ MagmaArmorAbilityName, [ScriptUtils.ResolveName<Frozen>()] },
|
||||
{ VitalSpiritAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
|
||||
{ WaterBubbleAbilityName, [ScriptUtils.ResolveName<Burned>()] },
|
||||
{ WaterVeilAbilityName, [ScriptUtils.ResolveName<Burned>()] },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Switch out to cure a status problem or heal HP with abilities like Natural Cure or Regenerator.
|
||||
/// </summary>
|
||||
private static bool CureStatusProblemBySwitchingOut(IExplicitAI ai, IPokemon pokemon, IBattle battle,
|
||||
IReadOnlyList<IPokemon> reserves)
|
||||
{
|
||||
if (pokemon.ActiveAbility == null)
|
||||
return false;
|
||||
|
||||
// Don't try to cure a status problem/heal a bit of HP if entry hazards will
|
||||
// KO the battler if it switches back in
|
||||
var entryHazardDamage = ExplicitAI.CalculateEntryHazardDamage(pokemon, pokemon.BattleData!.BattleSide);
|
||||
if (entryHazardDamage >= pokemon.CurrentHealth)
|
||||
return false;
|
||||
if (pokemon.StatusScript.Script is null)
|
||||
return false;
|
||||
|
||||
var abilityName = pokemon.ActiveAbility.Name;
|
||||
var statusName = pokemon.StatusScript.Script.Name;
|
||||
|
||||
// Check abilities that cure specific status conditions
|
||||
var canCureStatus = false;
|
||||
if (abilityName == NaturalCureAbilityName)
|
||||
{
|
||||
canCureStatus = true;
|
||||
}
|
||||
else if (StatusCureAbilities.TryGetValue(abilityName, out var statusList))
|
||||
{
|
||||
canCureStatus = statusList.Any(status => status == statusName);
|
||||
}
|
||||
|
||||
if (canCureStatus)
|
||||
{
|
||||
if (AIHelperFunctions.WantsStatusProblem(pokemon, statusName))
|
||||
return false;
|
||||
|
||||
// Don't bother if the status will cure itself soon
|
||||
if (pokemon.StatusScript.Script is Sleep { Turns: 1 })
|
||||
return false;
|
||||
|
||||
if (entryHazardDamage >= pokemon.MaxHealth / 4)
|
||||
return false;
|
||||
|
||||
// Don't bother curing a poisoning if Toxic Spikes will just re-poison
|
||||
if (pokemon.StatusScript.Script is Poisoned or BadlyPoisoned &&
|
||||
!reserves.Any(p => p.Types.Any(t => t.Name == "poison")))
|
||||
{
|
||||
if (pokemon.BattleData!.BattleSide.VolatileScripts.TryGet<ToxicSpikesEffect>(out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Not worth curing status problems that still allow actions if at high HP
|
||||
var isImmobilizing = pokemon.StatusScript.Script is Sleep or Frozen;
|
||||
if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2 && !isImmobilizing)
|
||||
return false;
|
||||
|
||||
if (ai.Random.GetInt(100) < 70)
|
||||
return true;
|
||||
}
|
||||
else if (abilityName == RegeneratorAbilityName)
|
||||
{
|
||||
// Not worth healing if battler would lose more HP from switching back in later
|
||||
if (entryHazardDamage >= pokemon.MaxHealth / 3)
|
||||
return false;
|
||||
|
||||
// Not worth healing HP if already at high HP
|
||||
if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2)
|
||||
return false;
|
||||
|
||||
// Don't bother if a foe is at low HP and could be knocked out instead
|
||||
var hasDamagingMove = pokemon.Moves.Any(m => m?.MoveData.Category != MoveCategory.Status);
|
||||
if (hasDamagingMove)
|
||||
{
|
||||
var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide)
|
||||
.SelectMany(x => x.Pokemon.WhereNotNull());
|
||||
var weakFoe = opponents.Any(opponent => opponent.CurrentHealth < opponent.MaxHealth / 3);
|
||||
if (weakFoe)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ai.Random.GetInt(100) < 70)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Dynamic.AI.Explicit;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.AI;
|
||||
|
||||
public static class ExplicitAIFunctionRegistration
|
||||
{
|
||||
public static void RegisterAIFunctions(ExplicitAIHandlers handlers)
|
||||
{
|
||||
var baseType = typeof(Script);
|
||||
foreach (var type in typeof(ExplicitAIFunctionRegistration).Assembly.GetTypes()
|
||||
.Where(t => baseType.IsAssignableFrom(t)))
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<ScriptAttribute>();
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
if (attribute.Category == ScriptCategory.Move)
|
||||
{
|
||||
InitializeMoveFailFunction(handlers, type, attribute);
|
||||
InitializeMoveScoreFunction(handlers, type, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
handlers.GeneralMoveAgainstTargetScore.Add("predicted_damage", AIDamageFunctions.PredictedDamageScore);
|
||||
|
||||
AISwitchFunctions.RegisterAISwitchFunctions(handlers);
|
||||
}
|
||||
|
||||
#region Reflection based function initialization
|
||||
|
||||
private static void InitializeMoveFailFunction(ExplicitAIHandlers handlers, Type type, ScriptAttribute attribute)
|
||||
{
|
||||
var failureMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
||||
m.GetCustomAttribute<AIMoveFailureFunctionAttribute>() != null);
|
||||
if (failureMethod == null)
|
||||
return;
|
||||
if (failureMethod.ReturnType != typeof(bool) || failureMethod.GetParameters().Length != 2 ||
|
||||
failureMethod.GetParameters()[0].ParameterType != typeof(IExplicitAI) ||
|
||||
failureMethod.GetParameters()[1].ParameterType != typeof(MoveOption))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {failureMethod.Name} in {type.Name} must return bool and take an IExplicitAI and a MoveOption as parameters.");
|
||||
}
|
||||
var aiParam = Expression.Parameter(typeof(IExplicitAI), "ai");
|
||||
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
|
||||
var functionExpression = Expression.Lambda<AIBoolHandler>(
|
||||
Expression.Call(null, failureMethod, aiParam, optionParam), aiParam, optionParam).Compile();
|
||||
handlers.MoveFailureCheck.Add(attribute.Name, functionExpression);
|
||||
}
|
||||
|
||||
private static void InitializeMoveScoreFunction(ExplicitAIHandlers handlers, Type type, ScriptAttribute attribute)
|
||||
{
|
||||
var scoreMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
||||
m.GetCustomAttribute<AIMoveScoreFunctionAttribute>() != null);
|
||||
if (scoreMethod == null)
|
||||
return;
|
||||
if (scoreMethod.ReturnType != typeof(void) || scoreMethod.GetParameters().Length != 3 ||
|
||||
scoreMethod.GetParameters()[0].ParameterType != typeof(IExplicitAI) ||
|
||||
scoreMethod.GetParameters()[1].ParameterType != typeof(MoveOption) ||
|
||||
scoreMethod.GetParameters()[2].ParameterType != typeof(int).MakeByRefType())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {scoreMethod.Name} in {type.Name} must return void and take an IExplicitAI, a MoveOption, and a ref int as parameters.");
|
||||
}
|
||||
var aiParam = Expression.Parameter(typeof(IExplicitAI), "ai");
|
||||
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
|
||||
var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score");
|
||||
var functionExpression = Expression.Lambda<AIScoreMoveHandler>(
|
||||
Expression.Call(null, scoreMethod, aiParam, optionParam, scoreParam), aiParam, optionParam, scoreParam)
|
||||
.Compile();
|
||||
handlers.MoveEffectScore.Add(attribute.Name, functionExpression);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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)
|
||||
{
|
||||
var failureMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
||||
m.GetCustomAttribute<AIMoveFailureFunctionAttribute>() != null);
|
||||
if (failureMethod != null)
|
||||
{
|
||||
if (failureMethod.ReturnType != typeof(bool) || failureMethod.GetParameters().Length != 1 ||
|
||||
failureMethod.GetParameters()[0].ParameterType != typeof(MoveOption))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {failureMethod.Name} in {type.Name} must return bool and take a single MoveOption parameter.");
|
||||
}
|
||||
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
|
||||
var functionExpression = Expression.Lambda<AIBoolHandler>(
|
||||
Expression.Call(null, failureMethod, optionParam), optionParam).Compile();
|
||||
handlers.MoveFailureCheck.Add(attribute.Name, functionExpression);
|
||||
}
|
||||
|
||||
var scoreMethod = type.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(m =>
|
||||
m.GetCustomAttribute<AIMoveScoreFunctionAttribute>() != null);
|
||||
if (scoreMethod != null)
|
||||
{
|
||||
if (scoreMethod.ReturnType != typeof(void) || scoreMethod.GetParameters().Length != 2 ||
|
||||
scoreMethod.GetParameters()[0].ParameterType != typeof(MoveOption) ||
|
||||
scoreMethod.GetParameters()[1].ParameterType != typeof(int).MakeByRefType())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {scoreMethod.Name} in {type.Name} must return void and take a MoveOption and an int by reference parameter.");
|
||||
}
|
||||
var optionParam = Expression.Parameter(typeof(MoveOption), "option");
|
||||
var scoreParam = Expression.Parameter(typeof(int).MakeByRefType(), "score");
|
||||
var functionExpression = Expression.Lambda<AIScoreMoveHandler>(
|
||||
Expression.Call(null, scoreMethod, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user