using System.Collections.Concurrent; using System.Text.RegularExpressions; using RadicadeServer.API.Storefront; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Players.Avatar; using RadicadeServer.RecNet.Enums.Store; namespace RadicadeServer.Utils { public static class RroRewardPools { public sealed record ItemSet(string Family, string Colour); public const string Unranked = ""; public const int SelectionSize = 3; public static readonly string[] QuestRanks = ["C", "B", "A", "S"]; private static readonly Dictionary Pools = new() { [GiftContext.Quest_Goblin_C] = [ new("Archer", "Tan"), new("Archer", "Yellow"), new("Knight", "Blue"), new("Knight", "Grey"), ], [GiftContext.Quest_Goblin_B] = [ new("Archer", "Red"), new("Knight", "Cherry"), ], [GiftContext.Quest_Goblin_A] = [ new("Archer", "Blue"), new("Archer", "Green"), new("Knight", "White"), new("Knight", "Yellow"), ], [GiftContext.Quest_Goblin_S] = [ new("Archer", "Black"), new("Knight", "Black"), ], [GiftContext.Quest_SciFi_C] = [ new("Space Marine", "Blue"), new("Space Marine", "Orange"), new("Smuggler", "Brown"), new("Smuggler", "Green"), ], [GiftContext.Quest_SciFi_B] = [ new("Space Marine", "Pink"), new("Space Marine", "Red"), new("Smuggler", "Cherry"), new("Smuggler", "Yellow"), ], [GiftContext.Quest_SciFi_A] = [ new("Space Marine", "Purple"), new("Space Marine", "White"), new("Smuggler", "Grey"), new("Smuggler", "Orange"), ], [GiftContext.Quest_SciFi_S] = [ new("Space Marine", "Black"), new("Smuggler", "Black"), ], [GiftContext.Quest_Cauldron_C] = [ new("Witch Hunter", "Purple"), new("Witch Hunter", "Green"), new("Wizard", "Blue"), new("Wizard", "Green"), ], [GiftContext.Quest_Cauldron_B] = [ new("Witch Hunter", "Red"), new("Wizard", "Red"), ], [GiftContext.Quest_Cauldron_A] = [ new("Witch Hunter", "White"), new("Wizard", "White"), ], [GiftContext.Quest_Cauldron_S] = [ new("Witch Hunter", "Black"), new("Wizard", "Black"), new("Wizard", "Yellow"), ], [GiftContext.Quest_Dracula1_C] = [new("Vampire Hunter", "Blue")], [GiftContext.Quest_Dracula1_B] = [new("Vampire Hunter", "Red")], [GiftContext.Quest_Dracula1_A] = [new("Vampire Hunter", "Leather")], [GiftContext.Quest_Dracula1_S] = [new("Vampire Hunter", "Midnight")], [GiftContext.Quest_Dracula1_SS] = [new("Dracula", "")], [GiftContext.StuntRunner_TheMainEvent_C] = [ new("Runner", "Green"), new("Runner", "Yellow"), ], [GiftContext.StuntRunner_TheMainEvent_B] = [ new("Runner", "Purple"), new("Runner", "Black"), ], [GiftContext.StuntRunner_TheMainEvent_A] = [ new("Stunt", "Fuchsia"), new("Stunt", "Blue"), ], [GiftContext.StuntRunner_TheMainEvent_S] = [ new("Stunt", "Green"), new("Stunt", "Red"), ], [GiftContext.StuntRunner_TheMainEvent_SS] = [new("Feathered Hair", "")], [GiftContext.Quest_Pirate1_C] = [ new("Scallywag Bracers", "Brown"), new("Scallywag Belt", "Brown"), new("Scallywag Bracers", "Red"), new("Scallywag Belt", "Red"), new("Pirate Captain Cuffs", "Green"), new("Pirate Captain Belt", "Green"), ], [GiftContext.Quest_Pirate1_B] = [ new("Scallywag Belt", "Brown"), new("Scallywag Vest", "Brown"), new("Scallywag Belt", "Red"), new("Scallywag Vest", "Red"), new("Pirate Captain Belt", "Red"), new("Pirate Captain Jacket", "Red"), new("Pirate Captain Belt", "Blue"), new("Pirate Captain Jacket", "Blue"), ], [GiftContext.Quest_Pirate1_A] = [ new("Scallywag Vest", "Brown"), new("Scallywag Hat", "Brown"), new("Scallywag Vest", "Red"), new("Scallywag Hat", "Red"), new("Pirate Captain Jacket", "Red"), new("Pirate Captain Hat", "Red"), new("Pirate Captain Jacket", "Blue"), new("Pirate Captain Hat", "Blue"), ], [GiftContext.Quest_Pirate1_S] = [ new("Pirate Captain Cuffs", "Gold"), new("Pirate Captain Belt", "Gold"), new("Pirate Captain Jacket", "Gold"), new("Pirate Captain Hat", "Gold"), new("Pirate Eyepatch", "Black"), new("Pirate Eyepatch", "Skull"), ], [GiftContext.Paintball_ClearCut] = [new("Lumberjack", "")], [GiftContext.Paintball_Homestead] = [new("Farmer Hat", ""), new("Farmer Overalls", "")], [GiftContext.Paintball_Quarry] = [new("Construction", "")], [GiftContext.Paintball_River] = [new("Angler", "")], [GiftContext.Paintball_Dam] = [new("Electrician", "")], [GiftContext.Paintball_DriveIn] = [new("Mechanic", "")], [GiftContext.Discgolf_Propulsion] = [new("Scientist", "")], [GiftContext.Discgolf_Lake] = [ new("Golfer Hat", "Tan"), new("Golfer Hat", "Blue"), new("Golfer Hat", "Grey"), new("Golfer Shirt", "Tan"), new("Golfer Shirt", "Blue"), new("Golfer Shirt", "Grey"), new("Golfer Gloves", "Tan"), new("Golfer Gloves", "Blue"), new("Golfer Gloves", "Grey"), ], [GiftContext.Charades] = [new("Beret", ""), new("Artist Beret", "")], [GiftContext.Soccer] = [new("Soccer", "")], [GiftContext.Paddleball] = [new("Tennis", "")], [GiftContext.Dodgeball] = [new("Dodgeball Shirt", "")], [GiftContext.Lasertag] = [], [GiftContext.Bowling] = [new("Dude Sweater", "")], [GiftContext.RecRoyale] = [], }; public static readonly IReadOnlyList Unresolved = [ "Crimson Cauldron S: the Brown Witch Hunter set - the catalogue's Witch Hunter pieces come in black, blue, green, leather, purple, red and white, and none in brown.", "Isle of Lost Skulls X: no special-tier reward is named.", "Isle of Lost Skulls B and A: the verified tables name a Pirate Captain Belt (Blue) for B and a Pirate Captain Hat (Blue) for A. The catalogue carries the Captain's blue jacket but no blue belt or hat, so those two pieces are not paid; the rest of each rank's table is.", "Crimson Cauldron S: the Brown Witch Hunter is a real S-rank colour, but no item in the shipped catalogue carries it - its Witch Hunter pieces are black, blue, green, leather, purple, red and white. The descriptor or the in-game name is needed to name it here.", "Rec Royale: its Season 1 rewards came from the Frontier Pass, a progression system of its own, so a completed match draws nothing. The Pass itself is not implemented.", "Crescendo X: no special-tier reward is named.", "Stunt Runner D and X: no reward is named for either tier, and the catalogue has no item for one.", "Rec Royale: no concrete reward item is named.", "Laser Tag: not a random-drop activity at all - a completed game pays tickets, and the shop they are spent in is not implemented yet.", "Dodgeball: the definitions call the set the Dodger Vest in cyan, purple and orange; the catalogue carries the same three pieces as Dodgeball Shirt in orange, purple and teal.", ]; public static readonly IReadOnlyList Deprecated = [ GiftContext.DEPRECATED_Paintball_ClearCut, GiftContext.DEPRECATED_Paintball_Homestead, GiftContext.DEPRECATED_Paintball_Quarry, GiftContext.DEPRECATED_Paintball_River, GiftContext.DEPRECATED_Paintball_Dam, GiftContext.DEPRECATED_Paintball_DriveIn, GiftContext.DEPRECATED_Discgolf_Propulsion, GiftContext.DEPRECATED_Discgolf_Lake, GiftContext.DEPRECATED_Charades, GiftContext.DEPRECATED_Soccer, GiftContext.DEPRECATED_Paddleball, GiftContext.DEPRECATED_Dodgeball, GiftContext.DEPRECATED_Lasertag, GiftContext.DEPRECATED_Bowling, ]; public static bool IsDeprecated(GiftContext context) => Deprecated.Contains(context); public static IReadOnlyCollection RewardableContexts => Pools.Keys; public static IReadOnlyList SetsFor(GiftContext context) => Pools.TryGetValue(context, out var sets) ? sets : []; private static readonly Dictionary> ByRoom = new(StringComparer.OrdinalIgnoreCase) { ["GoldenTrophy"] = Ranks( ("C", GiftContext.Quest_Goblin_C), ("B", GiftContext.Quest_Goblin_B), ("A", GiftContext.Quest_Goblin_A), ("S", GiftContext.Quest_Goblin_S)), ["TheRiseofJumbotron"] = Ranks( ("C", GiftContext.Quest_SciFi_C), ("B", GiftContext.Quest_SciFi_B), ("A", GiftContext.Quest_SciFi_A), ("S", GiftContext.Quest_SciFi_S)), ["CrimsonCauldron"] = Ranks( ("C", GiftContext.Quest_Cauldron_C), ("B", GiftContext.Quest_Cauldron_B), ("A", GiftContext.Quest_Cauldron_A), ("S", GiftContext.Quest_Cauldron_S)), ["Crescendo"] = Ranks( ("C", GiftContext.Quest_Dracula1_C), ("B", GiftContext.Quest_Dracula1_B), ("A", GiftContext.Quest_Dracula1_A), ("S", GiftContext.Quest_Dracula1_S)), ["IsleOfLostSkulls"] = Ranks( ("C", GiftContext.Quest_Pirate1_C), ("B", GiftContext.Quest_Pirate1_B), ("A", GiftContext.Quest_Pirate1_A), ("S", GiftContext.Quest_Pirate1_S)), ["StuntRunner"] = Ranks( ("C", GiftContext.StuntRunner_TheMainEvent_C), ("B", GiftContext.StuntRunner_TheMainEvent_B), ("A", GiftContext.StuntRunner_TheMainEvent_A), ("S", GiftContext.StuntRunner_TheMainEvent_S), ("SS", GiftContext.StuntRunner_TheMainEvent_SS), ("S+", GiftContext.StuntRunner_TheMainEvent_SS), ("SPlus", GiftContext.StuntRunner_TheMainEvent_SS)), ["PaintballClearcut"] = UnrankedContext(GiftContext.Paintball_ClearCut), ["Clearcut"] = UnrankedContext(GiftContext.Paintball_ClearCut), ["PaintballHomestead"] = UnrankedContext(GiftContext.Paintball_Homestead), ["Homestead"] = UnrankedContext(GiftContext.Paintball_Homestead), ["PaintballQuarry"] = UnrankedContext(GiftContext.Paintball_Quarry), ["Quarry"] = UnrankedContext(GiftContext.Paintball_Quarry), ["PaintballRiver"] = UnrankedContext(GiftContext.Paintball_River), ["River"] = UnrankedContext(GiftContext.Paintball_River), ["PaintballSpillway"] = UnrankedContext(GiftContext.Paintball_Dam), ["Spillway"] = UnrankedContext(GiftContext.Paintball_Dam), ["PaintballDriveIn"] = UnrankedContext(GiftContext.Paintball_DriveIn), ["DriveIn"] = UnrankedContext(GiftContext.Paintball_DriveIn), ["DiscGolfPropulsion"] = UnrankedContext(GiftContext.Discgolf_Propulsion), ["PropulsionTestRange"] = UnrankedContext(GiftContext.Discgolf_Propulsion), ["DiscGolfLake"] = UnrankedContext(GiftContext.Discgolf_Lake), ["3DCharades"] = UnrankedContext(GiftContext.Charades), ["Legacy3DCharades"] = UnrankedContext(GiftContext.Charades), ["Soccer"] = UnrankedContext(GiftContext.Soccer), ["Paddleball"] = UnrankedContext(GiftContext.Paddleball), ["Dodgeball"] = UnrankedContext(GiftContext.Dodgeball), ["DodgeballVR"] = UnrankedContext(GiftContext.Dodgeball), ["LaserTag"] = UnrankedContext(GiftContext.Lasertag), ["Bowling"] = UnrankedContext(GiftContext.Bowling), ["BowlingAlley"] = UnrankedContext(GiftContext.Bowling), ["RecRoyaleSolos"] = UnrankedContext(GiftContext.RecRoyale), ["RecRoyaleSquads"] = UnrankedContext(GiftContext.RecRoyale), }; private static Dictionary Ranks(params (string Rank, GiftContext Context)[] ranks) => ranks.ToDictionary(r => r.Rank, r => r.Context, StringComparer.OrdinalIgnoreCase); private static readonly Dictionary RoomAliases = new(StringComparer.OrdinalIgnoreCase) { ["TheMainEvent"] = "StuntRunner", ["StuntRunnerTheMainEvent"] = "StuntRunner", ["StuntRunnerMainEvent"] = "StuntRunner", }; private static string? ResolveRoomName(string? roomName) { if (string.IsNullOrWhiteSpace(roomName)) return null; var trimmed = roomName.Trim(); return RoomAliases.TryGetValue(trimmed, out var target) ? target : trimmed; } public static string? RoomForContext(GiftContext context) { foreach (var (room, ranks) in ByRoom) { if (ranks.Values.Contains(context)) return room; } return ConsumableRooms.TryGetValue(context, out var consumableRoom) ? consumableRoom : null; } private static readonly Dictionary ConsumableRooms = new() { [GiftContext.Quest_Goblin_Consumable] = "GoldenTrophy", [GiftContext.Quest_Cauldron_Consumable] = "CrimsonCauldron", [GiftContext.Quest_Pirate1_Consumable] = "IsleOfLostSkulls", [GiftContext.Quest_Dracula1_Consumable] = "Crescendo", [GiftContext.Quest_Scifi_Consumable] = "TheRiseofJumbotron", [GiftContext.StuntRunner_TheMainEvent_Consumable] = "StuntRunner", }; private static Dictionary UnrankedContext(GiftContext context) => new(StringComparer.OrdinalIgnoreCase) { [Unranked] = context }; private static readonly Dictionary NoContexts = new(); public static IReadOnlyDictionary ContextsForRoom(string? roomName) { var resolved = ResolveRoomName(roomName); if (resolved == null) return NoContexts; return ByRoom.TryGetValue(resolved, out var ranks) ? ranks : NoContexts; } public static bool IsRewardActivity(string? roomName) => ContextsForRoom(roomName).Count > 0; public static bool IsQuestContext(GiftContext context) { var room = RoomForContext(context); if (room == null) return false; var contexts = ContextsForRoom(room); return QuestRanks.Any(rank => contexts.ContainsKey(rank)); } public static bool TryContext(string? roomName, string? rank, out GiftContext context) { context = GiftContext.None; var contexts = ContextsForRoom(roomName); if (contexts.Count == 0) return false; return contexts.TryGetValue(string.IsNullOrWhiteSpace(rank) ? Unranked : rank.Trim(), out context); } public static List LowerRanksOf(GiftContext context) { var lower = new List(); foreach (var ranks in ByRoom.Values) { if (!ranks.ContainsValue(context)) continue; var index = Array.FindIndex(QuestRanks, rank => ranks.TryGetValue(rank, out var paid) && paid == context); if (index < 0) break; for (int i = index - 1; i >= 0; i--) if (ranks.TryGetValue(QuestRanks[i], out var weaker)) lower.Add(weaker); break; } return lower; } public static bool PaysNoItemReward(string? roomName) => string.Equals(roomName?.Trim(), "RecRoyaleSolos", StringComparison.OrdinalIgnoreCase) || string.Equals(roomName?.Trim(), "RecRoyaleSquads", StringComparison.OrdinalIgnoreCase); public static bool OffersSelection(string? roomName) => ContextsForRoom(roomName).ContainsKey(Unranked); public static bool TryFloorContext(string? roomName, out GiftContext context) { context = GiftContext.None; var contexts = ContextsForRoom(roomName); if (contexts.Count == 0) return false; if (contexts.TryGetValue(Unranked, out var only)) { context = only; return true; } foreach (var rank in QuestRanks) { if (contexts.TryGetValue(rank, out var ranked)) { context = ranked; return true; } } return false; } public static List Resolve(ItemSet set, string? catalogueJson = null) { var colour = Normalise(set.Colour); var pattern = string.IsNullOrEmpty(colour) ? @"^\s*" + Regex.Escape(set.Family) + @"\b" : @"^\s*" + Regex.Escape(set.Family) + @"\b.*\(\s*" + Regex.Escape(colour) + @"\s*\)\s*$"; var rx = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); return LevelUpGiftService.ClothingCatalogue(catalogueJson) .Where(item => rx.IsMatch(Normalise(item.FriendlyName))) .OrderBy(item => item.AvatarItemId) .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) .ToList(); } private static string Normalise(string? value) => (value ?? "").Replace("gray", "grey", StringComparison.OrdinalIgnoreCase); private static readonly ConcurrentDictionary> PoolCache = new(); public static List Resolve(GiftContext context, string? catalogueJson = null) => string.IsNullOrWhiteSpace(catalogueJson) ? PoolCache.GetOrAdd(context, ctx => ResolveCore(ctx, null)) : ResolveCore(context, catalogueJson); private static List ResolveCore(GiftContext context, string? catalogueJson) => SetsFor(context) .SelectMany(set => Resolve(set, catalogueJson)) .GroupBy(item => item.AvatarItemDesc, StringComparer.Ordinal) .Select(group => group.First()) .OrderBy(item => item.AvatarItemId) .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) .ToList(); public static AvatarItem? Pick( GiftContext context, ISet? owned = null, Random? random = null, string? catalogueJson = null) { var pool = Resolve(context, catalogueJson); if (pool.Count == 0) return null; var ownedKeys = owned ?? new HashSet(StringComparer.OrdinalIgnoreCase); var unowned = pool .Where(item => !ownedKeys.Contains(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc))) .ToList(); return unowned.Count == 0 ? null : unowned[(random ?? Random.Shared).Next(unowned.Count)]; } public static Gift? BuildGift( GiftContext context, ISet? owned = null, Random? random = null, string? catalogueJson = null) { var pick = Pick(context, owned, random, catalogueJson); if (pick == null) return null; return new Gift { FriendlyName = pick.FriendlyName, Tooltip = pick.FriendlyName, AvatarItemDesc = pick.AvatarItemDesc, AvatarItemType = AvatarItemType.Outfit, Rarity = (Rarity)pick.Rarity, CurrencyType = CurrencyType.Invalid, Currency = 0, Xp = 0, Context = context }; } public static List BuildDrops( GiftContext context, bool selection, ISet? owned = null, Random? random = null, string? catalogueJson = null) { var drops = new List(); var claimed = owned == null ? new HashSet(StringComparer.OrdinalIgnoreCase) : new HashSet(owned, StringComparer.OrdinalIgnoreCase); var rng = random ?? Random.Shared; for (int i = 0; i < (selection ? SelectionSize : 1); i++) { var gift = BuildGift(context, claimed, rng, catalogueJson); if (gift == null) break; drops.Add(gift); claimed.Add(LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)); } return drops; } public static bool PaysTickets(string? roomName) => string.Equals(roomName?.Trim(), "LaserTag", StringComparison.OrdinalIgnoreCase); public const int LaserTagTicketsPerGame = 25; public const int LaserTagFirstGamesBonus = 100; public const int LaserTagBonusGamesPerDay = 5; public static double DropChanceFor(GiftContext context) => context == GiftContext.Bowling ? 1.0 / 250.0 : 1.0; public const int CompletionTokenAmount = 50; public static Gift TokenGift(GiftContext context, int amount, string? message = null) => new() { GiftDropId = StorefrontShared.NewGiftDropId(), FriendlyName = TokenTitle(amount), Tooltip = string.IsNullOrWhiteSpace(message) ? "Reward for playing!" : message, CurrencyType = CurrencyType.RecCenterTokens, Currency = amount, Context = context, Rarity = amount >= 200 ? Rarity.Rare : Rarity.Common }; public static string TokenTitle(int amount) => $"{amount} Tokens"; public const double TokenBoxChance = 0.12; public static List BuildFallbackSelection( ISet? owned = null, Random? random = null, string? catalogueJson = null, string? message = null) { var rng = random ?? Random.Shared; var drops = BuildFallbackDrops(selection: true, owned: owned, random: random, catalogueJson: catalogueJson) .Take(SelectionSize) .ToList(); if (drops.Count == 0) return [TokenGift(GiftContext.GameRewards_Tokens, CompletionTokenAmount, message)]; var tokenBox = TokenGift(GiftContext.GameRewards_Tokens, CompletionTokenAmount, message); if (drops.Count < SelectionSize) drops.Add(tokenBox); else if (rng.NextDouble() < TokenBoxChance) drops[^1] = tokenBox; return drops; } public static List BuildDailyChallengeSelection( ISet? owned = null, Random? random = null, string? catalogueJson = null) { var drops = BuildFallbackDrops(selection: true, owned: owned, random: random, catalogueJson: catalogueJson) .Take(SelectionSize) .ToList(); foreach (var drop in drops) drop.Context = GiftContext.Daily_Challenge_Complete; return drops; } public const GiftContext ReacquisitionContext = GiftContext.Reacquisition; private static readonly int[] NonRareRarities = [10, 20]; private static List? _reacquisitionPool; private static readonly Regex RewardFamilyPattern = new( @"^\s*(" + string.Join("|", Pools.Values.SelectMany(sets => sets) .Select(set => Regex.Escape(set.Family)) .Distinct(StringComparer.OrdinalIgnoreCase)) + @")\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static List ReacquisitionPool(string? catalogueJson = null) { if (!string.IsNullOrWhiteSpace(catalogueJson)) return FilterNonRare(LevelUpGiftService.ClothingCatalogue(catalogueJson)); return _reacquisitionPool ??= FilterNonRare(LevelUpGiftService.ClothingCatalogue(null)); } private static List FilterNonRare(List catalogue) => catalogue .Where(item => item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc) && NonRareRarities.Contains(item.Rarity) && !LevelUpRewardPool.IsAlwaysDenied(item) && !IsRewardSetItem(item)) .OrderBy(item => item.AvatarItemId) .ThenBy(item => item.AvatarItemDesc, StringComparer.Ordinal) .ToList(); public static bool IsRewardSetItem(AvatarItem item) => item != null && RewardFamilyPattern.IsMatch(item.FriendlyName ?? string.Empty); private static readonly HashSet EmptyOwned = new(StringComparer.OrdinalIgnoreCase); public static List BuildFallbackDrops( bool selection, ISet? owned = null, Random? random = null, string? catalogueJson = null, string? consumableCatalogJson = null, LevelUpRewardPool.Pool? consumablePool = null) { var rng = random ?? Random.Shared; var has = owned == null ? new HashSet(StringComparer.OrdinalIgnoreCase) : new HashSet(owned, StringComparer.OrdinalIgnoreCase); if (!selection) { var consumable = BuildFallbackConsumable(has, rng, consumableCatalogJson, consumablePool); return consumable == null ? [] : [consumable]; } var pool = ReacquisitionPool(catalogueJson); var unowned = pool .Where(item => !has.Contains(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc))) .ToList(); return (unowned.Count > 0 ? unowned : pool) .OrderBy(_ => rng.Next()) .Take(SelectionSize) .Select(item => new Gift { FriendlyName = item.FriendlyName, Tooltip = item.FriendlyName, AvatarItemDesc = item.AvatarItemDesc, AvatarItemType = AvatarItemType.Outfit, Rarity = (Rarity)item.Rarity, CurrencyType = CurrencyType.Invalid, Currency = 0, Xp = 0, Context = ReacquisitionContext }) .ToList(); } public static Gift? BuildFallbackConsumable( ISet? owned = null, Random? random = null, string? consumableCatalogJson = null, LevelUpRewardPool.Pool? pool = null) { var rng = random ?? Random.Shared; var has = owned ?? EmptyOwned; var gift = LevelUpGiftService.BuildConsumableGift(consumableCatalogJson, has, rng, pool) ?? LevelUpGiftService.BuildConsumableGift(consumableCatalogJson, EmptyOwned, rng, pool); if (gift == null) return null; gift.Context = ReacquisitionContext; return gift; } } }