diff --git a/Common/Data/Excel/MonsterCardExcel.cs b/Common/Data/Excel/MonsterCardExcel.cs new file mode 100644 index 0000000..f25b257 --- /dev/null +++ b/Common/Data/Excel/MonsterCardExcel.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("item/templates/monster_card.json")] +public class MonsterCardExcel : ExcelResource +{ + [JsonProperty("Genre")] public uint Genre { get; set; } + [JsonProperty("Detail")] public uint Detail { get; set; } + [JsonProperty("Particular")] public uint Particular { get; set; } + [JsonProperty("Level")] public uint Level { get; set; } + [JsonProperty("Color")] public uint Color { get; set; } + [JsonProperty("RikiId")] public uint RikiId { get; set; } + [JsonProperty("CostValue")] public uint CostValue { get; set; } + + [JsonIgnore] + public ulong TemplateId => GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level); + + public override uint GetId() => Particular; + + public override void Loaded() + { + GameData.MonsterCardData[TemplateId] = this; + } +} diff --git a/Common/Data/Excel/VirCaptureCaptureRegionExcel.cs b/Common/Data/Excel/VirCaptureCaptureRegionExcel.cs index 9e7f2bf..c1c3869 100644 --- a/Common/Data/Excel/VirCaptureCaptureRegionExcel.cs +++ b/Common/Data/Excel/VirCaptureCaptureRegionExcel.cs @@ -9,6 +9,7 @@ public class VirCaptureCaptureRegionExcel : ExcelResource [JsonProperty("StartTime")] public string StartTime { get; set; } = ""; [JsonProperty("EndTime")] public string EndTime { get; set; } = ""; [JsonProperty("MapId")] public uint MapId { get; set; } + [JsonProperty("LevelRegionName")] public string LevelRegionName { get; set; } = ""; public override uint GetId() => Id; diff --git a/Common/Data/Excel/VirCaptureLevelListExcel.cs b/Common/Data/Excel/VirCaptureLevelListExcel.cs new file mode 100644 index 0000000..00dc16a --- /dev/null +++ b/Common/Data/Excel/VirCaptureLevelListExcel.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("dlc/vircapture/levellist.json")] +public class VirCaptureLevelListExcel : ExcelResource +{ + [JsonProperty("Level")] public uint Level { get; set; } + [JsonProperty("Num")] public uint Num { get; set; } + [JsonProperty("MaxCost")] public uint MaxCost { get; set; } + + public override uint GetId() => Level; + + public override void Loaded() + { + GameData.VirCaptureLevelListData[Level] = this; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index ffa628d..f9780e5 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -54,6 +54,8 @@ public static class GameData public static Dictionary VirCaptureSeasonData { get; private set; } = []; public static Dictionary VirCaptureTrialTimeData { get; private set; } = []; public static Dictionary VirCaptureCaptureRegionData { get; private set; } = []; + public static Dictionary VirCaptureLevelListData { get; private set; } = []; + public static Dictionary MonsterCardData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/GameServer/Game/Inventory/InventoryManager.cs b/GameServer/Game/Inventory/InventoryManager.cs index 5141eee..b8cedd4 100644 --- a/GameServer/Game/Inventory/InventoryManager.cs +++ b/GameServer/Game/Inventory/InventoryManager.cs @@ -208,6 +208,27 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) return InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == templateId); } + public async ValueTask AddMonsterCardItem(uint detail, uint particular, uint level = 1, bool sendPacket = true) + { + const ItemTypeEnum genre = ItemTypeEnum.TYPE_MONSTER_CARD; + var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level); + if (!GameData.MonsterCardData.ContainsKey(templateId)) + return null; + + var monsterInfo = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = InventoryData.NextUniqueUid++, + ItemType = genre, + ItemCount = 1 + }; + InventoryData.Items[monsterInfo.UniqueId] = monsterInfo; + + if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([monsterInfo])); + + return monsterInfo; + } + private static uint GetSuppliesMaxCount(SuppliesExcel suppliesData) => suppliesData.Genre == 5 && suppliesData.Detail == 4 ? 999999u : 99999u; diff --git a/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureCaptureRewardResolver.cs b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureCaptureRewardResolver.cs new file mode 100644 index 0000000..2c87845 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureCaptureRewardResolver.cs @@ -0,0 +1,134 @@ +using MikuSB.Data.Excel; +using MikuSB.Util; +using Newtonsoft.Json.Linq; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture; + +internal static class VirCaptureCaptureRewardResolver +{ + private static readonly Lock CacheLock = new(); + private static readonly Dictionary> RegionCache = []; + private static readonly Dictionary>> BossCache = []; + + public static List? ResolveGdpl(VirCaptureCaptureRegionExcel captureRegion, uint regionId) + { + if (string.IsNullOrWhiteSpace(captureRegion.LevelRegionName)) + return null; + + var regionMap = GetOrLoadRegionMap(captureRegion.LevelRegionName); + if (!regionMap.TryGetValue(regionId, out var regionReward)) + return null; + + if (regionReward.PalType == 2) + return GetOrLoadBossMap(captureRegion.LevelRegionName).GetValueOrDefault(regionId); + + return regionReward.Rewards1; + } + + private static Dictionary GetOrLoadRegionMap(string mapName) + { + lock (CacheLock) + { + if (RegionCache.TryGetValue(mapName, out var cached)) + return cached; + + var loaded = new Dictionary(); + var path = Path.Combine( + ConfigManager.Config.Path.ResourcePath, + "dlc", + "vircapture", + mapName, + "region_info.json"); + + if (File.Exists(path)) + { + var array = JArray.Parse(File.ReadAllText(path)); + foreach (var token in array) + { + var id = ReadUInt(token["Id"]); + if (id == 0) + continue; + + loaded[id] = new VirCaptureLevelRegionReward + { + PalType = ReadInt(token["PalType"]), + Rewards1 = token["Rewards1"]?.ToObject>() ?? [] + }; + } + } + + RegionCache[mapName] = loaded; + return loaded; + } + } + + private static Dictionary> GetOrLoadBossMap(string mapName) + { + lock (CacheLock) + { + if (BossCache.TryGetValue(mapName, out var cached)) + return cached; + + var loaded = new Dictionary>(); + var path = Path.Combine( + ConfigManager.Config.Path.ResourcePath, + "dlc", + "vircapture", + mapName, + "boss.json"); + + if (File.Exists(path)) + { + var array = JArray.Parse(File.ReadAllText(path)); + foreach (var token in array) + { + var regionId = ReadUInt(token["RegionId"]); + var boss = token["Boss"]?.ToObject>(); + if (regionId == 0 || boss == null || boss.Count < 4) + continue; + + loaded.TryAdd(regionId, boss); + } + } + + BossCache[mapName] = loaded; + return loaded; + } + } + + private sealed class VirCaptureLevelRegionReward + { + public int PalType { get; init; } + public List Rewards1 { get; init; } = []; + } + + private static uint ReadUInt(JToken? token) + { + if (token == null || token.Type == JTokenType.Null) + return 0; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => Math.Max(0u, (uint)token.Value()), + JTokenType.String when uint.TryParse(token.Value(), out var value) => value, + JTokenType.String => 0, + _ => 0 + }; + } + + private static int ReadInt(JToken? token) + { + if (token == null || token.Type == JTokenType.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, + JTokenType.String => 0, + _ => 0 + }; + } +} diff --git a/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureLevel_SaveCapture.cs b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureLevel_SaveCapture.cs index d812c44..7661fdb 100644 --- a/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureLevel_SaveCapture.cs +++ b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureLevel_SaveCapture.cs @@ -1,4 +1,6 @@ using MikuSB.Database; +using MikuSB.Data; +using MikuSB.Enums.Item; using MikuSB.Proto; using System.Text.Json; using System.Text.Json.Nodes; @@ -22,16 +24,71 @@ public class VirCaptureLevel_SaveCapture : ICallGSHandler var sync = new NtfSyncPlayer(); VirCaptureStateHelper.SetPointState(player, (uint)req.LevelId, (uint)req.RegionId, 2u, sync); + if (!GameData.VirCaptureCaptureRegionData.TryGetValue((uint)req.LevelId, out var captureRegion)) + { + await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var rewardGdpl = VirCaptureCaptureRewardResolver.ResolveGdpl(captureRegion, (uint)req.RegionId); + if (rewardGdpl == null || rewardGdpl.Count < 4 || rewardGdpl[0] != (uint)ItemTypeEnum.TYPE_MONSTER_CARD) + { + await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}", sync); + return; + } + + var grantedItem = await player.InventoryManager.AddMonsterCardItem( + rewardGdpl[1], + rewardGdpl[2], + rewardGdpl[3], + sendPacket: false); + if (grantedItem == null) + { + await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", "{\"sErr\":\"error.BadParam\"}", sync); + return; + } + + sync.Items.Add(grantedItem.ToProto()); + SyncVirCaptureCounters(player, grantedItem.TemplateId, sync); + DatabaseHelper.SaveDatabaseType(player.Data); + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); var response = new JsonObject { ["nLevelID"] = req.LevelId, - ["nRegionId"] = req.RegionId + ["nRegionId"] = req.RegionId, + ["nAddItemId"] = grantedItem.UniqueId, + ["tbGDPL"] = new JsonArray(rewardGdpl.Select(x => JsonValue.Create((int)x)).ToArray()) }; await CallGSRouter.SendScript(connection, "VirCaptureLevel_SaveCapture", response.ToJsonString(), sync); } + + private static void SyncVirCaptureCounters(MikuSB.GameServer.Game.Player.PlayerInstance player, ulong templateId, NtfSyncPlayer sync) + { + var bagCount = (uint)player.InventoryManager.InventoryData.Items.Values.Count(x => x.ItemType == ItemTypeEnum.TYPE_MONSTER_CARD); + VirCaptureStateHelper.SetUnsignedAttr(player, 5, bagCount, sync); + + if (!GameData.MonsterCardData.TryGetValue(templateId, out var monsterCard) || monsterCard.RikiId == 0) + return; + + var rikiAttr = player.Data.Attrs.FirstOrDefault(x => x.Gid == 135 && x.Sid == monsterCard.RikiId); + if (rikiAttr == null) + { + rikiAttr = new Database.Player.PlayerAttr + { + Gid = 135, + Sid = monsterCard.RikiId, + Val = 0 + }; + player.Data.Attrs.Add(rikiAttr); + } + + rikiAttr.Val += 1; + sync.Custom[player.ToPackedAttrKey(135, monsterCard.RikiId)] = rikiAttr.Val; + sync.Custom[player.ToShiftedAttrKey(135, monsterCard.RikiId)] = rikiAttr.Val; + } } internal sealed class VirCaptureSaveCaptureParam diff --git a/GameServer/Server/CallGS/Handlers/VirCapture/VirCapture_ChangeFormation.cs b/GameServer/Server/CallGS/Handlers/VirCapture/VirCapture_ChangeFormation.cs new file mode 100644 index 0000000..16b8416 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/VirCapture/VirCapture_ChangeFormation.cs @@ -0,0 +1,140 @@ +using MikuSB.Data; +using MikuSB.Database; +using MikuSB.Enums.Item; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture; + +[CallGSApi("VirCapture_ChangeFormation")] +public class VirCapture_ChangeFormation : ICallGSHandler +{ + private const uint StrGroupId = 57; + private const uint FormationSid = 1; + private const uint VirCaptureGroupId = 128; + private const uint CurLevelSid = 3; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null) + { + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var player = connection.Player!; + var formation = ReadFormation(player); + var addId = (uint)Math.Max(0, req.Id); + var unloadId = (uint)Math.Max(0, req.UnloadId); + + var unloadIndex = unloadId == 0 ? -1 : formation.FindIndex(x => x == unloadId); + if (unloadId > 0 && unloadIndex < 0) + { + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (addId > 0) + { + if (formation.Contains(addId)) + { + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var addItem = player.InventoryManager.GetNormalItem(addId); + if (addItem == null || addItem.ItemType != ItemTypeEnum.TYPE_MONSTER_CARD) + { + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}"); + return; + } + } + + if (unloadIndex >= 0) + formation.RemoveAt(unloadIndex); + + if (addId > 0) + { + if (unloadIndex >= 0 && unloadIndex <= formation.Count) + formation.Insert(unloadIndex, addId); + else + formation.Add(addId); + } + + if (!ValidateFormation(player, formation)) + { + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var json = JsonSerializer.Serialize(formation); + player.SetStrAttr(StrGroupId, FormationSid, json); + + DatabaseHelper.SaveDatabaseType(player.Data); + + var sync = new NtfSyncPlayer(); + sync.CustomStr[player.ToShiftedAttrKey(StrGroupId, FormationSid)] = json; + + var response = new JsonObject + { + ["nId"] = req.Id, + ["nUnloadId"] = req.UnloadId, + ["bAdd"] = addId > 0 + }; + + await CallGSRouter.SendScript(connection, "VirCapture_ChangeFormation", response.ToJsonString(), sync); + } + + private static List ReadFormation(MikuSB.GameServer.Game.Player.PlayerInstance player) + { + var raw = player.Data.StrAttrs.FirstOrDefault(x => x.Gid == StrGroupId && x.Sid == FormationSid)?.Val; + if (string.IsNullOrWhiteSpace(raw)) + return []; + + try + { + return JsonSerializer.Deserialize>(raw) ?? []; + } + catch + { + return []; + } + } + + private static bool ValidateFormation(MikuSB.GameServer.Game.Player.PlayerInstance player, List formation) + { + var curLevel = player.Data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == CurLevelSid)?.Val ?? 1; + if (!GameData.VirCaptureLevelListData.TryGetValue(curLevel, out var levelCfg)) + return formation.Count == 0; + + if (formation.Count > levelCfg.Num) + return false; + + uint totalCost = 0; + foreach (var itemId in formation) + { + var item = player.InventoryManager.GetNormalItem(itemId); + if (item == null || item.ItemType != ItemTypeEnum.TYPE_MONSTER_CARD) + return false; + + if (!GameData.MonsterCardData.TryGetValue(item.TemplateId, out var monsterCfg)) + return false; + + totalCost += monsterCfg.CostValue; + } + + return totalCost <= levelCfg.MaxCost; + } +} + +internal sealed class VirCaptureChangeFormationParam +{ + [JsonPropertyName("nId")] + public int Id { get; set; } + + [JsonPropertyName("nUnloadId")] + public int UnloadId { get; set; } +}