Move data and data loading to plugin libraries.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-05-16 13:01:23 +02:00
parent b6ff51c9df
commit 810cdbb15a
46 changed files with 108405 additions and 155 deletions

View File

@@ -0,0 +1,135 @@
using System;
using PkmnLib.Dynamic.Libraries;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7BattleStatCalculator : IBattleStatCalculator
{
/// <inheritdoc />
public void CalculateFlatStats(IPokemon pokemon, StatisticSet<uint> stats)
{
stats.SetStatistic(Statistic.Hp, CalculateHealthStat(pokemon));
stats.SetStatistic(Statistic.Attack, CalculateNormalStat(pokemon, Statistic.Attack));
stats.SetStatistic(Statistic.Defense, CalculateNormalStat(pokemon, Statistic.Defense));
stats.SetStatistic(Statistic.SpecialAttack, CalculateNormalStat(pokemon, Statistic.SpecialAttack));
stats.SetStatistic(Statistic.SpecialDefense, CalculateNormalStat(pokemon, Statistic.SpecialDefense));
stats.SetStatistic(Statistic.Speed, CalculateNormalStat(pokemon, Statistic.Speed));
}
/// <inheritdoc />
public uint CalculateFlatStat(IPokemon pokemon, Statistic stat)
{
return stat switch
{
Statistic.Hp => CalculateHealthStat(pokemon),
_ => CalculateNormalStat(pokemon, stat),
};
}
/// <inheritdoc />
public void CalculateBoostedStats(IPokemon pokemon, StatisticSet<uint> stats)
{
stats.SetStatistic(Statistic.Hp, CalculateBoostedStat(pokemon, Statistic.Hp));
stats.SetStatistic(Statistic.Attack, CalculateBoostedStat(pokemon, Statistic.Attack));
stats.SetStatistic(Statistic.Defense, CalculateBoostedStat(pokemon, Statistic.Defense));
stats.SetStatistic(Statistic.SpecialAttack, CalculateBoostedStat(pokemon, Statistic.SpecialAttack));
stats.SetStatistic(Statistic.SpecialDefense, CalculateBoostedStat(pokemon, Statistic.SpecialDefense));
stats.SetStatistic(Statistic.Speed, CalculateBoostedStat(pokemon, Statistic.Speed));
}
/// <inheritdoc />
public uint CalculateBoostedStat(IPokemon pokemon, Statistic stat)
{
var flatStat = CalculateFlatStat(pokemon, stat);
var boostModifier = GetStatBoostModifier(pokemon, stat);
var boostedStat = flatStat * boostModifier;
if (boostedStat > uint.MaxValue)
boostedStat = uint.MaxValue;
return (uint)boostedStat;
}
/// <inheritdoc />
public byte CalculateModifiedAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex,
byte moveAccuracy)
{
var accuracyModifier = 1.0f;
executingMove.RunScriptHook(x =>
x.ChangeAccuracyModifier(executingMove, target, hitIndex, ref accuracyModifier));
var modifiedAccuracy = (int)(moveAccuracy * accuracyModifier);
// ReSharper disable once AccessToModifiedClosure
executingMove.RunScriptHook(x => x.ChangeAccuracy(executingMove, target, hitIndex, ref modifiedAccuracy));
if (modifiedAccuracy == 255)
return 255;
var targetEvasion = target.StatBoost.Evasion;
var ignoreEvasion = false;
executingMove.RunScriptHook(x => x.BypassEvasionStatBoosts(executingMove, target, hitIndex, ref ignoreEvasion));
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,
};
modifiedAccuracy = (int)(modifiedAccuracy * statModifier);
modifiedAccuracy = modifiedAccuracy switch
{
> 255 => 255,
< 0 => 0,
_ => modifiedAccuracy,
};
// NOTE: the main games also consider friendship here, but we don't yet have the concept of a "player Pokémon"
// in the battle system, so for now we're just ignoring that.
return (byte)modifiedAccuracy;
}
private static uint CalculateHealthStat(IPokemon pokemon)
{
var baseValue = (ulong)pokemon.Form.BaseStats.Hp;
var iv = (ulong)pokemon.IndividualValues.Hp;
var ev = (ulong)pokemon.EffortValues.Hp;
var level = (ulong)pokemon.Level;
var health = (2 * baseValue + iv + ev / 4) * level / 100 + level + 10;
if (health > uint.MaxValue)
health = uint.MaxValue;
return (uint)health;
}
private static uint CalculateNormalStat(IPokemon pokemon, Statistic statistic)
{
var baseValue = (ulong)pokemon.Form.BaseStats.GetStatistic(statistic);
var iv = (ulong)pokemon.IndividualValues.GetStatistic(statistic);
var ev = (ulong)pokemon.EffortValues.GetStatistic(statistic);
var level = (ulong)pokemon.Level;
var unmodified = (2 * baseValue + iv + ev / 4) * level / 100 + 5;
var natureModifier = pokemon.Nature.GetStatModifier(statistic);
var modified = unmodified * natureModifier;
if (modified > uint.MaxValue)
modified = uint.MaxValue;
return (uint)modified;
}
private static float GetStatBoostModifier(IPokemon pokemon, Statistic statistic)
{
var boost = pokemon.StatBoost.GetStatistic(statistic);
return boost switch
{
-6 => 2.0f / 8.0f,
-5 => 2.0f / 7.0f,
-4 => 2.0f / 6.0f,
-3 => 2.0f / 5.0f,
-2 => 2.0f / 4.0f,
-1 => 2.0f / 3.0f,
0 => 1.0f,
1 => 3.0f / 2.0f,
2 => 4.0f / 2.0f,
3 => 5.0f / 2.0f,
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"),
};
}
}

