mirror of
https://github.com/EpinelPS/EpinelPS.git
synced 2025-12-12 15:04:36 +01:00
feat: implement equipment awakening system with interception updates (#57)
- Implement comprehensive equipment awakening system with multiple new endpoints: * Awakening.cs: Handle equipment awakening process * ChangeOption.cs: Change equipment awakening options * GetAwakeningDetail.cs: Get detailed awakening information * LockOption.cs: Lock awakening options (with Disposable option) * ResetOption.cs: Reset awakening options * UpgradeOption.cs: Upgrade awakening options - Enhance interception system: * Simplify GetInterceptData to use fixed normal group ID (1) * Update InterceptionHelper to support type 1 in addition to type 0 * Modify GetInterceptData to use simplified special ID calculation - Implement new inventory system features: * Replace ClearAllEquipment with AllClearEquipment * Enhance GetInventoryData to properly handle HarmonyCubes and Awakenings * Update WearEquipmentList for improved equipment management - Update data models and game data: * Modify GetConditionReward to handle valueMax == 0 cases * Update EquipmentAwakeningData model with proper default values * Update ResetableData with DailyCounselCount as dictionary instead of struct field - Additional improvements: * Create GameAssemblyProcessor utility * Enhance level infinite controller * Update server selector UI * Organize protocol message documentation
This commit is contained in:
@@ -254,6 +254,14 @@ namespace EpinelPS.Data
|
||||
[LoadRecord("EventMVGMissionTable.json", "Id")]
|
||||
public readonly Dictionary<int, EventMVGMissionRecord_Raw> EventMvgMissionTable = [];
|
||||
|
||||
[LoadRecord("EquipmentOptionTable.json", "Id")]
|
||||
public readonly Dictionary<int, EquipmentOptionRecord> EquipmentOptionTable = [];
|
||||
|
||||
[LoadRecord("EquipmentOptionCostTable.json", "Id")]
|
||||
public readonly Dictionary<int, EquipmentOptionCostRecord> EquipmentOptionCostTable = [];
|
||||
|
||||
[LoadRecord("ItemEquipCorpSettingTable.json", "Id")]
|
||||
public readonly Dictionary<int, ItemEquipCorpSettingRecord> ItemEquipCorpSettingTable = [];
|
||||
static async Task<GameData> 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<KeyValuePair<int, ConditionRewardRecord>> results = ConditionRewards.Where(x => x.Value.Group == groupId && x.Value.ValueMin <= damage && x.Value.ValueMax >= damage);
|
||||
IEnumerable<KeyValuePair<int, ConditionRewardRecord>> 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>();
|
||||
ReqGetInterceptData req = await ReadData<ReqGetInterceptData>();
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
EpinelPS/LobbyServer/Inventory/AllClearEquipment.cs
Normal file
60
EpinelPS/LobbyServer/Inventory/AllClearEquipment.cs
Normal file
@@ -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<ReqAllClearEquipment>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ReqAllClearEquipment>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
350
EpinelPS/LobbyServer/Inventory/equipment/Awakening.cs
Normal file
350
EpinelPS/LobbyServer/Inventory/equipment/Awakening.cs
Normal file
@@ -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<ReqEquipmentAwakening>();
|
||||
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<int> excludedStateEffectIds = new List<int>();
|
||||
|
||||
// 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<int> excludedStateEffectIds, int level)
|
||||
{
|
||||
List<EquipmentOptionRecord> allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values
|
||||
.Where(x => x.EquipmentOptionGroupId == 100000 &&
|
||||
x.StateEffectList?.Any(se => se.StateEffectLevel == level) == true)
|
||||
.ToList();
|
||||
|
||||
HashSet<int> excludedEffectGroupIds = new HashSet<int>();
|
||||
|
||||
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<EquipmentOptionRecord> availableOptions = allAwakeningOptions
|
||||
.Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId))
|
||||
.ToList();
|
||||
|
||||
Dictionary<int, List<EquipmentOptionRecord>> 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<EffectGroupWithWeight> weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum);
|
||||
|
||||
int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups);
|
||||
|
||||
List<EquipmentOptionRecord> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules
|
||||
/// </summary>
|
||||
/// <param name="excludedEffectGroupIds">List of excluded state_effect_group_ids</param>
|
||||
/// <param name="level">The level of options to consider</param>
|
||||
/// <returns>Sum of base probabilities (as decimal percentage)</returns>
|
||||
private double CalculateExcludedProbabilitySumByEffectGroup(HashSet<int> excludedEffectGroupIds, int level)
|
||||
{
|
||||
if (excludedEffectGroupIds.Count == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double totalExcluded = 0.0;
|
||||
|
||||
foreach (int effectGroupId in excludedEffectGroupIds)
|
||||
{
|
||||
List<EquipmentOptionRecord> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to store effect group with its calculated weight for probability selection
|
||||
/// </summary>
|
||||
public class EffectGroupWithWeight
|
||||
{
|
||||
public int EffectGroupId { get; set; }
|
||||
public double Weight { get; set; }
|
||||
public double BaseProbability { get; set; }
|
||||
public double DynamicProbability { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates dynamic probabilities for available effect groups using the formula:
|
||||
/// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities)
|
||||
/// </summary>
|
||||
/// <param name="optionsByEffectGroup">Dictionary of available options grouped by effect group ID</param>
|
||||
/// <param name="excludedProbabilitySum">Sum of probabilities of excluded effects</param>
|
||||
/// <returns>List of weighted effect groups for random selection</returns>
|
||||
private List<EffectGroupWithWeight> CalculateDynamicProbabilitiesForEffectGroups(Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup, double excludedProbabilitySum)
|
||||
{
|
||||
List<EffectGroupWithWeight> weightedEffectGroups = new List<EffectGroupWithWeight>();
|
||||
|
||||
double probabilityDenominator = 100.0 - excludedProbabilitySum;
|
||||
|
||||
if (probabilityDenominator <= 0)
|
||||
{
|
||||
probabilityDenominator = 1.0;
|
||||
}
|
||||
foreach (KeyValuePair<int, List<EquipmentOptionRecord>> kvp in optionsByEffectGroup)
|
||||
{
|
||||
int effectGroupId = kvp.Key;
|
||||
List<EquipmentOptionRecord> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects an effect group randomly based on calculated weights
|
||||
/// </summary>
|
||||
/// <param name="weightedEffectGroups">List of weighted effect groups</param>
|
||||
/// <returns>Selected effect_group_id</returns>
|
||||
private int SelectWeightedRandomEffectGroup(List<EffectGroupWithWeight> 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<int> excludedStateEffectIds)
|
||||
{
|
||||
if (!excludedStateEffectIds.Contains(optionId))
|
||||
{
|
||||
excludedStateEffectIds.Add(optionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
EpinelPS/LobbyServer/Inventory/equipment/ChangeOption.cs
Normal file
117
EpinelPS/LobbyServer/Inventory/equipment/ChangeOption.cs
Normal file
@@ -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<ReqAwakeningChangeOption>();
|
||||
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<EquipmentAwakeningData> duplicates = user.EquipmentAwakenings.Where(x => x.Isn == req.Isn).ToList();
|
||||
|
||||
if (req.IsChanged)
|
||||
{
|
||||
if (duplicates.Count > 1)
|
||||
{
|
||||
List<EquipmentAwakeningData> 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<EquipmentAwakeningData> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
EpinelPS/LobbyServer/Inventory/equipment/GetAwakeningDetail.cs
Normal file
174
EpinelPS/LobbyServer/Inventory/equipment/GetAwakeningDetail.cs
Normal file
@@ -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<ReqGetAwakeningDetail>();
|
||||
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<EquipmentOptionRecord> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
137
EpinelPS/LobbyServer/Inventory/equipment/LockOption.cs
Normal file
137
EpinelPS/LobbyServer/Inventory/equipment/LockOption.cs
Normal file
@@ -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<ReqAwakeningLockOption>();
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
475
EpinelPS/LobbyServer/Inventory/equipment/ResetOption.cs
Normal file
475
EpinelPS/LobbyServer/Inventory/equipment/ResetOption.cs
Normal file
@@ -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<ReqAwakeningResetOption>();
|
||||
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<int> lockedOptionStateEffectIds = new List<int>();
|
||||
|
||||
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<int> 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<int> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new option ID using the Overload system's non-repeating effect types and dynamic probability formula
|
||||
/// </summary>
|
||||
/// <param name="excludedStateEffectIds">List of state_effect_ids that are already taken and should be excluded</param>
|
||||
/// <returns>A new state_effect_id or 0 if none available</returns>
|
||||
private int GenerateNewOptionIdWithDynamicProbability(List<int> excludedStateEffectIds)
|
||||
{
|
||||
// Get all awakening options (equipment_option_group_id == 100000)
|
||||
List<EquipmentOptionRecord> allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values
|
||||
.Where(x => x.EquipmentOptionGroupId == 100000)
|
||||
.ToList();
|
||||
|
||||
// Filter out options that have already been taken (non-repeating principle)
|
||||
HashSet<int> excludedEffectGroupIds = new HashSet<int>();
|
||||
|
||||
// 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<EquipmentOptionRecord> availableOptions = allAwakeningOptions
|
||||
.Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId))
|
||||
.ToList();
|
||||
|
||||
Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup = availableOptions
|
||||
.GroupBy(option => option.StateEffectGroupId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
double excludedProbabilitySum = CalculateExcludedProbabilitySumByEffectGroup(excludedEffectGroupIds);
|
||||
|
||||
List<EffectGroupWithWeight> weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum);
|
||||
int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups);
|
||||
|
||||
List<EquipmentOptionRecord> optionsInSelectedGroup = optionsByEffectGroup[selectedEffectGroupId];
|
||||
int selectedStateEffectId = SelectOptionFromGroup(optionsInSelectedGroup);
|
||||
return selectedStateEffectId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to store effect group with its calculated weight for probability selection
|
||||
/// </summary>
|
||||
public class EffectGroupWithWeight
|
||||
{
|
||||
public int EffectGroupId { get; set; }
|
||||
public double Weight { get; set; }
|
||||
public double BaseProbability { get; set; }
|
||||
public double DynamicProbability { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules
|
||||
/// </summary>
|
||||
/// <param name="excludedEffectGroupIds">List of excluded state_effect_group_ids</param>
|
||||
/// <returns>Sum of base probabilities (as decimal percentage)</returns>
|
||||
private double CalculateExcludedProbabilitySumByEffectGroup(HashSet<int> excludedEffectGroupIds)
|
||||
{
|
||||
if (excludedEffectGroupIds.Count == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double totalExcluded = 0.0;
|
||||
|
||||
foreach (int effectGroupId in excludedEffectGroupIds)
|
||||
{
|
||||
List<EquipmentOptionRecord> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates dynamic probabilities for available effect groups using the formula:
|
||||
/// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities)
|
||||
/// </summary>
|
||||
/// <param name="optionsByEffectGroup">Dictionary of available options grouped by effect group ID</param>
|
||||
/// <param name="excludedProbabilitySum">Sum of probabilities of excluded effects</param>
|
||||
/// <returns>List of weighted effect groups for random selection</returns>
|
||||
private List<EffectGroupWithWeight> CalculateDynamicProbabilitiesForEffectGroups(Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup, double excludedProbabilitySum)
|
||||
{
|
||||
List<EffectGroupWithWeight> weightedEffectGroups = new List<EffectGroupWithWeight>();
|
||||
|
||||
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<int, List<EquipmentOptionRecord>> kvp in optionsByEffectGroup)
|
||||
{
|
||||
int effectGroupId = kvp.Key;
|
||||
List<EquipmentOptionRecord> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects an effect group randomly based on calculated weights
|
||||
/// </summary>
|
||||
/// <param name="weightedEffectGroups">List of weighted effect groups</param>
|
||||
/// <returns>Selected effect_group_id or 0 if none available</returns>
|
||||
private int SelectWeightedRandomEffectGroup(List<EffectGroupWithWeight> 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();
|
||||
|
||||
/// <summary>
|
||||
/// Selects an option from an effect group based on option_ratio weights and returns a state_effect_id
|
||||
/// </summary>
|
||||
/// <param name="options">List of options in the effect group</param>
|
||||
/// <returns>Selected state_effect_id</returns>
|
||||
private int SelectOptionFromGroup(List<EquipmentOptionRecord> 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
286
EpinelPS/LobbyServer/Inventory/equipment/UpgradeOption.cs
Normal file
286
EpinelPS/LobbyServer/Inventory/equipment/UpgradeOption.cs
Normal file
@@ -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<ReqAwakeningUpgradeOption>();
|
||||
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<EquipmentOptionRecord> 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<EquipmentOptionRecord> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ReqAwakeningDisposableLockOption>();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,19 @@ namespace EpinelPS.Models
|
||||
// For harmony cubes that can be equipped to multiple characters
|
||||
public List<long> 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<string> CompletedScenarios = [];
|
||||
@@ -109,9 +122,9 @@ namespace EpinelPS.Models
|
||||
public List<int> CompletedDailyMissions = [];
|
||||
public int DailyMissionPoints;
|
||||
public SimroomData SimRoomData = new();
|
||||
|
||||
public bool UnlimitedCounseling = false;
|
||||
|
||||
public Dictionary<int, int> DailyCounselCount = [];
|
||||
|
||||
}
|
||||
public class WeeklyResetableData
|
||||
{
|
||||
|
||||
@@ -53,6 +53,7 @@ public class User
|
||||
public WeeklyResetableData WeeklyResetableData = new();
|
||||
public List<ItemData> Items = [];
|
||||
public List<CharacterModel> Characters = [];
|
||||
public List<EquipmentAwakeningData> EquipmentAwakenings = [];
|
||||
public long[] RepresentationTeamDataNew = [];
|
||||
public Dictionary<int, ClearedTutorialData> ClearedTutorialData = [];
|
||||
|
||||
@@ -85,7 +86,7 @@ public class User
|
||||
public List<int> Memorial = [];
|
||||
public List<int> JukeboxBgm = [];
|
||||
public List<NetUserFavoriteItemData> FavoriteItems = [];
|
||||
|
||||
|
||||
public List<NetUserFavoriteItemQuestData> FavoriteItemQuests = [];
|
||||
public Dictionary<int, int> TowerProgress = [];
|
||||
|
||||
|
||||
37
EpinelPS/Utils/EquipmentUtils.cs
Normal file
37
EpinelPS/Utils/EquipmentUtils.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using EpinelPS.Data;
|
||||
using EpinelPS.Database;
|
||||
|
||||
namespace EpinelPS.Utils
|
||||
{
|
||||
public class EquipmentUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Deducts materials from user's inventory and updates the response
|
||||
/// </summary>
|
||||
/// <param name="material">The material item to deduct</param>
|
||||
/// <param name="materialCost">Amount of material to deduct</param>
|
||||
/// <param name="user">The user whose inventory to update</param>
|
||||
/// <param name="responseItems">The response items list to update</param>
|
||||
/// <returns>True if deduction was successful, false otherwise</returns>
|
||||
public static bool DeductMaterials(ItemData material, int materialCost, User user, IList<NetUserItemData> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user