From a0f50f9e995bb16e8e3585d1de29295c9e1dca75 Mon Sep 17 00:00:00 2001 From: Ggafrik <906823881@qq.com> Date: Wed, 2 Jul 2025 00:21:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- default.project.json | 6 + excel/equipment.xlsx | Bin 0 -> 8698 bytes excel/excel2json.py | 90 ++++++++ export.py | 90 ++++++++ src/ReplicatedStorage/Json/Equipment.json | 3 + src/ReplicatedStorage/Tools/Utils.luau | 63 ++++++ src/Server/Proxy/ArchiveProxy.luau | 153 +++++++++++++ src/Server/Proxy/EquipmentProxy.luau | 91 ++++++++ src/Server/Proxy/PlayerInfoProxy.luau | 25 ++ src/Server/ServerMain/init.server.luau | 169 ++++---------- .../ClientMain/MeleeMobAlign.luau | 93 ++++++++ .../ClientMain/MobClient.luau | 213 ++++++++++++++++++ .../ClientMain/PlayerListStats.luau | 63 ++++++ .../ClientMain/Transportation.luau | 213 ++++++++++++++++++ .../ClientMain/init.client.luau | 105 +++++++++ 15 files changed, 1249 insertions(+), 128 deletions(-) create mode 100644 excel/equipment.xlsx create mode 100644 excel/excel2json.py create mode 100644 export.py create mode 100644 src/ReplicatedStorage/Json/Equipment.json create mode 100644 src/ReplicatedStorage/Tools/Utils.luau create mode 100644 src/Server/Proxy/ArchiveProxy.luau create mode 100644 src/Server/Proxy/EquipmentProxy.luau create mode 100644 src/Server/Proxy/PlayerInfoProxy.luau create mode 100644 src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau create mode 100644 src/StarterPlayerScripts/ClientMain/MobClient.luau create mode 100644 src/StarterPlayerScripts/ClientMain/PlayerListStats.luau create mode 100644 src/StarterPlayerScripts/ClientMain/Transportation.luau create mode 100644 src/StarterPlayerScripts/ClientMain/init.client.luau diff --git a/default.project.json b/default.project.json index 328e536..a69113d 100644 --- a/default.project.json +++ b/default.project.json @@ -18,6 +18,9 @@ "$className": "StarterPlayerScripts", "BilGui": { "$path": "src/StarterPlayerScripts/BilGui" + }, + "ClientMain": { + "$path": "src/StarterPlayerScripts/ClientMain" } } }, @@ -32,6 +35,9 @@ "Data": { "$path": "src/ReplicatedStorage/Data" }, + "Json": { + "$path": "src/ReplicatedStorage/Json" + }, "Tools": { "$path": "src/ReplicatedStorage/Tools" }, diff --git a/excel/equipment.xlsx b/excel/equipment.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0464a3e50307f5dbda7fbcb2ce53b032dc91e60d GIT binary patch literal 8698 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 literal 0 HcmV?d00001 diff --git a/excel/excel2json.py b/excel/excel2json.py new file mode 100644 index 0000000..a6ebf7c --- /dev/null +++ b/excel/excel2json.py @@ -0,0 +1,90 @@ +import os +import json +import openpyxl +import re + +excel_dir = 'excel' +output_dir = 'src/ReplicatedStorage/Data' + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +def parse_array_field(value, elem_type): + """ + 解析数组类型字段,支持[1,2,3]或1,2,3写法,自动去除空格和空字符串。 + elem_type: 'int', 'float', 'string'等 + """ + if value is None: + return [] + s = str(value).strip() + # 支持带中括号写法 + if s.startswith("[") and s.endswith("]"): + s = s[1:-1] + # 支持英文逗号和中文逗号 + items = re.split(r'[,,]', s) + result = [] + for v in items: + v = v.strip() + if v == "": + continue + if elem_type == "int": + try: + result.append(int(v)) + except Exception: + pass + elif elem_type == "float": + try: + result.append(float(v)) + except Exception: + pass + else: + result.append(v) + return result + +for filename in os.listdir(excel_dir): + if filename.endswith('.xlsx'): + filepath = os.path.join(excel_dir, filename) + wb = openpyxl.load_workbook(filepath) + for sheet_name in wb.sheetnames: + ws = wb[sheet_name] + rows = list(ws.iter_rows(values_only=True)) + if len(rows) < 2: + continue + headers = rows[0] + types = rows[1] + # 只保留字段名和类型都不为空的字段 + valid_indices = [ + i for i, (h, t) in enumerate(zip(headers, types)) + if h is not None and str(h).strip() != "" and t is not None and str(t).strip() != "" + ] + valid_headers = [headers[i] for i in valid_indices] + valid_types = [types[i] for i in valid_indices] + data = [] + for row in rows[2:]: + filtered_row = [row[i] if i < len(row) else None for i in valid_indices] + row_dict = {} + for h, t, v in zip(valid_headers, valid_types, filtered_row): + if t.endswith("[]"): + elem_type = t[:-2] + row_dict[h] = parse_array_field(v, elem_type) + else: + row_dict[h] = v + id_value = row_dict.get("id") + # 只导出id为数字且不为空的行 + if id_value is not None and isinstance(id_value, (int, float)) and str(int(id_value)).isdigit(): + data.append(row_dict) + if not data: + continue + out_name = f"{sheet_name}.json" + out_path = os.path.join(output_dir, out_name) + # 写入json,每个对象单独一行 + with open(out_path, 'w', encoding='utf-8') as f: + f.write('[\n') + for i, obj in enumerate(data): + line = json.dumps(obj, ensure_ascii=False, separators=(',', ':')) + if i < len(data) - 1: + f.write(line + ',\n') + else: + f.write(line + '\n') + f.write(']') + print(f"导出: {out_path}") \ No newline at end of file diff --git a/export.py b/export.py new file mode 100644 index 0000000..8d1b6c3 --- /dev/null +++ b/export.py @@ -0,0 +1,90 @@ +import os +import json +import openpyxl +import re + +excel_dir = 'excel' +output_dir = 'src/ReplicatedStorage/Json' + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +def parse_array_field(value, elem_type): + """ + 解析数组类型字段,支持[1,2,3]或1,2,3写法,自动去除空格和空字符串。 + elem_type: 'int', 'float', 'string'等 + """ + if value is None: + return [] + s = str(value).strip() + # 支持带中括号写法 + if s.startswith("[") and s.endswith("]"): + s = s[1:-1] + # 支持英文逗号和中文逗号 + items = re.split(r'[,,]', s) + result = [] + for v in items: + v = v.strip() + if v == "": + continue + if elem_type == "int": + try: + result.append(int(v)) + except Exception: + pass + elif elem_type == "float": + try: + result.append(float(v)) + except Exception: + pass + else: + result.append(v) + return result + +for filename in os.listdir(excel_dir): + if filename.endswith('.xlsx'): + filepath = os.path.join(excel_dir, filename) + wb = openpyxl.load_workbook(filepath) + for sheet_name in wb.sheetnames: + ws = wb[sheet_name] + rows = list(ws.iter_rows(values_only=True)) + if len(rows) < 2: + continue + headers = rows[0] + types = rows[1] + # 只保留字段名和类型都不为空的字段 + valid_indices = [ + i for i, (h, t) in enumerate(zip(headers, types)) + if h is not None and str(h).strip() != "" and t is not None and str(t).strip() != "" + ] + valid_headers = [headers[i] for i in valid_indices] + valid_types = [types[i] for i in valid_indices] + data = [] + for row in rows[2:]: + filtered_row = [row[i] if i < len(row) else None for i in valid_indices] + row_dict = {} + for h, t, v in zip(valid_headers, valid_types, filtered_row): + if t.endswith("[]"): + elem_type = t[:-2] + row_dict[h] = parse_array_field(v, elem_type) + else: + row_dict[h] = v + id_value = row_dict.get("id") + # 只导出id为数字且不为空的行 + if id_value is not None and isinstance(id_value, (int, float)) and str(int(id_value)).isdigit(): + data.append(row_dict) + if not data: + continue + out_name = f"{sheet_name}.json" + out_path = os.path.join(output_dir, out_name) + # 写入json,每个对象单独一行 + with open(out_path, 'w', encoding='utf-8') as f: + f.write('[\n') + for i, obj in enumerate(data): + line = json.dumps(obj, ensure_ascii=False, separators=(',', ':')) + if i < len(data) - 1: + f.write(line + ',\n') + else: + f.write(line + '\n') + f.write(']') + print(f"导出: {out_path}") diff --git a/src/ReplicatedStorage/Json/Equipment.json b/src/ReplicatedStorage/Json/Equipment.json new file mode 100644 index 0000000..77b0b59 --- /dev/null +++ b/src/ReplicatedStorage/Json/Equipment.json @@ -0,0 +1,3 @@ +[ +{"id":1,"type":1,"name":1,"attributes":[1,20,2,20]} +] \ No newline at end of file diff --git a/src/ReplicatedStorage/Tools/Utils.luau b/src/ReplicatedStorage/Tools/Utils.luau new file mode 100644 index 0000000..4324d93 --- /dev/null +++ b/src/ReplicatedStorage/Tools/Utils.luau @@ -0,0 +1,63 @@ +local Utils = {} + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Variables +local PlayerDataFolder = ReplicatedStorage.PlayerData + +--> Constants + +-------------------------------------------------------------------------------- + +function Utils:GetPlayerDataFolder(Player: Player) + local pData = PlayerDataFolder:FindFirstChild(Player.UserId) + if pData then return pData end + warn("玩家数据不存在: " .. Player.Name) + return nil +end + +function Utils:CreateFolder(Name: string, Parent: Instance) + local Folder = Instance.new("Folder") + Folder.Name = Name + Folder.Parent = Parent + return Folder +end + +function Utils:SetAttributesList(Object: Instance, Attributes: table) + for Attribute, Value in Attributes do + Object:SetAttribute(Attribute, Value) + end +end + +function Utils:GenUniqueId(t: table) + local min_id = 1 + while t[min_id] ~= nil do + min_id = min_id + 1 + end + return min_id +end + +function Utils:GetJsonIdData(JsonName: string, id: number) + local JsonData = require(ReplicatedStorage.Json[JsonName]) + for _, item in ipairs(JsonData) do + if item.id == id then + return item + end + end + return nil -- 没找到对应id +end + +function Utils:GetIdDataFromJson(JsonData: table, id: number) + -- 遍历JsonData,查找id字段等于目标id的项 + for _, item in ipairs(JsonData) do + if item.id == id then + return item + end + end + return nil -- 没有找到对应id +end + +-------------------------------------------------------------------------------- + +return Utils \ No newline at end of file diff --git a/src/Server/Proxy/ArchiveProxy.luau b/src/Server/Proxy/ArchiveProxy.luau new file mode 100644 index 0000000..c88ea6d --- /dev/null +++ b/src/Server/Proxy/ArchiveProxy.luau @@ -0,0 +1,153 @@ +-- 数据存储代理 +local ArchiveProxy = {} + +--> Services +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local DataStoreService = game:GetService("DataStoreService") +local ServerStorage = game:GetService("ServerStorage") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") + +--> Dependencies +local GameConfig = require(ReplicatedStorage.Data.GameConfig) +local ContentLibrary = require(ReplicatedStorage.Modules.ContentLibrary) + +--> Variables +local UserData = DataStoreService:GetDataStore("UserData") +local SameKeyCooldown = {} + +-------------------------------------------------------------------------------- + +function ArchiveProxy:IsPlayerDataLoaded(Player: Player) + local timeout = 5 + local start = os.clock() + while not Player:GetAttribute("DataLoaded") do + if os.clock() - start > timeout then + return false -- 超时 + end + Player:GetAttributeChangedSignal("DataLoaded"):Wait() + end + return true -- 成功加载 +end + +-------------------------------------------------------------------------------- + +local PlayerData = Instance.new("Configuration") +PlayerData.Name = "PlayerData" +PlayerData.Parent = ReplicatedStorage + +local _warn = warn +local function warn(warning: string) + _warn("DataManager Failure: ".. warning) +end + +-- Attempt to save user data. Returns whether or not the request was successful. +local function SaveData(Player: Player): boolean + if not Player:GetAttribute("DataLoaded") then + return false + end + + local pData = PlayerData:FindFirstChild(Player.UserId) + local StarterGear = Player:FindFirstChild("StarterGear") + if not pData or not StarterGear then + return false + end + + -- Same Key Cooldown (can't write to the same key within 6 seconds) + if SameKeyCooldown[Player.UserId] then + repeat task.wait() until not SameKeyCooldown[Player.UserId] + end + SameKeyCooldown[Player.UserId] = true + task.delay(6, function() + SameKeyCooldown[Player.UserId] = nil + end) + + -- Compile "DataToSave" table, which we pass to GlobalDataStore:SetAsync -- + local DataToSave = {} + + -- Save to DataStore -- + local Success + for i = 1, 3 do + Success = xpcall(function() + return UserData:SetAsync("user/".. Player.UserId, DataToSave, {Player.UserId}) + end, warn) + + if Success then + break + end + task.wait(6) + end + + if Success then + print(("DataManager: User %s's data saved successfully."):format(Player.Name)) + else + warn(("DataManager: User %s's data failed to save."):format(Player.Name)) + end + + return Success +end + +-- Attempt to load user data. Returns whether or not the request was successful, as well as the data if it was. +local function LoadData(Player: Player): (boolean, any) + local Success, Response = xpcall(function() + return UserData:GetAsync("user/".. Player.UserId) + end, warn) + + if Success and Response then + print(("DataManager: User %s's data loaded into the game with Level '%s'."):format(Player.Name, Response.Stats.Level)) + else + print(("DataManager: User %s had no data to load from."):format(Player.Name)) + end + return Success, Response +end + +local function OnPlayerAdded(Player: Player) + local Success, Data = LoadData(Player) + + if not Success then + CollectionService:AddTag(Player, "DataFailed") + Player:Kick("Data unable to load. DataStore Service may be down. Please rejoin later.") + return + end + + if not ArchiveProxy.pData then + ArchiveProxy.pData = {} + end + + ArchiveProxy.pData[Player.UserId] = Data + Player:SetAttribute("DataLoaded", true) +end + +local function OnPlayerRemoving(Player: Player) + SaveData(Player) + ArchiveProxy.pData[Player.UserId] = nil + ReplicatedStorage.Remotes.PlayerRemoving:Fire(Player) +end + +Players.PlayerAdded:Connect(OnPlayerAdded) +for _, Player in Players:GetPlayers() do + OnPlayerAdded(Player) +end + +-- Save on leave +Players.PlayerRemoving:Connect(OnPlayerRemoving) + + +-- Server closing (save) +game:BindToClose(function() + task.wait(RunService:IsStudio() and 1 or 10) +end) + + +-- Auto-save +task.spawn(function() + while true do + task.wait(60) + for _, Player in Players:GetPlayers() do + task.defer(SaveData, Player) + end + end +end) + +return ArchiveProxy \ No newline at end of file diff --git a/src/Server/Proxy/EquipmentProxy.luau b/src/Server/Proxy/EquipmentProxy.luau new file mode 100644 index 0000000..c2cf13e --- /dev/null +++ b/src/Server/Proxy/EquipmentProxy.luau @@ -0,0 +1,91 @@ +-- 装备代理 +local EquipmentProxy = {} + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Variables +local Utils = require(ReplicatedStorage.Tools.Utils) +local EquipmentJsonData = require(ReplicatedStorage.Json.Equipment) +local ArchiveProxy = require(ReplicatedStorage.Modules.ArchiveProxy) + +--> Constants +local STORE_NAME = "Equipment" + +-------------------------------------------------------------------------------- + +local function GetPlayerEquipmentFolder(Player: Player) + local pData = Utils:GetPlayerDataFolder(Player) + if not pData then return end + local EquipmentFolder = pData:FindFirstChild("Equipment") + return EquipmentFolder +end + + +local function CreateEquipmentInstance(Player: Player, UniqueId: number, EquipmentData: table) + if Player or UniqueId or EquipmentData then + warn('创建装备实例失败: ' .. Player.Name .. ' ' .. UniqueId .. ' ' .. EquipmentData) + return + end + local PlayerEquipmentFolder = GetPlayerEquipmentFolder(Player) + if not PlayerEquipmentFolder then return end + + local Config = Instance.new("Configuration") + Config.Name = UniqueId + Utils:SetAttributesList(Config, PlayerEquipmentFolder) + Config.Parent = PlayerEquipmentFolder + return Config +end + +-------------------------------------------------------------------------------- + +function EquipmentProxy:InitPlayer(Player: Player) + local pData = Utils:GetPlayerDataFolder(Player) + if not pData then return end + local EquipmentFolder = Utils:CreateFolder("Equipment", pData) + + -- 初始化数据存储 + if not ArchiveProxy.pData[Player.UserId] then + ArchiveProxy.pData[Player.UserId] = {} + end + + -- 初始化装备 + for uniqueId, EquipmentData in ArchiveProxy.pData[Player.UserId] do + CreateEquipmentInstance(Player, uniqueId, EquipmentData) + end +end + +local EXCEPT_KEYS = { "id", "orgId", "name"} +-- 添加装备到背包 +function EquipmentProxy:AddEquipment(Player: Player, EquipmentId: number) + local pData = Utils:GetPlayerDataFolder(Player) + if not pData then return end + + local EquipmentData = Utils:GetJsonIdData("Equipment", EquipmentId) + if not EquipmentData then return end + + local UniqueId = Utils:GenUniqueId(ArchiveProxy.pData[Player.UserId]) + local ResultData = {} + for key, value in pairs(EquipmentData) do + if not table.find(EXCEPT_KEYS, key) then + ResultData[key] = value + end + end + + ResultData.id = UniqueId + ResultData.orgId = EquipmentId + ResultData.wearing = false + + ArchiveProxy.pData[Player.UserId][UniqueId] = ResultData + CreateEquipmentInstance(Player, UniqueId, ResultData) +end + + +function EquipmentProxy:OnPlayerRemoving(Player: Player) + +end + + +ReplicatedStorage.Remotes.PlayerRemoving:Connect(EquipmentProxy.OnPlayerRemoving) + +return EquipmentProxy \ No newline at end of file diff --git a/src/Server/Proxy/PlayerInfoProxy.luau b/src/Server/Proxy/PlayerInfoProxy.luau new file mode 100644 index 0000000..770a733 --- /dev/null +++ b/src/Server/Proxy/PlayerInfoProxy.luau @@ -0,0 +1,25 @@ +-- 玩家基础信息代理 +local PlayerInfoProxy = {} + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Variables + +--> Constants +local STORE_NAME = "PlayerInfo" +-------------------------------------------------------------------------------- + +function PlayerInfoProxy:InitPlayer(Player: Player) + +end + + +function PlayerInfoProxy:OnPlayerRemoving(Player: Player) + +end + + +ReplicatedStorage.Remotes.PlayerRemoving:Connect(PlayerInfoProxy.OnPlayerRemoving) + +return PlayerInfoProxy \ No newline at end of file diff --git a/src/Server/ServerMain/init.server.luau b/src/Server/ServerMain/init.server.luau index 0803035..f0d9d68 100644 --- a/src/Server/ServerMain/init.server.luau +++ b/src/Server/ServerMain/init.server.luau @@ -34,69 +34,32 @@ local function CreateFolder(Name: string, Parent: Instance) return Folder end +-- 初始化workspace目录 local Temporary = CreateFolder("Temporary", workspace) local ProjectileCache = CreateFolder("ProjectileCache", Temporary) local Characters = CreateFolder("Characters", workspace) -local function OnPlayerAdded(Player: Player) - local pData = PlayerData:WaitForChild(Player.UserId) - local Level = pData.Stats.Level - local XP = pData.Stats.XP - - local function OnCharacterAdded(Character: Model) - local Humanoid = Character:WaitForChild("Humanoid") :: Humanoid - local HumanoidAttributes = HumanoidAttributes.new(Humanoid) - - local s = tick() - while Character.Parent ~= Characters and (tick()+5 > s) do - task.wait() - pcall(function() - Character.Parent = Characters - end) - end - end - - if Player.Character then - task.spawn(OnCharacterAdded, Player.Character) - end - Player.CharacterAdded:Connect(OnCharacterAdded) - - -- Starter items - -- 这里不会每次进入都给玩家装备吗? - for _, StarterItem in GameConfig.StarterItems do - local Module = require(ServerStorage.Modules[StarterItem[1] .."Lib"]) - Module:Give(Player, ContentLibrary[StarterItem[1]][StarterItem[2]]) - end - - -- If the hotbar is completely empty, fill it with starter items - pData:WaitForChild("Hotbar") - local isEmpty = true - for _, ValueObject in pData.Hotbar:GetChildren() do - if ValueObject.Value ~= "" then - isEmpty = false - end - end - - if isEmpty then - for n, StarterItem in GameConfig.StarterItems do - if n <= 9 then - pData.Hotbar[tostring(n)].Value = StarterItem[2] +-- 初始化玩家信息存储目录(沟通作用,具体数据还得后端处理) +local PlayerDataFolder = Instance.new("Configuration") +PlayerDataFolder.Name = "PlayerData" +PlayerDataFolder.Parent = ReplicatedStorage + +-- 加载Proxy目录下的所有代理 +local Proxies = {} +local ProxyFolder = script.Parent.Parent:FindFirstChild("Proxy") +if ProxyFolder then + for _, proxyModule in ipairs(ProxyFolder:GetChildren()) do + if proxyModule:IsA("ModuleScript") then + local success, result = pcall(require, proxyModule) + if success then + -- 去掉文件名后缀 + local name = proxyModule.Name + Proxies[name] = result + else + warn("加载代理模块失败: " .. proxyModule.Name, result) end end end - - -- Player leveling - -- 登录检测经验升级 - PlayerLeveling:TryLevelUp(Player, Level, XP) - -- 检测经验变化升级 - XP.Changed:Connect(function() - PlayerLeveling:TryLevelUp(Player, Level, XP) - end) -end - -Players.PlayerAdded:Connect(OnPlayerAdded) -for _, Player in Players:GetPlayers() do - task.spawn(OnPlayerAdded, Player) end -- Initially require all server-sided & shared modules @@ -109,80 +72,30 @@ for _, Location in {ReplicatedStorage.Modules, ServerStorage.Modules} do end end ----- Hotbar Persistence -------------------------------------------------------- +-------------------------------------------------------------------------------- -ReplicatedStorage.Remotes.HotbarItemChanged.OnServerEvent:Connect(function(Player, SlotNumber: number, ItemName: string) - if not SlotNumber or typeof(SlotNumber) ~= "number" then return end - if not ItemName or typeof(ItemName) ~= "string" or #ItemName > 200 then return end - - local pData = PlayerData:FindFirstChild(Player.UserId) - local Hotbar = pData and pData:FindFirstChild("Hotbar") - local ValueObject = Hotbar and Hotbar:FindFirstChild(SlotNumber) - - if ValueObject then - ValueObject.Value = ItemName +local function OnPlayerAdded(Player: Player) + if not Proxies.ArchiveProxy:IsPlayerDataLoaded(Player) then + warn("玩家数据未加载: " .. Player.Name) + return end -end) ----- Shop ---------------------------------------------------------------------- - -ReplicatedStorage.Remotes.BuyItem.OnServerInvoke = function(Player, ItemType: string, ItemName: string) - if not ItemType or not ItemName then return end - if typeof(ItemType) ~= "string" or typeof(ItemName) ~= "string" then return end - - local pData = PlayerData:FindFirstChild(Player.UserId) - local Item = ContentLibrary[ItemType] and ContentLibrary[ItemType][ItemName] - if not pData or not Item then return end - - if not Item.Config.Cost then - return false, "This item isn't for sale" - end - - if pData.Items[ItemType]:FindFirstChild(ItemName) then - return false, "You already own this item" - end - - if pData.Stats.Level.Value < Item.Config.Level then - return false, "Your level is too low to purchase this item" - end - - local Currency = pData.Stats[Item.Config.Cost[1]] - - if Currency.Value < Item.Config.Cost[2] then - return false, "Your gold is too low to purchase this item" - end - - Currency.Value -= Item.Config.Cost[2] - - local Module = require(ServerStorage.Modules[ItemType .."Lib"]) - Module:Give(Player, Item) - - return true + local pData = Instance.new("Configuration") + pData.Name = Player.UserId + pData.Parent = PlayerDataFolder + -- 加载对应玩家的其他系统代理 + Proxies.EquipmentProxy:InitPlayer(Player) + Proxies.PlayerInfoProxy:InitPlayer(Player) end -ReplicatedStorage.Remotes.SellItem.OnServerInvoke = function(Player, ItemType: string, ItemName: string) - if not ItemType or not ItemName then return end - if typeof(ItemType) ~= "string" or typeof(ItemName) ~= "string" then return end - - local pData = PlayerData:FindFirstChild(Player.UserId) - local Item = ContentLibrary[ItemType] and ContentLibrary[ItemType][ItemName] - if not pData or not Item then return end - - if not Item.Config.Sell and not Item.Config.Cost then - return false, "This item can't be sold" - end - - if not pData.Items[ItemType]:FindFirstChild(ItemName) then - return false, "You don't own this item" - end - - local Currency = pData.Stats[Item.Config.Sell and Item.Config.Sell[1] or Item.Config.Cost[1]] - local Return = Item.Config.Sell and Item.Config.Sell[2] or math.floor(Item.Config.Cost[2] / 2) - - local Module = require(ServerStorage.Modules[ItemType .."Lib"]) - Module:Trash(Player, Item) - - Currency.Value += Return - - return true -end \ No newline at end of file +local function OnPlayerRemoving(Player: Player) + local pData = PlayerDataFolder:FindFirstChild(Player.UserId) + if pData then pData:Destroy() end +end + +Players.PlayerAdded:Connect(OnPlayerAdded) +for _, Player in Players:GetPlayers() do + task.spawn(OnPlayerAdded, Player) +end + +Players.PlayerRemoving:Connect(OnPlayerRemoving) \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau b/src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau new file mode 100644 index 0000000..c914c75 --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau @@ -0,0 +1,93 @@ +--[[ + Evercyan @ March 2023 + MeleeMobAlign + + MeleeMobAlign is a client-sided script that automatically rotates your body rotation to face any + nearby mobs when holding a melee weapon (Config.WeaponType == "Melee"). + + This feature is used in Infinity's Occultation Update, as it greatly improves user experience when + fighting any enemy with a melee weapon, especially on platforms like mobile, where shift lock may not be a feature. +]] + +--> Services +local CollectionService = game:GetService("CollectionService") +local UserInputService = game:GetService("UserInputService") +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") + +--> Player +local Player = Players.LocalPlayer + +--> Variables +local Focus: Model? + +--> Configuration +local Enabled = true + +-------------------------------------------------------------------------------- + +if not Enabled then + return {} +end + +local function GetNearestMob() + local Character = Player.Character + if not Character then return end + + local Closest = {MobInstance = nil, Distance = math.huge} + + for _, MobInstance in CollectionService:GetTagged("Mob") do + local MobConfig = MobInstance:FindFirstChild("MobConfig") and require(MobInstance.MobConfig) + local Enemy = MobInstance:FindFirstChild("Enemy") + if not MobConfig or not Enemy or Enemy.Health == 0 then continue end + + local Distance = (Character:GetPivot().Position - MobInstance:GetPivot().Position).Magnitude + local MaxDistance = MobConfig.FollowDistance + + if (Distance < MaxDistance) and (Distance < Closest.Distance) then + Closest.MobInstance = MobInstance + Closest.Distance = Distance + end + end + + return Closest.MobInstance +end + +task.defer(function() + while true do + task.wait(1/5) + Focus = GetNearestMob() + end +end) + +RunService:BindToRenderStep("MeleeLock", Enum.RenderPriority.Character.Value + 1, function(DeltaTime: number) + local Character = Player.Character + local Humanoid = Character and Character:FindFirstChild("Humanoid") :: Humanoid? + local HumanoidRootPart = Character and Character:FindFirstChild("HumanoidRootPart") :: BasePart? + if not Humanoid or not HumanoidRootPart then return end + + local Success = false + + if Focus and UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then + local Tool = Character:FindFirstChildOfClass("Tool") + local ItemConfig = Tool and require(Tool:FindFirstChild("ItemConfig")) + + if ItemConfig and ItemConfig.WeaponType == "Melee" then + local CurrentRotation = HumanoidRootPart.CFrame.Rotation + local GoalRotation = CFrame.lookAt(HumanoidRootPart.Position, Focus:GetPivot().Position).Rotation + local _, Y, _ = CurrentRotation:Lerp(GoalRotation, DeltaTime * 30):ToOrientation() + local X, _, Z = CurrentRotation:ToOrientation() + + Humanoid.AutoRotate = false + HumanoidRootPart.CFrame = CFrame.Angles(X, Y, Z) + HumanoidRootPart.Position + + Success = true + end + end + + if not Success then + Humanoid.AutoRotate = true + end +end) + +return {} \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/MobClient.luau b/src/StarterPlayerScripts/ClientMain/MobClient.luau new file mode 100644 index 0000000..cda8dfb --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/MobClient.luau @@ -0,0 +1,213 @@ +--[[ + Evercyan @ March 2023 + MobClient + + Unlike MobLib which handles server-sided code, and most of it in general, we run + some code on the client for things such as custom health overlays & overwriting humanoid state types. + + If you want to edit mob code to add new behavior or edit existing behavior, you likely want to refer + to the server-sided code under ServerStorage. +]] + +--> Services +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +--> Player +local Player = Players.LocalPlayer + +--> Dependencies +local Tween = require(ReplicatedStorage.Modules.Tween) +local Maid = require(ReplicatedStorage.Modules.Maid) + +--> Variables +local Mobs = {} + +--> Configuration +local MobRankColors = { + ["Boss"] = Color3.fromRGB(128, 217, 255), + ["Superboss"] = Color3.fromRGB(255, 126, 126) +} + +-- Folder +local ClientMainPrefabs = Player.PlayerScripts.ClientMainPrefabs + +-------------------------------------------------------------------------------- + +-- WaitForChild keeps yielding, even if the Instance is removed. +-- Using this for safe yielding with StreamingEnabled! +local function safeWait(Item: Instance, Name: string): Instance? + if not Item then + return + elseif Item:FindFirstChild(Name) then + return Item:FindFirstChild(Name) + end + + local ItemAdded = Instance.new("BindableEvent") + local Maid = Maid.new() + + Maid:Add(Item.ChildAdded:Connect(function(Child) + if Child.Name == Name then + ItemAdded:Fire(Child) + ItemAdded:Destroy() + Maid:Destroy() + end + end)) + Maid:Add(Item.Destroying:Connect(function() + ItemAdded:Fire() + ItemAdded:Destroy() + Maid:Destroy() + end)) + + return ItemAdded.Event:Wait() +end + +-- Creates an "AnimationTrack" instance which is stored on the client for playing +local function LoadAnimationTrack(MobInstance: Model, Name: string, Priority: string) : AnimationTrack? + local Enemy = MobInstance:FindFirstChild("Enemy") :: Humanoid + local Animator = Enemy and Enemy:FindFirstChild("Animator") :: Animator + local MobConfig = MobInstance:FindFirstChild("MobConfig") and require(MobInstance:FindFirstChild("MobConfig")) + if not Animator or not MobConfig then return nil end + + local Animation + if MobConfig.CustomAnimations[Name] then + Animation = Instance.new("Animation") + Animation.AnimationId = "rbxassetid://".. MobConfig.CustomAnimations[Name] + else + Animation = ClientMainPrefabs.MobClient.DefaultAnimations[Name] + end + + local AnimationTrack = Animator:LoadAnimation(Animation) + AnimationTrack.Priority = Enum.AnimationPriority[Priority or "Core"] + + return AnimationTrack +end + +-- Creates and returns the overhead gui instance for mobs +local function SetupOverheadGui(MobInstance: Model, Root: BasePart, MobConfig): BillboardGui + local Gui = ClientMainPrefabs.MobClient:WaitForChild("BillboardGui"):Clone() + Gui.Canvas.MobName.Text = `{MobConfig.Name} [{MobConfig.Level[1]}]` + if MobConfig.Rank then + Gui.Canvas.MobRank.Text = MobConfig.Rank + Gui.Canvas.MobRank.TextColor3 = MobRankColors[MobConfig.Rank] or Color3.new(1, 1, 1) + Gui.Canvas.MobRank.Visible = true + else + Gui.Canvas.MobRank.Visible = false + end + + local CoordinateFrame: CFrame, Size: Vector3 = MobInstance:GetBoundingBox() + Gui.Adornee = Root + Gui.StudsOffsetWorldSpace = Vector3.new(0, (CoordinateFrame.Position.Y+Size.Y/2) - Root.Position.Y+2, 0) + Gui.Enabled = true + + Gui.Canvas.GroupTransparency = 1 + Tween:Play(Gui.Canvas, {0.25, "Circular"}, {GroupTransparency = 0}) + + return Gui +end + +local function PerMob(MobInstance: Model) + if Mobs[MobInstance] then return end + + local Enemy = safeWait(MobInstance, "Enemy") :: Humanoid + local Root = safeWait(MobInstance, "HumanoidRootPart") :: BasePart + local MobConfig = safeWait(MobInstance, "MobConfig") and require(MobInstance:FindFirstChild("MobConfig")) + if not Enemy or not Root or not MobConfig then return end + + local Maid = Maid.new() + + -- Set humanoid states (helps prevent falling down & useless calculations - you're unlikely to have an enemy climbing without pathfinding) + for _, EnumName in {"FallingDown", "Seated", "Flying", "Swimming", "Climbing"} do + local HumanoidStateType = Enum.HumanoidStateType[EnumName] + Enemy:SetStateEnabled(HumanoidStateType, false) + if Enemy:GetState() == HumanoidStateType then + Enemy:ChangeState(Enum.HumanoidStateType.Running) + end + end + + -- Animations / Behavior --------------------------------------------------- + + local AnimationTracks = { + Running = LoadAnimationTrack(MobInstance, "Running", "Core"), + Jumping = LoadAnimationTrack(MobInstance, "Jumping", "Movement"), + Hit = LoadAnimationTrack(MobInstance, "Hit", "Action") + } + + Maid:Add(Enemy.Running:Connect(function(Speed) + if Speed > 0.01 then + local Percent = Speed/Enemy.WalkSpeed + if not AnimationTracks.Running.IsPlaying then + AnimationTracks.Running:Play() + end + AnimationTracks.Running:AdjustSpeed(Percent) + else + AnimationTracks.Running:Stop() + end + end)) + + Maid:Add(Enemy.Jumping:Connect(function() + AnimationTracks.Jumping.TimePosition = 0 + if not AnimationTracks.Jumping.IsPlaying then + AnimationTracks.Jumping:Play() + end + end)) + + Maid:Add(Enemy.Died:Once(function() + Root:ApplyImpulse(-Root.CFrame.LookVector * Root.AssemblyMass*50) -- Ragdoll Impulse + end)) + + Maid:Add(Root:GetPropertyChangedSignal("Anchored"):Connect(function() + if Root.Anchored then + AnimationTracks.Running:Stop() + end + end)) + + -- Mob's Overhead GUI ------------------------------------------------------ + + local Gui = SetupOverheadGui(MobInstance, Root, MobConfig) + Gui.Parent = MobInstance + + local function UpdateFill() + local Percent = math.clamp(Enemy.Health/Enemy.MaxHealth, 0, 1) + Tween:Play(Gui.Canvas.HealthBar.Fill, {0.5, "Circular"}, {Size = UDim2.new(Percent, 0, 1, 0)}) + end + + Gui.Canvas.HealthBar.Fill.Size = UDim2.new(0, 0, 1, 0) + Enemy.HealthChanged:Connect(UpdateFill) + UpdateFill() + + Maid:Add(Enemy.Died:Once(function() + Tween:Play(Gui.Canvas, {0.5, "Circular"}, {GroupTransparency = 1}) + end)) + + ---- + + local Mob = {} + Mob.Instance = MobInstance + Mob.AnimationTracks = AnimationTracks + Mobs[MobInstance] = Mob + + Maid:Add(MobInstance.Destroying:Connect(function() + Mobs[MobInstance] = nil + Maid:Destroy() + end)) +end + +CollectionService:GetInstanceAddedSignal("Mob"):Connect(PerMob) +for _, MobInstance in CollectionService:GetTagged("Mob") do + task.spawn(PerMob, MobInstance) +end + +ReplicatedStorage.Remotes.MobDamagedPlayer.OnClientEvent:Connect(function(MobInstance: Model, Damage: number) + local Mob = Mobs[MobInstance] + if not Mob then return end + + if Mob.AnimationTracks.Hit.IsPlaying then + Mob.AnimationTracks.Hit.TimePosition = 0 + else + Mob.AnimationTracks.Hit:Play() + end +end) + +return {} \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/PlayerListStats.luau b/src/StarterPlayerScripts/ClientMain/PlayerListStats.luau new file mode 100644 index 0000000..2a692dd --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/PlayerListStats.luau @@ -0,0 +1,63 @@ +--[[ + Evercyan @ March 2023 + PlayerListStats + + PlayerListStats creates a leaderstats folder under every Player on the client side (each client has their own instances) + which shows their levels. They are placeholders, meaning the real data is under ReplicatedStorage.PlayerData. + If you change stats under leaderstats instead of a pData config, it will not save, and will likely be overwritten. +]] + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +--> Player +local Player = Players.LocalPlayer + +--> References +local PlayerData = ReplicatedStorage:WaitForChild("PlayerData") + +--> Dependencies +local FormatNumber = require(ReplicatedStorage.Modules.FormatNumber) + +--> Configuration +local StatsToShow = {"Level"} + +-------------------------------------------------------------------------------- + +local function FormatValue(Value: any): string + return typeof(Value) == "number" and FormatNumber(Value, "Suffix") or tostring(Value) +end + +local function OnPlayerAdded(Player: Player) + local pData = PlayerData:WaitForChild(Player.UserId, 5) + local Stats = pData and pData:WaitForChild("Stats", 5) + if not Stats then return end + + local leaderstats = Instance.new("Folder") + leaderstats.Name = "leaderstats" + + for _, StatName in StatsToShow do + local Stat = Stats:WaitForChild(StatName, 5) + + if Stat then + local ValueObject = Instance.new("StringValue") + ValueObject.Name = StatName + ValueObject.Value = FormatValue(Stat.Value) + ValueObject.Parent = leaderstats + + Stat.Changed:Connect(function() + ValueObject.Value = FormatValue(Stat.Value) + end) + end + end + + leaderstats.Parent = Player +end + +Players.PlayerAdded:Connect(OnPlayerAdded) +for _, Player in Players:GetPlayers() do + task.defer(OnPlayerAdded, Player) +end + +return {} \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/Transportation.luau b/src/StarterPlayerScripts/ClientMain/Transportation.luau new file mode 100644 index 0000000..d600ee8 --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/Transportation.luau @@ -0,0 +1,213 @@ +--[[ + Evercyan @ March 2023 + Transportation + + Transportation handles all level door & portal logic, including the teleport transition. +]] + +--> Services +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +--> Player +local Player = Players.LocalPlayer + +--> References +local PlayerData = ReplicatedStorage:WaitForChild("PlayerData") + +--> Dependencies +local FormatNumber = require(ReplicatedStorage.Modules.FormatNumber) +local Tween = require(ReplicatedStorage.Modules.Tween) + +-- Folder +local ClientMainPrefabs = Player.PlayerScripts.ClientMainPrefabs + +-------------------------------------------------------------------------------- + +local function CreateGui(Class): BillboardGui + local Gui = ClientMainPrefabs.Transportation:WaitForChild("BillboardGui"):Clone() + Gui.Canvas.Destination.Text = Class.Config.Name + if Class.Config.Level then + Gui.Canvas.LevelReq.Text = "Level ".. FormatNumber(Class.Config.Level, "Suffix") + else + Gui.Canvas.LevelReq.Visible = false + end + Gui.Enabled = true + return Gui +end + +local function CanPlayerAccess(Player: Player, Class): boolean + local pData = PlayerData:FindFirstChild(Player.UserId) + + if pData and pData.Stats.Level.Value >= Class.Config.Level then + return true + end + + return false +end + +---- LEVEL DOORS --------------------------------------------------------------- + +local LevelDoor = {} +LevelDoor.__index = LevelDoor + +LevelDoor.Items = {} + +-- Level Doors require a "Hitbox" part. Any included instance is automatically hidden when opened. +function LevelDoor.new(Model: Model) + local Base = Model:WaitForChild("Base", math.huge) :: BasePart + local Hitbox = Model:WaitForChild("Hitbox", math.huge) :: BasePart + local Config = require(Model:WaitForChild("Config")) + + local self = setmetatable({}, LevelDoor) + self.Instance = Model + self.Config = Config + + local Gui = CreateGui(self) + Gui.Adornee = Base + Gui.Parent = Model + + Hitbox.Touched:Connect(function(HitPart) + local plr = Players:GetPlayerFromCharacter(HitPart.Parent) + if Player == plr and CanPlayerAccess(Player, self) then + self:Open() + end + end) + + table.insert(LevelDoor.Items, self) +end + +function LevelDoor:Open() + if not self.Opened then + self.Opened = true + + for _, Instance in self.Instance:GetDescendants() do + if Instance:IsA("BasePart") and Instance.Name ~= "Hitbox" then + Tween:Play(Instance, {1, "Circular"}, {Transparency = 1}) + Instance.CanCollide = false + elseif Instance:IsA("CanvasGroup") then + Tween:Play(Instance, {1, "Circular"}, {GroupTransparency = 1}) + end + end + end +end + +for _, Model in CollectionService:GetTagged("Level Door") do + task.spawn(LevelDoor.new, Model) +end +CollectionService:GetInstanceAddedSignal("Level Door"):Connect(LevelDoor.new) + +---- PORTALS ------------------------------------------------------------------- + +local TeleportScreen = ClientMainPrefabs.Transportation:WaitForChild("TeleportScreen") +TeleportScreen:WaitForChild("Canvas"):WaitForChild("Foreground").GroupTransparency = 1 +TeleportScreen.Enabled = true +TeleportScreen.Parent = Player:WaitForChild("PlayerGui") + +local TeleportScreenId = 0 +local function PushTeleportScreen(Portal) + TeleportScreenId += 1 + local Id = TeleportScreenId + + local Canvas = TeleportScreen:WaitForChild("Canvas") + local Background = Canvas:WaitForChild("Background") + local Foreground = Canvas:WaitForChild("Foreground") + + Foreground.DestinationName.Text = Portal.Config.Name + Background.BackgroundColor3 = Portal.Config.Color + for _, Section in Foreground.Ring:GetChildren() do + Tween:Play(Section.UIGradient, {0, "Linear"}, {Rotation = 90}) + end + + Background.UIGradient.Rotation = 15 + Background.UIGradient.Offset = Vector2.new(-1.5, 0) + Tween:Play(Background.UIGradient, {1.3, "Sine", "Out"}, {Offset = Vector2.new(1.5, 0)}) + + Tween:Play(Foreground, {0, "Linear"}, {GroupTransparency = 1}) + + task.wait(0.6) + if TeleportScreenId ~= Id then return end + + Tween:Play(Foreground, {1, "Circular"}, {GroupTransparency = 0}) + + for _, SectionName in {"TopRight", "TopLeft", "BottomLeft", "BottomRight"} do + local Section = Foreground.Ring[SectionName] + Tween:Play(Section.UIGradient, {0.3, "Linear"}, {Rotation = -1}).Completed:Wait() + if TeleportScreenId ~= Id then + break + end + end + + if TeleportScreenId == Id then + Background.UIGradient.Rotation = 195 + Background.UIGradient.Offset = Vector2.new(-1.5, 0) + Tween:Play(Background.UIGradient, {2, "Circular"}, {Offset = Vector2.new(1.5, 0)}) + + Tween:Play(Foreground, {1, "Circular"}, {GroupTransparency = 1}) + end +end + + +local TpCooldown = false + +local Portal = {} +Portal.__index = Portal + +Portal.Items = {} + +-- Portals require a "Base" part. +function Portal.new(Model: Model) + local Base = Model:WaitForChild("Base", math.huge) :: BasePart + local Hitbox = Model:WaitForChild("Hitbox", math.huge) :: BasePart + local Config = require(Model:WaitForChild("Config")) + + local self = setmetatable({}, Portal) + self.Instance = Model + self.Config = Config + + local Gui = CreateGui(self) + Gui.Adornee = Hitbox + Gui.Parent = Model + + Hitbox.Touched:Connect(function(HitPart) + local plr = Players:GetPlayerFromCharacter(HitPart.Parent) + + if Player == plr and CanPlayerAccess(Player, self) then + self:Teleport(Player) + end + end) + + table.insert(Portal.Items, self) +end + +function Portal:Teleport(Player: Player) + if TpCooldown then + return + end + TpCooldown = true + + local Character = Player.Character + if Character then + local TP = workspace.TP:WaitForChild(self.Config.TP) :: BasePart + + if workspace.StreamingEnabled then + Player:RequestStreamAroundAsync(TP.Position, 5) + end + + PushTeleportScreen(self) + Character:PivotTo(TP.CFrame + Vector3.yAxis*4) + end + + TpCooldown = false +end + +for _, Model in CollectionService:GetTagged("Portal") do + task.spawn(Portal.new, Model) +end +CollectionService:GetInstanceAddedSignal("Portal"):Connect(Portal.new) + +return { + LevelDoors = LevelDoor.Items, + Portals = Portal.Items +} \ No newline at end of file diff --git a/src/StarterPlayerScripts/ClientMain/init.client.luau b/src/StarterPlayerScripts/ClientMain/init.client.luau new file mode 100644 index 0000000..fe4edb2 --- /dev/null +++ b/src/StarterPlayerScripts/ClientMain/init.client.luau @@ -0,0 +1,105 @@ +--[[ + Evercyan @ March 2023 + ClientMain + + Unloads all client-sided code related to the kit, located under this script. + If you have anything you wish to add for general client code, feel free to add it at the bottom. +]] + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local StarterGui = game:GetService("StarterGui") +local Players = game:GetService("Players") + +--> Player +local Player = Players.LocalPlayer +local PlayerGui = Player:WaitForChild("PlayerGui") + +--> Dependencies +local FormatNumber = require(ReplicatedStorage.Modules.FormatNumber) +local Tween = require(ReplicatedStorage.Modules.Tween) +local SFX = require(ReplicatedStorage.Modules.SFX) + +--> Variables +local Random = Random.new() + +-- Folder +local ClientMainPrefabs = Player.PlayerScripts.ClientMainPrefabs + +-------------------------------------------------------------------------------- + +-- Initially require client-sided modules +for _, Item in script:GetChildren() do + if Item:IsA("ModuleScript") then + task.defer(require, Item) + end +end +script.ChildAdded:Connect(function(Item) + if Item:IsA("ModuleScript") then + require(Item) + end +end) + +-- Click Sound +PlayerGui.DescendantAdded:Connect(function(Instance) + if Instance:IsA("GuiButton") then + Instance.Activated:Connect(function() + if not Instance:GetAttribute("DisableSound") then + SFX:Play2D(9119720940) + end + end) + end +end) + +-- Weapon Damage Counters +-- 伤害面板 +local lastCounter +local function CreateDamageCounter(Position: Vector3, Damage: number) + if lastCounter then + lastCounter:Destroy() + end + + local DamageCounter = ClientMainPrefabs:WaitForChild("DamageCounter"):Clone() + local Gui = DamageCounter.DMGBillboard + + Gui.Enabled = true + Gui.Canvas.Damage.Text = FormatNumber(Damage, "Suffix") + Gui.Canvas.GroupTransparency = 1 + Tween:Play(Gui.Canvas, {0.5, "Exponential"}, {GroupTransparency = 0}) + + DamageCounter.CFrame = CFrame.new(Position - Vector3.yAxis) + Tween:Play(DamageCounter, {0.5, "Exponential"}, {CFrame = CFrame.new(Position)}) + + lastCounter = DamageCounter + DamageCounter.Parent = workspace:WaitForChild("Temporary") + + task.delay(2, function() + if lastCounter == DamageCounter then + Tween:Play(Gui.Canvas, {0.5, "Circular"}, {GroupTransparency = 1}) + Tween:Play(DamageCounter, {0.5, "Circular"}, {CFrame = CFrame.new(Position + Vector3.yAxis)}).Completed:Once(function() + DamageCounter:Destroy() + end) + end + end) +end + +ReplicatedStorage.Remotes.PlayerDamagedMob.OnClientEvent:Connect(function(MobInstance: Model, Damage: number) + if not MobInstance then return end + + local Character = Player.Character + local Right = Character and (Character:FindFirstChild("Right Arm") or Character:FindFirstChild("RightHand")) + local Attachment = Right and Right:FindFirstChild("RightGripAttachment") + if not Attachment then return end + + local Position = Attachment.WorldPosition + (Attachment.WorldCFrame.LookVector*5) + (Random:NextUnitVector()*.3) + + CreateDamageCounter(Position, Damage) +end) + +ReplicatedStorage.Remotes.SendNotification.OnClientEvent:Connect(function(Title: string, Text: string, IconId: number?) + StarterGui:SetCore("SendNotification", { + Title = Title, + Text = Text, + Icon = IconId and ("rbxassetid://".. IconId) + }) +end) \ No newline at end of file