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:
fxz2018
2025-10-08 09:30:27 +08:00
committed by GitHub
parent cbbefeb51a
commit 206fa429ee
20 changed files with 2039 additions and 101 deletions

View File

@@ -254,6 +254,14 @@ namespace EpinelPS.Data
[LoadRecord("EventMVGMissionTable.json", "Id")] [LoadRecord("EventMVGMissionTable.json", "Id")]
public readonly Dictionary<int, EventMVGMissionRecord_Raw> EventMvgMissionTable = []; 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() static async Task<GameData> BuildAsync()
{ {
await Load(); await Load();
@@ -718,17 +726,10 @@ namespace EpinelPS.Data
return data.HardFieldId; return data.HardFieldId;
else return data.FieldId; 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) 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()) if (results.Any())
return results.FirstOrDefault().Value.RewardId; return results.FirstOrDefault().Value.RewardId;
else return 0; else return 0;

View File

@@ -16,7 +16,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 20001, Id = 20001,
EventSystemType = (int)EventType.PickupGachaEvent, EventSystemType = (int)EventSystemType.PickupGachaEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks, EventEndDate = DateTime.Now.AddDays(20).Ticks,
@@ -31,7 +31,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 70077, Id = 70077,
EventSystemType = (int)EventType.PickupGachaEvent, EventSystemType = (int)EventSystemType.PickupGachaEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks, EventEndDate = DateTime.Now.AddDays(20).Ticks,
@@ -44,7 +44,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 10046, Id = 10046,
EventSystemType = (int)EventType.LoginEvent, EventSystemType = (int)EventSystemType.LoginEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -57,7 +57,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 40066, Id = 40066,
EventSystemType = (int)EventType.StoryEvent, EventSystemType = (int)EventSystemType.StoryEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -70,7 +70,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 60066, Id = 60066,
EventSystemType = (int)EventType.ChallengeModeEvent, EventSystemType = (int)EventSystemType.ChallengeModeEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -83,7 +83,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 70078, Id = 70078,
EventSystemType = (int)EventType.PickupGachaEvent, EventSystemType = (int)EventSystemType.PickupGachaEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -96,7 +96,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 70079, Id = 70079,
EventSystemType = (int)EventType.PickupGachaEvent, EventSystemType = (int)EventSystemType.PickupGachaEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -112,7 +112,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 140052, Id = 140052,
EventSystemType = RewardUpEvent, EventSystemType = (int)EventSystemType.RewardUpEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,
@@ -130,7 +130,7 @@ namespace EpinelPS.LobbyServer.Event
EventData = new NetEventData() EventData = new NetEventData()
{ {
Id = 170017, Id = 170017,
EventSystemType = TriggerMissionEventReward, EventSystemType = (int)EventSystemType.TriggerMissionEventReward,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks, EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks, EventDisableDate = DateTime.Now.AddDays(20).Ticks,

View File

@@ -1,5 +1,6 @@
using EpinelPS.Database; using EpinelPS.Database;
using EpinelPS.Utils; using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Intercept namespace EpinelPS.LobbyServer.Intercept
{ {
@@ -8,17 +9,32 @@ namespace EpinelPS.LobbyServer.Intercept
{ {
protected override async Task HandleAsync() protected override async Task HandleAsync()
{ {
ReqGetInterceptData req = await ReadData<ReqGetInterceptData>(); ReqGetInterceptData req = await ReadData<ReqGetInterceptData>();
int specialId = GetCurrentInterceptionIds();
ResGetInterceptData response = new() ResGetInterceptData response = new()
{ {
NormalInterceptGroup = 1, NormalInterceptGroup = 1,
SpecialInterceptId = 1, // TODO switch this out each reset SpecialInterceptId = specialId,
TicketCount = User.ResetableData.InterceptionTickets, TicketCount = User.ResetableData.InterceptionTickets,
MaxTicketCount = JsonDb.Instance.MaxInterceptionCount MaxTicketCount = JsonDb.Instance.MaxInterceptionCount
}; };
await WriteDataAsync(response); 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;
}
} }
} }

View 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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,5 +1,5 @@
using EpinelPS.Utils; using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory namespace EpinelPS.LobbyServer.Inventory
{ {
[PacketPath("/inventory/get")] [PacketPath("/inventory/get")]
@@ -13,9 +13,36 @@ namespace EpinelPS.LobbyServer.Inventory
ResGetInventoryData response = new(); ResGetInventoryData response = new();
foreach (ItemData item in user.Items) 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 }); 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); await WriteDataAsync(response);
} }

View File

