| | | 1 | | namespace RaidLoop.Core; |
| | | 2 | | |
| | | 3 | | public 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 | | |
| | 4 | 9 | | public LootTable(IReadOnlyList<(Item Item, int Weight)> entries) |
| | | 10 | | { |
| | 4 | 11 | | if (entries is null) |
| | | 12 | | { |
| | 0 | 13 | | throw new ArgumentNullException(nameof(entries)); |
| | | 14 | | } |
| | | 15 | | |
| | 12 | 16 | | if (entries.Any(entry => entry.Weight <= 0)) |
| | | 17 | | { |
| | 0 | 18 | | throw new ArgumentOutOfRangeException(nameof(entries), "All weights must be positive."); |
| | | 19 | | } |
| | | 20 | | |
| | 4 | 21 | | _entries = entries.ToArray(); |
| | 4 | 22 | | } |
| | | 23 | | |
| | 15 | 24 | | public LootTable(LootTierProfile tierProfile, IReadOnlyList<Item> items) |
| | | 25 | | { |
| | 15 | 26 | | ArgumentNullException.ThrowIfNull(tierProfile); |
| | 15 | 27 | | ArgumentNullException.ThrowIfNull(items); |
| | | 28 | | |
| | 15 | 29 | | _entries = []; |
| | 15 | 30 | | _tierProfile = tierProfile; |
| | 15 | 31 | | _itemsByTier = items |
| | 113 | 32 | | .GroupBy(item => item.Rarity) |
| | 141 | 33 | | .ToDictionary(group => group.Key, group => group.ToList()); |
| | 15 | 34 | | } |
| | | 35 | | |
| | | 36 | | public List<Item> Draw(IRng rng, int count) |
| | | 37 | | { |
| | 10014 | 38 | | return Draw(rng, count, null); |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | public List<Item> Draw(IRng rng, int count, LootBooster? booster) |
| | | 42 | | { |
| | 10015 | 43 | | if (rng is null) |
| | | 44 | | { |
| | 0 | 45 | | throw new ArgumentNullException(nameof(rng)); |
| | | 46 | | } |
| | | 47 | | |
| | 10015 | 48 | | if (count <= 0) |
| | | 49 | | { |
| | 1 | 50 | | return []; |
| | | 51 | | } |
| | | 52 | | |
| | 10014 | 53 | | if (_tierProfile is not null && _itemsByTier is not null) |
| | | 54 | | { |
| | 10011 | 55 | | return DrawByTier(rng, count, booster); |
| | | 56 | | } |
| | | 57 | | |
| | 3 | 58 | | if (_entries.Length == 0) |
| | | 59 | | { |
| | 0 | 60 | | return []; |
| | | 61 | | } |
| | | 62 | | |
| | 3 | 63 | | var working = _entries.ToList(); |
| | 3 | 64 | | var drawn = new List<Item>(Math.Min(count, working.Count)); |
| | | 65 | | |
| | 9 | 66 | | while (working.Count > 0 && drawn.Count < count) |
| | | 67 | | { |
| | 17 | 68 | | var totalWeight = working.Sum(entry => entry.Weight); |
| | 6 | 69 | | var roll = rng.Next(0, totalWeight); |
| | 6 | 70 | | var cumulative = 0; |
| | | 71 | | |
| | 12 | 72 | | for (var i = 0; i < working.Count; i++) |
| | | 73 | | { |
| | 6 | 74 | | cumulative += working[i].Weight; |
| | 6 | 75 | | if (roll >= cumulative) |
| | | 76 | | { |
| | | 77 | | continue; |
| | | 78 | | } |
| | | 79 | | |
| | 6 | 80 | | drawn.Add(working[i].Item); |
| | 6 | 81 | | working.RemoveAt(i); |
| | 6 | 82 | | break; |
| | | 83 | | } |
| | | 84 | | } |
| | | 85 | | |
| | 3 | 86 | | if (drawn.Count > 0 && !string.IsNullOrWhiteSpace(GameEventLog.CurrentRaidId)) |
| | | 87 | | { |
| | 0 | 88 | | GameEventLog.Append(new GameEvent( |
| | 0 | 89 | | "loot.drawn", |
| | 0 | 90 | | GameEventLog.CurrentRaidId, |
| | 0 | 91 | | GameEventLog.CreateItemSnapshots(drawn), |
| | 0 | 92 | | DateTimeOffset.UtcNow)); |
| | | 93 | | } |
| | | 94 | | |
| | 3 | 95 | | return drawn; |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | private List<Item> DrawByTier(IRng rng, int count, LootBooster? booster) |
| | | 99 | | { |
| | 10011 | 100 | | var working = _itemsByTier! |
| | 110097 | 101 | | .ToDictionary(entry => entry.Key, entry => entry.Value.ToList()); |
| | 60054 | 102 | | var totalItemCount = working.Sum(entry => entry.Value.Count); |
| | 10011 | 103 | | var drawn = new List<Item>(Math.Min(count, totalItemCount)); |
| | | 104 | | |
| | 20026 | 105 | | while (drawn.Count < count) |
| | | 106 | | { |
| | 10015 | 107 | | var availableTiers = working |
| | 50063 | 108 | | .Where(entry => entry.Value.Count > 0) |
| | 50061 | 109 | | .Select(entry => entry.Key) |
| | 10015 | 110 | | .ToArray(); |
| | | 111 | | |
| | 10015 | 112 | | if (availableTiers.Length == 0) |
| | | 113 | | { |
| | | 114 | | break; |
| | | 115 | | } |
| | | 116 | | |
| | 10015 | 117 | | var rolledTier = _tierProfile!.Roll(rng, availableTiers); |
| | 10015 | 118 | | var shiftedTier = ShiftTier(rolledTier, booster?.TierShift ?? 0); |
| | 10015 | 119 | | var resolvedTier = ResolveAvailableTier(shiftedTier, working); |
| | 10015 | 120 | | var tierItems = working[resolvedTier]; |
| | 10015 | 121 | | var index = rng.Next(0, tierItems.Count); |
| | | 122 | | |
| | 10015 | 123 | | drawn.Add(tierItems[index]); |
| | 10015 | 124 | | tierItems.RemoveAt(index); |
| | | 125 | | } |
| | | 126 | | |
| | 10011 | 127 | | if (drawn.Count > 0 && !string.IsNullOrWhiteSpace(GameEventLog.CurrentRaidId)) |
| | | 128 | | { |
| | 10008 | 129 | | GameEventLog.Append(new GameEvent( |
| | 10008 | 130 | | "loot.drawn", |
| | 10008 | 131 | | GameEventLog.CurrentRaidId, |
| | 10008 | 132 | | GameEventLog.CreateItemSnapshots(drawn), |
| | 10008 | 133 | | DateTimeOffset.UtcNow)); |
| | | 134 | | } |
| | | 135 | | |
| | 10011 | 136 | | return drawn; |
| | | 137 | | } |
| | | 138 | | |
| | | 139 | | private static Rarity ShiftTier(Rarity tier, int shift) |
| | | 140 | | { |
| | 10015 | 141 | | var shifted = Math.Clamp((int)tier + shift, (int)Rarity.Common, (int)Rarity.Legendary); |
| | 10015 | 142 | | return (Rarity)shifted; |
| | | 143 | | } |
| | | 144 | | |
| | | 145 | | private static Rarity ResolveAvailableTier(Rarity preferredTier, IReadOnlyDictionary<Rarity, List<Item>> working) |
| | | 146 | | { |
| | 20030 | 147 | | for (var tier = (int)preferredTier; tier >= (int)Rarity.Common; tier--) |
| | | 148 | | { |
| | 10015 | 149 | | if (working.TryGetValue((Rarity)tier, out var items) && items.Count > 0) |
| | | 150 | | { |
| | 10015 | 151 | | return (Rarity)tier; |
| | | 152 | | } |
| | | 153 | | } |
| | | 154 | | |
| | 0 | 155 | | for (var tier = (int)preferredTier + 1; tier <= (int)Rarity.Legendary; tier++) |
| | | 156 | | { |
| | 0 | 157 | | if (working.TryGetValue((Rarity)tier, out var items) && items.Count > 0) |
| | | 158 | | { |
| | 0 | 159 | | return (Rarity)tier; |
| | | 160 | | } |
| | | 161 | | } |
| | | 162 | | |
| | 0 | 163 | | throw new InvalidOperationException("No items remain in any loot tier."); |
| | | 164 | | } |
| | | 165 | | } |