< Summary

Information
Class: RaidLoop.Core.CombatBalance
Assembly: RaidLoop.Core
File(s): /home/runner/work/RaidLoop/RaidLoop/src/RaidLoop.Core/CombatBalance.cs
Line coverage
87%
Covered lines: 207
Uncovered lines: 29
Coverable lines: 236
Total lines: 498
Line coverage: 87.7%
Branch coverage
73%
Covered branches: 287
Total branches: 391
Branch coverage: 73.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
GetAbilityModifier(...)100%11100%
GetRangedAttackBonusFromDexterity(...)100%11100%
GetDefenseFromDexterity(...)100%22100%
GetMaxHealthFromConstitution(...)100%11100%
GetCarryCapacityFromStrength(...)100%11100%
GetMaxEncumbranceFromStrength(...)100%11100%
GetEncumbranceTier(...)100%44100%
GetEffectiveDexterityModifier(...)100%44100%
GetEncumbranceAttackPenalty(...)100%44100%
GetCharismaModifier(...)100%11100%
GetMaxShopRarityFromChaBonus(...)100%66100%
GetShopPrice(...)50%22100%
ResolveAttackRoll(...)100%44100%
GetDamageRange(...)100%11100%
RollDamage(...)100%22100%
SupportsSingleShot(...)100%22100%
SupportsSingleShot(...)100%11100%
SupportsBurstFire(...)97.05%353490.9%
SupportsBurstFire(...)100%11100%
SupportsFullAuto(...)87.5%8887.5%
SupportsFullAuto(...)100%11100%
GetBurstAttackPenalty(...)100%1212100%
GetArmorReduction(...)100%1212100%
ApplyArmorReduction(...)100%11100%
GetBuyPrice(...)60.21%989391.66%
GetBuyPrice(...)100%11100%
GetMagazineCapacity(...)94.44%373690.9%
GetMagazineCapacity(...)100%11100%
WeaponUsesAmmo(...)100%11100%
WeaponUsesAmmo(...)100%11100%
GetBackpackCapacity(...)100%1212100%
GetBackpackCapacity(...)100%22100%
IsMedkit(...)75%4480%
GetTotalEncumbrance(...)100%11100%
NormalizeItemName(...)100%44100%
NormalizeWeaponName(...)58.33%6604835.71%
NormalizeArmorName(...)61.11%4203633.33%
GetLightLoadLimit(...)100%11100%
GetMediumLoadLimit(...)100%11100%
GetHeavyLoadLimit(...)100%22100%
GetDamageDieCount(...)75%4485.71%
GetDamageDieSize(...)45%654075%
GetItemLookupToken(...)71.42%191471.42%

File(s)

/home/runner/work/RaidLoop/RaidLoop/src/RaidLoop.Core/CombatBalance.cs

