< Summary

Information
Class: RaidLoop.Core.LootTable
Assembly: RaidLoop.Core
File(s): /home/runner/work/RaidLoop/RaidLoop/src/RaidLoop.Core/LootTable.cs
Line coverage
83%
Covered lines: 64
Uncovered lines: 13
Coverable lines: 77
Total lines: 165
Line coverage: 83.1%
Branch coverage
68%
Covered branches: 33
Total branches: 48
Branch coverage: 68.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%4471.42%
.ctor(...)100%11100%
Draw(...)100%11100%
Draw(...)81.81%302274.07%
DrawByTier(...)100%1010100%
ShiftTier(...)100%11100%
ResolveAvailableTier(...)25%391242.85%

File(s)

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

#LineLine coverage
 1namespace RaidLoop.Core;
 2
 3public sealed class LootTable
 4{
 5    private readonly (Item Item, int Weight)[] _entries;
 6    private readonly LootTierProfile? _tierProfile;
 7    private readonly Dictionary<Rarity, List<Item>>? _itemsByTier;
 8
 49    public LootTable(IReadOnlyList<(Item Item, int Weight)> entries)
 10    {
 411        if (entries is null)
 12        {
 013            throw new ArgumentNullException(nameof(entries));
 14        }
 15
 1216        if (entries.Any(entry => entry.Weight <= 0))
 17        {
 018            throw new ArgumentOutOfRangeException(nameof(entries), "All weights must be positive.");
 19        }
 20
 421        _entries = entries.ToArray();
 422    }
 23
 1524    public LootTable(LootTierProfile tierProfile, IReadOnlyList<Item> items)
 25    {
 1526        ArgumentNullException.ThrowIfNull(tierProfile);
 1527        ArgumentNullException.ThrowIfNull(items);
 28
 1529        _entries = [];
 1530        _tierProfile = tierProfile;
 1531        _itemsByTier = items
 11332            .GroupBy(item => item.Rarity)
 14133            .ToDictionary(group => group.Key, group => group.ToList());
 1534    }
 35
 36    public List<Item> Draw(IRng rng, int count)
 37    {
 1001438        return Draw(rng, count, null);
 39    }
 40
 41    public List<Item> Draw(IRng rng, int count, LootBooster? booster)
 42    {
 1001543        if (rng is null)
 44        {
 045            throw new ArgumentNullException(nameof(rng));
 46        }
 47
 1001548        if (count <= 0)
 49        {
 150            return [];
 51        }
 52
 1001453        if (_tierProfile is not null && _itemsByTier is not null)
 54        {
 1001155            return DrawByTier(rng, count, booster);
 56        }
 57
 358        if (_entries.Length == 0)
 59        {
 060            return [];
 61        }
 62
 363        var working = _entries.ToList();
 364        var drawn = new List<Item>(Math.Min(count, working.Count));
 65
 966        while (working.Count > 0 && drawn.Count < count)
 67        {
 1768            var totalWeight = working.Sum(entry => entry.Weight);
 669            var roll = rng.Next(0, totalWeight);
 670            var cumulative = 0;
 71
 1272            for (var i = 0; i < working.Count; i++)
 73            {
 674                cumulative += working[i].Weight;
 675                if (roll >= cumulative)
 76                {
 77                    continue;
 78                }
 79
 680                drawn.Add(working[i].Item);
 681                working.RemoveAt(i);
 682                break;
 83            }
 84        }
 85
 386        if (drawn.Count > 0 && !string.IsNullOrWhiteSpace(GameEventLog.CurrentRaidId))
 87        {
 088            GameEventLog.Append(new GameEvent(
 089                "loot.drawn",
 090                GameEventLog.CurrentRaidId,
 091                GameEventLog.CreateItemSnapshots(drawn),
 092                DateTimeOffset.UtcNow));
 93        }
 94
 395        return drawn;
 96    }
 97
 98    private List<Item> DrawByTier(IRng rng, int count, LootBooster? booster)
 99    {
 10011100        var working = _itemsByTier!
 110097101            .ToDictionary(entry => entry.Key, entry => entry.Value.ToList());
 60054102        var totalItemCount = working.Sum(entry => entry.Value.Count);
 10011103        var drawn = new List<Item>(Math.Min(count, totalItemCount));
 104
 20026105        while (drawn.Count < count)
 106        {
 10015107            var availableTiers = working
 50063108                .Where(entry => entry.Value.Count > 0)
 50061109                .Select(entry => entry.Key)
 10015110                .ToArray();
 111
 10015112            if (availableTiers.Length == 0)
 113            {
 114                break;
 115            }
 116
 10015117            var rolledTier = _tierProfile!.Roll(rng, availableTiers);
 10015118            var shiftedTier = ShiftTier(rolledTier, booster?.TierShift ?? 0);
 10015119            var resolvedTier = ResolveAvailableTier(shiftedTier, working);
 10015120            var tierItems = working[resolvedTier];
 10015121            var index = rng.Next(0, tierItems.Count);
 122
 10015123            drawn.Add(tierItems[index]);
 10015124            tierItems.RemoveAt(index);
 125        }
 126
 10011127        if (drawn.Count > 0 && !string.IsNullOrWhiteSpace(GameEventLog.CurrentRaidId))
 128        {
 10008129            GameEventLog.Append(new GameEvent(
 10008130                "loot.drawn",
 10008131                GameEventLog.CurrentRaidId,
 10008132                GameEventLog.CreateItemSnapshots(drawn),
 10008133                DateTimeOffset.UtcNow));
 134        }
 135
 10011136        return drawn;
 137    }
 138
 139    private static Rarity ShiftTier(Rarity tier, int shift)
 140    {
 10015141        var shifted = Math.Clamp((int)tier + shift, (int)Rarity.Common, (int)Rarity.Legendary);
 10015142        return (Rarity)shifted;
 143    }
 144
 145    private static Rarity ResolveAvailableTier(Rarity preferredTier, IReadOnlyDictionary<Rarity, List<Item>> working)
 146    {
 20030147        for (var tier = (int)preferredTier; tier >= (int)Rarity.Common; tier--)
 148        {
 10015149            if (working.TryGetValue((Rarity)tier, out var items) && items.Count > 0)
 150            {
 10015151                return (Rarity)tier;
 152            }
 153        }
 154
 0155        for (var tier = (int)preferredTier + 1; tier <= (int)Rarity.Legendary; tier++)
 156        {
 0157            if (working.TryGetValue((Rarity)tier, out var items) && items.Count > 0)
 158            {
 0159                return (Rarity)tier;
 160            }
 161        }
 162
 0163        throw new InvalidOperationException("No items remain in any loot tier.");
 164    }
 165}