Files
2026-10-16 23:33:13 -07:00

644 lines
28 KiB
C#

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<GiftContext, ItemSet[]> 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<string> 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<GiftContext> 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<GiftContext> RewardableContexts => Pools.Keys;
public static IReadOnlyList<ItemSet> SetsFor(GiftContext context) =>
Pools.TryGetValue(context, out var sets) ? sets : [];
private static readonly Dictionary<string, Dictionary<string, GiftContext>> 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<string, GiftContext> Ranks(params (string Rank, GiftContext Context)[] ranks) =>
ranks.ToDictionary(r => r.Rank, r => r.Context, StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, string> 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<GiftContext, string> 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<string, GiftContext> UnrankedContext(GiftContext context) =>
new(StringComparer.OrdinalIgnoreCase) { [Unranked] = context };
private static readonly Dictionary<string, GiftContext> NoContexts = new();
public static IReadOnlyDictionary<string, GiftContext> 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<GiftContext> LowerRanksOf(GiftContext context)
{
var lower = new List<GiftContext>();
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<AvatarItem> 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<GiftContext, List<AvatarItem>> PoolCache = new();
public static List<AvatarItem> Resolve(GiftContext context, string? catalogueJson = null) =>
string.IsNullOrWhiteSpace(catalogueJson)
? PoolCache.GetOrAdd(context, ctx => ResolveCore(ctx, null))
: ResolveCore(context, catalogueJson);
private static List<AvatarItem> 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<string>? owned = null,
Random? random = null,
string? catalogueJson = null)
{
var pool = Resolve(context, catalogueJson);
if (pool.Count == 0) return null;
var ownedKeys = owned ?? new HashSet<string>(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<string>? 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<Gift> BuildDrops(
GiftContext context,
bool selection,
ISet<string>? owned = null,
Random? random = null,
string? catalogueJson = null)
{
var drops = new List<Gift>();
var claimed = owned == null
? new HashSet<string>(StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(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<Gift> BuildFallbackSelection(
ISet<string>? 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<Gift> BuildDailyChallengeSelection(
ISet<string>? 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<AvatarItem>? _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<AvatarItem> ReacquisitionPool(string? catalogueJson = null)
{
if (!string.IsNullOrWhiteSpace(catalogueJson))
return FilterNonRare(LevelUpGiftService.ClothingCatalogue(catalogueJson));
return _reacquisitionPool ??= FilterNonRare(LevelUpGiftService.ClothingCatalogue(null));
}
private static List<AvatarItem> FilterNonRare(List<AvatarItem> 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<string> EmptyOwned = new(StringComparer.OrdinalIgnoreCase);
public static List<Gift> BuildFallbackDrops(
bool selection,
ISet<string>? 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<string>(StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(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<string>? 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;
}
}
}