更新
This commit is contained in:
parent
8157d756b0
commit
a0f50f9e99
@ -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"
|
||||
},
|
||||
|
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
|
||||
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 PlayerDataFolder = Instance.new("Configuration")
|
||||
PlayerDataFolder.Name = "PlayerData"
|
||||
PlayerDataFolder.Parent = ReplicatedStorage
|
||||
|
||||
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]
|
||||
-- 加载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
|
||||
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"
|
||||
local function OnPlayerAdded(Player: Player)
|
||||
if not Proxies.ArchiveProxy:IsPlayerDataLoaded(Player) then
|
||||
warn("玩家数据未加载: " .. Player.Name)
|
||||
return
|
||||
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
|
||||
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)
|
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