diff --git a/documentation/DocumentationTest.MateStatDocumentation.approved.md b/documentation/DocumentationTest.MateStatDocumentation.approved.md new file mode 100644 index 0000000..84867ca --- /dev/null +++ b/documentation/DocumentationTest.MateStatDocumentation.approved.md @@ -0,0 +1,53 @@ +# Pet Stat Table + +Race 0, before the entry's flat modifiers, for a few growth factors (melee/ranged/magic). + +## Growth 0/0/0 +- Level 1 - Defence: 18/17/13 - Dodge: 31 - Melee: 32~40 hit 27 - Ranged: 27~35 hit 34 - Magic: 22~30 hit 70 +- Level 2 - Defence: 20/20/15 - Dodge: 36 - Melee: 36~46 hit 32 - Ranged: 31~41 hit 40 - Magic: 26~36 hit 70 +- Level 10 - Defence: 41/43/35 - Dodge: 76 - Melee: 69~93 hit 72 - Ranged: 64~88 hit 88 - Magic: 59~83 hit 70 +- Level 20 - Defence: 67/72/61 - Dodge: 126 - Melee: 111~151 hit 122 - Ranged: 106~146 hit 148 - Magic: 101~141 hit 70 +- Level 40 - Defence: 118/129/111 - Dodge: 226 - Melee: 195~267 hit 222 - Ranged: 190~262 hit 268 - Magic: 185~257 hit 70 +- Level 60 - Defence: 170/186/162 - Dodge: 326 - Melee: 279~383 hit 322 - Ranged: 274~378 hit 388 - Magic: 269~373 hit 70 +- Level 80 - Defence: 221/243/213 - Dodge: 426 - Melee: 363~499 hit 422 - Ranged: 358~494 hit 508 - Magic: 353~489 hit 70 +- Level 99 - Defence: 271/297/262 - Dodge: 521 - Melee: 443~609 hit 517 - Ranged: 438~604 hit 622 - Magic: 433~599 hit 70 + +## Growth 10/0/0 +- Level 1 - Defence: 18/17/13 - Dodge: 31 - Melee: 32~40 hit 27 - Ranged: 27~35 hit 34 - Magic: 22~30 hit 70 +- Level 2 - Defence: 21/20/15 - Dodge: 36 - Melee: 37~47 hit 32 - Ranged: 31~41 hit 40 - Magic: 26~36 hit 70 +- Level 10 - Defence: 46/43/35 - Dodge: 76 - Melee: 78~102 hit 72 - Ranged: 64~88 hit 88 - Magic: 59~83 hit 70 +- Level 20 - Defence: 77/72/61 - Dodge: 126 - Melee: 130~170 hit 122 - Ranged: 106~146 hit 148 - Magic: 101~141 hit 70 +- Level 40 - Defence: 138/129/111 - Dodge: 226 - Melee: 234~306 hit 222 - Ranged: 190~262 hit 268 - Magic: 185~257 hit 70 +- Level 60 - Defence: 200/186/162 - Dodge: 326 - Melee: 338~442 hit 322 - Ranged: 274~378 hit 388 - Magic: 269~373 hit 70 +- Level 80 - Defence: 261/243/213 - Dodge: 426 - Melee: 442~578 hit 422 - Ranged: 358~494 hit 508 - Magic: 353~489 hit 70 +- Level 99 - Defence: 320/297/262 - Dodge: 521 - Melee: 541~707 hit 517 - Ranged: 438~604 hit 622 - Magic: 433~599 hit 70 + +## Growth 0/10/0 +- Level 1 - Defence: 18/17/13 - Dodge: 31 - Melee: 32~40 hit 27 - Ranged: 27~35 hit 34 - Magic: 22~30 hit 70 +- Level 2 - Defence: 20/20/15 - Dodge: 37 - Melee: 36~46 hit 33 - Ranged: 32~42 hit 42 - Magic: 26~36 hit 70 +- Level 10 - Defence: 41/47/35 - Dodge: 85 - Melee: 69~93 hit 81 - Ranged: 73~97 hit 106 - Magic: 59~83 hit 70 +- Level 20 - Defence: 67/81/61 - Dodge: 145 - Melee: 111~151 hit 141 - Ranged: 125~165 hit 186 - Magic: 101~141 hit 70 +- Level 40 - Defence: 118/148/111 - Dodge: 265 - Melee: 195~267 hit 261 - Ranged: 229~301 hit 346 - Magic: 185~257 hit 70 +- Level 60 - Defence: 170/215/162 - Dodge: 385 - Melee: 279~383 hit 381 - Ranged: 333~437 hit 506 - Magic: 269~373 hit 70 +- Level 80 - Defence: 221/282/213 - Dodge: 505 - Melee: 363~499 hit 501 - Ranged: 437~573 hit 666 - Magic: 353~489 hit 70 +- Level 99 - Defence: 271/346/262 - Dodge: 619 - Melee: 443~609 hit 615 - Ranged: 536~702 hit 818 - Magic: 433~599 hit 70 + +## Growth 0/0/10 +- Level 1 - Defence: 18/17/13 - Dodge: 31 - Melee: 32~40 hit 27 - Ranged: 27~35 hit 34 - Magic: 22~30 hit 70 +- Level 2 - Defence: 20/20/16 - Dodge: 36 - Melee: 36~46 hit 32 - Ranged: 31~41 hit 40 - Magic: 27~37 hit 70 +- Level 10 - Defence: 41/43/40 - Dodge: 76 - Melee: 69~93 hit 72 - Ranged: 64~88 hit 88 - Magic: 68~92 hit 70 +- Level 20 - Defence: 67/72/71 - Dodge: 126 - Melee: 111~151 hit 122 - Ranged: 106~146 hit 148 - Magic: 120~160 hit 70 +- Level 40 - Defence: 118/129/131 - Dodge: 226 - Melee: 195~267 hit 222 - Ranged: 190~262 hit 268 - Magic: 224~296 hit 70 +- Level 60 - Defence: 170/186/192 - Dodge: 326 - Melee: 279~383 hit 322 - Ranged: 274~378 hit 388 - Magic: 328~432 hit 70 +- Level 80 - Defence: 221/243/253 - Dodge: 426 - Melee: 363~499 hit 422 - Ranged: 358~494 hit 508 - Magic: 432~568 hit 70 +- Level 99 - Defence: 271/297/311 - Dodge: 521 - Melee: 443~609 hit 517 - Ranged: 438~604 hit 622 - Magic: 531~697 hit 70 + +## Growth 5/5/5 +- Level 1 - Defence: 18/17/13 - Dodge: 31 - Melee: 32~40 hit 27 - Ranged: 27~35 hit 34 - Magic: 22~30 hit 70 +- Level 2 - Defence: 20/20/15 - Dodge: 36 - Melee: 36~46 hit 32 - Ranged: 31~41 hit 40 - Magic: 26~36 hit 70 +- Level 10 - Defence: 43/45/37 - Dodge: 80 - Melee: 73~97 hit 76 - Ranged: 68~92 hit 96 - Magic: 63~87 hit 70 +- Level 20 - Defence: 72/76/66 - Dodge: 135 - Melee: 120~160 hit 131 - Ranged: 115~155 hit 166 - Magic: 110~150 hit 70 +- Level 40 - Defence: 128/138/121 - Dodge: 245 - Melee: 214~286 hit 241 - Ranged: 209~281 hit 306 - Magic: 204~276 hit 70 +- Level 60 - Defence: 185/200/177 - Dodge: 355 - Melee: 308~412 hit 351 - Ranged: 303~407 hit 446 - Magic: 298~402 hit 70 +- Level 80 - Defence: 241/262/233 - Dodge: 465 - Melee: 402~538 hit 461 - Ranged: 397~533 hit 586 - Magic: 392~528 hit 70 +- Level 99 - Defence: 295/322/286 - Dodge: 570 - Melee: 492~658 hit 566 - Ranged: 487~653 hit 720 - Magic: 482~648 hit 70 diff --git a/src/NosCore.Algorithm/MateStatService/IMateStatService.cs b/src/NosCore.Algorithm/MateStatService/IMateStatService.cs new file mode 100644 index 0000000..2eca674 --- /dev/null +++ b/src/NosCore.Algorithm/MateStatService/IMateStatService.cs @@ -0,0 +1,83 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using NosCore.Algorithm.MonsterStatService; + +namespace NosCore.Algorithm.MateStatService +{ + /// + /// Service for calculating a pet's attack, defence and dodge at its own level, before the flat modifiers of its + /// monster entry + /// + public interface IMateStatService + { + /// + /// Gets the close combat defence of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// The growth factors of the pet's monster entry + /// The defence value + int GetCloseDefence(byte race, byte level, MateGrowth growth); + + /// + /// Gets the ranged defence of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// The growth factors of the pet's monster entry + /// The defence value + int GetDistanceDefence(byte race, byte level, MateGrowth growth); + + /// + /// Gets the magic defence of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// The growth factors of the pet's monster entry + /// The defence value + int GetMagicDefence(byte race, byte level, MateGrowth growth); + + /// + /// Gets the dodge of a pet, against close and ranged attacks alike + /// + /// The race of the pet's monster entry + /// The pet level + /// The growth factors of the pet's monster entry + /// The dodge value + int GetDodge(byte race, byte level, MateGrowth growth); + + /// + /// Gets the minimum damage of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// Whether the pet attacks in melee, at range or with magic + /// The growth factors of the pet's monster entry + /// The damage value + int GetMinimumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth); + + /// + /// Gets the maximum damage of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// Whether the pet attacks in melee, at range or with magic + /// The growth factors of the pet's monster entry + /// The damage value + int GetMaximumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth); + + /// + /// Gets the hit rate of a pet + /// + /// The race of the pet's monster entry + /// The pet level + /// Whether the pet attacks in melee, at range or with magic + /// The growth factors of the pet's monster entry + /// The hit rate value + int GetHitRate(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth); + } +} diff --git a/src/NosCore.Algorithm/MateStatService/MateGrowth.cs b/src/NosCore.Algorithm/MateStatService/MateGrowth.cs new file mode 100644 index 0000000..f0d45a4 --- /dev/null +++ b/src/NosCore.Algorithm/MateStatService/MateGrowth.cs @@ -0,0 +1,16 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.Algorithm.MateStatService +{ + /// + /// The growth factors of a pet's monster entry, in tenths of a stat point per level + /// + /// Grows close defence and melee damage + /// Grows ranged defence, dodge, hit rate and ranged damage + /// Grows magic defence and magic damage + public readonly record struct MateGrowth(short Melee, short Ranged, short Magic); +} diff --git a/src/NosCore.Algorithm/MateStatService/MateStatService.cs b/src/NosCore.Algorithm/MateStatService/MateStatService.cs new file mode 100644 index 0000000..61dfa26 --- /dev/null +++ b/src/NosCore.Algorithm/MateStatService/MateStatService.cs @@ -0,0 +1,56 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using NosCore.Algorithm.MonsterStatService; + +namespace NosCore.Algorithm.MateStatService +{ + /// + /// Calculates a pet's attack, defence and dodge at its own level + /// + /// + /// A pet follows the monster curves of its race with its weapon and armour at the pet's level, and grows each + /// curve's level share by its growth factor: tenths per level above the first for dodge, damage and hit rate, + /// twentieths for the defences. + /// + public class MateStatService : IMateStatService + { + /// + public int GetCloseDefence(byte race, byte level, MateGrowth growth) => + MonsterCurves.CloseDefence(race, level, level, growth.Melee); + + /// + public int GetDistanceDefence(byte race, byte level, MateGrowth growth) => + MonsterCurves.DistanceDefence(race, level, level, growth.Ranged); + + /// + public int GetMagicDefence(byte race, byte level, MateGrowth growth) => + MonsterCurves.MagicDefence(race, level, level, growth.Magic); + + /// + public int GetDodge(byte race, byte level, MateGrowth growth) => + MonsterCurves.Dodge(race, level, level, growth.Ranged); + + /// + public int GetMinimumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth) => + MonsterCurves.MinimumDamage(race, level, level, attackClass, AttackGrowth(attackClass, growth)); + + /// + public int GetMaximumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth) => + MonsterCurves.MaximumDamage(race, level, level, attackClass, AttackGrowth(attackClass, growth)); + + /// + public int GetHitRate(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth) => + MonsterCurves.HitRate(race, level, level, attackClass, growth.Ranged); + + private static int AttackGrowth(MonsterAttackClass attackClass, MateGrowth growth) => attackClass switch + { + MonsterAttackClass.Ranged => growth.Ranged, + MonsterAttackClass.Magic => growth.Magic, + _ => growth.Melee, + }; + } +} diff --git a/src/NosCore.Algorithm/MonsterStatService/MonsterCurves.cs b/src/NosCore.Algorithm/MonsterStatService/MonsterCurves.cs new file mode 100644 index 0000000..eaf939a --- /dev/null +++ b/src/NosCore.Algorithm/MonsterStatService/MonsterCurves.cs @@ -0,0 +1,138 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.Algorithm.MonsterStatService +{ + /// + /// The race curves shared by monsters and pets. A pet adds its growth factor, in tenths per level above the + /// first, to the level share of each curve: half of it for the defences, all of it for dodge, damage and hit rate. + /// + internal static class MonsterCurves + { + private const int ArmorDefencePerLevel = 2; + private const int ArmorDodgePerLevel = 4; + private const int WeaponMinimumDamagePerLevel = 3; + private const int WeaponMaximumDamagePerLevel = 5; + private const int WeaponHitRatePerLevel = 4; + private const int MagicHitRate = 70; + private const int DefenceGrowthDivisor = 20; + private const int AttackGrowthDivisor = 10; + + private static readonly RaceCurves Default = new( + new(1, 2, 1, 15), new(1, 2, 0, 14), new(1, 2, 1, 10), new(1, 1, 0, 26), + new(1, 1, 0, 28), new(1, 1, 0, 34), new(1, 1, 0, 22), + new(1, 1, 0, 23), new(1, 1, 0, 29), 28, + new(1, 1, 0, 18), new(1, 1, 0, 24)); + + // Race 4 has two level 30 entries whose published stats fit none of these curves, so the low end + // of that race is unconfirmed. + private static readonly RaceCurves?[] Races = + [ + Default, + new(new(1, 1, 0, 19), new(1, 1, 0, 17), new(1, 1, 0, 18), new(2, 1, 0, 33), + new(2, 1, 0, 35), new(2, 1, 0, 41), new(2, 1, 0, 29), + new(2, 1, 0, 30), new(2, 1, 0, 36), 42, + new(2, 1, 0, 33), new(2, 1, 0, 39)), + new(new(3, 4, 1, 14), new(1, 2, 1, 15), new(2, 5, 3, 14), new(1, 1, 0, 29), + new(3, 2, 1, 25), new(3, 2, 1, 31), new(1, 1, 0, 25), + new(1, 1, 0, 26), new(1, 1, 0, 32), 34, + new(4, 5, 1, 26), new(4, 5, 1, 32)), + new(new(1, 2, 1, 14), new(1, 2, 1, 15), new(1, 2, 1, 14), new(1, 1, 0, 29), + new(1, 1, 0, 26), new(1, 1, 0, 32), new(1, 1, 0, 25), + new(1, 1, 0, 26), new(1, 1, 0, 32), 34, + new(1, 1, 0, 26), new(1, 1, 0, 32)), + new(new(3, 5, 4, 16), new(3, 5, 4, 17), new(1, 1, 0, 16), new(6, 5, 4, 33), + new(6, 5, 4, 30), new(6, 5, 4, 36), new(6, 5, 4, 29), + new(6, 5, 4, 30), new(6, 5, 4, 36), 42, + new(2, 1, 0, 30), new(2, 1, 0, 36)), + new(new(2, 5, 0, 13), new(2, 5, 0, 14), new(2, 5, 0, 13), new(4, 5, 1, 26), + new(4, 5, 1, 23), new(4, 5, 1, 29), new(4, 5, 1, 22), + new(4, 5, 1, 23), new(4, 5, 1, 29), 28, + new(4, 5, 1, 23), new(4, 5, 1, 29)), + new(new(1, 2, 0, 11), new(1, 2, 1, 15), new(3, 4, 1, 24), new(1, 1, 0, 29), + new(1, 1, 0, 19), new(1, 1, 0, 25), new(1, 1, 0, 25), + new(1, 1, 0, 26), new(1, 1, 0, 32), 34, + new(3, 2, 1, 45), new(3, 2, 1, 51)), + null, + new(new(0, 1, 0, 10), new(0, 1, 0, 11), new(0, 1, 0, 10), new(0, 1, 0, 20), + new(0, 1, 0, 17), new(0, 1, 0, 23), new(0, 1, 0, 16), + new(0, 1, 0, 17), new(0, 1, 0, 23), 16, + new(0, 1, 0, 17), new(0, 1, 0, 23)), + ]; + + public static int CloseDefence(byte race, byte level, byte armorLevel, int meleeGrowth = 0) => + For(race).Close.At(level, meleeGrowth, DefenceGrowthDivisor) + (ArmorDefencePerLevel * armorLevel) + FloorDivide((2 * armorLevel) + 10, 25); + + public static int DistanceDefence(byte race, byte level, byte armorLevel, int rangedGrowth = 0) => + For(race).Distance.At(level, rangedGrowth, DefenceGrowthDivisor) + (ArmorDefencePerLevel * armorLevel) + FloorDivide((9 * armorLevel) + 20, 25); + + public static int MagicDefence(byte race, byte level, byte armorLevel, int magicGrowth = 0) => + For(race).Magic.At(level, magicGrowth, DefenceGrowthDivisor) + (ArmorDefencePerLevel * armorLevel) + FloorDivide(armorLevel + 5, 25); + + public static int Dodge(byte race, byte level, byte armorLevel, int rangedGrowth = 0) => + For(race).Dodge.At(level, rangedGrowth, AttackGrowthDivisor) + (ArmorDodgePerLevel * armorLevel); + + public static int MinimumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass, int growth = 0) + { + var curves = For(race); + var curve = attackClass switch + { + MonsterAttackClass.Ranged => curves.RangedMinimum, + MonsterAttackClass.Magic => curves.MagicMinimum, + _ => curves.MeleeMinimum, + }; + return curve.At(level, growth, AttackGrowthDivisor) + (WeaponMinimumDamagePerLevel * weaponLevel) + WeaponUpgradeBonus(weaponLevel); + } + + public static int MaximumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass, int growth = 0) + { + var curves = For(race); + var curve = attackClass switch + { + MonsterAttackClass.Ranged => curves.RangedMaximum, + MonsterAttackClass.Magic => curves.MagicMaximum, + _ => curves.MeleeMaximum, + }; + return curve.At(level, growth, AttackGrowthDivisor) + (WeaponMaximumDamagePerLevel * weaponLevel) - WeaponUpgradeBonus(weaponLevel); + } + + public static int HitRate(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass, int rangedGrowth = 0) + { + var curves = For(race); + return attackClass switch + { + MonsterAttackClass.Magic => MagicHitRate, + MonsterAttackClass.Ranged => (2 * curves.MeleeHitRate.LevelShare(level, rangedGrowth, AttackGrowthDivisor)) + (WeaponHitRatePerLevel * weaponLevel) + curves.RangedHitRateConstant, + _ => curves.MeleeHitRate.At(level, rangedGrowth, AttackGrowthDivisor) + (WeaponHitRatePerLevel * weaponLevel), + }; + } + + private static RaceCurves For(byte race) => race < Races.Length ? Races[race] ?? Default : Default; + + private static int WeaponUpgradeBonus(byte weaponLevel) => FloorDivide(weaponLevel - 1, 5); + + private static int FloorDivide(int value, int divisor) => + value >= 0 ? value / divisor : -((-value + divisor - 1) / divisor); + + /// + /// The race's share of the level, floor((numerator * level + offset) / denominator), plus a constant. A + /// growth factor adds growth / growthDivisor per level above the first inside the same floor. + /// + private readonly record struct Curve(int Numerator, int Denominator, int Offset, int Constant) + { + public int LevelShare(byte level, int growth, int growthDivisor) => + FloorDivide((((Numerator * level) + Offset) * growthDivisor) + (Denominator * growth * (level - 1)), Denominator * growthDivisor); + + public int At(byte level, int growth, int growthDivisor) => LevelShare(level, growth, growthDivisor) + Constant; + } + + private sealed record RaceCurves( + Curve Close, Curve Distance, Curve Magic, Curve Dodge, + Curve MeleeMinimum, Curve MeleeMaximum, Curve MeleeHitRate, + Curve RangedMinimum, Curve RangedMaximum, int RangedHitRateConstant, + Curve MagicMinimum, Curve MagicMaximum); + } +} diff --git a/src/NosCore.Algorithm/MonsterStatService/MonsterStatService.cs b/src/NosCore.Algorithm/MonsterStatService/MonsterStatService.cs index 31edbcc..3cb2f22 100644 --- a/src/NosCore.Algorithm/MonsterStatService/MonsterStatService.cs +++ b/src/NosCore.Algorithm/MonsterStatService/MonsterStatService.cs @@ -19,12 +19,6 @@ namespace NosCore.Algorithm.MonsterStatService /// public class MonsterStatService(IMateHpService mateHpService, IMateMpService mateMpService) : IMonsterStatService { - private const int ArmorDefencePerLevel = 2; - private const int ArmorDodgePerLevel = 4; - private const int WeaponMinimumDamagePerLevel = 3; - private const int WeaponMaximumDamagePerLevel = 5; - private const int WeaponHitRatePerLevel = 4; - private const int MagicHitRate = 70; private const int CriticalChance = 4; private const int CriticalRate = 70; @@ -37,48 +31,6 @@ private static readonly (byte Level, int Tenths)[] HpTiers = (37, 12), ]; - private static readonly RaceCurves Default = new( - new(1, 2, 1, 15), new(1, 2, 0, 14), new(1, 2, 1, 10), new(1, 1, 0, 26), - new(1, 1, 0, 28), new(1, 1, 0, 34), new(1, 1, 0, 22), - new(1, 1, 0, 23), new(1, 1, 0, 29), 28, - new(1, 1, 0, 18), new(1, 1, 0, 24)); - - // Race 4 has two level 30 entries whose published stats fit none of these curves, so the low end - // of that race is unconfirmed. - private static readonly RaceCurves?[] Races = - [ - Default, - new(new(1, 1, 0, 19), new(1, 1, 0, 17), new(1, 1, 0, 18), new(2, 1, 0, 33), - new(2, 1, 0, 35), new(2, 1, 0, 41), new(2, 1, 0, 29), - new(2, 1, 0, 30), new(2, 1, 0, 36), 42, - new(2, 1, 0, 33), new(2, 1, 0, 39)), - new(new(3, 4, 1, 14), new(1, 2, 1, 15), new(2, 5, 3, 14), new(1, 1, 0, 29), - new(3, 2, 1, 25), new(3, 2, 1, 31), new(1, 1, 0, 25), - new(1, 1, 0, 26), new(1, 1, 0, 32), 34, - new(4, 5, 1, 26), new(4, 5, 1, 32)), - new(new(1, 2, 1, 14), new(1, 2, 1, 15), new(1, 2, 1, 14), new(1, 1, 0, 29), - new(1, 1, 0, 26), new(1, 1, 0, 32), new(1, 1, 0, 25), - new(1, 1, 0, 26), new(1, 1, 0, 32), 34, - new(1, 1, 0, 26), new(1, 1, 0, 32)), - new(new(3, 5, 4, 16), new(3, 5, 4, 17), new(1, 1, 0, 16), new(6, 5, 4, 33), - new(6, 5, 4, 30), new(6, 5, 4, 36), new(6, 5, 4, 29), - new(6, 5, 4, 30), new(6, 5, 4, 36), 42, - new(2, 1, 0, 30), new(2, 1, 0, 36)), - new(new(2, 5, 0, 13), new(2, 5, 0, 14), new(2, 5, 0, 13), new(4, 5, 1, 26), - new(4, 5, 1, 23), new(4, 5, 1, 29), new(4, 5, 1, 22), - new(4, 5, 1, 23), new(4, 5, 1, 29), 28, - new(4, 5, 1, 23), new(4, 5, 1, 29)), - new(new(1, 2, 0, 11), new(1, 2, 1, 15), new(3, 4, 1, 24), new(1, 1, 0, 29), - new(1, 1, 0, 19), new(1, 1, 0, 25), new(1, 1, 0, 25), - new(1, 1, 0, 26), new(1, 1, 0, 32), 34, - new(3, 2, 1, 45), new(3, 2, 1, 51)), - null, - new(new(0, 1, 0, 10), new(0, 1, 0, 11), new(0, 1, 0, 10), new(0, 1, 0, 20), - new(0, 1, 0, 17), new(0, 1, 0, 23), new(0, 1, 0, 16), - new(0, 1, 0, 17), new(0, 1, 0, 23), 16, - new(0, 1, 0, 17), new(0, 1, 0, 23)), - ]; - /// public long GetMaxHp(byte race, byte level) { @@ -99,86 +51,33 @@ public long GetMaxHp(byte race, byte level) public long GetMaxMp(byte race, byte level) => mateMpService.GetMp(race, level, 0); /// - public int GetCloseDefence(byte race, byte level, byte armorLevel) => - For(race).Close.At(level) + (ArmorDefencePerLevel * armorLevel) + FloorDivide((2 * armorLevel) + 10, 25); + public int GetCloseDefence(byte race, byte level, byte armorLevel) => MonsterCurves.CloseDefence(race, level, armorLevel); /// - public int GetDistanceDefence(byte race, byte level, byte armorLevel) => - For(race).Distance.At(level) + (ArmorDefencePerLevel * armorLevel) + FloorDivide((9 * armorLevel) + 20, 25); + public int GetDistanceDefence(byte race, byte level, byte armorLevel) => MonsterCurves.DistanceDefence(race, level, armorLevel); /// - public int GetMagicDefence(byte race, byte level, byte armorLevel) => - For(race).Magic.At(level) + (ArmorDefencePerLevel * armorLevel) + FloorDivide(armorLevel + 5, 25); + public int GetMagicDefence(byte race, byte level, byte armorLevel) => MonsterCurves.MagicDefence(race, level, armorLevel); /// - public int GetDodge(byte race, byte level, byte armorLevel) => - For(race).Dodge.At(level) + (ArmorDodgePerLevel * armorLevel); + public int GetDodge(byte race, byte level, byte armorLevel) => MonsterCurves.Dodge(race, level, armorLevel); /// - public int GetMinimumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) - { - var curves = For(race); - var curve = attackClass switch - { - MonsterAttackClass.Ranged => curves.RangedMinimum, - MonsterAttackClass.Magic => curves.MagicMinimum, - _ => curves.MeleeMinimum, - }; - return curve.At(level) + (WeaponMinimumDamagePerLevel * weaponLevel) + WeaponUpgradeBonus(weaponLevel); - } + public int GetMinimumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) => + MonsterCurves.MinimumDamage(race, level, weaponLevel, attackClass); /// - public int GetMaximumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) - { - var curves = For(race); - var curve = attackClass switch - { - MonsterAttackClass.Ranged => curves.RangedMaximum, - MonsterAttackClass.Magic => curves.MagicMaximum, - _ => curves.MeleeMaximum, - }; - return curve.At(level) + (WeaponMaximumDamagePerLevel * weaponLevel) - WeaponUpgradeBonus(weaponLevel); - } + public int GetMaximumDamage(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) => + MonsterCurves.MaximumDamage(race, level, weaponLevel, attackClass); /// - public int GetHitRate(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) - { - var curves = For(race); - return attackClass switch - { - MonsterAttackClass.Magic => MagicHitRate, - MonsterAttackClass.Ranged => (2 * curves.MeleeHitRate.LevelShare(level)) + (WeaponHitRatePerLevel * weaponLevel) + curves.RangedHitRateConstant, - _ => curves.MeleeHitRate.At(level) + (WeaponHitRatePerLevel * weaponLevel), - }; - } + public int GetHitRate(byte race, byte level, byte weaponLevel, MonsterAttackClass attackClass) => + MonsterCurves.HitRate(race, level, weaponLevel, attackClass); /// public int GetCriticalChance() => CriticalChance; /// public int GetCriticalRate() => CriticalRate; - - private static RaceCurves For(byte race) => race < Races.Length ? Races[race] ?? Default : Default; - - private static int WeaponUpgradeBonus(byte weaponLevel) => FloorDivide(weaponLevel - 1, 5); - - private static int FloorDivide(int value, int divisor) => - value >= 0 ? value / divisor : -((-value + divisor - 1) / divisor); - - /// - /// The race's share of the monster level, floor((numerator * level + offset) / denominator), plus a constant - /// - private readonly record struct Curve(int Numerator, int Denominator, int Offset, int Constant) - { - public int LevelShare(byte level) => FloorDivide((Numerator * level) + Offset, Denominator); - - public int At(byte level) => LevelShare(level) + Constant; - } - - private sealed record RaceCurves( - Curve Close, Curve Distance, Curve Magic, Curve Dodge, - Curve MeleeMinimum, Curve MeleeMaximum, Curve MeleeHitRate, - Curve RangedMinimum, Curve RangedMaximum, int RangedHitRateConstant, - Curve MagicMinimum, Curve MagicMaximum); } } diff --git a/test/NosCore.Algorithm.Tests/DocumentationTest.cs b/test/NosCore.Algorithm.Tests/DocumentationTest.cs index b4e64ba..639bcde 100644 --- a/test/NosCore.Algorithm.Tests/DocumentationTest.cs +++ b/test/NosCore.Algorithm.Tests/DocumentationTest.cs @@ -831,5 +831,61 @@ public void MonsterStatDocumentation() Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md")); } + + [TestMethod] + public void MateStatDocumentation() + { + var service = new MateStatService.MateStatService(); + var melee = MonsterStatService.MonsterAttackClass.Melee; + var ranged = MonsterStatService.MonsterAttackClass.Ranged; + var magic = MonsterStatService.MonsterAttackClass.Magic; + + // Published pet stats with the entries' flat modifiers taken off: a level 40 race 2 melee pet growing + // 12/5/2, a level 99 race 2 archer growing 4/12/0 and a level 99 race 0 melee pet growing 1/10/0. + var brawler = new MateStatService.MateGrowth(12, 5, 2); + Assert.AreEqual(150, service.GetCloseDefence(2, 40, brawler)); + Assert.AreEqual(140, service.GetDistanceDefence(2, 40, brawler)); + Assert.AreEqual(115, service.GetMagicDefence(2, 40, brawler)); + Assert.AreEqual(248, service.GetDodge(2, 40, brawler)); + Assert.AreEqual(259, service.GetMinimumDamage(2, 40, melee, brawler)); + Assert.AreEqual(331, service.GetMaximumDamage(2, 40, melee, brawler)); + Assert.AreEqual(244, service.GetHitRate(2, 40, melee, brawler)); + var archer = new MateStatService.MateGrowth(4, 12, 0); + Assert.AreEqual(314, service.GetCloseDefence(2, 99, archer)); + Assert.AreEqual(357, service.GetDistanceDefence(2, 99, archer)); + Assert.AreEqual(641, service.GetDodge(2, 99, archer)); + Assert.AreEqual(558, service.GetMinimumDamage(2, 99, ranged, archer)); + Assert.AreEqual(724, service.GetMaximumDamage(2, 99, ranged, archer)); + Assert.AreEqual(862, service.GetHitRate(2, 99, ranged, archer)); + var sharpshooter = new MateStatService.MateGrowth(1, 10, 0); + Assert.AreEqual(275, service.GetCloseDefence(0, 99, sharpshooter)); + Assert.AreEqual(346, service.GetDistanceDefence(0, 99, sharpshooter)); + Assert.AreEqual(619, service.GetDodge(0, 99, sharpshooter)); + Assert.AreEqual(452, service.GetMinimumDamage(0, 99, melee, sharpshooter)); + Assert.AreEqual(615, service.GetHitRate(0, 99, melee, sharpshooter)); + Assert.AreEqual(136, service.GetMagicDefence(2, 40, new MateStatService.MateGrowth(4, 0, 13))); + Assert.AreEqual(235, service.GetMinimumDamage(2, 40, magic, new MateStatService.MateGrowth(4, 0, 13))); + + var resultBuilder = new StringBuilder("# Pet Stat Table"); + resultBuilder.AppendLine(); + resultBuilder.AppendLine(); + resultBuilder.AppendLine("Race 0, before the entry's flat modifiers, for a few growth factors (melee/ranged/magic)."); + MateStatService.MateGrowth[] growths = { new(0, 0, 0), new(10, 0, 0), new(0, 10, 0), new(0, 0, 10), new(5, 5, 5) }; + byte[] levels = { 1, 2, 10, 20, 40, 60, 80, 99 }; + foreach (var growth in growths) + { + resultBuilder.AppendLine(); + resultBuilder.AppendLine($"## Growth {growth.Melee}/{growth.Ranged}/{growth.Magic}"); + foreach (var level in levels) + { + var classes = string.Join(" - ", Enum.GetValues().Select(attackClass => + $"{attackClass}: {service.GetMinimumDamage(0, level, attackClass, growth)}~{service.GetMaximumDamage(0, level, attackClass, growth)} hit {service.GetHitRate(0, level, attackClass, growth)}")); + resultBuilder.AppendLine( + $"- Level {level,2} - Defence: {service.GetCloseDefence(0, level, growth)}/{service.GetDistanceDefence(0, level, growth)}/{service.GetMagicDefence(0, level, growth)} - Dodge: {service.GetDodge(0, level, growth)} - {classes}"); + } + } + + Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md")); + } } }