更新
This commit is contained in:
parent
8157d756b0
commit
a0f50f9e99
@ -18,6 +18,9 @@
|
|||||||
"$className": "StarterPlayerScripts",
|
"$className": "StarterPlayerScripts",
|
||||||
"BilGui": {
|
"BilGui": {
|
||||||
"$path": "src/StarterPlayerScripts/BilGui"
|
"$path": "src/StarterPlayerScripts/BilGui"
|
||||||
|
},
|
||||||
|
"ClientMain": {
|
||||||
|
"$path": "src/StarterPlayerScripts/ClientMain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -32,6 +35,9 @@
|
|||||||
"Data": {
|
"Data": {
|
||||||
"$path": "src/ReplicatedStorage/Data"
|
"$path": "src/ReplicatedStorage/Data"
|
||||||
},
|
},
|
||||||
|
"Json": {
|
||||||
|
"$path": "src/ReplicatedStorage/Json"
|
||||||
|
},
|
||||||
"Tools": {
|
"Tools": {
|
||||||
"$path": "src/ReplicatedStorage/Tools"
|
"$path": "src/ReplicatedStorage/Tools"
|
||||||
},
|
},
|
||||||
|
BIN
excel/equipment.xlsx
Normal file
BIN
excel/equipment.xlsx
Normal file
Binary file not shown.
90
excel/excel2json.py
Normal file
90
excel/excel2json.py
Normal file
@ -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}")
|
90
export.py
Normal file
90
export.py
Normal file
@ -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}")
|
3
src/ReplicatedStorage/Json/Equipment.json
Normal file
3
src/ReplicatedStorage/Json/Equipment.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
{"id":1,"type":1,"name":1,"attributes":[1,20,2,20]}
|
||||||
|
]
|
63
src/ReplicatedStorage/Tools/Utils.luau
Normal file
63
src/ReplicatedStorage/Tools/Utils.luau
Normal file
@ -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
|
153
src/Server/Proxy/ArchiveProxy.luau
Normal file
153
src/Server/Proxy/ArchiveProxy.luau
Normal file
@ -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
|
91
src/Server/Proxy/EquipmentProxy.luau
Normal file
91
src/Server/Proxy/EquipmentProxy.luau
Normal file
@ -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
|
25
src/Server/Proxy/PlayerInfoProxy.luau
Normal file
25
src/Server/Proxy/PlayerInfoProxy.luau
Normal file
@ -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
|
@ -34,69 +34,32 @@ local function CreateFolder(Name: string, Parent: Instance)
|
|||||||
return Folder
|
return Folder
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 初始化workspace目录
|
||||||
local Temporary = CreateFolder("Temporary", workspace)
|
local Temporary = CreateFolder("Temporary", workspace)
|
||||||
local ProjectileCache = CreateFolder("ProjectileCache", Temporary)
|
local ProjectileCache = CreateFolder("ProjectileCache", Temporary)
|
||||||
local Characters = CreateFolder("Characters", workspace)
|
local Characters = CreateFolder("Characters", workspace)
|
||||||
|
|
||||||
local function OnPlayerAdded(Player: Player)
|
-- 初始化玩家信息存储目录(沟通作用,具体数据还得后端处理)
|
||||||
local pData = PlayerData:WaitForChild(Player.UserId)
|
local PlayerDataFolder = Instance.new("Configuration")
|
||||||
local Level = pData.Stats.Level
|
PlayerDataFolder.Name = "PlayerData"
|
||||||
local XP = pData.Stats.XP
|
PlayerDataFolder.Parent = ReplicatedStorage
|
||||||
|
|
||||||
local function OnCharacterAdded(Character: Model)
|
-- 加载Proxy目录下的所有代理
|
||||||
local Humanoid = Character:WaitForChild("Humanoid") :: Humanoid
|
local Proxies = {}
|
||||||
local HumanoidAttributes = HumanoidAttributes.new(Humanoid)
|
local ProxyFolder = script.Parent.Parent:FindFirstChild("Proxy")
|
||||||
|
if ProxyFolder then
|
||||||
local s = tick()
|
for _, proxyModule in ipairs(ProxyFolder:GetChildren()) do
|
||||||
while Character.Parent ~= Characters and (tick()+5 > s) do
|
if proxyModule:IsA("ModuleScript") then
|
||||||
task.wait()
|
local success, result = pcall(require, proxyModule)
|
||||||
pcall(function()
|
if success then
|
||||||
Character.Parent = Characters
|
-- 去掉文件名后缀
|
||||||
end)
|
local name = proxyModule.Name
|
||||||
end
|
Proxies[name] = result
|
||||||
end
|
else
|
||||||
|
warn("加载代理模块失败: " .. proxyModule.Name, result)
|
||||||
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]
|
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Initially require all server-sided & shared modules
|
-- Initially require all server-sided & shared modules
|
||||||
@ -109,80 +72,30 @@ for _, Location in {ReplicatedStorage.Modules, ServerStorage.Modules} do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---- Hotbar Persistence --------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
ReplicatedStorage.Remotes.HotbarItemChanged.OnServerEvent:Connect(function(Player, SlotNumber: number, ItemName: string)
|
local function OnPlayerAdded(Player: Player)
|
||||||
if not SlotNumber or typeof(SlotNumber) ~= "number" then return end
|
if not Proxies.ArchiveProxy:IsPlayerDataLoaded(Player) then
|
||||||
if not ItemName or typeof(ItemName) ~= "string" or #ItemName > 200 then return end
|
warn("玩家数据未加载: " .. Player.Name)
|
||||||
|
return
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
---- Shop ----------------------------------------------------------------------
|
local pData = Instance.new("Configuration")
|
||||||
|
pData.Name = Player.UserId
|
||||||
ReplicatedStorage.Remotes.BuyItem.OnServerInvoke = function(Player, ItemType: string, ItemName: string)
|
pData.Parent = PlayerDataFolder
|
||||||
if not ItemType or not ItemName then return end
|
-- 加载对应玩家的其他系统代理
|
||||||
if typeof(ItemType) ~= "string" or typeof(ItemName) ~= "string" then return end
|
Proxies.EquipmentProxy:InitPlayer(Player)
|
||||||
|
Proxies.PlayerInfoProxy:InitPlayer(Player)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ReplicatedStorage.Remotes.SellItem.OnServerInvoke = function(Player, ItemType: string, ItemName: string)
|
local function OnPlayerRemoving(Player: Player)
|
||||||
if not ItemType or not ItemName then return end
|
local pData = PlayerDataFolder:FindFirstChild(Player.UserId)
|
||||||
if typeof(ItemType) ~= "string" or typeof(ItemName) ~= "string" then return end
|
if pData then pData:Destroy() end
|
||||||
|
end
|
||||||
local pData = PlayerData:FindFirstChild(Player.UserId)
|
|
||||||
local Item = ContentLibrary[ItemType] and ContentLibrary[ItemType][ItemName]
|
Players.PlayerAdded:Connect(OnPlayerAdded)
|
||||||
if not pData or not Item then return end
|
for _, Player in Players:GetPlayers() do
|
||||||
|
task.spawn(OnPlayerAdded, Player)
|
||||||
if not Item.Config.Sell and not Item.Config.Cost then
|
end
|
||||||
return false, "This item can't be sold"
|
|
||||||
end
|
Players.PlayerRemoving:Connect(OnPlayerRemoving)
|
||||||
|
|
||||||
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
|
|
93
src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau
Normal file
93
src/StarterPlayerScripts/ClientMain/MeleeMobAlign.luau
Normal file
@ -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 {}
|
213
src/StarterPlayerScripts/ClientMain/MobClient.luau
Normal file
213
src/StarterPlayerScripts/ClientMain/MobClient.luau
Normal file
@ -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 = `<b>{MobConfig.Name}</b> <font size='16'>[{MobConfig.Level[1]}]</font>`
|
||||||
|
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 {}
|
63
src/StarterPlayerScripts/ClientMain/PlayerListStats.luau
Normal file
63
src/StarterPlayerScripts/ClientMain/PlayerListStats.luau
Normal file
@ -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 {}
|
213
src/StarterPlayerScripts/ClientMain/Transportation.luau
Normal file
213
src/StarterPlayerScripts/ClientMain/Transportation.luau
Normal file
@ -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
|
||||||
|
}
|
105
src/StarterPlayerScripts/ClientMain/init.client.luau
Normal file
105
src/StarterPlayerScripts/ClientMain/init.client.luau
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user