Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions documentation/DocumentationTest.MateStatDocumentation.approved.md
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions src/NosCore.Algorithm/MateStatService/IMateStatService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
// -----------------------------------

using NosCore.Algorithm.MonsterStatService;

namespace NosCore.Algorithm.MateStatService
{
/// <summary>
/// Service for calculating a pet's attack, defence and dodge at its own level, before the flat modifiers of its
/// monster entry
/// </summary>
public interface IMateStatService
{
/// <summary>
/// Gets the close combat defence of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The defence value</returns>
int GetCloseDefence(byte race, byte level, MateGrowth growth);

/// <summary>
/// Gets the ranged defence of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The defence value</returns>
int GetDistanceDefence(byte race, byte level, MateGrowth growth);

/// <summary>
/// Gets the magic defence of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The defence value</returns>
int GetMagicDefence(byte race, byte level, MateGrowth growth);

/// <summary>
/// Gets the dodge of a pet, against close and ranged attacks alike
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The dodge value</returns>
int GetDodge(byte race, byte level, MateGrowth growth);

/// <summary>
/// Gets the minimum damage of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="attackClass">Whether the pet attacks in melee, at range or with magic</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The damage value</returns>
int GetMinimumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth);

/// <summary>
/// Gets the maximum damage of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="attackClass">Whether the pet attacks in melee, at range or with magic</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The damage value</returns>
int GetMaximumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth);

/// <summary>
/// Gets the hit rate of a pet
/// </summary>
/// <param name="race">The race of the pet's monster entry</param>
/// <param name="level">The pet level</param>
/// <param name="attackClass">Whether the pet attacks in melee, at range or with magic</param>
/// <param name="growth">The growth factors of the pet's monster entry</param>
/// <returns>The hit rate value</returns>
int GetHitRate(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth);
}
}
16 changes: 16 additions & 0 deletions src/NosCore.Algorithm/MateStatService/MateGrowth.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
// -----------------------------------

namespace NosCore.Algorithm.MateStatService
{
/// <summary>
/// The growth factors of a pet's monster entry, in tenths of a stat point per level
/// </summary>
/// <param name="Melee">Grows close defence and melee damage</param>
/// <param name="Ranged">Grows ranged defence, dodge, hit rate and ranged damage</param>
/// <param name="Magic">Grows magic defence and magic damage</param>
public readonly record struct MateGrowth(short Melee, short Ranged, short Magic);
}
56 changes: 56 additions & 0 deletions src/NosCore.Algorithm/MateStatService/MateStatService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
// -----------------------------------

using NosCore.Algorithm.MonsterStatService;

namespace NosCore.Algorithm.MateStatService
{
/// <summary>
/// Calculates a pet's attack, defence and dodge at its own level
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class MateStatService : IMateStatService
{
/// <inheritdoc />
public int GetCloseDefence(byte race, byte level, MateGrowth growth) =>
MonsterCurves.CloseDefence(race, level, level, growth.Melee);

/// <inheritdoc />
public int GetDistanceDefence(byte race, byte level, MateGrowth growth) =>
MonsterCurves.DistanceDefence(race, level, level, growth.Ranged);

/// <inheritdoc />
public int GetMagicDefence(byte race, byte level, MateGrowth growth) =>
MonsterCurves.MagicDefence(race, level, level, growth.Magic);

/// <inheritdoc />
public int GetDodge(byte race, byte level, MateGrowth growth) =>
MonsterCurves.Dodge(race, level, level, growth.Ranged);

/// <inheritdoc />
public int GetMinimumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth) =>
MonsterCurves.MinimumDamage(race, level, level, attackClass, AttackGrowth(attackClass, growth));

/// <inheritdoc />
public int GetMaximumDamage(byte race, byte level, MonsterAttackClass attackClass, MateGrowth growth) =>
MonsterCurves.MaximumDamage(race, level, level, attackClass, AttackGrowth(attackClass, growth));

/// <inheritdoc />
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,
};
}
}
138 changes: 138 additions & 0 deletions src/NosCore.Algorithm/MonsterStatService/MonsterCurves.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
// -----------------------------------

namespace NosCore.Algorithm.MonsterStatService
{
/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// 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.
/// </summary>
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);
}
}
Loading
Loading