#LineLine coverage
 1namespace RaidLoop.Core;
 2
 3public enum AttackMode
 4{
 5    Standard,
 6    Burst,
 7    FullAuto
 8}
 9
 10public interface IRng
 11{
 12    int Next(int minInclusive, int maxExclusive);
 13}
 14
 15public sealed class RandomRng : IRng
 16{
 17    private readonly Random _random;
 18
 19    public RandomRng(Random random)
 20    {
 21        _random = random;
 22    }
 23
 24    public int Next(int minInclusive, int maxExclusive)
 25    {
 26        return _random.Next(minInclusive, maxExclusive);
 27    }
 28}
 29
 30public readonly record struct DamageRange(int Min, int Max);
 31
 32public enum EncumbranceTier
 33{
 34    Light,
 35    Medium,
 36    Heavy
 37}
 38
 39public static class CombatBalance
 40{
 141    private static readonly int[] HeavyLoadByStrength =
 142    [
 143        10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
 144        115, 130, 150, 175, 200, 230, 260, 300, 350, 400,
 145        460, 520, 600, 700, 800, 920, 1040, 1200, 1400
 146    ];
 47
 48    public static int GetAbilityModifier(int score)
 49    {
 3650        return PlayerStatRules.GetAbilityModifier(score);
 51    }
 52
 53    public static int GetRangedAttackBonusFromDexterity(int dexterity)
 54    {
 455        return GetAbilityModifier(dexterity);
 56    }
 57
 58    public static int GetDefenseFromDexterity(int dexterity, int? maxDexBonus = null)
 59    {
 660        var dexterityBonus = GetAbilityModifier(dexterity);
 661        if (maxDexBonus.HasValue)
 62        {
 163            dexterityBonus = Math.Min(dexterityBonus, maxDexBonus.Value);
 64        }
 65
 666        return 10 + dexterityBonus;
 67    }
 68
 69    public static int GetMaxHealthFromConstitution(int constitution)
 70    {
 371        return 10 + (2 * Math.Max(0, constitution));
 72    }
 73
 74    public static int GetCarryCapacityFromStrength(int strength)
 75    {
 276        return 10 + Math.Max(0, strength - PlayerStatRules.MinimumScore);
 77    }
 78
 79    public static int GetMaxEncumbranceFromStrength(int strength)
 80    {
 6881        return GetHeavyLoadLimit(strength);
 82    }
 83
 84    public static EncumbranceTier GetEncumbranceTier(int strength, int carriedWeight)
 85    {
 1086        var lightLimit = GetLightLoadLimit(strength);
 1087        var mediumLimit = GetMediumLoadLimit(strength);
 88
 1089        if (carriedWeight <= lightLimit)
 90        {
 191            return EncumbranceTier.Light;
 92        }
 93
 994        if (carriedWeight <= mediumLimit)
 95        {
 596            return EncumbranceTier.Medium;
 97        }
 98
 499        return EncumbranceTier.Heavy;
 100    }
 101
 102    public static int GetEffectiveDexterityModifier(int dexterity, EncumbranceTier encumbranceTier)
 103    {
 4104        var dexterityModifier = GetAbilityModifier(dexterity);
 4105        var maxDexterityModifier = encumbranceTier switch
 4106        {
 1107            EncumbranceTier.Medium => 3,
 2108            EncumbranceTier.Heavy => 1,
 1109            _ => int.MaxValue
 4110        };
 111
 4112        return Math.Min(dexterityModifier, maxDexterityModifier);
 113    }
 114
 115    public static int GetEncumbranceAttackPenalty(EncumbranceTier encumbranceTier)
 116    {
 3117        return encumbranceTier switch
 3118        {
 1119            EncumbranceTier.Medium => 3,
 1120            EncumbranceTier.Heavy => 6,
 1121            _ => 0
 3122        };
 123    }
 124
 125    public static int GetCharismaModifier(int charisma)
 126    {
 16127        return GetAbilityModifier(charisma);
 128    }
 129
 130    public static Rarity GetMaxShopRarityFromChaBonus(int charismaModifier)
 131    {
 13132        return charismaModifier switch
 13133        {
 1134            >= 4 => Rarity.Legendary,
 1135            3 => Rarity.Epic,
 1136            2 => Rarity.Rare,
 9137            1 => Rarity.Uncommon,
 1138            _ => Rarity.Common
 13139        };
 140    }
 141
 142    public static int GetShopPrice(int basePrice, int charismaModifier, bool isBuying)
 143    {
 3144        var modifier = Math.Max(0, charismaModifier);
 3145        var multiplier = isBuying
 3146            ? 1m - (0.05m * modifier)
 3147            : 1m + (0.05m * modifier);
 148
 3149        return Math.Max(1, (int)Math.Round(basePrice * multiplier, MidpointRounding.AwayFromZero));
 150    }
 151
 152    public static bool ResolveAttackRoll(int roll, int attackBonus, int defense)
 153    {
 6154        if (roll == 1)
 155        {
 1156            return false;
 157        }
 158
 5159        if (roll == 20)
 160        {
 1161            return true;
 162        }
 163
 4164        return roll + attackBonus >= defense;
 165    }
 166
 167    public static DamageRange GetDamageRange(string weaponName, AttackMode mode)
 168    {
 17169        var normalizedWeapon = NormalizeItemName(weaponName);
 17170        var dieCount = GetDamageDieCount(mode);
 17171        var dieSize = GetDamageDieSize(normalizedWeapon);
 172
 17173        return new DamageRange(dieCount, dieCount * dieSize);
 174    }
 175
 176    public static int RollDamage(string weaponName, AttackMode mode, IRng rng)
 177    {
 2178        var dieCount = GetDamageDieCount(mode);
 2179        var dieSize = GetDamageDieSize(NormalizeItemName(weaponName));
 2180        var total = 0;
 181
 16182        for (var currentDie = 0; currentDie < dieCount; currentDie++)
 183        {
 6184            total += rng.Next(1, dieSize + 1);
 185        }
 186
 2187        return total;
 188    }
 189
 190    public static bool SupportsSingleShot(string weaponName)
 191    {
 10192        return NormalizeItemName(weaponName) switch
 10193        {
 1194            "pkp" => false,
 9195            _ => true
 10196        };
 197    }
 198
 199    public static bool SupportsSingleShot(Item? weapon)
 200    {
 3201        return SupportsSingleShot(GetItemLookupToken(weapon));
 202    }
 203
 204    public static bool SupportsBurstFire(string weaponName)
 205    {
 10206        return NormalizeItemName(weaponName) switch
 10207        {
 1208            "makarov" => true,
 1209            "ppsh" => true,
 4210            "ak74" => true,
 1211            "ak47" => true,
 1212            "svds" => true,
 1213            "pkp" => true,
 0214            "Rusty Knife" => false,
 1215            _ => false
 10216        };
 217    }
 218
 219    public static bool SupportsBurstFire(Item? weapon)
 220    {
 3221        return SupportsBurstFire(GetItemLookupToken(weapon));
 222    }
 223
 224    public static bool SupportsFullAuto(string weaponName)
 225    {
 10226        return NormalizeItemName(weaponName) switch
 10227        {
 1228            "rusty_knife" => false,
 1229            "makarov" => false,
 1230            "svds" => false,
 0231            "Rusty Knife" => false,
 7232            _ => true
 10233        };
 234    }
 235
 236    public static bool SupportsFullAuto(Item? weapon)
 237    {
 3238        return SupportsFullAuto(GetItemLookupToken(weapon));
 239    }
 240
 241    public static int GetBurstAttackPenalty(string weaponName)
 242    {
 7243        return NormalizeItemName(weaponName) switch
 7244        {
 1245            "makarov" => 3,
 1246            "ppsh" => 2,
 1247            "ak74" => 2,
 1248            "ak47" => 2,
 1249            "svds" => 2,
 1250            "pkp" => 2,
 1251            _ => 3
 7252        };
 253    }
 254
 255    public static int GetArmorReduction(string armorName)
 256    {
 11257        return NormalizeItemName(armorName) switch
 11258        {
 1259            "nfm_thor" => 6,
 1260            "6b43_zabralo_sh_body_armor" => 5,
 2261            "fort_defender_2" => 4,
 2262            "6b13_assault_armor" => 3,
 2263            "bnti_kirasa_n" => 2,
 2264            "6b2_body_armor" => 1,
 1265            _ => 0
 11266        };
 267    }
 268
 269    public static int ApplyArmorReduction(int incomingDamage, int armorReduction)
 270    {
 3271        var incoming = Math.Max(0, incomingDamage);
 3272        var reduced = incoming - Math.Max(0, armorReduction);
 3273        return Math.Max(1, reduced);
 274    }
 275
 276    public static int GetBuyPrice(string itemName)
 277    {
 54278        return NormalizeItemName(itemName) switch
 54279        {
 1280            "bandage" => 60,
 3281            "medkit" => 120,
 1282            "ammo_box" => 80,
 6283            "makarov" => 240,
 4284            "ppsh" => 650,
 5285            "ak74" => 1250,
 2286            "svds" => 2200,
 2287            "ak47" => 1500,
 2288            "pkp" => 3200,
 5289            "6b2_body_armor" => 380,
 3290            "bnti_kirasa_n" => 640,
 4291            "6b13_assault_armor" => 900,
 2292            "fort_defender_2" => 1500,
 2293            "6b43_zabralo_sh_body_armor" => 1800,
 2294            "nfm_thor" => 2600,
 3295            "small_backpack" => 100,
 3296            "large_backpack" => 200,
 0297            "tactical_backpack" => 300,
 2298            "tasmanian_tiger_trooper_35" => 1600,
 2299            "6sh118" => 2400,
 0300            _ => 100
 54301        };
 302    }
 303
 304    public static int GetBuyPrice(Item item)
 305    {
 24306        return GetBuyPrice(GetItemLookupToken(item));
 307    }
 308
 309    public static int GetMagazineCapacity(string weaponName)
 310    {
 29311        return NormalizeItemName(weaponName) switch
 29312        {
 2313            "rusty_knife" => 0,
 2314            "ppsh" => 35,
 16315            "ak74" => 30,
 2316            "svds" => 20,
 2317            "ak47" => 30,
 2318            "pkp" => 100,
 0319            "Rusty Knife" => 0,
 3320            _ => 8
 29321        };
 322    }
 323
 324    public static int GetMagazineCapacity(Item? weapon)
 325    {
 14326        return GetMagazineCapacity(GetItemLookupToken(weapon));
 327    }
 328
 329    public static bool WeaponUsesAmmo(string weaponName)
 330    {
 7331        return GetMagazineCapacity(weaponName) > 0;
 332    }
 333
 334    public static bool WeaponUsesAmmo(Item? weapon)
 335    {
 11336        return GetMagazineCapacity(weapon) > 0;
 337    }
 338
 339    public static int GetBackpackCapacity(string? backpackName)
 340    {
 17341        return NormalizeItemName(backpackName ?? string.Empty) switch
 17342        {
 2343            "6sh118" => 10,
 1344            "tasmanian_tiger_trooper_35" => 8,
 2345            "tactical_backpack" => 6,
 3346            "large_backpack" => 4,
 4347            "small_backpack" => 3,
 5348            _ => 2
 17349        };
 350    }
 351
 352    public static int GetBackpackCapacity(Item? backpack)
 353    {
 6354        if (backpack is null)
 355        {
 1356            return GetBackpackCapacity((string?)null);
 357        }
 358
 5359        return GetBackpackCapacity(GetItemLookupToken(backpack));
 360    }
 361
 362    public static bool IsMedkit(Item? item)
 363    {
 19364        if (item is null)
 365        {
 0366            return false;
 367        }
 368
 19369        if (item.ItemDefId > 0)
 370        {
 11371            return item.ItemDefId == 19;
 372        }
 373
 8374        return NormalizeItemName(GetItemLookupToken(item)) == "medkit";
 375    }
 376
 377    public static int GetTotalEncumbrance(IEnumerable<Item> items)
 378    {
 132379        var total = items.Sum(item => Math.Max(0, item.Weight));
 26380        return total;
 381    }
 382
 383    public static string NormalizeItemName(string itemName)
 384    {
 196385        if (ItemCatalog.TryGet(itemName, out var item) && item is not null)
 386        {
 185387            return item.Key;
 388        }
 389
 11390        var normalized = NormalizeWeaponName(itemName);
 11391        normalized = NormalizeArmorName(normalized);
 392
 11393        return normalized;
 394    }
 395
 396    private static string NormalizeWeaponName(string weaponName)
 397    {
 11398        return weaponName switch
 11399        {
 0400            "Makarov" => "makarov",
 0401            "PPSH" => "ppsh",
 0402            "AK74" => "ak74",
 0403            "SVDS" => "svds",
 0404            "AK47" => "ak47",
 0405            "PKP" => "pkp",
 1406            "Hunting Rifle" => "ak74",
 0407            "Rusty SMG" => "ppsh",
 0408            "Sawed Shotgun" => "ak47",
 0409            "Compact Carbine" => "ak74",
 10410            _ => weaponName
 11411        };
 412    }
 413
 414    private static string NormalizeArmorName(string armorName)
 415    {
 11416        return armorName switch
 11417        {
 0418            "6B2 body armor" => "6b2_body_armor",
 0419            "BNTI Kirasa-N" => "bnti_kirasa_n",
 0420            "6B13 assault armor" => "6b13_assault_armor",
 0421            "FORT Defender-2" => "fort_defender_2",
 0422            "6B43 Zabralo-Sh body armor" => "6b43_zabralo_sh_body_armor",
 0423            "NFM THOR" => "nfm_thor",
 0424            "Soft Vest" => "6b2_body_armor",
 0425            "Plate Carrier" => "6b13_assault_armor",
 11426            _ => armorName
 11427        };
 428    }
 429
 430    private static int GetLightLoadLimit(int strength)
 431    {
 10432        return GetHeavyLoadLimit(strength) / 3;
 433    }
 434
 435    private static int GetMediumLoadLimit(int strength)
 436    {
 10437        return (GetHeavyLoadLimit(strength) * 2) / 3;
 438    }
 439
 440    private static int GetHeavyLoadLimit(int strength)
 441    {
 92442        var normalizedStrength = Math.Max(1, strength);
 92443        if (normalizedStrength <= HeavyLoadByStrength.Length)
 444        {
 88445            return HeavyLoadByStrength[normalizedStrength - 1];
 446        }
 447
 4448        return GetHeavyLoadLimit(normalizedStrength - 10) * 4;
 449    }
 450
 451    private static int GetDamageDieCount(AttackMode mode)
 452    {
 19453        return mode switch
 19454        {
 9455            AttackMode.Standard => 2,
 5456            AttackMode.Burst => 3,
 5457            AttackMode.FullAuto => 4,
 0458            _ => 2
 19459        };
 460    }
 461
 462    private static int GetDamageDieSize(string weaponName)
 463    {
 19464        return NormalizeItemName(weaponName) switch
 19465        {
 4466            "ppsh" => 4,
 4467            "ak74" => 8,
 2468            "svds" => 12,
 4469            "ak47" => 10,
 2470            "pkp" => 12,
 3471            "makarov" => 6,
 0472            "rusty_knife" => 6,
 0473            "Rusty Knife" => 6,
 0474            _ => 6
 19475        };
 476    }
 477
 478    private static string GetItemLookupToken(Item? item)
 479    {
 60480        if (item is null)
 481        {
 0482            return string.Empty;
 483        }
 484
 60485        if (item.ItemDefId > 0 && ItemCatalog.TryGetByItemDefId(item.ItemDefId, out var authoredById) && authoredById is
 486        {
 50487            return authoredById.Key;
 488        }
 489
 10490        if (!string.IsNullOrWhiteSpace(item.Key) && ItemCatalog.TryGetByKey(item.Key, out var authoredByKey) && authored
 491        {
 0492            return authoredByKey.Key;
 493        }
 494
 10495        return item.Name;
 496    }
 497}
 498

Methods/Properties

.cctor()
GetAbilityModifier(System.Int32)
GetRangedAttackBonusFromDexterity(System.Int32)
GetDefenseFromDexterity(System.Int32,System.Nullable`1<System.Int32>)
GetMaxHealthFromConstitution(System.Int32)
GetCarryCapacityFromStrength(System.Int32)
GetMaxEncumbranceFromStrength(System.Int32)
GetEncumbranceTier(System.Int32,System.Int32)
GetEffectiveDexterityModifier(System.Int32,RaidLoop.Core.EncumbranceTier)
GetEncumbranceAttackPenalty(RaidLoop.Core.EncumbranceTier)
GetCharismaModifier(System.Int32)
GetMaxShopRarityFromChaBonus(System.Int32)
GetShopPrice(System.Int32,System.Int32,System.Boolean)
ResolveAttackRoll(System.Int32,System.Int32,System.Int32)
GetDamageRange(System.String,RaidLoop.Core.AttackMode)
RollDamage(System.String,RaidLoop.Core.AttackMode,RaidLoop.Core.IRng)
SupportsSingleShot(System.String)
SupportsSingleShot(RaidLoop.Core.Item)
SupportsBurstFire(System.String)
SupportsBurstFire(RaidLoop.Core.Item)
SupportsFullAuto(System.String)
SupportsFullAuto(RaidLoop.Core.Item)
GetBurstAttackPenalty(System.String)
GetArmorReduction(System.String)
ApplyArmorReduction(System.Int32,System.Int32)
GetBuyPrice(System.String)
GetBuyPrice(RaidLoop.Core.Item)
GetMagazineCapacity(System.String)
GetMagazineCapacity(RaidLoop.Core.Item)
WeaponUsesAmmo(System.String)
WeaponUsesAmmo(RaidLoop.Core.Item)
GetBackpackCapacity(System.String)
GetBackpackCapacity(RaidLoop.Core.Item)
IsMedkit(RaidLoop.Core.Item)
GetTotalEncumbrance(System.Collections.Generic.IEnumerable`1<RaidLoop.Core.Item>)
NormalizeItemName(System.String)
NormalizeWeaponName(System.String)
NormalizeArmorName(System.String)
GetLightLoadLimit(System.Int32)
GetMediumLoadLimit(System.Int32)
GetHeavyLoadLimit(System.Int32)
GetDamageDieCount(RaidLoop.Core.AttackMode)
GetDamageDieSize(System.String)
GetItemLookupToken(RaidLoop.Core.Item)