diff --git a/.gitignore b/.gitignore index 8453c2e..b9992e8 100644 --- a/.gitignore +++ b/.gitignore @@ -370,3 +370,4 @@ FodyWeavers.xsd *.rar /Lua_Script /.note +/Snowbreak_Data diff --git a/Common/Data/Excel/BreakLevelLimitExcel.cs b/Common/Data/Excel/BreakLevelLimitExcel.cs new file mode 100644 index 0000000..4278407 --- /dev/null +++ b/Common/Data/Excel/BreakLevelLimitExcel.cs @@ -0,0 +1,24 @@ +namespace MikuSB.Data.Excel; + +[ResourceEntity("break_level_limit.json")] +public class BreakLevelLimitExcel : ExcelResource +{ + public int ID { get; set; } + public uint Break0 { get; set; } + public uint Break1 { get; set; } + public uint Break2 { get; set; } + public uint Break3 { get; set; } + public uint Break4 { get; set; } + public uint Break5 { get; set; } + public uint Break6 { get; set; } + + public override uint GetId() + { + return (uint)ID; + } + + public override void Loaded() + { + GameData.BreakLevelLimitData[ID] = this; + } +} diff --git a/Common/Data/Excel/RecycleExcel.cs b/Common/Data/Excel/RecycleExcel.cs new file mode 100644 index 0000000..dbb665e --- /dev/null +++ b/Common/Data/Excel/RecycleExcel.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("recycle.json")] +public class RecycleExcel : ExcelResource +{ + public int ID { get; set; } + public JToken? RecycleBase { get; set; } + public JToken? RecycleRatio { get; set; } + + public override uint GetId() + { + return (uint)ID; + } + + public override void Loaded() + { + GameData.RecycleData[ID] = this; + } +} diff --git a/Common/Data/Excel/SuppliesExcel.cs b/Common/Data/Excel/SuppliesExcel.cs new file mode 100644 index 0000000..00bb32e --- /dev/null +++ b/Common/Data/Excel/SuppliesExcel.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("suplies.json")] +public class SuppliesExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + [JsonProperty("Color")] public JToken? ColorRaw { get; set; } + [JsonProperty("ProvideExp")] public JToken? ProvideExpRaw { get; set; } + [JsonProperty("ConsumeGold")] public JToken? ConsumeGoldRaw { get; set; } + + [JsonIgnore] public int Color => ReadInt(ColorRaw); + [JsonIgnore] public uint ProvideExp => ReadUInt(ProvideExpRaw); + [JsonIgnore] public uint ConsumeGold => ReadUInt(ConsumeGoldRaw); + + public override uint GetId() + { + return (uint)GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level); + } + + public override void Loaded() + { + GameData.SuppliesData[GetId()] = this; + } + + private static int ReadInt(JToken? token) + { + if (token == null) + { + return 0; + } + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (int)token.Value(), + JTokenType.String when int.TryParse(token.Value(), out var value) => value, + _ => 0 + }; + } + + private static uint ReadUInt(JToken? token) + { + var value = ReadInt(token); + return value > 0 ? (uint)value : 0; + } +} diff --git a/Common/Data/Excel/UpgradeExpExcel.cs b/Common/Data/Excel/UpgradeExpExcel.cs new file mode 100644 index 0000000..4288a6a --- /dev/null +++ b/Common/Data/Excel/UpgradeExpExcel.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("upgrade_exp.json")] +public class UpgradeExpExcel : ExcelResource +{ + public int Lv { get; set; } + [JsonProperty("CardNeedExp")] public JToken? CardNeedExpRaw { get; set; } + [JsonProperty("SSRCardNeedExp")] public JToken? SsrCardNeedExpRaw { get; set; } + [JsonProperty("SusNeedExp")] public JToken? SusNeedExpRaw { get; set; } + [JsonProperty("SSRSusNeedExp")] public JToken? SsrSusNeedExpRaw { get; set; } + [JsonProperty("WeaponNeedExp")] public JToken? WeaponNeedExpRaw { get; set; } + [JsonProperty("SSRWeaponNeedExp")] public JToken? SsrWeaponNeedExpRaw { get; set; } + + [JsonIgnore] public uint CardNeedExp => ReadUInt(CardNeedExpRaw); + [JsonIgnore] public uint SSRCardNeedExp => ReadUInt(SsrCardNeedExpRaw); + [JsonIgnore] public uint SusNeedExp => ReadUInt(SusNeedExpRaw); + [JsonIgnore] public uint SSRSusNeedExp => ReadUInt(SsrSusNeedExpRaw); + [JsonIgnore] public uint WeaponNeedExp => ReadUInt(WeaponNeedExpRaw); + [JsonIgnore] public uint SSRWeaponNeedExp => ReadUInt(SsrWeaponNeedExpRaw); + + public override uint GetId() + { + return (uint)Lv; + } + + public override void Loaded() + { + GameData.UpgradeExpData[Lv] = this; + } + + private static uint ReadUInt(JToken? token) + { + if (token == null) + { + return 0; + } + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (uint)Math.Max(0, token.Value()), + JTokenType.String when uint.TryParse(token.Value(), out var value) => value, + _ => 0 + }; + } +} diff --git a/Common/Data/Excel/WeaponExcel.cs b/Common/Data/Excel/WeaponExcel.cs index f9d2113..2c6b393 100644 --- a/Common/Data/Excel/WeaponExcel.cs +++ b/Common/Data/Excel/WeaponExcel.cs @@ -1,4 +1,7 @@ -namespace MikuSB.Data.Excel; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; [ResourceEntity("weapon.json")] public class WeaponExcel : ExcelResource @@ -7,8 +10,13 @@ public class WeaponExcel : ExcelResource public uint Detail { get; set; } public uint Particular { get; set; } public uint Level { get; set; } + [JsonProperty("Color")] public JToken? ColorRaw { get; set; } + [JsonProperty("InitBreak")] public JToken? InitBreakRaw { get; set; } public int Class { get; set; } public uint Icon { get; set; } + [JsonProperty("ProvideExp")] public JToken? ProvideExpRaw { get; set; } + [JsonProperty("ConsumeGold")] public JToken? ConsumeGoldRaw { get; set; } + [JsonProperty("RecycleID")] public JToken? RecycleIdRaw { get; set; } public int EvolutionMatID { get; set; } public int BreakMatID { get; set; } public int LevelLimitID { get; set; } @@ -17,6 +25,13 @@ public class WeaponExcel : ExcelResource public List DefaultSkillID { get; set; } = []; public List WeaponPartsLimit { get; set; } = []; public string I18n { get; set; } = ""; + + [JsonIgnore] public int Color => ReadInt(ColorRaw); + [JsonIgnore] public uint InitBreak => ReadUInt(InitBreakRaw); + [JsonIgnore] public uint ProvideExp => ReadUInt(ProvideExpRaw); + [JsonIgnore] public uint ConsumeGold => ReadUInt(ConsumeGoldRaw); + [JsonIgnore] public int RecycleID => ReadInt(RecycleIdRaw); + public override uint GetId() { return (uint)I18n.GetHashCode(); @@ -26,4 +41,26 @@ public class WeaponExcel : ExcelResource { GameData.WeaponData.Add(GetId(), this); } -} \ No newline at end of file + + private static int ReadInt(JToken? token) + { + if (token == null) + { + return 0; + } + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (int)token.Value(), + JTokenType.String when int.TryParse(token.Value(), out var value) => value, + _ => 0 + }; + } + + private static uint ReadUInt(JToken? token) + { + var value = ReadInt(token); + return value > 0 ? (uint)value : 0; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 4adf8b4..b225e9b 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -7,6 +7,10 @@ public static class GameData public static Dictionary CardData { get; private set; } = []; public static Dictionary WeaponData { get; private set; } = []; public static Dictionary CardSkinData { get; private set; } = []; + public static Dictionary SuppliesData { get; private set; } = []; + public static Dictionary UpgradeExpData { get; private set; } = []; + public static Dictionary BreakLevelLimitData { get; private set; } = []; + public static Dictionary RecycleData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/Common/Database/Inventory/InventoryData.cs b/Common/Database/Inventory/InventoryData.cs index 1cdb01d..a3f847d 100644 --- a/Common/Database/Inventory/InventoryData.cs +++ b/Common/Database/Inventory/InventoryData.cs @@ -23,6 +23,16 @@ public abstract class BaseGameItemInfo public uint UniqueId { get; set; } public ulong TemplateId { get; set; } public uint ItemCount { get; set; } + + public virtual Item ToProto() + { + return new Item + { + Id = UniqueId, + Template = TemplateId, + Count = ItemCount + }; + } } public abstract class GrowableItemInfo : BaseGameItemInfo @@ -30,12 +40,13 @@ public abstract class GrowableItemInfo : BaseGameItemInfo public bool IsLocked { get; set; } public uint Level { get; set; } public uint Exp { get; set; } + public uint Break { get; set; } public uint EquipAvatarId { get; set; } } public class GameWeaponInfo : GrowableItemInfo { - public Item ToProto() + public override Item ToProto() { var proto = new Item { @@ -44,17 +55,17 @@ public class GameWeaponInfo : GrowableItemInfo Count = ItemCount, Enhance = new Enhance { - Level = Level + Level = Level, + Exp = Exp, + Break = Break } }; return proto; } -} - -public class GameSkinInfo : BaseGameItemInfo +}public class GameSkinInfo : BaseGameItemInfo { public uint Level { get; set; } - public Item ToProto() + public override Item ToProto() { var proto = new Item { diff --git a/GameServer/Game/Character/CharacterManager.cs b/GameServer/Game/Character/CharacterManager.cs index 2edd9f6..f1677fa 100644 --- a/GameServer/Game/Character/CharacterManager.cs +++ b/GameServer/Game/Character/CharacterManager.cs @@ -57,4 +57,59 @@ public class CharacterManager(PlayerInstance player) : BasePlayerManager(player) var templateId = GameResourceTemplateId.FromGdpl((uint)genre,(uint)detail,(uint)particular,1); return CharacterData.Characters.Find(Character => Character.TemplateId == templateId); } -} \ No newline at end of file + + public async ValueTask RepairCharacterWeapons() + { + var changed = false; + var equippedWeaponIds = new HashSet(); + + foreach (var character in CharacterData.Characters) + { + var weapon = Player.InventoryManager.GetWeaponItem(character.WeaponUniqueId); + if (weapon == null) + { + var cardData = GameData.CardData.Values.FirstOrDefault(x => + GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == character.TemplateId); + if (cardData?.DefaultWeaponGPDL.Count >= 4) + { + weapon = await Player.InventoryManager.AddWeaponItem( + (ItemTypeEnum)cardData.DefaultWeaponGPDL[0], + cardData.DefaultWeaponGPDL[1], + cardData.DefaultWeaponGPDL[2], + cardData.DefaultWeaponGPDL[3]); + if (weapon != null) + { + character.WeaponUniqueId = weapon.UniqueId; + changed = true; + } + } + } + + if (weapon == null) + continue; + + if (weapon.EquipAvatarId != character.Guid) + { + weapon.EquipAvatarId = character.Guid; + changed = true; + } + + equippedWeaponIds.Add(weapon.UniqueId); + } + + foreach (var weapon in Player.InventoryManager.InventoryData.Weapons.Values) + { + if (!equippedWeaponIds.Contains(weapon.UniqueId) && weapon.EquipAvatarId != 0) + { + weapon.EquipAvatarId = 0; + changed = true; + } + } + + if (!changed) + return; + + DatabaseHelper.SaveDatabaseType(CharacterData); + DatabaseHelper.SaveDatabaseType(Player.InventoryManager.InventoryData); + } +} diff --git a/GameServer/Game/Inventory/InventoryManager.cs b/GameServer/Game/Inventory/InventoryManager.cs index 0d66a6e..ebb8c1b 100644 --- a/GameServer/Game/Inventory/InventoryManager.cs +++ b/GameServer/Game/Inventory/InventoryManager.cs @@ -22,6 +22,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) TemplateId = templateId, UniqueId = InventoryData.NextUniqueUid++, Level = level, + Break = weaponData.InitBreak, ItemCount = 1 }; InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo; diff --git a/GameServer/Game/Player/PlayerInstance.cs b/GameServer/Game/Player/PlayerInstance.cs index 21e7975..e644130 100644 --- a/GameServer/Game/Player/PlayerInstance.cs +++ b/GameServer/Game/Player/PlayerInstance.cs @@ -111,6 +111,7 @@ public class PlayerInstance(PlayerGameData data) public async ValueTask OnEnterGame() { if (!Initialized) await InitialPlayerManager(); + await CharacterManager.RepairCharacterWeapons(); } public async ValueTask OnLogin() @@ -218,6 +219,11 @@ public class PlayerInstance(PlayerGameData data) yield return (178, 1, 1_700_000_000); yield return (187, 1, 2); + // Cash.GetMoneyCount uses group 1 with sid = moneyId * 2 + 1 for most currencies. + // Fill a wide currency id range so every in-game currency starts effectively unlimited. + for (uint moneyId = 1; moneyId <= 200; moneyId++) + yield return (1, moneyId * 2 + 1, 999_999_999); + for (uint guideId = 1; guideId <= 150; guideId++) yield return (4, guideId, 999); diff --git a/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Upgrade.cs b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Upgrade.cs new file mode 100644 index 0000000..abfde95 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Upgrade.cs @@ -0,0 +1,384 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Inventory; +using MikuSB.Proto; +using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon; + +[CallGSApi("Weapon_Upgrade")] +public class Weapon_Upgrade : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.Id <= 0 || req.TbMat == null || req.TbMat.Count == 0) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + var targetWeapon = player.InventoryManager.GetWeaponItem((uint)req.Id); + if (targetWeapon == null) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + var config = WeaponUpgradeConfig.Load(); + if (!config.TryGetWeaponTemplate(targetWeapon.TemplateId, out var targetTemplate)) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var requestedMaterials = new Dictionary(); + foreach (var row in req.TbMat) + { + if (row == null || row.Count < 2) continue; + var itemId = (uint)Math.Max(0, row[0]); + var count = (uint)Math.Max(0, row[1]); + if (itemId == 0 || count == 0) continue; + requestedMaterials[itemId] = requestedMaterials.GetValueOrDefault(itemId) + count; + } + + if (requestedMaterials.Count == 0) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + ulong totalExp = 0; + var syncItems = new List(); + var equippedWeaponIds = player.CharacterManager.CharacterData.Characters + .Select(x => x.WeaponUniqueId) + .Where(x => x != 0) + .ToHashSet(); + + foreach (var (itemId, count) in requestedMaterials) + { + if (itemId == targetWeapon.UniqueId) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + var material = FindInventoryItem(player.InventoryManager.InventoryData, itemId); + if (material == null || material.ItemCount < count) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + if (material is GameWeaponInfo materialWeapon && + (materialWeapon.EquipAvatarId != 0 || equippedWeaponIds.Contains(materialWeapon.UniqueId))) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); + return; + } + + if (!TryGetMaterialGain(config, material, out var gainExp)) + { + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + totalExp += gainExp * count; + } + + foreach (var (itemId, count) in requestedMaterials) + { + var material = FindInventoryItem(player.InventoryManager.InventoryData, itemId)!; + material.ItemCount -= count; + + if (material.ItemCount == 0) + { + RemoveInventoryItem(player.InventoryManager.InventoryData, itemId); + syncItems.Add(BuildRemovedProto(material)); + } + else + { + syncItems.Add(material.ToProto()); + } + } + + var maxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, targetWeapon.Break); + var oldLevel = targetWeapon.Level == 0 ? 1u : targetWeapon.Level; + targetWeapon.Level = oldLevel; + var (newLevel, newExp) = config.ApplyWeaponExp(targetWeapon.Level, targetWeapon.Exp, totalExp, targetTemplate.Color, maxLevel); + targetWeapon.Level = newLevel; + targetWeapon.Exp = newExp; + + syncItems.Add(targetWeapon.ToProto()); + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + + var sync = new NtfSyncPlayer(); + sync.Items.AddRange(syncItems); + + var bMaxUnlock = maxLevel > 0 && targetWeapon.Level >= maxLevel; + var arg = $"{{\"bMaxUnLock\":{(bMaxUnlock ? "true" : "false")}}}"; + await CallGSRouter.SendScript(connection, "Weapon_Upgrade", arg, sync); + } + + private static BaseGameItemInfo? FindInventoryItem(InventoryData inventory, uint itemId) + { + if (inventory.Weapons.TryGetValue(itemId, out var weapon)) + { + return weapon; + } + + if (inventory.Skins.TryGetValue(itemId, out var skin)) + { + return skin; + } + + if (inventory.Items.TryGetValue(itemId, out var item)) + { + return item; + } + + return null; + } + + private static void RemoveInventoryItem(InventoryData inventory, uint itemId) + { + inventory.Weapons.Remove(itemId); + inventory.Skins.Remove(itemId); + inventory.Items.Remove(itemId); + } + + private static Item BuildRemovedProto(BaseGameItemInfo item) + { + var proto = item.ToProto(); + proto.Count = 0; + return proto; + } + + private static bool TryGetMaterialGain(WeaponUpgradeConfig config, BaseGameItemInfo item, out ulong exp) + { + exp = 0; + if (config.TryGetWeaponTemplate(item.TemplateId, out var weaponTemplate)) + { + exp = weaponTemplate.ProvideExp; + if (item is GameWeaponInfo weapon && weapon.Level > 1) + { + exp += config.GetWeaponRecycleExp(weaponTemplate, weapon.Level); + } + + return true; + } + + if (config.TryGetSuppliesTemplate(item.TemplateId, out var suppliesTemplate)) + { + exp = suppliesTemplate.ProvideExp; + return true; + } + + return false; + } +} + +internal sealed class WeaponUpgradeParam +{ + [JsonPropertyName("Id")] + public int Id { get; set; } + + [JsonPropertyName("tbMat")] + public List> TbMat { get; set; } = []; +} + +internal sealed class WeaponUpgradeConfig +{ + private readonly Dictionary _weaponNeedExpNormal; + private readonly Dictionary _weaponNeedExpSsr; + private readonly Dictionary _breakLimits; + private readonly Dictionary _recycleById; + private readonly Dictionary _weaponTemplates; + private readonly Dictionary _suppliesTemplates; + private readonly Dictionary _weaponRecycleExpNormal = []; + private readonly Dictionary _weaponRecycleExpSsr = []; + + public WeaponUpgradeConfig( + Dictionary weaponNeedExpNormal, + Dictionary weaponNeedExpSsr, + Dictionary breakLimits, + Dictionary recycleById, + Dictionary weaponTemplates, + Dictionary suppliesTemplates) + { + _weaponNeedExpNormal = weaponNeedExpNormal; + _weaponNeedExpSsr = weaponNeedExpSsr; + _breakLimits = breakLimits; + _recycleById = recycleById; + _weaponTemplates = weaponTemplates; + _suppliesTemplates = suppliesTemplates; + + BuildRecycleExpTable(_weaponNeedExpNormal, _weaponRecycleExpNormal); + BuildRecycleExpTable(_weaponNeedExpSsr, _weaponRecycleExpSsr); + } + + public static WeaponUpgradeConfig Load() + { + var normalExp = new Dictionary(); + var ssrExp = new Dictionary(); + foreach (var row in GameData.UpgradeExpData.Values) + { + normalExp[row.Lv] = row.WeaponNeedExp; + ssrExp[row.Lv] = row.SSRWeaponNeedExp; + } + + var breakLimits = new Dictionary(); + foreach (var row in GameData.BreakLevelLimitData.Values) + { + breakLimits[row.ID] = + [ + row.Break0, + row.Break1, + row.Break2, + row.Break3, + row.Break4, + row.Break5, + row.Break6 + ]; + } + + var recycleById = new Dictionary(); + foreach (var row in GameData.RecycleData.Values) + { + recycleById[row.ID] = new RecycleEntry( + GetUIntFlexible(row.RecycleBase), + GetDecimalFlexible(row.RecycleRatio)); + } + + var weaponTemplates = GameData.WeaponData.Values.ToDictionary( + x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level), + x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, x.RecycleID, x.BreakLimitID)); + var suppliesTemplates = GameData.SuppliesData.Values.ToDictionary( + x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level), + x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, 0, 0)); + + return new WeaponUpgradeConfig(normalExp, ssrExp, breakLimits, recycleById, weaponTemplates, suppliesTemplates); + } + + public bool TryGetWeaponTemplate(ulong templateId, out MaterialTemplate template) => + _weaponTemplates.TryGetValue(templateId, out template!); + + public bool TryGetSuppliesTemplate(ulong templateId, out MaterialTemplate template) => + _suppliesTemplates.TryGetValue(templateId, out template!); + + public ulong GetWeaponRecycleExp(MaterialTemplate template, uint level) + { + if (template.RecycleId <= 0 || !_recycleById.TryGetValue(template.RecycleId, out var recycle)) + { + return 0; + } + + var levelExp = template.Color == 5 ? _weaponRecycleExpSsr : _weaponRecycleExpNormal; + var baseExp = levelExp.GetValueOrDefault((int)level); + return (ulong)Math.Floor((recycle.RecycleBase + baseExp) * recycle.RecycleRatio); + } + + public uint GetWeaponMaxLevel(int breakLimitId, uint currentBreak) + { + if (!_breakLimits.TryGetValue(breakLimitId, out var limits) || limits.Length == 0) + { + return 0; + } + + var index = (int)Math.Min(currentBreak, (uint)(limits.Length - 1)); + return limits[index]; + } + + public (uint Level, uint Exp) ApplyWeaponExp(uint level, uint exp, ulong addExp, int color, uint maxLevel) + { + if (addExp == 0) + { + return (level, exp); + } + + if (maxLevel > 0 && level >= maxLevel) + { + return (maxLevel, checked((uint)(exp + addExp))); + } + + var destLevel = level; + var destExp = exp + addExp; + var needExp = GetWeaponNeedExp(color, destLevel); + + while (needExp > 0 && destExp >= needExp) + { + destExp -= needExp; + destLevel++; + + if (maxLevel > 0 && destLevel >= maxLevel) + { + return (maxLevel, checked((uint)destExp)); + } + + needExp = GetWeaponNeedExp(color, destLevel); + if (needExp == 0) + { + return (destLevel, checked((uint)destExp)); + } + } + + return (destLevel, checked((uint)destExp)); + } + + private uint GetWeaponNeedExp(int color, uint level) + { + return color == 5 + ? _weaponNeedExpSsr.GetValueOrDefault((int)level) + : _weaponNeedExpNormal.GetValueOrDefault((int)level); + } + + private static void BuildRecycleExpTable(Dictionary needExp, Dictionary recycleExp) + { + ulong current = 0; + foreach (var level in needExp.Keys.OrderBy(x => x)) + { + recycleExp[level] = current; + current += needExp[level]; + } + } + + private static uint GetUIntFlexible(JToken? token) + { + if (token == null) + { + return 0; + } + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (uint)Math.Max(0, token.Value()), + JTokenType.String when uint.TryParse(token.Value(), out var result) => result, + _ => 0 + }; + } + + private static decimal GetDecimalFlexible(JToken? token) + { + if (token == null) + { + return 0m; + } + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => token.Value(), + JTokenType.String when decimal.TryParse(token.Value(), out var result) => result, + _ => 0m + }; + } +} + +internal readonly record struct MaterialTemplate(int Color, uint ProvideExp, uint ConsumeGold, int RecycleId, int BreakLimitId); +internal readonly record struct RecycleEntry(uint RecycleBase, decimal RecycleRatio);