diff --git a/Common/Data/Excel/BreakExcel.cs b/Common/Data/Excel/BreakExcel.cs new file mode 100644 index 0000000..5abd201 --- /dev/null +++ b/Common/Data/Excel/BreakExcel.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("break.json")] +public class BreakExcel : ExcelResource +{ + [JsonProperty("ID")] public int Id { get; set; } + + [JsonProperty("Items1")] public List> Items1 { get; set; } = []; + [JsonProperty("Items2")] public List> Items2 { get; set; } = []; + [JsonProperty("Items3")] public List> Items3 { get; set; } = []; + [JsonProperty("Items4")] public List> Items4 { get; set; } = []; + [JsonProperty("Items5")] public List> Items5 { get; set; } = []; + [JsonProperty("Items6")] public List> Items6 { get; set; } = []; + + public List> GetItems(uint breakLevel) => breakLevel switch + { + 1 => Items1, + 2 => Items2, + 3 => Items3, + 4 => Items4, + 5 => Items5, + 6 => Items6, + _ => [] + }; + + public override uint GetId() => (uint)Id; + + public override void Loaded() + { + GameData.BreakData[Id] = this; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index fabd1fe..80d94b3 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -16,6 +16,7 @@ public static class GameData public static Dictionary ArItemData { get; private set; } = []; public static Dictionary ManifestationData { get; private set; } = []; public static Dictionary Rogue3DDifficultData { get; private set; } = []; + public static Dictionary BreakData { get; private set; } = []; public static Dictionary SpineData { get; private set; } = []; public static Dictionary NodeConditionData { get; private set; } = []; public static List SupportCardData { get; private set; } = []; diff --git a/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs new file mode 100644 index 0000000..96c2996 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs @@ -0,0 +1,98 @@ +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon; + +// s2c: function(sErr) — send "null" on success (json.decode("null") = nil = falsy in Lua) +[CallGSApi("Weapon_Break")] +public class Weapon_Break : ICallGSHandler +{ + private const uint MaxBreak = 6; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.WeaponId == 0) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\""); + return; + } + + var weapon = player.InventoryManager.InventoryData.Weapons.GetValueOrDefault((uint)req.WeaponId); + if (weapon == null) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\""); + return; + } + + if (weapon.Break >= MaxBreak) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.already_max_break\""); + return; + } + + var nextBreak = weapon.Break + 1; + + // Look up break cost from WeaponExcel → BreakExcel + var weaponExcel = GameData.WeaponData.Values.FirstOrDefault(x => + GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == weapon.TemplateId); + + var requestedMaterials = new Dictionary(); + if (weaponExcel != null && GameData.BreakData.TryGetValue(weaponExcel.BreakMatID, out var breakExcel)) + { + foreach (var row in breakExcel.GetItems(nextBreak)) + { + if (row.Count < 5) continue; + var tid = GameResourceTemplateId.FromGdpl( + (uint)row[0], (uint)row[1], (uint)row[2], (uint)row[3]); + requestedMaterials[tid] = requestedMaterials.GetValueOrDefault(tid) + (uint)row[4]; + } + } + + // Validate materials + foreach (var (tid, count) in requestedMaterials) + { + var item = player.InventoryManager.InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == tid); + if (item == null || item.ItemCount < count) + { + await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.not_material_for_break\""); + return; + } + } + + // Consume materials + var syncItems = new List(); + foreach (var (tid, count) in requestedMaterials) + { + var item = player.InventoryManager.InventoryData.Items.Values.First(x => x.TemplateId == tid); + item.ItemCount -= count; + var proto = item.ToProto(); + if (item.ItemCount == 0) + { + player.InventoryManager.InventoryData.Items.Remove(item.UniqueId); + proto.Count = 0; + } + syncItems.Add(proto); + } + + weapon.Break = nextBreak; + syncItems.Add(weapon.ToProto()); + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + + var sync = new NtfSyncPlayer(); + sync.Items.AddRange(syncItems); + + await CallGSRouter.SendScript(connection, "Weapon_Break", "null", sync); + } +} + +internal sealed class WeaponBreakParam +{ + [JsonPropertyName("Id")] + public int WeaponId { get; set; } +}