View File

@@ -0,0 +1,48 @@
using System;
using PkmnLib.Dynamic.Libraries;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7CaptureLibrary : ICaptureLibrary
{
/// <inheritdoc />
public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random)
{
var maxHealth = target.BoostedStats.Hp;
var currentHealth = target.CurrentHealth;
var catchRate = target.Species.CaptureRate;
byte bonusBall = 1;
if (target.Library.ScriptResolver.TryResolveBattleItemScript(captureItem, out var script) &&
script is PokeballScript pokeballScript)
{
bonusBall = pokeballScript.GetCatchRate(target);
}
byte bonusStatus = 1;
target.RunScriptHook(x => x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus));
var modifiedCatchRate = (3.0 * maxHealth - 2.0 * currentHealth) * catchRate * bonusBall / (3.0 * maxHealth);
modifiedCatchRate *= bonusStatus;
var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875);
byte shakes = 0;
if (modifiedCatchRate >= 255)
{
shakes = 4;
}
else
{
// FIXME: Implement critical capture
for (var i = 0; i < 4; i++)
{
if (random.GetInt(0, 65536) < shakeProbability)
{
shakes++;
}
}
}
var success = shakes >= 3;
return new CaptureResult(success, shakes, false);
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Linq;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7DamageCalculator(bool hasRandomness) : IDamageCalculator
{
/// <inheritdoc />
public uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData)
{
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status)
return 0;
if (hitData.Effectiveness == 0)
return 0;
var levelModifier = 2.0f * executingMove.User.Level / 5.0f + 2.0f;
var basePower = (float)hitData.BasePower;
var statModifier = GetStatModifier(executingMove, target, hitNumber, hitData);
var damageModifier = GetDamageModifier(executingMove, target, hitNumber);
var floatDamage = MathF.Floor(levelModifier * basePower);
floatDamage = MathF.Floor(floatDamage * statModifier);
floatDamage = MathF.Floor(floatDamage / 50.0f) + 2.0f;
floatDamage = MathF.Floor(floatDamage * damageModifier);
if (executingMove.TargetCount > 1)
floatDamage = MathF.Floor(floatDamage * 0.75f);
if (hitData.IsCritical)
{
var critModifier = 1.5f;
executingMove.RunScriptHook(script =>
script.ChangeCriticalModifier(executingMove, target, hitNumber, ref critModifier));
floatDamage = MathF.Floor(floatDamage * critModifier);
}
if (hasRandomness)
{
var battle = target.BattleData?.Battle;
if (battle == null)
throw new InvalidOperationException("Randomness is enabled, but no battle is set.");
var random = battle.Random;
var randomFactor = random.GetInt(85, 101) / 100.0f;
floatDamage = MathF.Floor(floatDamage * randomFactor);
}
if (hitData.Type != null && executingMove.User.Types.Contains(hitData.Type.Value))
{
var stabModifier = 1.5f;
executingMove.RunScriptHook(script =>
script.ChangeStabModifier(executingMove, target, hitNumber, ref stabModifier));
floatDamage = MathF.Floor(floatDamage * stabModifier);
}
floatDamage = MathF.Floor(floatDamage * hitData.Effectiveness);
uint damage = floatDamage switch
{
> uint.MaxValue => uint.MaxValue,
< 1 => 1,
_ => (uint)floatDamage,
};
executingMove.RunScriptHook(script => script.ChangeMoveDamage(executingMove, target, hitNumber, ref damage));
target.RunScriptHook(script => script.ChangeIncomingMoveDamage(executingMove, target, hitNumber, ref damage));
return damage;
}
/// <inheritdoc />
public ushort GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData)
{
if (executingMove.UseMove.Category == MoveCategory.Status)
return 0;
var basePower = (ushort)executingMove.UseMove.BasePower;
executingMove.RunScriptHook(script => script.ChangeBasePower(executingMove, target, hitNumber, ref basePower));
return basePower;
}
/// <inheritdoc />
public bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber)
{
if (executingMove.UseMove.Category == MoveCategory.Status)
return false;
byte critStage = 0;
executingMove.RunScriptHook(script =>
script.ChangeCriticalStage(executingMove, target, hitNumber, ref critStage));
var random = battle.Random;
return critStage switch
{
0 => random.GetInt(24) == 0,
1 => random.GetInt(8) == 0,
2 => random.GetInt(2) == 0,
_ => true,
};
}
private static float GetStatModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber,
IHitData hitData)
{
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status)
return 1;
var (offensive, defensive) = category switch
{
MoveCategory.Physical => (Statistic.Attack, Statistic.Defense),
_ => (Statistic.SpecialAttack, Statistic.SpecialDefense),
};
// Check if we can bypass the defensive stat boost on the target. We default to this if the
// move is critical, and the target has a defensive stat boost of > 0, but a script is
// allowed to change this.
var bypassDefense = hitData.IsCritical && target.StatBoost.GetStatistic(defensive) > 0;
executingMove.RunScriptHook(script =>
script.BypassDefensiveStatBoosts(executingMove, target, hitNumber, ref bypassDefense));
// Check if we can bypass the offensive stat boost on the user. We default to this if the
// move is critical, and the user has an offensive stat boost of < 0, but a script is
// allowed to change this.
var bypassOffense = hitData.IsCritical && executingMove.User.StatBoost.GetStatistic(offensive) < 0;
executingMove.RunScriptHook(script =>
script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense));
var userStats = executingMove.User.BoostedStats;
if (bypassOffense)
userStats = executingMove.User.FlatStats;
var offensiveStat = userStats.GetStatistic(offensive);
var targetStats = target.BoostedStats;
if (bypassDefense)
targetStats = target.FlatStats;
var defensiveStat = targetStats.GetStatistic(defensive);
var origOffensiveStat = offensiveStat;
executingMove.RunScriptHook(script =>
script.ChangeOffensiveStatValue(executingMove, target, hitNumber, defensiveStat, targetStats,
ref offensiveStat));
executingMove.RunScriptHook(script => script.ChangeDefensiveStatValue(executingMove, target, hitNumber,
origOffensiveStat, targetStats, ref defensiveStat));
var modifier = (float)offensiveStat / defensiveStat;
executingMove.RunScriptHook(script =>
script.ChangeDamageStatModifier(executingMove, target, hitNumber, ref modifier));
return modifier;
}
/// <summary>
/// Gets the damage modifier. This is a value that defaults to 1.0, but can be modified by scripts
/// to apply a raw modifier to the damage.
/// </summary>
private static float GetDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber)
{
var modifier = 1.0f;
executingMove.RunScriptHook(script =>
script.ChangeDamageModifier(executingMove, target, hitNumber, ref modifier));
return modifier;
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Linq;
using PkmnLib.Dynamic;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7MiscLibrary : IMiscLibrary
{
private readonly IMoveData _struggleData = new MoveDataImpl("struggle", new TypeIdentifier(0, "none"),
MoveCategory.Physical, 50, 255, 255, MoveTarget.Any, 0,
new SecondaryEffectImpl(-1, "struggle", new Dictionary<StringKey, object?>()), ["not_sketchable"]);
/// <inheritdoc />
public ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition) =>
new MoveChoice(user, new LearnedMoveImpl(_struggleData, MoveLearnMethod.Unknown), targetSide, targetPosition);
/// <inheritdoc />
public TimeOfDay GetTimeOfDay()
{
var time = StaticHelpers.GetCurrentDateTime().LocalDateTime;
var hour = time.Hour;
return hour switch
{
>= 0 and <= 5 => TimeOfDay.Night,
>= 6 and <= 9 => TimeOfDay.Morning,
>= 10 and <= 16 => TimeOfDay.Day,
17 => TimeOfDay.Evening,
_ => TimeOfDay.Night,
};
}
/// <inheritdoc />
public bool CanFlee(IBattle battle, IFleeChoice fleeChoice)
{
var user = fleeChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return false;
var opponentSide = battle.Sides[battleData.SideIndex == 0 ? 1 : 0];
var opponent = opponentSide.Pokemon.FirstOrDefault(x => x is not null);
if (opponent == null)
return true;
var userSpeed = user.FlatStats.Speed;
var opponentSpeed = opponent.FlatStats.Speed;
// If the player's active Pokémon's Speed is greater than or equal to the wild Pokémon's Speed, fleeing will
// always succeed
if (userSpeed >= opponentSpeed)
return true;
var userSide = battle.Sides[battleData.SideIndex];
userSide.RegisterFleeAttempt();
var fleeChance = (userSpeed * 32 / (opponentSpeed / 4) + 30 * userSide.FleeAttempts) / 256;
var random = battle.Random.GetInt(0, 100);
return random < fleeChance;
}
}