diff --git a/EpinelPS/Data/GameData.cs b/EpinelPS/Data/GameData.cs index 5e0ed95..5473355 100644 --- a/EpinelPS/Data/GameData.cs +++ b/EpinelPS/Data/GameData.cs @@ -254,6 +254,14 @@ namespace EpinelPS.Data [LoadRecord("EventMVGMissionTable.json", "Id")] public readonly Dictionary EventMvgMissionTable = []; + [LoadRecord("EquipmentOptionTable.json", "Id")] + public readonly Dictionary EquipmentOptionTable = []; + + [LoadRecord("EquipmentOptionCostTable.json", "Id")] + public readonly Dictionary EquipmentOptionCostTable = []; + + [LoadRecord("ItemEquipCorpSettingTable.json", "Id")] + public readonly Dictionary ItemEquipCorpSettingTable = []; static async Task BuildAsync() { await Load(); @@ -718,17 +726,10 @@ namespace EpinelPS.Data return data.HardFieldId; else return data.FieldId; } - internal string GetMapIdFromChapter(int chapter, string mod) - { - CampaignChapterRecord data = ChapterCampaignData[chapter - 1]; - if (mod == "Hard") - return data.HardFieldId; - else return data.FieldId; - } internal int GetConditionReward(int groupId, long damage) { - IEnumerable> results = ConditionRewards.Where(x => x.Value.Group == groupId && x.Value.ValueMin <= damage && x.Value.ValueMax >= damage); + IEnumerable> results = ConditionRewards.Where(x => x.Value.Group == groupId && x.Value.ValueMin <= damage && (x.Value.ValueMax == 0 || x.Value.ValueMax >= damage)); if (results.Any()) return results.FirstOrDefault().Value.RewardId; else return 0; diff --git a/EpinelPS/LobbyServer/Event/GetJoinedEvent.cs b/EpinelPS/LobbyServer/Event/GetJoinedEvent.cs index 2ca5302..532769d 100644 --- a/EpinelPS/LobbyServer/Event/GetJoinedEvent.cs +++ b/EpinelPS/LobbyServer/Event/GetJoinedEvent.cs @@ -16,7 +16,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 20001, - EventSystemType = (int)EventType.PickupGachaEvent, + EventSystemType = (int)EventSystemType.PickupGachaEvent, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventEndDate = DateTime.Now.AddDays(20).Ticks, @@ -31,7 +31,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 70077, - EventSystemType = (int)EventType.PickupGachaEvent, + EventSystemType = (int)EventSystemType.PickupGachaEvent, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventEndDate = DateTime.Now.AddDays(20).Ticks, @@ -44,7 +44,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 10046, - EventSystemType = (int)EventType.LoginEvent, + EventSystemType = (int)EventSystemType.LoginEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -57,7 +57,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 40066, - EventSystemType = (int)EventType.StoryEvent, + EventSystemType = (int)EventSystemType.StoryEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -70,7 +70,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 60066, - EventSystemType = (int)EventType.ChallengeModeEvent, + EventSystemType = (int)EventSystemType.ChallengeModeEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -83,7 +83,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 70078, - EventSystemType = (int)EventType.PickupGachaEvent, + EventSystemType = (int)EventSystemType.PickupGachaEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -96,7 +96,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 70079, - EventSystemType = (int)EventType.PickupGachaEvent, + EventSystemType = (int)EventSystemType.PickupGachaEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -112,7 +112,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 140052, - EventSystemType = RewardUpEvent, + EventSystemType = (int)EventSystemType.RewardUpEvent, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, @@ -130,7 +130,7 @@ namespace EpinelPS.LobbyServer.Event EventData = new NetEventData() { Id = 170017, - EventSystemType = TriggerMissionEventReward, + EventSystemType = (int)EventSystemType.TriggerMissionEventReward, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks, diff --git a/EpinelPS/LobbyServer/Intercept/GetInterceptData.cs b/EpinelPS/LobbyServer/Intercept/GetInterceptData.cs index 12127e6..22b5bbd 100644 --- a/EpinelPS/LobbyServer/Intercept/GetInterceptData.cs +++ b/EpinelPS/LobbyServer/Intercept/GetInterceptData.cs @@ -1,5 +1,6 @@ using EpinelPS.Database; using EpinelPS.Utils; +using EpinelPS.Data; namespace EpinelPS.LobbyServer.Intercept { @@ -8,17 +9,32 @@ namespace EpinelPS.LobbyServer.Intercept { protected override async Task HandleAsync() { - ReqGetInterceptData req = await ReadData(); + ReqGetInterceptData req = await ReadData(); + + int specialId = GetCurrentInterceptionIds(); ResGetInterceptData response = new() { NormalInterceptGroup = 1, - SpecialInterceptId = 1, // TODO switch this out each reset + SpecialInterceptId = specialId, TicketCount = User.ResetableData.InterceptionTickets, MaxTicketCount = JsonDb.Instance.MaxInterceptionCount }; await WriteDataAsync(response); } + + private int GetCurrentInterceptionIds() + { + var specialTable = GameData.Instance.InterceptSpecial; + var specialBosses = specialTable.Values.Where(x => x.Group == 1).OrderBy(x => x.Order).ToList(); + + var dayOfYear = DateTime.UtcNow.DayOfYear; + var specialIndex = dayOfYear % specialBosses.Count; + + var specialId = specialBosses[specialIndex].Id; + return specialId; + + } } } diff --git a/EpinelPS/LobbyServer/Inventory/AllClearEquipment.cs b/EpinelPS/LobbyServer/Inventory/AllClearEquipment.cs new file mode 100644 index 0000000..08afcb8 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/AllClearEquipment.cs @@ -0,0 +1,60 @@ +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/allclearequipment")] + public class AllClearEquipment : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAllClearEquipment req = await ReadData(); + User user = GetUser(); + + ResAllClearEquipment response = new() + { + Csn = req.Csn + }; + + foreach (ItemData item in user.Items.ToArray()) + { + if (item.Csn == req.Csn) + { + // Check if the item being unequipped is T10 + if (IsT10Equipment(item.ItemType)) + { + + response.Items.Add(NetUtils.ToNet(item)); + continue; + } + + item.Csn = 0; + item.Position = 0; + response.Items.Add(NetUtils.ToNet(item)); + } + } + + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private bool IsT10Equipment(int itemTypeId) + { + // Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01 + // T10 equipment has rarity "10" in positions 3-4 (0-based indexing) for 7-digit IDs + string itemTypeStr = itemTypeId.ToString(); + + // Check if this is an equipment item (starts with 3) and has 7 digits + if (itemTypeStr.Length == 7 && itemTypeStr[0] == '3') + { + // Extract the rarity part (positions 3-4) + string rarityPart = itemTypeStr.Substring(3, 2); + return rarityPart == "10"; + } + + return false; + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/ClearAllEquipment.cs b/EpinelPS/LobbyServer/Inventory/ClearAllEquipment.cs deleted file mode 100644 index 653cc8a..0000000 --- a/EpinelPS/LobbyServer/Inventory/ClearAllEquipment.cs +++ /dev/null @@ -1,35 +0,0 @@ -using EpinelPS.Database; -using EpinelPS.Utils; - -namespace EpinelPS.LobbyServer.Inventory -{ - [PacketPath("/inventory/allclearequipment")] - public class ClearAllEquipment : LobbyMsgHandler - { - protected override async Task HandleAsync() - { - ReqAllClearEquipment req = await ReadData(); - User user = GetUser(); - - ResAllClearEquipment response = new() - { - Csn = req.Csn - }; - - foreach (ItemData item in user.Items.ToArray()) - { - if (item.Csn == req.Csn) - { - // update character Id - item.Csn = 0; - - response.Items.Add(NetUtils.ToNet(item)); - } - } - - JsonDb.Save(); - - await WriteDataAsync(response); - } - } -} diff --git a/EpinelPS/LobbyServer/Inventory/GetInventoryData.cs b/EpinelPS/LobbyServer/Inventory/GetInventoryData.cs index a174b48..3493415 100644 --- a/EpinelPS/LobbyServer/Inventory/GetInventoryData.cs +++ b/EpinelPS/LobbyServer/Inventory/GetInventoryData.cs @@ -1,5 +1,5 @@ using EpinelPS.Utils; - +using EpinelPS.Data; namespace EpinelPS.LobbyServer.Inventory { [PacketPath("/inventory/get")] @@ -13,9 +13,36 @@ namespace EpinelPS.LobbyServer.Inventory ResGetInventoryData response = new(); foreach (ItemData item in user.Items) { + + ItemSubType itemSubType = GameData.Instance.GetItemSubType(item.ItemType); + if (itemSubType == ItemSubType.HarmonyCube) + { + NetUserHarmonyCubeData harmonyCubeData = new NetUserHarmonyCubeData() + { + Tid = item.ItemType, + Lv = item.Level, + Isn = item.Isn + }; + harmonyCubeData.CsnList.AddRange(item.CsnList); + response.HarmonyCubes.Add(harmonyCubeData); + continue; + } response.Items.Add(new NetUserItemData() { Count = item.Count, Tid = item.ItemType, Csn = item.Csn, Lv = item.Level, Exp = item.Exp, Corporation = item.Corp, Isn = item.Isn, Position = item.Position }); + } - // TODO: HarmonyCubes, RunAwakeningIsnList, UserRedeems + + + // Add all equipment awakenings + foreach (EquipmentAwakeningData awakening in user.EquipmentAwakenings) + { + response.Awakenings.Add(new NetEquipmentAwakening() + { + Isn = awakening.Isn, + Option = awakening.Option + }); + } + // TODO: UserRedeems + // Note: HarmonyCubes are now included in the Items list above await WriteDataAsync(response); } diff --git a/EpinelPS/LobbyServer/Inventory/WearEquipmentList.cs b/EpinelPS/LobbyServer/Inventory/WearEquipmentList.cs index 0025157..32ef5f4 100644 --- a/EpinelPS/LobbyServer/Inventory/WearEquipmentList.cs +++ b/EpinelPS/LobbyServer/Inventory/WearEquipmentList.cs @@ -1,5 +1,7 @@ using EpinelPS.Database; using EpinelPS.Utils; +using EpinelPS.Data; +using System.Linq; namespace EpinelPS.LobbyServer.Inventory { @@ -13,27 +15,120 @@ namespace EpinelPS.LobbyServer.Inventory ResWearEquipmentList response = new(); - // TODO optimize foreach (long item2 in req.IsnList) { int pos = NetUtils.GetItemPos(user, item2); + // Check if the item being equipped is T10 + ItemData? itemToCheck = user.Items.FirstOrDefault(x => x.Isn == item2); + if (itemToCheck != null && IsT10Equipment(itemToCheck.ItemType)) + { + // If trying to equip a T10 item, check if there's already a T10 item in that position + bool hasT10InPosition = user.Items.Any(x => x.Position == pos && x.Csn == req.Csn && IsT10Equipment(x.ItemType)); + if (hasT10InPosition) + { + // Don't allow replacing T10 equipment + continue; + } + } + + // Check if item still exists after previous operations + itemToCheck = user.Items.FirstOrDefault(x => x.Isn == item2); + if (itemToCheck == null) + { + // Item no longer exists, skip this iteration + continue; + } + // unequip previous items foreach (ItemData item in user.Items.ToArray()) { if (item.Position == pos && item.Csn == req.Csn) { + // Check if the item being unequipped is T10 + if (IsT10Equipment(item.ItemType)) + { + continue; + } + item.Csn = 0; item.Position = 0; + response.Items.Add(NetUtils.ToNet(item)); } } - foreach (ItemData item in user.Items.ToArray()) + // Find the item to equip + ItemData? targetItem = user.Items.FirstOrDefault(x => x.Isn == item2); + if (targetItem != null) { - if (item2 == item.Isn) + // Handle case where we have multiple copies of the same item + ItemData? equippedItem = null; + if (targetItem.Count > 1) + { + // Reduce count of original item + targetItem.Count--; + response.Items.Add(NetUtils.ToNet(targetItem)); + + // Create a new item instance to equip + equippedItem = new ItemData + { + ItemType = targetItem.ItemType, + Isn = user.GenerateUniqueItemId(), + Level = targetItem.Level, + Exp = targetItem.Exp, + Count = 1, + Corp = targetItem.Corp, + Position = pos // Set the position for the new item + }; + user.Items.Add(equippedItem); + } + else + { + // Use the existing item + equippedItem = targetItem; + } + + // equip the item + equippedItem.Csn = req.Csn; + equippedItem.Position = pos; + response.Items.Add(NetUtils.ToNet(equippedItem)); + } + } + + // Ensure all requested items are in the response + // This helps the client track the specific items that were requested + foreach (long requestedIsn in req.IsnList) + { + bool requestedItemAdded = response.Items.Any(x => x.Isn == requestedIsn); + if (!requestedItemAdded) + { + ItemData? requestedItem = user.Items.FirstOrDefault(x => x.Isn == requestedIsn); + if (requestedItem != null) + { + response.Items.Add(NetUtils.ToNet(requestedItem)); + } + else + { + // If item not found, add it with count 0 to indicate it was processed + response.Items.Add(new NetUserItemData() + { + Isn = requestedIsn, + Count = 0 + }); + } + } + } + + // Add all other equipped items for this character to the response + // This helps the client synchronize the full equipment state + foreach (ItemData item in user.Items) + { + if (item.Csn == req.Csn && item.Csn != 0) + { + // Check if this item was already added in the loop above + bool alreadyAdded = response.Items.Any(x => x.Isn == item.Isn); + if (!alreadyAdded) { - item.Csn = req.Csn; - item.Position = pos; response.Items.Add(NetUtils.ToNet(item)); } } @@ -43,5 +138,22 @@ namespace EpinelPS.LobbyServer.Inventory await WriteDataAsync(response); } + + private bool IsT10Equipment(int itemTypeId) + { + // Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01 + // T10 equipment has rarity "10" in positions 3-4 (0-based indexing) for 7-digit IDs + string itemTypeStr = itemTypeId.ToString(); + + // Check if this is an equipment item (starts with 3) and has 7 digits + if (itemTypeStr.Length == 7 && itemTypeStr[0] == '3') + { + // Extract the rarity part (positions 3-4) + string rarityPart = itemTypeStr.Substring(3, 2); + return rarityPart == "10"; + } + + return false; + } } } diff --git a/EpinelPS/LobbyServer/Inventory/equipment/Awakening.cs b/EpinelPS/LobbyServer/Inventory/equipment/Awakening.cs new file mode 100644 index 0000000..3e9189d --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/Awakening.cs @@ -0,0 +1,350 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/awakening")] + public class AwakeningEquipment : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqEquipmentAwakening req = await ReadData(); + User user = GetUser(); + + ResEquipmentAwakening response = new(); + + ItemData? equipmentToAwaken = user.Items.FirstOrDefault(x => x.Isn == req.Isn); + if (equipmentToAwaken == null) + { + await WriteDataAsync(response); + return; + } + + int materialCost = 1; + int materialId = 7080001; // Equipment option material + + ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId); + if (material == null || material.Count < materialCost) + { + await WriteDataAsync(response); + return; + } + + if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items)) + { + await WriteDataAsync(response); + return; + } + + int awakenedEquipmentTypeId = GetAwakenedEquipmentTypeId(equipmentToAwaken.ItemType); + + equipmentToAwaken.ItemType = awakenedEquipmentTypeId; + equipmentToAwaken.Level = 0; + equipmentToAwaken.Exp = 0; + equipmentToAwaken.Corp = 0; + + Random rng = new Random(); + + NetEquipmentAwakeningOption awakeningOption = new NetEquipmentAwakeningOption() + { + Option1Lock = false, + IsOption1DisposableLock = false, + Option2Lock = false, + IsOption2DisposableLock = false, + Option3Lock = false, + IsOption3DisposableLock = false + }; + + if (GameData.Instance.ItemEquipTable.TryGetValue(awakenedEquipmentTypeId, out ItemEquipRecord? equipRecord)) + { + try + { + GenerateAwakeningOptions(awakeningOption, equipRecord, rng); + } + catch (InvalidOperationException ex) + { + Logging.WriteLine($"Failed to generate awakening options: {ex.Message}", LogType.Error); + await WriteDataAsync(response); + return; + } + } + + user.EquipmentAwakenings.Add(new EquipmentAwakeningData() + { + Isn = equipmentToAwaken.Isn, + Option = awakeningOption, + IsNewData = false + }); + + response.Awakening = new NetEquipmentAwakening() + { + Isn = equipmentToAwaken.Isn, + Option = awakeningOption + }; + + response.Items.Add(NetUtils.ToNet(equipmentToAwaken)); + + JsonDb.Save(); + await WriteDataAsync(response); + } + + private int GetAwakenedEquipmentTypeId(int originalTypeId) + { + // Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01 + // Awakening changes T9 equipment (09) to T10 equipment (10) + return originalTypeId switch + { + // Attacker equipment awakening + 3110901 => 3111001, // Head T9 -> T10 + 3210901 => 3211001, // Body T9 -> T10 + 3310901 => 3311001, // Arm T9 -> T10 + 3410901 => 3411001, // Leg T9 -> T10 + + // Defender equipment awakening + 3120901 => 3121001, // Head T9 -> T10 + 3220901 => 3221001, // Body T9 -> T10 + 3320901 => 3321001, // Arm T9 -> T10 + 3420901 => 3421001, // Leg T9 -> T10 + + // Supporter equipment awakening + 3130901 => 3131001, // Head T9 -> T10 + 3230901 => 3231001, // Body T9 -> T10 + 3330901 => 3331001, // Arm T9 -> T10 + 3430901 => 3431001, // Leg T9 -> T10 + + // Default return original ID (awakening not supported) + _ => originalTypeId + }; + } + + private void GenerateAwakeningOptions(NetEquipmentAwakeningOption option, ItemEquipRecord equipRecord, Random rng) + { + List excludedStateEffectIds = new List(); + + // 1.0 = 100% chance for slot 1, 0.5 = 50% chance for slot 2, 0.3 = 30% chance for slot 3 + double[] slotActivationProbabilities = { 1.0, 0.5, 0.3 }; + + for (int i = 1; i <= 3; i++) + { + bool shouldActivateSlot = rng.NextDouble() < slotActivationProbabilities[i - 1]; + + if (shouldActivateSlot) + { + int selectedOptionId = GenerateNewOptionIdInit(excludedStateEffectIds, 11); + AddOptionToExclusionList(selectedOptionId, excludedStateEffectIds); + + switch (i) + { + case 1: + option.Option1Id = selectedOptionId; + break; + case 2: + option.Option2Id = selectedOptionId; + break; + case 3: + option.Option3Id = selectedOptionId; + break; + } + } + else + { + switch (i) + { + case 1: + option.Option1Id = 0; + break; + case 2: + option.Option2Id = 0; + break; + case 3: + option.Option3Id = 0; + break; + } + } + } + + option.Option1Lock = false; + option.IsOption1DisposableLock = false; + option.Option2Lock = false; + option.IsOption2DisposableLock = false; + option.Option3Lock = false; + option.IsOption3DisposableLock = false; + } + + private int GenerateNewOptionIdInit(List excludedStateEffectIds, int level) + { + List allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values + .Where(x => x.EquipmentOptionGroupId == 100000 && + x.StateEffectList?.Any(se => se.StateEffectLevel == level) == true) + .ToList(); + + HashSet excludedEffectGroupIds = new HashSet(); + + foreach (int stateEffectId in excludedStateEffectIds) + { + EquipmentOptionRecord? excludedOption = GameData.Instance.EquipmentOptionTable.Values + .FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == stateEffectId)); + if (excludedOption != null) + { + excludedEffectGroupIds.Add(excludedOption.StateEffectGroupId); + } + } + + List availableOptions = allAwakeningOptions + .Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId)) + .ToList(); + + Dictionary> optionsByEffectGroup = availableOptions + .GroupBy(option => option.StateEffectGroupId) + .ToDictionary(g => g.Key, g => g.ToList()); + + if (optionsByEffectGroup.Count == 0) + { + throw new InvalidOperationException("No available equipment options for awakening - this indicates a data consistency issue"); + } + + double excludedProbabilitySum = CalculateExcludedProbabilitySumByEffectGroup(excludedEffectGroupIds, level); + + List weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum); + + int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups); + + List optionsInSelectedGroup = optionsByEffectGroup[selectedEffectGroupId]; + + foreach (EquipmentOptionRecord option in optionsInSelectedGroup) + { + StateEffectList? stateEffect = option.StateEffectList?.FirstOrDefault(se => se.StateEffectLevel == level); + if (stateEffect != null) + { + return stateEffect.StateEffectId; + } + } + + throw new InvalidOperationException($"No state effect found with level {level} in selected effect group - this indicates a data consistency issue"); + } + + /// + /// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules + /// + /// List of excluded state_effect_group_ids + /// The level of options to consider + /// Sum of base probabilities (as decimal percentage) + private double CalculateExcludedProbabilitySumByEffectGroup(HashSet excludedEffectGroupIds, int level) + { + if (excludedEffectGroupIds.Count == 0) + { + return 0.0; + } + + double totalExcluded = 0.0; + + foreach (int effectGroupId in excludedEffectGroupIds) + { + List options = GameData.Instance.EquipmentOptionTable.Values + .Where(opt => opt.StateEffectGroupId == effectGroupId && + opt.EquipmentOptionGroupId == 100000 && + opt.StateEffectList?.Any(se => se.StateEffectLevel == level) == true) + .ToList(); + + if (options.Count > 0) + { + EquipmentOptionRecord firstOption = options.First(); + totalExcluded += firstOption.OptionGroupRatio / 100.0; + } + } + + return Math.Min(totalExcluded, 99.9); // Ensure we don't reach 100% to avoid division by zero + } + + /// + /// Helper class to store effect group with its calculated weight for probability selection + /// + public class EffectGroupWithWeight + { + public int EffectGroupId { get; set; } + public double Weight { get; set; } + public double BaseProbability { get; set; } + public double DynamicProbability { get; set; } + } + + /// + /// Calculates dynamic probabilities for available effect groups using the formula: + /// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities) + /// + /// Dictionary of available options grouped by effect group ID + /// Sum of probabilities of excluded effects + /// List of weighted effect groups for random selection + private List CalculateDynamicProbabilitiesForEffectGroups(Dictionary> optionsByEffectGroup, double excludedProbabilitySum) + { + List weightedEffectGroups = new List(); + + double probabilityDenominator = 100.0 - excludedProbabilitySum; + + if (probabilityDenominator <= 0) + { + probabilityDenominator = 1.0; + } + foreach (KeyValuePair> kvp in optionsByEffectGroup) + { + int effectGroupId = kvp.Key; + List options = kvp.Value; + + if (options.Count > 0) + { + EquipmentOptionRecord firstOption = options.First(); + double baseProbability = (double)firstOption.OptionGroupRatio / 100.0; + + double dynamicProbability = baseProbability / probabilityDenominator; + + double selectionWeight = dynamicProbability * 1000000; + + weightedEffectGroups.Add(new EffectGroupWithWeight + { + EffectGroupId = effectGroupId, + Weight = selectionWeight, + BaseProbability = baseProbability, + DynamicProbability = dynamicProbability + }); + } + } + + return weightedEffectGroups; + } + + /// + /// Selects an effect group randomly based on calculated weights + /// + /// List of weighted effect groups + /// Selected effect_group_id + private int SelectWeightedRandomEffectGroup(List weightedEffectGroups) + { + Random random = new Random(); + double totalWeight = weightedEffectGroups.Sum(weg => weg.Weight); + + double randomValue = random.NextDouble() * totalWeight; + + double cumulativeWeight = 0.0; + foreach (EffectGroupWithWeight weightedGroup in weightedEffectGroups) + { + cumulativeWeight += weightedGroup.Weight; + if (randomValue <= cumulativeWeight) + { + return weightedGroup.EffectGroupId; + } + } + + return weightedEffectGroups.Last().EffectGroupId; + } + + + private void AddOptionToExclusionList(int optionId, List excludedStateEffectIds) + { + if (!excludedStateEffectIds.Contains(optionId)) + { + excludedStateEffectIds.Add(optionId); + } + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Inventory/equipment/ChangeOption.cs b/EpinelPS/LobbyServer/Inventory/equipment/ChangeOption.cs new file mode 100644 index 0000000..b8303b0 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/ChangeOption.cs @@ -0,0 +1,117 @@ +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/changeoption")] + public class ChangeOption : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAwakeningChangeOption req = await ReadData(); + User user = GetUser(); + + ResAwakeningChangeOption response = new ResAwakeningChangeOption(); + + if (req.Isn <= 0) + { + await WriteDataAsync(response); + return; + } + + EquipmentAwakeningData? oldAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn && !x.IsNewData); + + if (oldAwakening == null) + { + oldAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + await WriteDataAsync(response); + return; + } + + List duplicates = user.EquipmentAwakenings.Where(x => x.Isn == req.Isn).ToList(); + + if (req.IsChanged) + { + if (duplicates.Count > 1) + { + List oldEntries = duplicates.Where(x => !x.IsNewData).ToList(); + foreach (EquipmentAwakeningData oldEntry in oldEntries) + { + user.EquipmentAwakenings.Remove(oldEntry); + } + + EquipmentAwakeningData? newEntry = duplicates.FirstOrDefault(x => x.IsNewData); + if (newEntry != null) + { + newEntry.IsNewData = false; + } + } + else if (duplicates.Count == 1) + { + EquipmentAwakeningData singleEntry = duplicates[0]; + if (singleEntry.IsNewData) + { + singleEntry.IsNewData = false; + } + } + + EquipmentAwakeningData? confirmedAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (confirmedAwakening != null) + { + response.Awakening = new NetEquipmentAwakening() + { + Isn = confirmedAwakening.Isn, + Option = new NetEquipmentAwakeningOption() + { + Option1Id = confirmedAwakening.Option.Option1Id, + Option1Lock = confirmedAwakening.Option.Option1Lock, + IsOption1DisposableLock = confirmedAwakening.Option.IsOption1DisposableLock, + Option2Id = confirmedAwakening.Option.Option2Id, + Option2Lock = confirmedAwakening.Option.Option2Lock, + IsOption2DisposableLock = confirmedAwakening.Option.IsOption2DisposableLock, + Option3Id = confirmedAwakening.Option.Option3Id, + Option3Lock = confirmedAwakening.Option.Option3Lock, + IsOption3DisposableLock = confirmedAwakening.Option.IsOption3DisposableLock + } + }; + } + } + else + { + if (duplicates.Count > 1) + { + List newEntries = duplicates.Where(x => x.IsNewData).ToList(); + foreach (EquipmentAwakeningData newEntry in newEntries) + { + user.EquipmentAwakenings.Remove(newEntry); + } + } + + EquipmentAwakeningData? originalAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (originalAwakening != null) + { + response.Awakening = new NetEquipmentAwakening() + { + Isn = originalAwakening.Isn, + Option = new NetEquipmentAwakeningOption() + { + Option1Id = originalAwakening.Option.Option1Id, + Option1Lock = originalAwakening.Option.Option1Lock, + IsOption1DisposableLock = originalAwakening.Option.IsOption1DisposableLock, + Option2Id = originalAwakening.Option.Option2Id, + Option2Lock = originalAwakening.Option.Option2Lock, + IsOption2DisposableLock = originalAwakening.Option.IsOption2DisposableLock, + Option3Id = originalAwakening.Option.Option3Id, + Option3Lock = originalAwakening.Option.Option3Lock, + IsOption3DisposableLock = originalAwakening.Option.IsOption3DisposableLock + } + }; + } + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Inventory/equipment/GetAwakeningDetail.cs b/EpinelPS/LobbyServer/Inventory/equipment/GetAwakeningDetail.cs new file mode 100644 index 0000000..0687060 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/GetAwakeningDetail.cs @@ -0,0 +1,174 @@ +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/getawakeningdetail")] + public class GetAwakeningDetail : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqGetAwakeningDetail req = await ReadData(); + User user = GetUser(); + + ResGetAwakeningDetail response = new ResGetAwakeningDetail(); + + // Validate input parameters + if (req.Isn <= 0) + { + await WriteDataAsync(response); + return; + } + + // Find the equipment awakening data + EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (awakening == null) + { + await WriteDataAsync(response); + return; + } + + // Set current options + response.CurrentOption = new NetEquipmentAwakeningOption() + { + Option1Id = awakening.Option.Option1Id, + Option1Lock = awakening.Option.Option1Lock, + IsOption1DisposableLock = awakening.Option.IsOption1DisposableLock, + Option2Id = awakening.Option.Option2Id, + Option2Lock = awakening.Option.Option2Lock, + IsOption2DisposableLock = awakening.Option.IsOption2DisposableLock, + Option3Id = awakening.Option.Option3Id, + Option3Lock = awakening.Option.Option3Lock, + IsOption3DisposableLock = awakening.Option.IsOption3DisposableLock + }; + + NetEquipmentAwakeningOption newOption = new NetEquipmentAwakeningOption(); + + // Process each option slot (1, 2, 3) + for (int i = 1; i <= 3; i++) + { + // Get current option ID for this slot + int currentOptionId = GetOptionIdForSlot(awakening.Option, i); + bool isLocked = IsOptionLocked(awakening.Option, i); + bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i); + + // If option is permanently locked or disposable locked, keep it unchanged + if (isLocked || isDisposableLocked) + { + // Keep the current option unchanged + SetOptionForSlot(newOption, i, currentOptionId, isLocked, isDisposableLocked); + continue; + } + + // If not locked, generate a new option + int newOptionId = GenerateNewOptionId(currentOptionId); + SetOptionForSlot(newOption, i, newOptionId, false, false); + } + + response.NewOption = newOption; + + await WriteDataAsync(response); + } + + private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Id, + 2 => option.Option2Id, + 3 => option.Option3Id, + _ => 0 + }; + } + + private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Lock, + 2 => option.Option2Lock, + 3 => option.Option3Lock, + _ => false + }; + } + + private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.IsOption1DisposableLock, + 2 => option.IsOption2DisposableLock, + 3 => option.IsOption3DisposableLock, + _ => false + }; + } + + private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked) + { + switch (slot) + { + case 1: + option.Option1Id = optionId; + option.Option1Lock = locked; + option.IsOption1DisposableLock = disposableLocked; + break; + case 2: + option.Option2Id = optionId; + option.Option2Lock = locked; + option.IsOption2DisposableLock = disposableLocked; + break; + case 3: + option.Option3Id = optionId; + option.Option3Lock = locked; + option.IsOption3DisposableLock = disposableLocked; + break; + } + } + + private int GenerateNewOptionId(int currentOptionId) + { + // Get the current option record + if (!GameData.Instance.EquipmentOptionTable.TryGetValue(currentOptionId, out EquipmentOptionRecord? currentOption)) + { + return currentOptionId; + } + + // Get the group ID of the current option + int groupId = currentOption.EquipmentOptionGroupId; + + // Find all options in the same group + List optionsInGroup = GameData.Instance.EquipmentOptionTable.Values + .Where(x => x.EquipmentOptionGroupId == groupId) + .ToList(); + + if (optionsInGroup.Count == 0) + { + return currentOptionId; + } + + // Calculate total ratio for probability calculation + long totalRatio = optionsInGroup.Sum(x => (long)x.OptionRatio); + + if (totalRatio == 0) + { + return currentOptionId; + } + + // Select a new option based on probability + Random random = new Random(); + long randomValue = random.NextInt64(0, totalRatio); + long cumulativeRatio = 0; + + foreach (EquipmentOptionRecord option in optionsInGroup) + { + cumulativeRatio += option.OptionRatio; + if (randomValue < cumulativeRatio) + { + return option.Id; + } + } + + return currentOptionId; + } + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Inventory/equipment/LockOption.cs b/EpinelPS/LobbyServer/Inventory/equipment/LockOption.cs new file mode 100644 index 0000000..92c8d06 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/LockOption.cs @@ -0,0 +1,137 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/lockoption")] + public class LockOption : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAwakeningLockOption req = await ReadData(); + User user = GetUser(); + + ResAwakeningLockOption response = new ResAwakeningLockOption(); + EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (awakening == null) + { + await WriteDataAsync(response); + return; + } + + int slot = 0; + if (awakening.Option.Option1Id == req.OptionId) + slot = 1; + else if (awakening.Option.Option2Id == req.OptionId) + slot = 2; + else if (awakening.Option.Option3Id == req.OptionId) + slot = 3; + + (int materialId, int materialCost) = GetMaterialInfoForAwakening(awakening.Option); + + + ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId); + if (material == null || material.Count < materialCost) + { + await WriteDataAsync(response); + return; + } + + UpdateLockStatus(awakening.Option, slot, req.IsLocked); + + if (req.IsLocked) + { + if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items)) + { + await WriteDataAsync(response); + return; + } + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private static int CalculateMaterialCost(NetEquipmentAwakeningOption option) + { + int lockedOptionCount = 0; + int disposableLockOptionCount = 0; + // Count already permanently locked options (not disposable locks) + if (option.Option1Id != 0 && option.Option1Lock && !option.IsOption1DisposableLock) + lockedOptionCount++; + if (option.Option2Id != 0 && option.Option2Lock && !option.IsOption2DisposableLock) + lockedOptionCount++; + if (option.Option3Id != 0 && option.Option3Lock && !option.IsOption3DisposableLock) + lockedOptionCount++; + + if (option.Option1Id != 0 && option.Option1Lock && option.IsOption1DisposableLock) + disposableLockOptionCount++; + if (option.Option2Id != 0 && option.Option2Lock && option.IsOption2DisposableLock) + disposableLockOptionCount++; + if (option.Option3Id != 0 && option.Option3Lock && option.IsOption3DisposableLock) + disposableLockOptionCount++; + + return GetPermanentLockCostId(lockedOptionCount,disposableLockOptionCount); + } + + private static int GetPermanentLockCostId(int lockedOptionCount,int disposableLockOptionCount) + { + // For permanent locks, use cost_group_id 100 + EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values + .FirstOrDefault(x => x.CostGroupId == 100 && x.CostLevel == lockedOptionCount && x.DisposableFixCostLevel == disposableLockOptionCount); + + int costId = costRecord?.CostId ?? 101001; + + return costId; + } + + private static void UpdateLockStatus(NetEquipmentAwakeningOption option, int slot, bool isLocked) + { + switch (slot) + { + case 1: + option.Option1Lock = isLocked; + if (isLocked) + { + option.IsOption1DisposableLock = false; + } + break; + case 2: + option.Option2Lock = isLocked; + if (isLocked) + { + option.IsOption2DisposableLock = false; + } + break; + case 3: + option.Option3Lock = isLocked; + if (isLocked) + { + option.IsOption3DisposableLock = false; + } + break; + } + } + + private static (int materialId, int materialCost) GetMaterialInfoForAwakening(NetEquipmentAwakeningOption option) + { + int costId = CalculateMaterialCost(option); + return GetMaterialInfo(costId); + } + + private static (int materialId, int materialCost) GetMaterialInfo(int costId) + { + if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) && + costRecord?.Costs != null && + costRecord.Costs.Count > 0) + { + return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue); + } + return (7080001, 2); // Default material ID and cost + } + + } +} \ No newline at end of file diff --git a/EpinelPS/LobbyServer/Inventory/equipment/ResetOption.cs b/EpinelPS/LobbyServer/Inventory/equipment/ResetOption.cs new file mode 100644 index 0000000..9f42a7c --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/ResetOption.cs @@ -0,0 +1,475 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/resetoption")] + public class ResetOption : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAwakeningResetOption req = await ReadData(); + User user = GetUser(); + + ResAwakeningResetOption response = new ResAwakeningResetOption(); + + EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (awakening == null) + { + await WriteDataAsync(response); + return; + } + + NetEquipmentAwakeningOption resetOption = new NetEquipmentAwakeningOption(); + Random random = new Random(); + + (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo = new (int optionId, bool isLocked, bool isDisposableLocked)[3]; + List lockedOptionStateEffectIds = new List(); + + int lockedOptionCount = 0; + for (int i = 1; i <= 3; i++) + { + int currentOptionId = GetOptionIdForSlot(awakening.Option, i); + bool isLocked = IsOptionLocked(awakening.Option, i); + bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i); + + slotLockInfo[i - 1] = (currentOptionId, isLocked, isDisposableLocked); + + // Count locked options for material cost calculation + if (isLocked || isDisposableLocked) + lockedOptionCount++; + + // Collect locked options for exclusion list + if (isLocked && currentOptionId != 0) + { + AddOptionToExclusionList(currentOptionId, lockedOptionStateEffectIds); + } + } + + int costId = GetCostIdByLockedOptionCount(lockedOptionCount); + + (int materialId, int materialCost) = GetMaterialInfo(costId); + + // Check if user has enough materials + ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId); + if (material == null || material.Count < materialCost) + { + Logging.WriteLine($"Insufficient materials for reset operation. Need {materialCost} of item {materialId}, but have {material?.Count ?? 0}", LogType.Warning); + await WriteDataAsync(response); + return; + } + + // Deduct materials for reset + if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items)) + { + Logging.WriteLine($"Insufficient materials for reset operation. Need {materialCost} of item {materialId}, but have {material?.Count ?? 0}", LogType.Warning); + await WriteDataAsync(response); + return; + } + + // Process each option slot (1, 2, 3) + ProcessOptionSlots(awakening, resetOption, slotLockInfo, lockedOptionStateEffectIds); + + // Create a new awakening entry with the same ISN to preserve the old data + EquipmentAwakeningData newAwakening = new EquipmentAwakeningData() + { + Isn = awakening.Isn, + Option = new NetEquipmentAwakeningOption() + { + Option1Id = resetOption.Option1Id, + Option1Lock = resetOption.Option1Lock, + IsOption1DisposableLock = resetOption.IsOption1DisposableLock, + Option2Id = resetOption.Option2Id, + Option2Lock = resetOption.Option2Lock, + IsOption2DisposableLock = resetOption.IsOption2DisposableLock, + Option3Id = resetOption.Option3Id, + Option3Lock = resetOption.Option3Lock, + IsOption3DisposableLock = resetOption.IsOption3DisposableLock + }, + IsNewData = true + }; + + user.EquipmentAwakenings.Add(newAwakening); + + // Add the reset options to the response + response.ResetOption = new NetEquipmentAwakeningOption() + { + Option1Id = resetOption.Option1Id, + Option1Lock = resetOption.Option1Lock, + IsOption1DisposableLock = resetOption.IsOption1DisposableLock, + Option2Id = resetOption.Option2Id, + Option2Lock = resetOption.Option2Lock, + IsOption2DisposableLock = resetOption.IsOption2DisposableLock, + Option3Id = resetOption.Option3Id, + Option3Lock = resetOption.Option3Lock, + IsOption3DisposableLock = resetOption.IsOption3DisposableLock + }; + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + + private void ProcessOptionSlots(EquipmentAwakeningData awakening, NetEquipmentAwakeningOption resetOption, (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo, List lockedOptionStateEffectIds) + { + Random random = new Random(); + // 1.0 = 100% chance for slot 1, 0.5 = 50% chance for slot 2, 0.3 = 30% chance for slot 3 + double[] slotActivationProbabilities = { 1.0, 0.5, 0.3 }; + + for (int i = 1; i <= 3; i++) + { + (int currentOptionId, bool isLocked, bool isDisposableLocked) = slotLockInfo[i - 1]; + + if (isLocked && !isDisposableLocked) + { + SetOptionForSlot(resetOption, i, currentOptionId, true, false); + } + else if (isLocked && isDisposableLocked) + { + SetOptionForSlot(resetOption, i, currentOptionId, false, false); + + UnlockDisposableOption(awakening.Option, i); + } + else + { + bool shouldActivateSlot = random.NextDouble() < slotActivationProbabilities[i - 1]; + + if (shouldActivateSlot) + { + // Generate new option using non-repeating system with dynamic probability + int newOptionId = GenerateNewOptionIdWithDynamicProbability(lockedOptionStateEffectIds); + SetOptionForSlot(resetOption, i, newOptionId, false, false); + + // Add the new option to locked list to prevent duplicates in subsequent slots + if (newOptionId != 0) + { + AddOptionToExclusionList(newOptionId, lockedOptionStateEffectIds); + } + } + else + { + SetOptionForSlot(resetOption, i, 0, false, false); + } + } + } + } + + private void UnlockDisposableOption(NetEquipmentAwakeningOption option, int slot) + { + SetOptionForSlot(option, slot, GetOptionIdForSlot(option, slot), false, false); + } + + private int GetCostIdByLockedOptionCount(int lockedOptionCount) + { + if (lockedOptionCount == 0) + { + return 100001; + } + + EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values + .FirstOrDefault(x => x.CostGroupId == 200 && x.CostLevel == lockedOptionCount); + + return costRecord?.CostId ?? 100001; + } + + + private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Id, + 2 => option.Option2Id, + 3 => option.Option3Id, + _ => 0 + }; + } + + + private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Lock, + 2 => option.Option2Lock, + 3 => option.Option3Lock, + _ => false + }; + } + + private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.IsOption1DisposableLock, + 2 => option.IsOption2DisposableLock, + 3 => option.IsOption3DisposableLock, + _ => false + }; + } + + private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked) + { + switch (slot) + { + case 1: + option.Option1Id = optionId; + option.Option1Lock = locked; + option.IsOption1DisposableLock = disposableLocked; + break; + case 2: + option.Option2Id = optionId; + option.Option2Lock = locked; + option.IsOption2DisposableLock = disposableLocked; + break; + case 3: + option.Option3Id = optionId; + option.Option3Lock = locked; + option.IsOption3DisposableLock = disposableLocked; + break; + } + } + + + private void AddOptionToExclusionList(int optionId, List lockedOptionStateEffectIds) + { + // Since optionId is already a state_effect_id, we can directly add it to the exclusion list + if (!lockedOptionStateEffectIds.Contains(optionId)) + { + lockedOptionStateEffectIds.Add(optionId); + + // Also get the effect group ID for this state_effect_id to exclude the effect group + EquipmentOptionRecord? optionRecord = GameData.Instance.EquipmentOptionTable.Values + .FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == optionId)); + } + } + + + + + /// + /// Generates a new option ID using the Overload system's non-repeating effect types and dynamic probability formula + /// + /// List of state_effect_ids that are already taken and should be excluded + /// A new state_effect_id or 0 if none available + private int GenerateNewOptionIdWithDynamicProbability(List excludedStateEffectIds) + { + // Get all awakening options (equipment_option_group_id == 100000) + List allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values + .Where(x => x.EquipmentOptionGroupId == 100000) + .ToList(); + + // Filter out options that have already been taken (non-repeating principle) + HashSet excludedEffectGroupIds = new HashSet(); + + // Get the effect group IDs for all excluded state effect IDs + foreach (int stateEffectId in excludedStateEffectIds) + { + EquipmentOptionRecord? excludedOption = GameData.Instance.EquipmentOptionTable.Values + .FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == stateEffectId)); + if (excludedOption != null) + { + excludedEffectGroupIds.Add(excludedOption.StateEffectGroupId); + } + } + + List availableOptions = allAwakeningOptions + .Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId)) + .ToList(); + + Dictionary> optionsByEffectGroup = availableOptions + .GroupBy(option => option.StateEffectGroupId) + .ToDictionary(g => g.Key, g => g.ToList()); + + double excludedProbabilitySum = CalculateExcludedProbabilitySumByEffectGroup(excludedEffectGroupIds); + + List weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum); + int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups); + + List optionsInSelectedGroup = optionsByEffectGroup[selectedEffectGroupId]; + int selectedStateEffectId = SelectOptionFromGroup(optionsInSelectedGroup); + return selectedStateEffectId; + } + + + + /// + /// Helper class to store effect group with its calculated weight for probability selection + /// + public class EffectGroupWithWeight + { + public int EffectGroupId { get; set; } + public double Weight { get; set; } + public double BaseProbability { get; set; } + public double DynamicProbability { get; set; } + } + + /// + /// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules + /// + /// List of excluded state_effect_group_ids + /// Sum of base probabilities (as decimal percentage) + private double CalculateExcludedProbabilitySumByEffectGroup(HashSet excludedEffectGroupIds) + { + if (excludedEffectGroupIds.Count == 0) + { + return 0.0; + } + + double totalExcluded = 0.0; + + foreach (int effectGroupId in excludedEffectGroupIds) + { + List options = GameData.Instance.EquipmentOptionTable.Values + .Where(opt => opt.StateEffectGroupId == effectGroupId && opt.EquipmentOptionGroupId == 100000) + .ToList(); + + if (options.Count > 0) + { + EquipmentOptionRecord firstOption = options.First(); + totalExcluded += firstOption.OptionGroupRatio / 100.0; + } + } + + // Cap the excluded probability sum to maintain mathematical validity + return Math.Min(totalExcluded, 99.9); // Ensure we don't reach 100% to avoid division by zero + } + + /// + /// Calculates dynamic probabilities for available effect groups using the formula: + /// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities) + /// + /// Dictionary of available options grouped by effect group ID + /// Sum of probabilities of excluded effects + /// List of weighted effect groups for random selection + private List CalculateDynamicProbabilitiesForEffectGroups(Dictionary> optionsByEffectGroup, double excludedProbabilitySum) + { + List weightedEffectGroups = new List(); + + double probabilityDenominator = 100.0 - excludedProbabilitySum; + + // Prevent division by zero or negative values (shouldn't happen due to capping in CalculateExcludedProbabilitySumByEffectGroup, but let's be safe) + if (probabilityDenominator <= 0) + { + Logging.WriteLine($"Warning: probabilityDenominator is {probabilityDenominator}, using uniform distribution", LogType.Warning); + probabilityDenominator = 1.0; // Use uniform distribution as fallback + } + + foreach (KeyValuePair> kvp in optionsByEffectGroup) + { + int effectGroupId = kvp.Key; + List options = kvp.Value; + + if (options.Count > 0) + { + EquipmentOptionRecord firstOption = options.First(); + double baseProbability = firstOption.OptionGroupRatio / 100.0; + + double dynamicProbability = baseProbability / probabilityDenominator; + + double selectionWeight = dynamicProbability * 1000000; + + weightedEffectGroups.Add(new EffectGroupWithWeight + { + EffectGroupId = effectGroupId, + Weight = selectionWeight, + BaseProbability = baseProbability, + DynamicProbability = dynamicProbability + }); + } + } + + return weightedEffectGroups; + } + + /// + /// Selects an effect group randomly based on calculated weights + /// + /// List of weighted effect groups + /// Selected effect_group_id or 0 if none available + private int SelectWeightedRandomEffectGroup(List weightedEffectGroups) + { + // Safety check to prevent data corruption + if (weightedEffectGroups == null || weightedEffectGroups.Count == 0) + throw new InvalidOperationException("No weighted effect groups available - this indicates a data consistency issue"); + + Random random = new Random(); + double totalWeight = weightedEffectGroups.Sum(weg => weg.Weight); + + // Prevent division by zero which could cause unexpected behavior + if (totalWeight <= 0) + throw new InvalidOperationException("Invalid group weights - this indicates a data consistency issue"); + + double randomValue = random.NextDouble() * totalWeight; + + double cumulativeWeight = 0.0; + foreach (EffectGroupWithWeight weightedGroup in weightedEffectGroups) + { + cumulativeWeight += weightedGroup.Weight; + if (randomValue <= cumulativeWeight) + { + return weightedGroup.EffectGroupId; + } + } + + return weightedEffectGroups.Last().EffectGroupId; + } + + private static readonly Random _random = new Random(); + + /// + /// Selects an option from an effect group based on option_ratio weights and returns a state_effect_id + /// + /// List of options in the effect group + /// Selected state_effect_id + private int SelectOptionFromGroup(List options) + { + // Safety check to prevent data corruption + if (options == null || options.Count == 0) + throw new InvalidOperationException("No options available in group - this indicates a data consistency issue"); + + long totalRatio = options.Sum(x => (long)x.OptionRatio); + + long randomValue = _random.NextInt64(0, totalRatio); + long cumulativeRatio = 0; + + foreach (EquipmentOptionRecord? option in options) + { + cumulativeRatio += option.OptionRatio; + if (randomValue < cumulativeRatio) + { + // Randomly select from the StateEffectList + if (option.StateEffectList == null || option.StateEffectList.Count == 0) + { + throw new InvalidOperationException($"StateEffectList is null or empty for option {option.Id}"); + } + int randomIndex = _random.Next(option.StateEffectList.Count); + return option.StateEffectList[randomIndex].StateEffectId; + } + } + + // Fallback: randomly select from the StateEffectList of the last option + EquipmentOptionRecord? lastOption = options.Last(); + if (lastOption?.StateEffectList == null || lastOption.StateEffectList.Count == 0) + { + throw new InvalidOperationException($"StateEffectList is null or empty for fallback option {lastOption?.Id}"); + } + int fallbackIndex = _random.Next(lastOption.StateEffectList.Count); + return lastOption.StateEffectList[fallbackIndex].StateEffectId; + } + + private static (int materialId, int materialCost) GetMaterialInfo(int costId) + { + if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) && + costRecord?.Costs != null && + costRecord.Costs.Count > 0) + { + return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue); + } + return (7080001, 1); // Default material ID and cost + } + } + +} diff --git a/EpinelPS/LobbyServer/Inventory/equipment/UpgradeOption.cs b/EpinelPS/LobbyServer/Inventory/equipment/UpgradeOption.cs new file mode 100644 index 0000000..5f40ef9 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/UpgradeOption.cs @@ -0,0 +1,286 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/upgradeoption")] + public class UpgradeOption : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAwakeningUpgradeOption req = await ReadData(); + User user = GetUser(); + + ResAwakeningUpgradeOption response = new ResAwakeningUpgradeOption(); + + // Validate input parameters + if (req.Isn <= 0) + { + Logging.WriteLine($"Invalid ISN: {req.Isn}", LogType.Warning); + await WriteDataAsync(response); + return; + } + + // Find the equipment awakening data (prefer old data over new data) + EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn && !x.IsNewData); + if (awakening == null) + { + await WriteDataAsync(response); + return; + } + + NetEquipmentAwakeningOption newOption = new NetEquipmentAwakeningOption(); + + (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo = new (int optionId, bool isLocked, bool isDisposableLocked)[3]; + + int lockedOptionCount = 0; + for (int i = 1; i <= 3; i++) + { + int currentOptionId = GetOptionIdForSlot(awakening.Option, i); + bool isLocked = IsOptionLocked(awakening.Option, i); + bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i); + + slotLockInfo[i - 1] = (currentOptionId, isLocked, isDisposableLocked); + + if (isLocked || isDisposableLocked) + lockedOptionCount++; + } + + // Get cost ID for upgrade based on locked option count + int costId = GetUpgradeCostId(lockedOptionCount); + + // Query actual material ID and cost from CostTable.json + (int materialId, int materialCost) = GetMaterialInfo(costId); + + ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId); + if (material == null || material.Count < materialCost) + { + await WriteDataAsync(response); + return; + } + + if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items)) + { + await WriteDataAsync(response); + return; + } + + // Process each option slot (1, 2, 3) + ProcessOptionSlots(awakening, newOption, slotLockInfo); + + // Create a new awakening entry with the same ISN to preserve the old data + EquipmentAwakeningData newAwakening = new EquipmentAwakeningData() + { + Isn = awakening.Isn, + Option = new NetEquipmentAwakeningOption() + { + Option1Id = newOption.Option1Id, + Option1Lock = newOption.Option1Lock, + IsOption1DisposableLock = newOption.IsOption1DisposableLock, + Option2Id = newOption.Option2Id, + Option2Lock = newOption.Option2Lock, + IsOption2DisposableLock = newOption.IsOption2DisposableLock, + Option3Id = newOption.Option3Id, + Option3Lock = newOption.Option3Lock, + IsOption3DisposableLock = newOption.IsOption3DisposableLock + }, + IsNewData = true // newAwakening + }; + + user.EquipmentAwakenings.Add(newAwakening); + + response.ResetOption = new NetEquipmentAwakeningOption() + { + Option1Id = newOption.Option1Id, + Option1Lock = newOption.Option1Lock, + IsOption1DisposableLock = newOption.IsOption1DisposableLock, + Option2Id = newOption.Option2Id, + Option2Lock = newOption.Option2Lock, + IsOption2DisposableLock = newOption.IsOption2DisposableLock, + Option3Id = newOption.Option3Id, + Option3Lock = newOption.Option3Lock, + IsOption3DisposableLock = newOption.IsOption3DisposableLock + }; + + JsonDb.Save(); + await WriteDataAsync(response); + } + + + private void ProcessOptionSlots(EquipmentAwakeningData awakening, NetEquipmentAwakeningOption newOption, (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo) + { + for (int i = 1; i <= 3; i++) + { + (int currentOptionId, bool isLocked, bool isDisposableLocked) = slotLockInfo[i - 1]; + + if (isLocked && !isDisposableLocked) + { + SetOptionForSlot(newOption, i, currentOptionId, isLocked, isDisposableLocked); + continue; + } + + if (isDisposableLocked) + { + SetOptionForSlot(newOption, i, currentOptionId, false, false); + + UnlockDisposableOption(awakening.Option, i); + continue; + } + + if (currentOptionId == 0) + { + SetOptionForSlot(newOption, i, 0, false, false); + continue; + } + + int newOptionId = GenerateNewOptionId(currentOptionId); + SetOptionForSlot(newOption, i, newOptionId, false, false); + } + } + + private void UnlockDisposableOption(NetEquipmentAwakeningOption option, int slot) + { + SetOptionForSlot(option, slot, GetOptionIdForSlot(option, slot), false, false); + } + + private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Id, + 2 => option.Option2Id, + 3 => option.Option3Id, + _ => 0 + }; + } + + private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.Option1Lock, + 2 => option.Option2Lock, + 3 => option.Option3Lock, + _ => false + }; + } + + private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot) + { + return slot switch + { + 1 => option.IsOption1DisposableLock, + 2 => option.IsOption2DisposableLock, + 3 => option.IsOption3DisposableLock, + _ => false + }; + } + + private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked) + { + switch (slot) + { + case 1: + option.Option1Id = optionId; + option.Option1Lock = locked; + option.IsOption1DisposableLock = disposableLocked; + break; + case 2: + option.Option2Id = optionId; + option.Option2Lock = locked; + option.IsOption2DisposableLock = disposableLocked; + break; + case 3: + option.Option3Id = optionId; + option.Option3Lock = locked; + option.IsOption3DisposableLock = disposableLocked; + break; + } + } + + private int GenerateNewOptionId(int currentStateEffectId) + { + EquipmentOptionRecord? currentOption = GameData.Instance.EquipmentOptionTable.Values + .FirstOrDefault(option => option.StateEffectList != null && option.StateEffectList.Any(se => se.StateEffectId == currentStateEffectId)); + + if (currentOption == null|| currentOption.EquipmentOptionGroupId != 100000) + { + throw new InvalidOperationException($"Current state_effect_id {currentStateEffectId} not found in any EquipmentOption"); + } + + + int stateEffectGroupId = currentOption.StateEffectGroupId; + + List optionsInGroup = GameData.Instance.EquipmentOptionTable.Values + .Where(x => x.EquipmentOptionGroupId == 100000 && x.StateEffectGroupId == stateEffectGroupId) + .ToList(); + + if (optionsInGroup.Count == 0) + { + throw new InvalidOperationException($"No awakening options found with state_effect_group_id {stateEffectGroupId}"); + } + + return SelectOptionFromGroup(optionsInGroup); + } + + + private static readonly Random _random = new Random(); + + private int SelectOptionFromGroup(List options) + { + if (options == null || options.Count == 0) + throw new InvalidOperationException("No options available in group - this indicates a data consistency issue"); + + long totalRatio = options.Sum(x => (long)x.OptionRatio); + + long randomValue = _random.NextInt64(0, totalRatio); + long cumulativeRatio = 0; + + foreach (EquipmentOptionRecord option in options) + { + cumulativeRatio += option.OptionRatio; + if (randomValue < cumulativeRatio) + { + // Randomly select from the StateEffectList + if (option.StateEffectList == null || option.StateEffectList.Count == 0) + { + throw new InvalidOperationException($"StateEffectList is null or empty for option {option.Id}"); + } + int randomIndex = _random.Next(option.StateEffectList.Count); + return option.StateEffectList[randomIndex].StateEffectId; + } + } + + // Fallback: randomly select from the StateEffectList of the last option + EquipmentOptionRecord? lastOption = options.Last(); + if (lastOption?.StateEffectList == null || lastOption.StateEffectList.Count == 0) + { + throw new InvalidOperationException($"StateEffectList is null or empty for fallback option {lastOption?.Id}"); + } + int fallbackIndex = _random.Next(lastOption.StateEffectList.Count); + return lastOption.StateEffectList[fallbackIndex].StateEffectId; + } + + private int GetUpgradeCostId(int lockedOptionCount) + { + + // For upgrade operation, use cost_group_id 200 + EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values + .FirstOrDefault(x => x.CostGroupId == 200 && x.CostLevel == lockedOptionCount); + + return costRecord?.CostId ?? 102001; + } + + private static (int materialId, int materialCost) GetMaterialInfo(int costId) + { + if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) && + costRecord?.Costs != null && + costRecord.Costs.Count > 0) + { + return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue); + } + return (7080001, 1); // Default material ID and cost + } + } +} diff --git a/EpinelPS/LobbyServer/Inventory/equipment/lockoption/Disposable.cs b/EpinelPS/LobbyServer/Inventory/equipment/lockoption/Disposable.cs new file mode 100644 index 0000000..8582122 --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/equipment/lockoption/Disposable.cs @@ -0,0 +1,142 @@ +using EpinelPS.Database; +using EpinelPS.Utils; +using EpinelPS.Data; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/equipment/lockoption/disposable")] + public class Disposable : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + ReqAwakeningDisposableLockOption req = await ReadData(); + User user = GetUser(); + + ResAwakeningDisposableLockOption response = new ResAwakeningDisposableLockOption(); + // Find the equipment awakening data + EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn); + if (awakening == null) + { + await WriteDataAsync(response); + return; + } + + int slot = 0; + if (awakening.Option.Option1Id == req.OptionId) + slot = 1; + else if (awakening.Option.Option2Id == req.OptionId) + slot = 2; + else if (awakening.Option.Option3Id == req.OptionId) + slot = 3; + + + (int materialId, int materialCost) = GetMaterialInfoForAwakening(awakening.Option); + + ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId); + if (material == null || material.Count < materialCost) + { + await WriteDataAsync(response); + return; + } + + switch (slot) + { + case 1: + if (req.IsLocked) + { + awakening.Option.Option1Lock = true; + awakening.Option.IsOption1DisposableLock = true; + } + else + { + awakening.Option.Option1Lock = false; + awakening.Option.IsOption1DisposableLock = false; + } + break; + case 2: + if (req.IsLocked) + { + awakening.Option.Option2Lock = true; + awakening.Option.IsOption2DisposableLock = true; + } + else + { + awakening.Option.Option2Lock = false; + awakening.Option.IsOption2DisposableLock = false; + } + break; + case 3: + if (req.IsLocked) + { + awakening.Option.Option3Lock = true; + awakening.Option.IsOption3DisposableLock = true; + } + else + { + awakening.Option.Option3Lock = false; + awakening.Option.IsOption3DisposableLock = false; + } + break; + } + + if (req.IsLocked) + { + if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items)) + { + await WriteDataAsync(response); + return; + } + } + + JsonDb.Save(); + await WriteDataAsync(response); + } + private static int CalculateMaterialCost(NetEquipmentAwakeningOption option) + { + int lockedOptionCount = 0; + int disposableLockOptionCount = 0; + if (option.Option1Id != 0 && option.Option1Lock && !option.IsOption1DisposableLock) + lockedOptionCount++; + if (option.Option2Id != 0 && option.Option2Lock && !option.IsOption2DisposableLock) + lockedOptionCount++; + if (option.Option3Id != 0 && option.Option3Lock && !option.IsOption3DisposableLock) + lockedOptionCount++; + + if (option.Option1Id != 0 && option.Option1Lock && option.IsOption1DisposableLock) + disposableLockOptionCount++; + if (option.Option2Id != 0 && option.Option2Lock && option.IsOption2DisposableLock) + disposableLockOptionCount++; + if (option.Option3Id != 0 && option.Option3Lock && option.IsOption3DisposableLock) + disposableLockOptionCount++; + + return GetDisposableFixCostIdByLevel(lockedOptionCount,disposableLockOptionCount); + } + + + private static int GetDisposableFixCostIdByLevel(int lockedOptionCount,int disposableLockOptionCount) + { + EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values + .FirstOrDefault(x => x.CostGroupId == 100 && x.CostLevel == lockedOptionCount && x.DisposableFixCostLevel == disposableLockOptionCount); + + return costRecord?.DisposableFixCostId ?? 101004; + } + + private static (int materialId, int materialCost) GetMaterialInfoForAwakening(NetEquipmentAwakeningOption option) + { + int costId = CalculateMaterialCost(option); + return GetMaterialInfo(costId); + } + private static (int materialId, int materialCost) GetMaterialInfo(int costId) + { + if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) && + costRecord?.Costs != null && + costRecord.Costs.Count > 0) + { + return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue); + } + + return (7080002, 20); // Default material ID and cost + } + } +} + diff --git a/EpinelPS/Models/DbModels.cs b/EpinelPS/Models/DbModels.cs index f9d17d5..d40f888 100644 --- a/EpinelPS/Models/DbModels.cs +++ b/EpinelPS/Models/DbModels.cs @@ -65,6 +65,19 @@ namespace EpinelPS.Models // For harmony cubes that can be equipped to multiple characters public List CsnList = []; } + + public class EquipmentAwakeningData + { + public long Isn; + public NetEquipmentAwakeningOption Option; + public bool IsNewData; + + public EquipmentAwakeningData() + { + Option = new NetEquipmentAwakeningOption(); + IsNewData = false; + } + } public class EventData { public List CompletedScenarios = []; @@ -109,9 +122,9 @@ namespace EpinelPS.Models public List CompletedDailyMissions = []; public int DailyMissionPoints; public SimroomData SimRoomData = new(); - - public bool UnlimitedCounseling = false; + public Dictionary DailyCounselCount = []; + } public class WeeklyResetableData { diff --git a/EpinelPS/Models/UserModel.cs b/EpinelPS/Models/UserModel.cs index 60af18f..d8ba124 100644 --- a/EpinelPS/Models/UserModel.cs +++ b/EpinelPS/Models/UserModel.cs @@ -53,6 +53,7 @@ public class User public WeeklyResetableData WeeklyResetableData = new(); public List Items = []; public List Characters = []; + public List EquipmentAwakenings = []; public long[] RepresentationTeamDataNew = []; public Dictionary ClearedTutorialData = []; @@ -85,7 +86,7 @@ public class User public List Memorial = []; public List JukeboxBgm = []; public List FavoriteItems = []; - + public List FavoriteItemQuests = []; public Dictionary TowerProgress = []; diff --git a/EpinelPS/Utils/EquipmentUtils.cs b/EpinelPS/Utils/EquipmentUtils.cs new file mode 100644 index 0000000..f53ad63 --- /dev/null +++ b/EpinelPS/Utils/EquipmentUtils.cs @@ -0,0 +1,37 @@ +using EpinelPS.Data; +using EpinelPS.Database; + +namespace EpinelPS.Utils +{ + public class EquipmentUtils + { + /// + /// Deducts materials from user's inventory and updates the response + /// + /// The material item to deduct + /// Amount of material to deduct + /// The user whose inventory to update + /// The response items list to update + /// True if deduction was successful, false otherwise + public static bool DeductMaterials(ItemData material, int materialCost, User user, IList responseItems) + { + if (material.Count < materialCost) + return false; + + material.Count -= materialCost; + if (material.Count <= 0) + { + user.Items.Remove(material); + NetUserItemData netItem = NetUtils.ToNet(material); + netItem.Count = 0; + responseItems.Add(netItem); + } + else + { + responseItems.Add(NetUtils.ToNet(material)); + } + + return true; + } + } +} diff --git a/EpinelPS/Utils/InterceptionHelper.cs b/EpinelPS/Utils/InterceptionHelper.cs index aa148b0..7c6aef8 100644 --- a/EpinelPS/Utils/InterceptionHelper.cs +++ b/EpinelPS/Utils/InterceptionHelper.cs @@ -9,11 +9,11 @@ namespace EpinelPS.Utils { InterceptionClearResult response = new(); - if (type != 1 && type != 2) throw new Exception("unknown interception type"); + //if (type != 1 && type != 2) throw new Exception("unknown interception type"); int conditionReward; int percentRewardGroup; - if (type == 0) + if (type == 0 || type == 1) { conditionReward = GameData.Instance.InterceptNormal[id].ConditionRewardGroup; percentRewardGroup = GameData.Instance.InterceptNormal[id].PercentConditionRewardGroup; diff --git a/EpinelPS/Utils/RewardUtils.cs b/EpinelPS/Utils/RewardUtils.cs index e7651d6..e249a87 100644 --- a/EpinelPS/Utils/RewardUtils.cs +++ b/EpinelPS/Utils/RewardUtils.cs @@ -119,57 +119,77 @@ namespace EpinelPS.Utils { AddSingleCurrencyObject(user, ref ret, (CurrencyType)rewardId, rewardCount); } - else if (rewardType == RewardType.Item || - rewardType.ToString().StartsWith("Equipment_")) + else if (rewardType == RewardType.Item ||rewardType.ToString().StartsWith("Equipment_")) { - // Check if user already has saId item. If it is level 1, increase item count. - // If user does not have item, generate a new item ID - if (user.Items.Where(x => x.ItemType == rewardId && x.Level == 1).Any()) + + int corpId = 0; // Default to 0 (None) + + if (rewardType.ToString().StartsWith("Equipment_")) { - ItemData? newItem = user.Items.Where(x => x.ItemType == rewardId && x.Level == 1).FirstOrDefault(); - if (newItem != null) - { - newItem.Count += rewardCount; + var corpSetting = GameData.Instance.ItemEquipCorpSettingTable.Values.FirstOrDefault(x => x.Key == rewardType); - // Tell the client the reward and its amount - ret.Item.Add(new NetItemData() - { - Count = rewardCount, - Tid = rewardId, - //Isn = newItem.Isn - }); - - // Tell the client the new amount of this item - ret.UserItems.Add(new NetUserItemData() - { - Isn = newItem.Isn, - Tid = newItem.ItemType, - Count = newItem.Count - }); - } - else + if (corpSetting != null) { - throw new Exception("should not occur"); + if (corpSetting.CorpType == CorporationType.RANDOM) + { + // Use weighted random selection - all corporations have equal chance + // Weights: MISSILIS(1)=20%, ELYSION(2)=20%, TETRA(3)=20%, PILGRIM(4)=20%, ABNORMAL(7)=20% + int[] corpIds = { 1, 2, 3, 4, 7 }; // All corporations have equal chance + corpId = corpIds[Rng.Next(0, corpIds.Length)]; + } + else + { + // Directly use the CorpType enum value as integer + corpId = (int)corpSetting.CorpType; + } } + } - else - { - int Id = user.GenerateUniqueItemId(); - user.Items.Add(new ItemData() { ItemType = rewardId, Isn = Id, Level = 1, Exp = 0, Count = rewardCount }); + // Check if user already has said item. If it is level 1, increase item count. + ItemData? existingItem = user.Items.FirstOrDefault(x => x.ItemType == rewardId && x.Level == 1 && x.Corp == corpId); + + if (existingItem != null) + { + existingItem.Count += rewardCount; + + // Tell the client the reward and its amount ret.Item.Add(new NetItemData() { Count = rewardCount, Tid = rewardId, - //Isn = Id + Corporation = corpId }); - // Tell the client the new amount of this item (which is the same as user dId not have item previously) + // Tell the client the new amount of this item ret.UserItems.Add(new NetUserItemData() { - Isn = Id, + Isn = existingItem.Isn, + Tid = existingItem.ItemType, + Count = existingItem.Count, + Corporation = existingItem.Corp + }); + } + else + { + int id = user.GenerateUniqueItemId(); + var newItem = new ItemData() { ItemType = rewardId, Isn = id, Level = 0, Exp = 0, Count = rewardCount, Corp = corpId }; + user.Items.Add(newItem); + + ret.Item.Add(new NetItemData() + { + Count = rewardCount, Tid = rewardId, - Count = rewardCount + Corporation = corpId + }); + + // Tell the client the new amount of this item + ret.UserItems.Add(new NetUserItemData() + { + Isn = newItem.Isn, + Tid = newItem.ItemType, + Count = newItem.Count, + Corporation = newItem.Corp }); } } diff --git a/EpinelPS/Utils/Rng.cs b/EpinelPS/Utils/Rng.cs index a862383..8d5574e 100644 --- a/EpinelPS/Utils/Rng.cs +++ b/EpinelPS/Utils/Rng.cs @@ -6,6 +6,11 @@ namespace EpinelPS.Utils { private static readonly Random random = new(); + public static int Next(int minValue, int maxValue) + { + return random.Next(minValue, maxValue); + } + public static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";