using System; using System.Linq; using PkmnLib.Dynamic.Libraries; using PkmnLib.Static; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.Libraries; public class Gen7DamageCalculator(bool hasRandomness) : IDamageCalculator { /// 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 (executingMove.User.Types.Contains(hitData.Type)) { 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; } /// public byte GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData) { if (executingMove.UseMove.Category == MoveCategory.Status) return 0; var basePower = executingMove.UseMove.BasePower; executingMove.RunScriptHook(script => script.ChangeBasePower(executingMove, target, hitNumber, ref basePower)); return basePower; } /// 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); executingMove.RunScriptHook(script => script.ChangeOffensiveStatValue(executingMove, target, hitNumber, ref offensiveStat)); executingMove.RunScriptHook(script => script.ChangeDefensiveStatValue(executingMove, target, hitNumber, ref defensiveStat)); var modifier = (float)offensiveStat / defensiveStat; executingMove.RunScriptHook(script => script.ChangeDamageStatModifier(executingMove, target, hitNumber, ref modifier)); return modifier; } /// /// 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. /// 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; } }