< Summary

Information
Class: RaidLoop.Core.CombatBalance
Assembly: RaidLoop.Core
File(s): /home/runner/work/RaidLoop/RaidLoop/src/RaidLoop.Core/CombatBalance.cs
Line coverage
81%
Covered lines: 209
Uncovered lines: 48
Coverable lines: 257
Total lines: 522
Line coverage: 81.3%
Branch coverage
63%
Covered branches: 284
Total branches: 444
Branch coverage: 63.9%
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(...)8.19%14506128%
NormalizeWeaponName(...)54.16%8884828.57%
NormalizeArmorName(...)61.11%4203633.33%
GetLightLoadLimit(...)100%11100%
GetMediumLoadLimit(...)100%11100%
GetHeavyLoadLimit(...)100%22100%
GetDamageDieCount(...)75%4485.71%
GetDamageDieSize(...)45%654075%
GetItemLookupToken(...)80%121075%

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    {
 7281        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    {
 31364        if (item is null)
 365        {
 0366            return false;
 367        }
 368
 31369        if (item.ItemDefId > 0)
 370        {
 20371            return item.ItemDefId == 19;
 372        }
 373
 11374        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    {
 203385        if (ItemCatalog.TryGet(itemName, out var item) && item is not null)
 386        {
 193387            var lookupToken = ItemCatalog.GetLookupToken(item.ItemDefId);
 193388            if (!string.IsNullOrWhiteSpace(lookupToken))
 389            {
 193390                return lookupToken;
 391            }
 392
 0393            var canonicalName = NormalizeWeaponName(item.Name);
 0394            canonicalName = NormalizeArmorName(canonicalName);
 0395            return canonicalName switch
 0396            {
 0397                "Rusty Knife" => "rusty_knife",
 0398                "Small Backpack" => "small_backpack",
 0399                "Large Backpack" => "large_backpack",
 0400                "Tactical Backpack" => "tactical_backpack",
 0401                "Tasmanian Tiger Trooper 35" => "tasmanian_tiger_trooper_35",
 0402                "6Sh118" => "6sh118",
 0403                "Medkit" => "medkit",
 0404                "Bandage" => "bandage",
 0405                "Ammo Box" => "ammo_box",
 0406                "Scrap Metal" => "scrap_metal",
 0407                "Rare Scope" => "rare_scope",
 0408                "Legendary Trigger Group" => "legendary_trigger_group",
 0409                _ => canonicalName
 0410            };
 411        }
 412
 10413        var normalized = NormalizeWeaponName(itemName);
 10414        normalized = NormalizeArmorName(normalized);
 415
 10416        return normalized;
 417    }
 418
 419    private static string NormalizeWeaponName(string weaponName)
 420    {
 10421        return weaponName switch
 10422        {
 0423            "Makarov" => "makarov",
 0424            "PPSH" => "ppsh",
 0425            "AK74" => "ak74",
 0426            "SVDS" => "svds",
 0427            "AK47" => "ak47",
 0428            "PKP" => "pkp",
 0429            "Hunting Rifle" => "ak74",
 0430            "Rusty SMG" => "ppsh",
 0431            "Sawed Shotgun" => "ak47",
 0432            "Compact Carbine" => "ak74",
 10433            _ => weaponName
 10434        };
 435    }
 436
 437    private static string NormalizeArmorName(string armorName)
 438    {
 10439        return armorName switch
 10440        {
 0441            "6B2 body armor" => "6b2_body_armor",
 0442            "BNTI Kirasa-N" => "bnti_kirasa_n",
 0443            "6B13 assault armor" => "6b13_assault_armor",
 0444            "FORT Defender-2" => "fort_defender_2",
 0445            "6B43 Zabralo-Sh body armor" => "6b43_zabralo_sh_body_armor",
 0446            "NFM THOR" => "nfm_thor",
 0447            "Soft Vest" => "6b2_body_armor",
 0448            "Plate Carrier" => "6b13_assault_armor",
 10449            _ => armorName
 10450        };
 451    }
 452
 453    private static int GetLightLoadLimit(int strength)
 454    {
 10455        return GetHeavyLoadLimit(strength) / 3;
 456    }
 457
 458    private static int GetMediumLoadLimit(int strength)
 459    {
 10460        return (GetHeavyLoadLimit(strength) * 2) / 3;
 461    }
 462
 463    private static int GetHeavyLoadLimit(int strength)
 464    {
 96465        var normalizedStrength = Math.Max(1, strength);
 96466        if (normalizedStrength <= HeavyLoadByStrength.Length)
 467        {
 92468            return HeavyLoadByStrength[normalizedStrength - 1];
 469        }
 470
 4471        return GetHeavyLoadLimit(normalizedStrength - 10) * 4;
 472    }
 473
 474    private static int GetDamageDieCount(AttackMode mode)
 475    {
 19476        return mode switch
 19477        {
 9478            AttackMode.Standard => 2,
 5479            AttackMode.Burst => 3,
 5480            AttackMode.FullAuto => 4,
 0481            _ => 2
 19482        };
 483    }
 484
 485    private static int GetDamageDieSize(string weaponName)
 486    {
 19487        return NormalizeItemName(weaponName) switch
 19488        {
 4489            "ppsh" => 4,
 4490            "ak74" => 8,
 2491            "svds" => 12,
 4492            "ak47" => 10,
 2493            "pkp" => 12,
 3494            "makarov" => 6,
 0495            "rusty_knife" => 6,
 0496            "Rusty Knife" => 6,
 0497            _ => 6
 19498        };
 499    }
 500
 501    private static string GetItemLookupToken(Item? item)
 502    {
 63503        if (item is null)
 504        {
 0505            return string.Empty;
 506        }
 507
 63508        if (item.ItemDefId > 0 && ItemCatalog.TryGetByItemDefId(item.ItemDefId, out var authoredById) && authoredById is
 509        {
 50510            var lookupToken = ItemCatalog.GetLookupToken(authoredById.ItemDefId);
 50511            if (!string.IsNullOrWhiteSpace(lookupToken))
 512            {
 50513                return lookupToken;
 514            }
 515
 0516            return authoredById.Name;
 517        }
 518
 13519        return item.Name;
 520    }
 521}
 522

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)