using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Players.Avatar; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Leveling; using RadicadeServer.RecNet.Progression; using RadicadeServer.RecNet.Store; namespace RadicadeServer.Utils { public static class LevelUpGiftService { public static List ParseMap() { var map = FirstTimeUtils.LevelProgressionMap.FromJson>() ?? new List(); return map.OrderBy(x => x.Level).ToList(); } public const int MaxLevel = 50; public static int ComputeLevel(List map, int xp) { int cumulative = 0; int level = 1; foreach (var entry in map.OrderBy(x => x.Level)) { if (entry.Level <= 0) continue; cumulative += entry.RequiredXp; if (xp < cumulative) break; level = entry.Level; } return level > MaxLevel ? MaxLevel : level; } public static int ThresholdXpForLevel(List map, int level) { int threshold = 0; foreach (var entry in map.OrderBy(x => x.Level)) { if (entry.Level <= 0 || entry.Level > level) continue; threshold += entry.RequiredXp; } return threshold; } public static int RequiredXpForLevel(List map, int level) => ThresholdXpForLevel(map, level) - ThresholdXpForLevel(map, level - 1); public static int DisplayLevel(int level) => level > MaxLevel ? MaxLevel : level; public static int XpIntoCurrentLevel(List map, int level, int totalXp) { if (level >= MaxLevel) return RequiredXpForLevel(map, MaxLevel); int reached = ComputeLevel(map, totalXp); int from = level < reached ? level : reached; int into = totalXp - ThresholdXpForLevel(map, from); return into < 0 ? 0 : into; } public static int XpIntoCurrentLevel(ProgressionData data) => XpIntoCurrentLevel(ParseMap(), data.Level, data.XP); public const bool LevelUpGiftDropsAreSeeded = false; private static List? _avatarItems; private static List? _consumables; internal static List ClothingCatalogue(string? json = null) { if (!string.IsNullOrWhiteSpace(json)) return ParseClothingCatalogue(json); return _avatarItems ??= ParseClothingCatalogue(DBConfig.DefaultAvatarCatalog); } private static List ParseClothingCatalogue(string json) => (json.FromJson>() ?? new List()) .Where(x => x != null && !string.IsNullOrWhiteSpace(x.AvatarItemDesc) && !x.IsBaseAvatarItem) .OrderBy(x => x.AvatarItemId) .ThenBy(x => x.AvatarItemDesc, StringComparer.Ordinal) .ToList(); private static List ConsumableCatalogue(string? json = null) { if (!string.IsNullOrWhiteSpace(json)) return ParseConsumableCatalogue(json); return _consumables ??= ParseConsumableCatalogue(DBConfig.DefaultConsumableItems); } private static List ParseConsumableCatalogue(string json) => (json.FromJson>() ?? new List()) .Where(x => x != null && !string.IsNullOrWhiteSpace(x.ConsumableItemDesc)) .OrderBy(x => x.ConsumableItemDesc, StringComparer.Ordinal) .ToList(); public static Gift? BuildItemReward( int level, string? avatarCatalogJson = null, string? consumableCatalogJson = null, ISet? owned = null, Random? random = null, LevelUpRewardPool.Pool? pool = null) { if (!LevelRewardTable.IsItemLevel(level)) return null; var ownedKeys = owned ?? new HashSet(StringComparer.OrdinalIgnoreCase); var rng = random ?? Random.Shared; return LevelRewardTable.KindForLevel(level) == LevelRewardTable.RewardKind.Consumable ? BuildConsumableReward(level, consumableCatalogJson, ownedKeys, rng, pool) : BuildClothingReward(level, avatarCatalogJson, consumableCatalogJson, ownedKeys, rng, pool); } private static Gift? BuildClothingReward( int level, string? avatarCatalogJson, string? consumableCatalogJson, ISet owned, Random random, LevelUpRewardPool.Pool? pool) { var rarity = LevelRewardTable.RarityForLevel(level); var pick = LevelUpRewardPool.PickAvatarItem(ClothingCatalogue(avatarCatalogJson), rarity, owned, random, pool); if (pick == null) { return BuildConsumableReward(level, consumableCatalogJson, owned, random, pool); } var label = LevelRewardTable.StarLabel(rarity); return new Gift { FriendlyName = $"Level {level} Reward - {label} {pick.FriendlyName}", Tooltip = $"Congrats on getting to Level {level}!", AvatarItemDesc = pick.AvatarItemDesc, AvatarItemType = AvatarItemType.Outfit, Rarity = rarity, Currency = 0, Xp = 0 }; } private static Gift? BuildConsumableReward( int level, string? consumableCatalogJson, ISet owned, Random random, LevelUpRewardPool.Pool? pool) { var pick = LevelUpRewardPool.PickConsumable(ConsumableCatalogue(consumableCatalogJson), owned, random, pool); if (pick == null) { return null; } var token = ConsumableCatalog.ToClientDesc(pick.ConsumableItemDesc); if (string.IsNullOrWhiteSpace(token)) return null; return new Gift { FriendlyName = $"Level {level} Reward - {pick.FriendlyName}", Tooltip = $"Congrats on getting to Level {level}!", ConsumableItemDesc = token, Rarity = pick.Rarity, Currency = 0, Xp = 0 }; } public static Gift? BuildConsumableGift( string? consumableCatalogJson, ISet owned, Random random, LevelUpRewardPool.Pool? pool = null) { var pick = LevelUpRewardPool.PickConsumable(ConsumableCatalogue(consumableCatalogJson), owned, random, pool); if (pick == null) return null; var token = ConsumableCatalog.ToClientDesc(pick.ConsumableItemDesc); if (string.IsNullOrWhiteSpace(token)) return null; return new Gift { FriendlyName = pick.FriendlyName, Tooltip = pick.FriendlyName, ConsumableItemDesc = token, Rarity = pick.Rarity, Currency = 0, Xp = 0 }; } public static async Task> CollectOwnedKeysAsync( RecDBContext db, long playerId, GiftContext? pendingContext = GiftContext.LevelUp) { var owned = new HashSet(StringComparer.OrdinalIgnoreCase); var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); if (unlocks != null) { foreach (var item in unlocks.GetAvatarItems()) if (!string.IsNullOrWhiteSpace(item?.AvatarItemDesc)) owned.Add(LevelUpRewardPool.OwnershipKey(item.AvatarItemDesc)); foreach (var item in unlocks.GetConsumables()) if (!string.IsNullOrWhiteSpace(item?.ConsumableItemDesc)) owned.Add(LevelUpRewardPool.OwnershipKey(item.ConsumableItemDesc)); } var gifts = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); foreach (var pending in StorefrontShared.ParseGiftList(gifts?.Data)) { if (pending == null) continue; if (pendingContext.HasValue && pending.Context != pendingContext.Value) continue; Remember(owned, pending); } return owned; } private static void Remember(HashSet owned, Gift gift) { if (!string.IsNullOrWhiteSpace(gift.AvatarItemDesc)) owned.Add(LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)); if (!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) owned.Add(LevelUpRewardPool.OwnershipKey(gift.ConsumableItemDesc)); } public static async Task> DeliverLevelUpGiftsAsync(RecDBContext db, long playerId, bool recomputeFromXp, int? fromLevel = null, bool notify = true) { var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); if (progression == null) return new List(); var map = ParseMap(); int oldLevel = fromLevel ?? progression.Level; int target = progression.Level; if (recomputeFromXp) { int computed = ComputeLevel(map, progression.XP); if (computed > target) target = computed; } if (target <= oldLevel) return new List(); var json = progression.GetProgressionJson() ?? new ProgressionJson(); var delivered = json.DeliveredLevelUpGiftLevels ?? new List(); var owned = await CollectOwnedKeysAsync(db, playerId); var random = Random.Shared; var toPersist = new List(); foreach (var entry in map.OrderBy(e => e.Level)) { if (entry.Level <= oldLevel) continue; if (entry.Level > target) break; if (delivered.Contains(entry.Level)) continue; var gift = LevelRewardTable.IsItemLevel(entry.Level) ? BuildItemReward(entry.Level, owned: owned, random: random) : null; if (gift == null) continue; Remember(owned, gift); gift.GiftDropId = StorefrontShared.NewGiftDropId(); gift.Context = GiftContext.LevelUp; gift.FromPlayerId = 0; toPersist.Add(gift); delivered.Add(entry.Level); } if (toPersist.Count == 0) return new List(); json.DeliveredLevelUpGiftLevels = delivered; progression.Progress = json.ToJson(); progression.Level = target; foreach (var gift in toPersist) { await StorefrontShared.PersistGiftBoxAsync(db, playerId, gift, notify: false); } await db.SaveChangesAsync(); if (notify) { foreach (var gift in toPersist) await StorefrontShared.SendGiftPackageNotificationAsync(playerId, gift); } return toPersist; } } }