@@ -1,5 +1,7 @@
using EpinelPS.Database; using EpinelPS.Database;
using EpinelPS.Utils; using EpinelPS.Utils;
using EpinelPS.Data;
using System.Linq;
namespace EpinelPS.LobbyServer.Inventory namespace EpinelPS.LobbyServer.Inventory
{ {
@@ -13,27 +15,120 @@ namespace EpinelPS.LobbyServer.Inventory
ResWearEquipmentList response = new(); ResWearEquipmentList response = new();
// TODO optimize
foreach (long item2 in req.IsnList) foreach (long item2 in req.IsnList)
{ {
int pos = NetUtils.GetItemPos(user, item2); 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 // unequip previous items
foreach (ItemData item in user.Items.ToArray()) foreach (ItemData item in user.Items.ToArray())
{ {
if (item.Position == pos && item.Csn == req.Csn) 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.Csn = 0;
item.Position = 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)); response.Items.Add(NetUtils.ToNet(item));
} }
} }
@@ -43,5 +138,22 @@ namespace EpinelPS.LobbyServer.Inventory
await WriteDataAsync(response); 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;
}
} }
} }

View 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);
}
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -65,6 +65,19 @@ namespace EpinelPS.Models
// For harmony cubes that can be equipped to multiple characters // For harmony cubes that can be equipped to multiple characters
public List<long> CsnList = []; 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 class EventData
{ {
public List<string> CompletedScenarios = []; public List<string> CompletedScenarios = [];
@@ -110,8 +123,8 @@ namespace EpinelPS.Models
public int DailyMissionPoints; public int DailyMissionPoints;
public SimroomData SimRoomData = new(); public SimroomData SimRoomData = new();
public bool UnlimitedCounseling = false;
public Dictionary<int, int> DailyCounselCount = []; public Dictionary<int, int> DailyCounselCount = [];
} }
public class WeeklyResetableData public class WeeklyResetableData
{ {

View File

@@ -53,6 +53,7 @@ public class User
public WeeklyResetableData WeeklyResetableData = new(); public WeeklyResetableData WeeklyResetableData = new();
public List<ItemData> Items = []; public List<ItemData> Items = [];
public List<CharacterModel> Characters = []; public List<CharacterModel> Characters = [];
public List<EquipmentAwakeningData> EquipmentAwakenings = [];
public long[] RepresentationTeamDataNew = []; public long[] RepresentationTeamDataNew = [];
public Dictionary<int, ClearedTutorialData> ClearedTutorialData = []; public Dictionary<int, ClearedTutorialData> ClearedTutorialData = [];

View 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;
}
}
}

View File

@@ -9,11 +9,11 @@ namespace EpinelPS.Utils
{ {
InterceptionClearResult response = new(); 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 conditionReward;
int percentRewardGroup; int percentRewardGroup;
if (type == 0) if (type == 0 || type == 1)
{ {
conditionReward = GameData.Instance.InterceptNormal[id].ConditionRewardGroup; conditionReward = GameData.Instance.InterceptNormal[id].ConditionRewardGroup;
percentRewardGroup = GameData.Instance.InterceptNormal[id].PercentConditionRewardGroup; percentRewardGroup = GameData.Instance.InterceptNormal[id].PercentConditionRewardGroup;

View File

@@ -119,57 +119,77 @@ namespace EpinelPS.Utils
{ {
AddSingleCurrencyObject(user, ref ret, (CurrencyType)rewardId, rewardCount); AddSingleCurrencyObject(user, ref ret, (CurrencyType)rewardId, rewardCount);
} }
else if (rewardType == RewardType.Item || else if (rewardType == RewardType.Item ||rewardType.ToString().StartsWith("Equipment_"))
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 int corpId = 0; // Default to 0 (None)
if (user.Items.Where(x => x.ItemType == rewardId && x.Level == 1).Any())
if (rewardType.ToString().StartsWith("Equipment_"))
{ {
ItemData? newItem = user.Items.Where(x => x.ItemType == rewardId && x.Level == 1).FirstOrDefault(); var corpSetting = GameData.Instance.ItemEquipCorpSettingTable.Values.FirstOrDefault(x => x.Key == rewardType);
if (newItem != null)
{
newItem.Count += rewardCount;
// Tell the client the reward and its amount if (corpSetting != null)
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
{ {
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(); // Check if user already has said item. If it is level 1, increase item count.
user.Items.Add(new ItemData() { ItemType = rewardId, Isn = Id, Level = 1, Exp = 0, Count = rewardCount }); 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() ret.Item.Add(new NetItemData()
{ {
Count = rewardCount, Count = rewardCount,
Tid = rewardId, 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() 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, 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
}); });
} }
} }

View File

@@ -6,6 +6,11 @@ namespace EpinelPS.Utils
{ {
private static readonly Random random = new(); 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) public static string RandomString(int length)
{ {
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";