namespace Supercell.Laser.Logic.Util { using Newtonsoft.Json; using Supercell.Laser.Logic.Command.Home; using Supercell.Laser.Logic.Data; using Supercell.Laser.Logic.Data.Helper; using Supercell.Laser.Logic.Home; using Supercell.Laser.Logic.Home.Gatcha; public static class VoucherManager { private static readonly object SyncRoot = new object(); public static bool TryRedeemVoucher(HomeMode homeMode, string voucherCode, out LogicGiveDeliveryItemsCommand command, out string reason) { command = null; reason = null; if (homeMode?.Avatar == null) { reason = "home-mode-missing"; return false; } lock (SyncRoot) { List vouchers = LoadVouchers(); VoucherDefinition voucher = vouchers.FirstOrDefault(v => string.Equals(v.Voucher, voucherCode, StringComparison.OrdinalIgnoreCase)); if (voucher == null) { reason = "voucher-not-found"; return false; } if (voucher.WhoUsed.Contains(homeMode.Avatar.AccountId)) { reason = "voucher-already-used"; return false; } command = new LogicGiveDeliveryItemsCommand(); foreach (VoucherItemDefinition item in voucher.Items ?? Enumerable.Empty()) { if (!TryAppendVoucherReward(homeMode, command, item)) { // Ignoring unsupported items instead of warning, to keep it clean. } } if (command.DeliveryUnits.Count == 0) { reason = "voucher-empty"; return false; } command.Execute(homeMode); voucher.WhoUsed.Add(homeMode.Avatar.AccountId); string offerClaimKey = $"voucher:{voucherCode.ToLowerInvariant()}"; if (!homeMode.Home.OffersClaimed.Contains(offerClaimKey)) { homeMode.Home.OffersClaimed.Add(offerClaimKey); } SaveVouchers(vouchers); return true; } } private static bool TryAppendVoucherReward(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, VoucherItemDefinition item) { if (!string.IsNullOrWhiteSpace(item.RewardType)) { return TryAppendCustomVoucherReward(homeMode, command, item); } int count = Math.Max(1, item.Multiplier); switch (item.ID) { case 0: case 6: case 10: case 14: int gatchaType = item.ID == 0 || item.ID == 6 ? 10 : (item.ID == 10 ? 11 : 12); for (int i = 0; i < count; i++) { DeliveryUnit unit = new DeliveryUnit(gatchaType); homeMode.SimulateGatcha(unit); command.DeliveryUnits.Add(unit); } return true; case 1: return AddDrop(command, 100, new GatchaDrop(7) { Count = count }); case 3: return TryAddSpecificBrawler(homeMode, command, GlobalId.CreateGlobalId((int)DataType.Character, item.BrawlerID)); case 4: case 24: return TryAddSpecificSkin(homeMode, command, item.SkinID, count); case 8: return AddDrop(command, 100, new GatchaDrop(6) { Count = count, DataGlobalId = GlobalId.CreateGlobalId((int)DataType.Character, item.BrawlerID) }); case 16: return AddDrop(command, 100, new GatchaDrop(8) { Count = count }); case 17: return AddDrop(command, 100, new GatchaDrop(12) { Count = count }); case 19: return AddDrop(command, 100, new GatchaDrop(10) { Count = count, DataGlobalId = GlobalId.CreateGlobalId((int)DataType.Emote, item.SkinID) }); default: return false; } } private static bool TryAppendCustomVoucherReward(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, VoucherItemDefinition item) { int count = Math.Max(1, item.Multiplier); string rewardType = item.RewardType.Trim().ToLowerInvariant(); switch (rewardType) { case "box_small": return AddBoxRewards(homeMode, command, 10, count); case "box_big": return AddBoxRewards(homeMode, command, 11, count); case "box_mega": return AddBoxRewards(homeMode, command, 12, count); case "brawler_random": return TryAddRandomBrawler(homeMode, command, item.RewardData); case "skin_random": return TryAddRandomSkin(homeMode, command, item.RewardData); default: return false; } } private static bool AddBoxRewards(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, int gatchaType, int count) { for (int i = 0; i < count; i++) { DeliveryUnit unit = new DeliveryUnit(gatchaType); homeMode.SimulateGatcha(unit); command.DeliveryUnits.Add(unit); } return true; } private static bool TryAddRandomBrawler(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, string rarityRaw) { string rarity = NormalizeRarity(rarityRaw); List candidates = DataTables.Get(DataType.Character) .GetDatas() .OfType() .Where(x => x.IsHero() && !x.Disabled) .Where(x => !homeMode.Avatar.HasHero(x.GetGlobalId())) .Where(x => { CardData unlockCard = DataTables.Get(DataType.Card).GetData(x.Name + "_unlock"); return unlockCard != null && string.Equals(NormalizeRarity(unlockCard.Rarity), rarity, StringComparison.Ordinal); }) .ToList(); if (candidates.Count == 0) { return false; } CharacterData selected = candidates[Random.Shared.Next(candidates.Count)]; return TryAddSpecificBrawler(homeMode, command, selected.GetGlobalId()); } private static bool TryAddRandomSkin(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, string rarityRaw) { string rarity = NormalizeRarity(rarityRaw); List candidates = DataTables.Get(DataType.Skin) .GetDatas() .OfType() .Where(x => IsVoucherSkinCandidate(homeMode, x, requireOwnedHero: false)) .Where(x => string.Equals(MapSkinRarity(x), rarity, StringComparison.Ordinal)) .ToList(); if (candidates.Count == 0) { return false; } List ownedHeroCandidates = candidates .Where(x => TryGetSkinCharacter(x, out CharacterData character) && homeMode.Avatar.HasHero(character.GetGlobalId())) .ToList(); List pool = ownedHeroCandidates.Count > 0 ? ownedHeroCandidates : candidates; SkinData selected = pool[Random.Shared.Next(pool.Count)]; return TryAddSkinReward(homeMode, command, selected); } private static bool TryAddSpecificBrawler(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, int characterGlobalId) { CharacterData character = DataTables.Get(DataType.Character).GetDataByGlobalId(characterGlobalId); if (character == null || !character.IsHero() || character.Disabled || homeMode.Avatar.HasHero(character.GetGlobalId())) { return false; } CardData unlockCard = DataTables.Get(DataType.Card).GetData(character.Name + "_unlock"); if (unlockCard == null) { return false; } return AddDrop(command, 100, new GatchaDrop(1) { Count = 1, DataGlobalId = character.GetGlobalId() }); } private static bool TryAddSpecificSkin(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, int skinId, int count) { SkinData skin = DataTables.Get(DataType.Skin).GetDataByGlobalId(skinId); if (skin == null) { skin = DataTables.Get(DataType.Skin).GetDataByGlobalId(GlobalId.CreateGlobalId((int)DataType.Skin, skinId)); } if (skin == null) { return false; } bool added = false; for (int i = 0; i < count; i++) { if (!TryAddSkinReward(homeMode, command, skin)) { break; } added = true; } return added; } private static bool TryAddSkinReward(HomeMode homeMode, LogicGiveDeliveryItemsCommand command, SkinData skin) { if (!IsVoucherSkinCandidate(homeMode, skin, requireOwnedHero: false)) { return false; } if (!TryGetSkinCharacter(skin, out CharacterData character)) { return false; } if (!homeMode.Avatar.HasHero(character.GetGlobalId()) && !TryAddSpecificBrawler(homeMode, command, character.GetGlobalId())) { return false; } return AddDrop(command, 100, new GatchaDrop(9) { Count = 1, SkinGlobalId = skin.GetGlobalId() }); } private static bool IsVoucherSkinCandidate(HomeMode homeMode, SkinData skin, bool requireOwnedHero) { if (skin == null || string.IsNullOrWhiteSpace(skin.Name)) { return false; } if (skin.Name.EndsWith("Default", StringComparison.OrdinalIgnoreCase)) { return false; } if (skin.Name.EndsWith("Silver", StringComparison.OrdinalIgnoreCase) || skin.Name.EndsWith("Gold", StringComparison.OrdinalIgnoreCase)) { return false; } if (skin.Name == "RocketGirlRanger" || skin.Name == "PowerLevelerKnight" || skin.Name == "BlowerTrader") { return false; } if (skin.CostGems <= 0 && skin.CostLegendaryTrophies <= 1) { return false; } if (homeMode.Home.UnlockedSkins.Contains(skin.GetGlobalId())) { return false; } if (!TryGetSkinCharacter(skin, out CharacterData character)) { return false; } return !requireOwnedHero || homeMode.Avatar.HasHero(character.GetGlobalId()); } private static bool TryGetSkinCharacter(SkinData skin, out CharacterData character) { character = null; if (skin == null || string.IsNullOrWhiteSpace(skin.Conf)) { return false; } SkinConfData skinConf = DataTables.Get(DataType.SkinConf).GetData(skin.Conf); if (skinConf == null || string.IsNullOrWhiteSpace(skinConf.Character)) { return false; } character = DataTables.Get(DataType.Character).GetData(skinConf.Character); return character != null && character.IsHero() && !character.Disabled; } private static string NormalizeRarity(string value) { string normalized = (value ?? string.Empty).Trim().ToLowerInvariant().Replace("-", string.Empty).Replace("_", string.Empty).Replace(" ", string.Empty); return normalized switch { "superrare" or "superraree" or "сверхредкий" => "superrare", "rare" or "редкий" => "rare", "epic" or "эпический" => "epic", "mythic" or "мифический" => "mythic", "legendary" or "легендарный" => "legendary", _ => normalized }; } private static string MapSkinRarity(SkinData skin) { if (skin.CostGems >= 299) { return "legendary"; } if (skin.CostGems >= 149) { return "epic"; } if (skin.CostGems >= 79) { return "superrare"; } return "rare"; } private static bool AddDrop(LogicGiveDeliveryItemsCommand command, int deliveryType, GatchaDrop drop) { DeliveryUnit unit = new DeliveryUnit(deliveryType); unit.AddDrop(drop); command.DeliveryUnits.Add(unit); return true; } private static List LoadVouchers() { string path = LogicHelperConfig.vouchersPath; if (!File.Exists(path)) return new List(); return JsonConvert.DeserializeObject>(File.ReadAllText(path)) ?? new List(); } private static void SaveVouchers(List vouchers) { string path = LogicHelperConfig.vouchersPath; string directoryPath = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directoryPath)) { Directory.CreateDirectory(directoryPath); } File.WriteAllText(path, JsonConvert.SerializeObject(vouchers, Formatting.Indented)); } public sealed class VoucherDefinition { [JsonProperty] public string Voucher { get; set; } [JsonProperty] public List Items { get; set; } = new List(); [JsonProperty] public List WhoUsed { get; set; } = new List(); } public sealed class VoucherItemDefinition { [JsonProperty] public int ID { get; set; } [JsonProperty] public int Multiplier { get; set; } = 1; [JsonProperty] public int BrawlerID { get; set; } = -1; [JsonProperty] public int SkinID { get; set; } = -1; [JsonProperty] public string RewardType { get; set; } [JsonProperty] public string RewardData { get; set; } } } }