This commit is contained in:
Ggafrik 2025-07-02 00:21:32 +08:00
parent 8157d756b0
commit a0f50f9e99
15 changed files with 1249 additions and 128 deletions

View File

@ -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

Binary file not shown.

90
excel/excel2json.py Normal file
View 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
View 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}")

View File

@ -0,0 +1,3 @@
[
{"id":1,"type":1,"name":1,"attributes":[1,20,2,20]}
]

View 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

View 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

View 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

View 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

View File

@ -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)

View 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 {}

View 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 {}

View 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 {}

View 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
}

View 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)