< Summary

Information
Class: RaidLoop.Core.DamageRange
Assembly: RaidLoop.Core
File(s): /home/runner/work/RaidLoop/RaidLoop/src/RaidLoop.Core/CombatBalance.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 498
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Min()100%11100%

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
 3430public 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{
 41    private static readonly int[] HeavyLoadByStrength =
 42    [
 43        10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
 44        115, 130, 150, 175, 200, 230, 260, 300, 350, 400,
 45        460, 520, 600, 700, 800, 920, 1040, 1200, 1400
 46    ];
 47
 48    public static int GetAbilityModifier(int score)
 49    {
 50        return PlayerStatRules.GetAbilityModifier(score);
 51    }
 52
 53    public static int GetRangedAttackBonusFromDexterity(int dexterity)
 54    {
 55        return GetAbilityModifier(dexterity);
 56    }
 57
 58    public static int GetDefenseFromDexterity(int dexterity, int? maxDexBonus = null)
 59    {
 60        var dexterityBonus = GetAbilityModifier(dexterity);
 61        if (maxDexBonus.HasValue)
 62        {
 63            dexterityBonus = Math.Min(dexterityBonus, maxDexBonus.Value);
 64        }
 65
 66        return 10 + dexterityBonus;
 67    }
 68
 69    public static int GetMaxHealthFromConstitution(int constitution)
 70    {
 71        return 10 + (2 * Math.Max(0, constitution));
 72    }
 73
 74    public static int GetCarryCapacityFromStrength(int strength)
 75    {
 76        return 10 + Math.Max(0, strength - PlayerStatRules.MinimumScore);
 77    }
 78
 79    public static int GetMaxEncumbranceFromStrength(int strength)
 80    {
 81        return GetHeavyLoadLimit(strength);
 82    }
 83
 84    public static EncumbranceTier GetEncumbranceTier(int strength, int carriedWeight)
 85    {
 86        var lightLimit = GetLightLoadLimit(strength);
 87        var mediumLimit = GetMediumLoadLimit(strength);
 88
 89        if (carriedWeight <= lightLimit)
 90        {
 91            return EncumbranceTier.Light;
 92        }
 93
 94        if (carriedWeight <= mediumLimit)
 95        {
 96            return EncumbranceTier.Medium;
 97        }
 98
 99        return EncumbranceTier.Heavy;
 100    }
 101
 102    public static int GetEffectiveDexterityModifier(int dexterity, EncumbranceTier encumbranceTier)
 103    {
 104        var dexterityModifier = GetAbilityModifier(dexterity);
 105        var maxDexterityModifier = encumbranceTier switch
 106        {
 107            EncumbranceTier.Medium => 3,
 108            EncumbranceTier.Heavy => 1,
 109            _ => int.MaxValue
 110        };
 111
 112        return Math.Min(dexterityModifier, maxDexterityModifier);
 113    }
 114
 115    public static int GetEncumbranceAttackPenalty(EncumbranceTier encumbranceTier)
 116    {
 117        return encumbranceTier switch
 118        {
 119            EncumbranceTier.Medium => 3,
 120            EncumbranceTier.Heavy => 6,
 121            _ => 0
 122        };
 123    }
 124
 125    public static int GetCharismaModifier(int charisma)
 126    {
 127        return GetAbilityModifier(charisma);
 128    }
 129
 130    public static Rarity GetMaxShopRarityFromChaBonus(int charismaModifier)
 131    {
 132        return charismaModifier switch
 133        {
 134            >= 4 => Rarity.Legendary,
 135            3 => Rarity.Epic,
 136            2 => Rarity.Rare,
 137            1 => Rarity.Uncommon,
 138            _ => Rarity.Common
 139        };
 140    }
 141
 142    public static int GetShopPrice(int basePrice, int charismaModifier, bool isBuying)
 143    {
 144        var modifier = Math.Max(0, charismaModifier);
 145        var multiplier = isBuying
 146            ? 1m - (0.05m * modifier)
 147            : 1m + (0.05m * modifier);
 148
 149        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    {
 154        if (roll == 1)
 155        {
 156            return false;
 157        }
 158
 159        if (roll == 20)
 160        {
 161            return true;
 162        }
 163
 164        return roll + attackBonus >= defense;
 165    }
 166
 167    public static DamageRange GetDamageRange(string weaponName, AttackMode mode)
 168    {
 169        var normalizedWeapon = NormalizeItemName(weaponName);
 170        var dieCount = GetDamageDieCount(mode);
 171        var dieSize = GetDamageDieSize(normalizedWeapon);
 172
 173        return new DamageRange(dieCount, dieCount * dieSize);
 174    }
 175
 176    public static int RollDamage(string weaponName, AttackMode mode, IRng rng)
 177    {
 178        var dieCount = GetDamageDieCount(mode);
 179        var dieSize = GetDamageDieSize(NormalizeItemName(weaponName));
 180        var total = 0;
 181
 182        for (var currentDie = 0; currentDie < dieCount; currentDie++)
 183        {
 184            total += rng.Next(1, dieSize + 1);
 185        }
 186
 187        return total;
 188    }
 189
 190    public static bool SupportsSingleShot(string weaponName)
 191    {
 192        return NormalizeItemName(weaponName) switch
 193        {
 194            "pkp" => false,
 195            _ => true
 196        };
 197    }
 198
 199    public static bool SupportsSingleShot(Item? weapon)
 200    {
 201        return SupportsSingleShot(GetItemLookupToken(weapon));
 202    }
 203
 204    public static bool SupportsBurstFire(string weaponName)
 205    {
 206        return NormalizeItemName(weaponName) switch
 207        {
 208            "makarov" => true,
 209            "ppsh" => true,
 210            "ak74" => true,
 211            "ak47" => true,
 212            "svds" => true,
 213            "pkp" => true,
 214            "Rusty Knife" => false,
 215            _ => false
 216        };
 217    }
 218
 219    public static bool SupportsBurstFire(Item? weapon)
 220    {
 221        return SupportsBurstFire(GetItemLookupToken(weapon));
 222    }
 223
 224    public static bool SupportsFullAuto(string weaponName)
 225    {
 226        return NormalizeItemName(weaponName) switch
 227        {
 228            "rusty_knife" => false,
 229            "makarov" => false,
 230            "svds" => false,
 231            "Rusty Knife" => false,
 232            _ => true
 233        };
 234    }
 235
 236    public static bool SupportsFullAuto(Item? weapon)
 237    {
 238        return SupportsFullAuto(GetItemLookupToken(weapon));
 239    }
 240
 241    public static int GetBurstAttackPenalty(string weaponName)
 242    {
 243        return NormalizeItemName(weaponName) switch
 244        {
 245            "makarov" => 3,
 246            "ppsh" => 2,
 247            "ak74" => 2,
 248            "ak47" => 2,
 249            "svds" => 2,
 250            "pkp" => 2,
 251            _ => 3
 252        };
 253    }
 254
 255    public static int GetArmorReduction(string armorName)
 256    {
 257        return NormalizeItemName(armorName) switch
 258        {
 259            "nfm_thor" => 6,
 260            "6b43_zabralo_sh_body_armor" => 5,
 261            "fort_defender_2" => 4,
 262            "6b13_assault_armor" => 3,
 263            "bnti_kirasa_n" => 2,
 264            "6b2_body_armor" => 1,
 265            _ => 0
 266        };
 267    }
 268
 269    public static int ApplyArmorReduction(int incomingDamage, int armorReduction)
 270    {
 271        var incoming = Math.Max(0, incomingDamage);
 272        var reduced = incoming - Math.Max(0, armorReduction);
 273        return Math.Max(1, reduced);
 274    }
 275
 276    public static int GetBuyPrice(string itemName)
 277    {
 278        return NormalizeItemName(itemName) switch
 279        {
 280            "bandage" => 60,
 281            "medkit" => 120,
 282            "ammo_box" => 80,
 283            "makarov" => 240,
 284            "ppsh" => 650,
 285            "ak74" => 1250,
 286            "svds" => 2200,
 287            "ak47" => 1500,
 288            "pkp" => 3200,
 289            "6b2_body_armor" => 380,
 290            "bnti_kirasa_n" => 640,
 291            "6b13_assault_armor" => 900,
 292            "fort_defender_2" => 1500,
 293            "6b43_zabralo_sh_body_armor" => 1800,
 294            "nfm_thor" => 2600,
 295            "small_backpack" => 100,
 296            "large_backpack" => 200,
 297            "tactical_backpack" => 300,
 298            "tasmanian_tiger_trooper_35" => 1600,
 299            "6sh118" => 2400,
 300            _ => 100
 301        };
 302    }
 303
 304    public static int GetBuyPrice(Item item)
 305    {
 306        return GetBuyPrice(GetItemLookupToken(item));
 307    }
 308
 309    public static int GetMagazineCapacity(string weaponName)
 310    {
 311        return NormalizeItemName(weaponName) switch
 312        {
 313            "rusty_knife" => 0,
 314            "ppsh" => 35,
 315            "ak74" => 30,
 316            "svds" => 20,
 317            "ak47" => 30,
 318            "pkp" => 100,
 319            "Rusty Knife" => 0,
 320            _ => 8
 321        };
 322    }
 323
 324    public static int GetMagazineCapacity(Item? weapon)
 325    {
 326        return GetMagazineCapacity(GetItemLookupToken(weapon));
 327    }
 328
 329    public static bool WeaponUsesAmmo(string weaponName)
 330    {
 331        return GetMagazineCapacity(weaponName) > 0;
 332    }
 333
 334    public static bool WeaponUsesAmmo(Item? weapon)
 335    {
 336        return GetMagazineCapacity(weapon) > 0;
 337    }
 338
 339    public static int GetBackpackCapacity(string? backpackName)
 340    {
 341        return NormalizeItemName(backpackName ?? string.Empty) switch
 342        {
 343            "6sh118" => 10,
 344            "tasmanian_tiger_trooper_35" => 8,
 345            "tactical_backpack" => 6,
 346            "large_backpack" => 4,
 347            "small_backpack" => 3,
 348            _ => 2
 349        };
 350    }
 351
 352    public static int GetBackpackCapacity(Item? backpack)
 353    {
 354        if (backpack is null)
 355        {
 356            return GetBackpackCapacity((string?)null);
 357        }
 358
 359        return GetBackpackCapacity(GetItemLookupToken(backpack));
 360    }
 361
 362    public static bool IsMedkit(Item? item)
 363    {
 364        if (item is null)
 365        {
 366            return false;
 367        }
 368
 369        if (item.ItemDefId > 0)
 370        {
 371            return item.ItemDefId == 19;
 372        }
 373
 374        return NormalizeItemName(GetItemLookupToken(item)) == "medkit";
 375    }
 376
 377    public static int GetTotalEncumbrance(IEnumerable<Item> items)
 378    {
 379        var total = items.Sum(item => Math.Max(0, item.Weight));
 380        return total;
 381    }
 382
 383    public static string NormalizeItemName(string itemName)
 384    {
 385        if (ItemCatalog.TryGet(itemName, out var item) && item is not null)
 386        {
 387            return item.Key;
 388        }
 389
 390        var normalized = NormalizeWeaponName(itemName);
 391        normalized = NormalizeArmorName(normalized);
 392
 393        return normalized;
 394    }
 395
 396    private static string NormalizeWeaponName(string weaponName)
 397    {
 398        return weaponName switch
 399        {
 400            "Makarov" => "makarov",
 401            "PPSH" => "ppsh",
 402            "AK74" => "ak74",
 403            "SVDS" => "svds",
 404            "AK47" => "ak47",
 405            "PKP" => "pkp",
 406            "Hunting Rifle" => "ak74",
 407            "Rusty SMG" => "ppsh",
 408            "Sawed Shotgun" => "ak47",
 409            "Compact Carbine" => "ak74",
 410            _ => weaponName
 411        };
 412    }
 413
 414    private static string NormalizeArmorName(string armorName)
 415    {
 416        return armorName switch
 417        {
 418            "6B2 body armor" => "6b2_body_armor",
 419            "BNTI Kirasa-N" => "bnti_kirasa_n",
 420            "6B13 assault armor" => "6b13_assault_armor",
 421            "FORT Defender-2" => "fort_defender_2",
 422            "6B43 Zabralo-Sh body armor" => "6b43_zabralo_sh_body_armor",
 423            "NFM THOR" => "nfm_thor",
 424            "Soft Vest" => "6b2_body_armor",
 425            "Plate Carrier" => "6b13_assault_armor",
 426            _ => armorName
 427        };
 428    }
 429
 430    private static int GetLightLoadLimit(int strength)
 431    {
 432        return GetHeavyLoadLimit(strength) / 3;
 433    }
 434
 435    private static int GetMediumLoadLimit(int strength)
 436    {
 437        return (GetHeavyLoadLimit(strength) * 2) / 3;
 438    }
 439
 440    private static int GetHeavyLoadLimit(int strength)
 441    {
 442        var normalizedStrength = Math.Max(1, strength);
 443        if (normalizedStrength <= HeavyLoadByStrength.Length)
 444        {
 445            return HeavyLoadByStrength[normalizedStrength - 1];
 446        }
 447
 448        return GetHeavyLoadLimit(normalizedStrength - 10) * 4;
 449    }
 450
 451    private static int GetDamageDieCount(AttackMode mode)
 452    {
 453        return mode switch
 454        {
 455            AttackMode.Standard => 2,
 456            AttackMode.Burst => 3,
 457            AttackMode.FullAuto => 4,
 458            _ => 2
 459        };
 460    }
 461
 462    private static int GetDamageDieSize(string weaponName)
 463    {
 464        return NormalizeItemName(weaponName) switch
 465        {
 466            "ppsh" => 4,
 467            "ak74" => 8,
 468            "svds" => 12,
 469            "ak47" => 10,
 470            "pkp" => 12,
 471            "makarov" => 6,
 472            "rusty_knife" => 6,
 473            "Rusty Knife" => 6,
 474            _ => 6
 475        };
 476    }
 477
 478    private static string GetItemLookupToken(Item? item)
 479    {
 480        if (item is null)
 481        {
 482            return string.Empty;
 483        }
 484
 485        if (item.ItemDefId > 0 && ItemCatalog.TryGetByItemDefId(item.ItemDefId, out var authoredById) && authoredById is
 486        {
 487            return authoredById.Key;
 488        }
 489
 490        if (!string.IsNullOrWhiteSpace(item.Key) && ItemCatalog.TryGetByKey(item.Key, out var authoredByKey) && authored
 491        {
 492            return authoredByKey.Key;
 493        }
 494
 495        return item.Name;
 496    }
 497}
 498

Methods/Properties

get_Min()