Deukhoofd 1feb27e826
All checks were successful
Build / Build (push) Successful in 50s
More work on refactor to interfaces
2025-06-29 12:03:51 +02:00

182 lines
8.1 KiB
C#

using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDamageCalculator
{
/// <inheritdoc />
public uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target,
int targetCount, byte hitNumber, IHitData hitData)
{
if (category == MoveCategory.Status)
return 0;
if (hitData.Effectiveness == 0)
return 0;
var levelModifier = 2.0f * user.Level / 5.0f + 2.0f;
var basePower = (float)hitData.BasePower;
var statModifier = GetStatModifier(executingMove, category, user, target, hitNumber, hitData);
var damageModifier = executingMove == null ? 1.0f : 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 (targetCount > 1)
floatDamage = MathF.Floor(floatDamage * 0.75f);
if (hitData.IsCritical)
{
var critModifier = 1.5f;
executingMove?.RunScriptHookInterface<IScriptChangeCriticalModifier>(script =>
script.ChangeCriticalModifier(executingMove, target, hitNumber, ref critModifier));
floatDamage = MathF.Floor(floatDamage * critModifier);
}
if (configuration.DamageCalculatorHasRandomness)
{
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);
}
var stabModifier = 1.0f;
var isStab = false;
if (hitData.Type != null && user.Types.Contains(hitData.Type.Value))
{
stabModifier = 1.5f;
isStab = true;
}
executingMove?.RunScriptHookInterface<IScriptChangeStabModifier>(script =>
script.ChangeStabModifier(executingMove, target, hitNumber, isStab, 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,
};
if (executingMove is not null)
{
executingMove.RunScriptHookInterface<IScriptChangeMoveDamage>(script =>
script.ChangeMoveDamage(executingMove, target, hitNumber, ref damage));
target.RunScriptHookInterface<IScriptChangeIncomingMoveDamage>(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.RunScriptHookInterface<IScriptChangeBasePower>(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.RunScriptHookInterface<IScriptChangeCriticalStage>(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, MoveCategory category, IPokemon user,
IPokemon target, byte hitNumber, IHitData hitData)
{
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?.RunScriptHookInterface<IScriptBypassDefensiveStatBoosts>(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 && user.StatBoost.GetStatistic(offensive) < 0;
executingMove?.RunScriptHookInterface<IScriptBypassOffensiveStatBoosts>(script =>
script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense));
var userStats = user.BoostedStats;
if (bypassOffense)
userStats = user.FlatStats;
var offensiveStat = userStats.GetStatistic(offensive);
var targetStats = target.BoostedStats;
if (bypassDefense)
targetStats = target.FlatStats;
var defensiveStat = targetStats.GetStatistic(defensive);
var origOffensiveStat = offensiveStat;
if (executingMove != null)
{
executingMove.RunScriptHookInterface<IScriptChangeOffensiveStatValue>(script =>
script.ChangeOffensiveStatValue(executingMove, target, hitNumber, defensiveStat, targetStats, offensive,
ref offensiveStat));
executingMove.RunScriptHookInterface<IScriptChangeDefensiveStatValue>(script =>
script.ChangeDefensiveStatValue(executingMove, target, hitNumber, origOffensiveStat, targetStats,
defensive, ref defensiveStat));
target.RunScriptHookInterface<IScriptChangeIncomingMoveOffensiveStatValue>(script =>
script.ChangeIncomingMoveOffensiveStatValue(executingMove, target, hitNumber, defensiveStat,
targetStats, offensive, ref offensiveStat));
target.RunScriptHookInterface<IScriptChangeIncomingMoveDefensiveStatValue>(script =>
script.ChangeIncomingMoveDefensiveStatValue(executingMove, target, hitNumber, origOffensiveStat,
targetStats, defensive, ref defensiveStat));
}
var modifier = (float)offensiveStat / defensiveStat;
executingMove?.RunScriptHookInterface<IScriptChangeDamageStatModifier>(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.RunScriptHookInterface<IScriptChangeDamageModifier>(script =>
script.ChangeDamageModifier(executingMove, target, hitNumber, ref modifier));
target.RunScriptHookInterface<IScriptChangeIncomingMoveDamageModifier>(script =>
script.ChangeIncomingMoveDamageModifier(executingMove, target, hitNumber, ref modifier));
return modifier;
}
}