project init
This commit is contained in:
commit
3ff5337437
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Project place file
|
||||
*.rbxl
|
||||
*.rbxlx
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
Packages/
|
||||
ServerPackages/
|
||||
sourcemap.json
|
||||
wally.lock
|
||||
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# boom-bots
|
||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) 7.7.0-rc.1.
|
||||
|
||||
## Getting Started
|
||||
To build the place from scratch, use:
|
||||
|
||||
```bash
|
||||
rojo build -o "boom-bots.rbxlx"
|
||||
```
|
||||
|
||||
Next, open `boom-bots.rbxlx` in Roblox Studio and start the Rojo server:
|
||||
|
||||
```bash
|
||||
rojo serve
|
||||
```
|
||||
|
||||
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||
8
aftman.toml
Normal file
8
aftman.toml
Normal file
@ -0,0 +1,8 @@
|
||||
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
|
||||
# For more information, see https://github.com/LPGhatguy/aftman
|
||||
|
||||
# To add a new tool, add an entry to this table.
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
|
||||
wally = "UpliftGames/wally@0.3.2"
|
||||
# rojo = "rojo-rbx/rojo@6.2.0"
|
||||
49
default.project.json
Normal file
49
default.project.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "boom-bots",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"Client": {
|
||||
"$path": "src/shared/client"
|
||||
},
|
||||
"Data": {
|
||||
"$path": "src/shared/data"
|
||||
},
|
||||
"Janitor": {
|
||||
"$className": "ModuleScript",
|
||||
"$path": "src/shared/janitor"
|
||||
},
|
||||
"ProfileStore": {
|
||||
"$className": "ModuleScript",
|
||||
"$properties": {
|
||||
"SourceAssetId": 109379033046155.0
|
||||
},
|
||||
"$path": "src/shared/profilestore"
|
||||
},
|
||||
"Shared": {
|
||||
"$path": "src/shared/shared"
|
||||
},
|
||||
"Vex": {
|
||||
"$className": "ModuleScript",
|
||||
"$properties": {
|
||||
"SourceAssetId": 8491559721.0
|
||||
},
|
||||
"$path": "src/shared/vex"
|
||||
}
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$path": "src/server"
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"StarterPlayerScripts": {
|
||||
"$path": "src/client"
|
||||
},
|
||||
"$properties": {
|
||||
"AvatarJointUpgrade_SerializedRollout": "Enabled",
|
||||
"CameraMaxZoomDistance": 128.0,
|
||||
"CharacterBreakJointsOnDeath": false,
|
||||
"CharacterUseJumpPower": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
rokit.toml
Normal file
8
rokit.toml
Normal file
@ -0,0 +1,8 @@
|
||||
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
|
||||
# For more information, see https://github.com/rojo-rbx/rokit
|
||||
|
||||
# New tools can be added by running `rokit add <tool>` in a terminal.
|
||||
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.7.0-rc.1"
|
||||
wally = "UpliftGames/wally@0.3.2"
|
||||
1
selene.toml
Normal file
1
selene.toml
Normal file
@ -0,0 +1 @@
|
||||
std = "roblox"
|
||||
15
src/client/Client/Start.client.luau
Normal file
15
src/client/Client/Start.client.luau
Normal file
@ -0,0 +1,15 @@
|
||||
--!strict
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Remotes = ReplicatedStorage.Remote
|
||||
--local rev_PlayerLoaded = Remotes.PlayerLoaded
|
||||
local ModuleLoader = require(ReplicatedStorage.Shared.ModuleLoader)
|
||||
|
||||
-- you can optionally modify settings (also change it via the attributes on ModuleLoader)
|
||||
ModuleLoader.ChangeSettings({
|
||||
FOLDER_SEARCH_DEPTH = 1,
|
||||
YIELD_THRESHOLD = 10,
|
||||
VERBOSE_LOADING = false,
|
||||
WAIT_FOR_SERVER = true,
|
||||
})
|
||||
-- pass any containers for your custom services to the Start() function
|
||||
ModuleLoader.Start(script)
|
||||
151
src/server/Admin/AdminCommands.luau
Normal file
151
src/server/Admin/AdminCommands.luau
Normal file
@ -0,0 +1,151 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local MessagingService = game:GetService("MessagingService")
|
||||
local Players = game:GetService("Players")
|
||||
local ValueNames = require("../Data/ValueNames")
|
||||
local DataStoreNames = require("../Data/DataStoreNames")
|
||||
local DataManager = require("../Data/DataManager")
|
||||
local RunService = game:GetService("RunService")
|
||||
local AdminCore = require("./AdminCore")
|
||||
|
||||
-- Remote event to send system messages to clients
|
||||
local ChatEvent = ReplicatedStorage.Remote.Message
|
||||
|
||||
local AdminCommands = {}
|
||||
|
||||
function AdminCommands:Init()
|
||||
|
||||
-- Money change command
|
||||
local moneyChangeCommand = Instance.new("TextChatCommand")
|
||||
moneyChangeCommand.Name = "MoneyChangeCommand"
|
||||
moneyChangeCommand.PrimaryAlias = "/money"
|
||||
moneyChangeCommand.Parent = game:GetService("TextChatService"):WaitForChild("TextChatCommands")
|
||||
|
||||
moneyChangeCommand.Triggered:Connect(function(textSource, message)
|
||||
local player = Players:GetPlayerByUserId(textSource.UserId)
|
||||
if player and AdminCore.CheckIfAdmin(player) then
|
||||
local args = string.split(message, " ")
|
||||
if #args >= 3 then
|
||||
local targetPlayerName = args[2]
|
||||
local amount = tonumber(args[3])
|
||||
if amount then
|
||||
return AdminCore.MoneyCommand(player,targetPlayerName, amount)
|
||||
else
|
||||
ChatEvent:FireClient(player, "Invalid amount: " .. args[3])
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "Usage: /money [playerName] [amount]")
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "You don't have permission to use this command.")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Wins change command
|
||||
local winsChangeCommand = Instance.new("TextChatCommand")
|
||||
winsChangeCommand.Name = "WinsChangeCommand"
|
||||
winsChangeCommand.PrimaryAlias = "/wins"
|
||||
winsChangeCommand.Parent = game:GetService("TextChatService"):WaitForChild("TextChatCommands")
|
||||
|
||||
winsChangeCommand.Triggered:Connect(function(textSource, message)
|
||||
local player = Players:GetPlayerByUserId(textSource.UserId)
|
||||
if player and AdminCore.CheckIfAdmin(player) then
|
||||
local args = string.split(message, " ")
|
||||
if #args >= 3 then
|
||||
local targetPlayerName = args[2]
|
||||
local amount = tonumber(args[3])
|
||||
if amount then
|
||||
return AdminCore.WinsCommand(player,targetPlayerName, amount)
|
||||
else
|
||||
ChatEvent:FireClient(player, "Invalid amount: " .. args[3])
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "Usage: /wins [playerName] [amount]")
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "You don't have permission to use this command.")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Kick command
|
||||
local kickCommand = Instance.new("TextChatCommand")
|
||||
kickCommand.Name = "KickCommand"
|
||||
kickCommand.PrimaryAlias = "/kick"
|
||||
kickCommand.Parent = game:GetService("TextChatService"):WaitForChild("TextChatCommands")
|
||||
|
||||
kickCommand.Triggered:Connect(function(textSource, message)
|
||||
local player = Players:GetPlayerByUserId(textSource.UserId)
|
||||
if player and AdminCore.CheckIfAdmin(player) then
|
||||
local args = string.split(message, " ")
|
||||
if #args >= 2 then
|
||||
local targetPlayerName = args[2]
|
||||
local reason = args[3] or "No reason provided"
|
||||
return AdminCore.KickPlayer(player,targetPlayerName, nil, reason)
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "You don't have permission to use this command.")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Ban command
|
||||
local banCommand = Instance.new("TextChatCommand")
|
||||
banCommand.Name = "BanCommand"
|
||||
banCommand.PrimaryAlias = "/ban"
|
||||
banCommand.Parent = game:GetService("TextChatService"):WaitForChild("TextChatCommands")
|
||||
|
||||
banCommand.Triggered:Connect(function(textSource, message)
|
||||
local player = Players:GetPlayerByUserId(textSource.UserId)
|
||||
if player and AdminCore.CheckIfAdmin(player) then
|
||||
local args = string.split(message, " ")
|
||||
if #args >= 2 then
|
||||
local targetPlayerName = args[2]
|
||||
if string.find(targetPlayerName, "@") ~= 1 then
|
||||
ChatEvent:FireClient(player, "/ban [@username] [duration in minutes] [reason] (duration and reason are optional)")
|
||||
end
|
||||
targetPlayerName = string.sub(targetPlayerName, 2)
|
||||
local duration = tonumber(args[3]) or 0
|
||||
if duration then
|
||||
duration = duration * 60
|
||||
end
|
||||
if duration <= 0 or duration == nil then
|
||||
duration = -1
|
||||
end
|
||||
local reason = args[4] or "No reason provided"
|
||||
|
||||
return AdminCore.BanPlayer(player,targetPlayerName,duration,reason)
|
||||
else
|
||||
ChatEvent:FireClient(player, "/ban [@username] [duration in minutes] [reason] (duration and reason are optional)")
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "You don't have permission to use this command.")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Unban command
|
||||
local unbanCommand = Instance.new("TextChatCommand")
|
||||
unbanCommand.Name = "UnbanCommand"
|
||||
unbanCommand.PrimaryAlias = "/unban"
|
||||
unbanCommand.Parent = game:GetService("TextChatService"):WaitForChild("TextChatCommands")
|
||||
|
||||
unbanCommand.Triggered:Connect(function(textSource, message)
|
||||
local player = Players:GetPlayerByUserId(textSource.UserId)
|
||||
if player and AdminCore.CheckIfAdmin(player) then
|
||||
local args = string.split(message, " ")
|
||||
if #args >= 2 then
|
||||
local targetPlayerName = args[2]
|
||||
if string.find(targetPlayerName, "@") ~= 1 then
|
||||
ChatEvent:FireClient(player, "/unban [@username]")
|
||||
else
|
||||
targetPlayerName = string.sub(targetPlayerName, 2)
|
||||
return AdminCore.UnbanPlayer(player,targetPlayerName)
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "/unban [@username]")
|
||||
end
|
||||
else
|
||||
ChatEvent:FireClient(player, "You don't have permission to use this command.")
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
return AdminCommands
|
||||
10
src/server/Admin/AdminCommands.meta.json
Normal file
10
src/server/Admin/AdminCommands.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ServerOnly": true
|
||||
}
|
||||
}
|
||||
340
src/server/Admin/AdminCore.luau
Normal file
340
src/server/Admin/AdminCore.luau
Normal file
@ -0,0 +1,340 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local MessagingService = game:GetService("MessagingService")
|
||||
local Players = game:GetService("Players")
|
||||
local ValueNames = require("../Data/ValueNames")
|
||||
local DataStoreNames = require("../Data/DataStoreNames")
|
||||
local DataManager = require("../Data/DataManager")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local GroupId = game.CreatorId
|
||||
|
||||
-- Remote event to send system messages to clients
|
||||
local ChatEvent = ReplicatedStorage.Remote.Message
|
||||
|
||||
local PlayerList: {{number | string}} = {}
|
||||
|
||||
local GlobalPlayerList: {{{number | string} | number}} = {}
|
||||
|
||||
|
||||
|
||||
local AdminCore = {}
|
||||
|
||||
AdminCore.Topics = {
|
||||
["MONEY_COMMAND"] = "AdminMoneyCommand",
|
||||
["WINS_COMMAND"] = "AdminWinsCommand",
|
||||
["COMMAND_RESPONSE"] = "AdminCommandResponse",
|
||||
["KICK_COMMAND"] = "AdminKickCommand",
|
||||
["GET_ALL_PLAYERS"] = "GetAllPlayers",
|
||||
["RETURN_ALL_PLAYERS"] = "ReturnAllPlayers",
|
||||
["BAN_PLAYER"] = "AdminBanPlayer",
|
||||
["UNBAN_PLAYER"] = "AdminUnbanPlayer",
|
||||
}
|
||||
|
||||
|
||||
function AdminCore.CheckIfAdmin(Player: Player)
|
||||
local Role = Player:GetRoleInGroupAsync(GroupId)
|
||||
if Role == "Developer" or Role == "Owner" or Role == "Admin" or RunService:IsStudio() then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Subscribe to command responses from other servers
|
||||
local function setupMessagingListeners()
|
||||
-- Listen for money command execution requests
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.MONEY_COMMAND, function(message)
|
||||
local data = message.Data
|
||||
local targetPlayer = Players:FindFirstChild(data.targetPlayerName)
|
||||
|
||||
if targetPlayer then
|
||||
-- Execute the command on this server where the target player is
|
||||
DataManager.SetValue(targetPlayer, ValueNames.Cash, data.amount)
|
||||
|
||||
-- Send response back to the admin's server
|
||||
MessagingService:PublishAsync(AdminCore.Topics.COMMAND_RESPONSE, {
|
||||
adminUserId = data.adminUserId,
|
||||
success = true,
|
||||
message = "Changed " .. targetPlayer.Name .. "'s money to " .. data.amount,
|
||||
commandType = "money"
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Listen for wins command execution requests
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.WINS_COMMAND, function(message)
|
||||
local data = message.Data
|
||||
local targetPlayer = Players:FindFirstChild(data.targetPlayerName)
|
||||
|
||||
if targetPlayer then
|
||||
-- Execute the command on this server where the target player is
|
||||
DataManager.SetValue(targetPlayer, ValueNames.Wins, data.amount)
|
||||
|
||||
-- Send response back to the admin's server
|
||||
MessagingService:PublishAsync(AdminCore.Topics.COMMAND_RESPONSE, {
|
||||
adminUserId = data.adminUserId,
|
||||
success = true,
|
||||
message = "Changed " .. targetPlayer.Name .. "'s wins to " .. data.amount,
|
||||
commandType = "wins"
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Listen for kick command requests
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.KICK_COMMAND, function(message)
|
||||
local data = message.Data
|
||||
local targetPlayer = Players:FindFirstChild(data.targetPlayerName)
|
||||
|
||||
if targetPlayer then
|
||||
-- Execute the kick on this server
|
||||
targetPlayer:Kick(data.reason or "You have been kicked by an administrator.")
|
||||
|
||||
-- Send response back to the admin's server
|
||||
MessagingService:PublishAsync(AdminCore.Topics.COMMAND_RESPONSE, {
|
||||
adminUserId = data.adminUserId,
|
||||
success = true,
|
||||
message = "Kicked " .. targetPlayer.Name,
|
||||
commandType = "kick"
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Listen for Get All Players
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.GET_ALL_PLAYERS, function()
|
||||
MessagingService:PublishAsync(AdminCore.Topics.RETURN_ALL_PLAYERS, {
|
||||
players = PlayerList,
|
||||
serverId = game.JobId
|
||||
})
|
||||
end)
|
||||
|
||||
-- Listen for Return All Players
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.RETURN_ALL_PLAYERS, function(message)
|
||||
-- Update global PlayerList with players from other servers
|
||||
local server = message.Data.serverId
|
||||
-- find server and remove old data in GlobalPlayerList ({{{userId | Username} | serverid}})
|
||||
for i, data in pairs(GlobalPlayerList) do
|
||||
if data.serverId == server then
|
||||
table.remove(GlobalPlayerList, i)
|
||||
end
|
||||
end
|
||||
table.insert(GlobalPlayerList, message.Data)
|
||||
end)
|
||||
|
||||
-- Listen for command responses (feedback to admin)
|
||||
MessagingService:SubscribeAsync(AdminCore.Topics.COMMAND_RESPONSE, function(message)
|
||||
local data = message.Data
|
||||
local adminPlayer = Players:GetPlayerByUserId(data.adminUserId)
|
||||
|
||||
if adminPlayer then
|
||||
ChatEvent:FireClient(adminPlayer, data.message)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function AdminCore:Init()
|
||||
|
||||
setupMessagingListeners()
|
||||
|
||||
local RemoteFunction: RemoteFunction = ReplicatedStorage.Remote:WaitForChild("Admin")
|
||||
|
||||
RemoteFunction.OnServerInvoke = function(player,topic,targetPlayerName,amount,reason)
|
||||
if topic == AdminCore.Topics.MONEY_COMMAND then
|
||||
return AdminCore.MoneyCommand(player,targetPlayerName,amount)
|
||||
elseif topic == AdminCore.Topics.WINS_COMMAND then
|
||||
return AdminCore.WinsCommand(player,targetPlayerName,amount)
|
||||
elseif topic == AdminCore.Topics.KICK_COMMAND then
|
||||
return AdminCore.KickPlayer(player,targetPlayerName,reason)
|
||||
elseif topic == AdminCore.Topics.BAN_PLAYER then
|
||||
return AdminCore.BanPlayer(player,targetPlayerName,amount,reason)
|
||||
elseif topic == AdminCore.Topics.UNBAN_PLAYER then
|
||||
return AdminCore.UnbanPlayer(player,targetPlayerName)
|
||||
elseif topic == AdminCore.Topics.GET_ALL_PLAYERS then
|
||||
return AdminCore.GetAllPlayers(player)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Fix for studo testing
|
||||
local function GetUserName(Id: number)
|
||||
if Id < 0 then
|
||||
return false
|
||||
else
|
||||
return Players:GetNameFromUserIdAsync(Id)
|
||||
end
|
||||
end
|
||||
--------------------------------------------------
|
||||
|
||||
|
||||
|
||||
-- Global playerlist stuff
|
||||
|
||||
for _, player in pairs(Players:GetPlayers()) do
|
||||
local Name = GetUserName(player.UserId) or player.Name
|
||||
table.insert(PlayerList, {["Name"] = Name, ["Id"] = player.UserId})
|
||||
print(PlayerList)
|
||||
MessagingService:PublishAsync(AdminCore.Topics.RETURN_ALL_PLAYERS, {
|
||||
players = PlayerList,
|
||||
serverId = game.JobId
|
||||
})
|
||||
end
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
local Name = GetUserName(player.UserId) or player.Name
|
||||
table.insert(PlayerList, {["Name"] = Name, ["Id"] = player.UserId})
|
||||
print(PlayerList)
|
||||
MessagingService:PublishAsync(AdminCore.Topics.RETURN_ALL_PLAYERS, {
|
||||
players = PlayerList,
|
||||
serverId = game.JobId
|
||||
})
|
||||
end)
|
||||
|
||||
Players.PlayerRemoving:Connect(function(player)
|
||||
for i, v in pairs(PlayerList) do
|
||||
if v["Id"] == player.UserId then
|
||||
table.remove(PlayerList, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
MessagingService:PublishAsync(AdminCore.Topics.RETURN_ALL_PLAYERS, {
|
||||
players = PlayerList,
|
||||
serverId = game.JobId
|
||||
})
|
||||
end)
|
||||
|
||||
MessagingService:PublishAsync(AdminCore.Topics.GET_ALL_PLAYERS)
|
||||
end
|
||||
|
||||
|
||||
function AdminCore.MoneyCommand(adminPlayer : Player,targetPlayerName : string,amount : number)
|
||||
local amount = amount
|
||||
-- Send money command request to the admin's server
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
if amount then
|
||||
local targetPlayer = Players:FindFirstChild(targetPlayerName)
|
||||
if targetPlayer then
|
||||
-- Target is on this server, execute directly
|
||||
DataManager.SetValue(targetPlayer, ValueNames.Cash, amount)
|
||||
return true, "Changed " .. targetPlayer.Name .. "'s money to " .. amount
|
||||
else
|
||||
-- Target might be on another server, broadcast cross-server command
|
||||
MessagingService:PublishAsync(AdminCore.Topics.MONEY_COMMAND, {
|
||||
adminUserId = adminPlayer.UserId,
|
||||
targetPlayerName = targetPlayerName,
|
||||
amount = amount
|
||||
})
|
||||
return true, "Searching for " .. targetPlayerName .. " across all servers..."
|
||||
end
|
||||
else
|
||||
return false, "Invalid amount: " .. amount
|
||||
end
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
function AdminCore.WinsCommand(adminPlayer : Player,targetPlayerName : string,amount : number)
|
||||
local amount = amount
|
||||
-- Send money command request to the admin's server
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
if amount then
|
||||
local targetPlayer = Players:FindFirstChild(targetPlayerName)
|
||||
if targetPlayer then
|
||||
-- Target is on this server, execute directly
|
||||
DataManager.SetValue(targetPlayer, ValueNames.Wins, amount)
|
||||
return true, "Changed " .. targetPlayer.Name .. "'s wins to " .. amount
|
||||
else
|
||||
-- Target might be on another server, broadcast cross-server command
|
||||
MessagingService:PublishAsync(AdminCore.Topics.WINS_COMMAND, {
|
||||
adminUserId = adminPlayer.UserId,
|
||||
targetPlayerName = targetPlayerName,
|
||||
amount = amount
|
||||
})
|
||||
return true, "Searching for " .. targetPlayerName .. " across all servers..."
|
||||
end
|
||||
else
|
||||
return false, "Invalid amount: " .. amount
|
||||
end
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
function AdminCore.KickPlayer(adminPlayer : Player,targetPlayerName : string,reason : string)
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
local targetPlayer = Players:FindFirstChild(targetPlayerName)
|
||||
if targetPlayer then
|
||||
targetPlayer:Kick(reason)
|
||||
return true, "Kicked " .. targetPlayer.Name
|
||||
else
|
||||
MessagingService:PublishAsync(AdminCore.Topics.KICK_COMMAND, {
|
||||
adminUserId = adminPlayer.UserId,
|
||||
targetPlayerName = targetPlayerName,
|
||||
reason = reason
|
||||
})
|
||||
end
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
function AdminCore.BanPlayer(adminPlayer : Player,targetPlayerName : string,duration: number,reason : string)
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
local targetPlayer = Players:GetUserIdFromNameAsync(targetPlayerName)
|
||||
if targetPlayer then
|
||||
Players:BanAsync({UserIds = {targetPlayer.UserId},Duration = duration,DisplayReason = reason,PrivateReason = reason})
|
||||
return true, "Banned " .. targetPlayer.Name
|
||||
else
|
||||
return false, "Player not found"
|
||||
end
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
function AdminCore.UnbanPlayer(adminPlayer : Player,targetPlayerName : string)
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
local targetPlayer = Players:GetUserIdFromNameAsync(targetPlayerName)
|
||||
if targetPlayer then
|
||||
Players:UnbanAsync({UserIds = {targetPlayer}, ApplyToUniverse = true})
|
||||
return true, "Unbanned " .. targetPlayerName
|
||||
else
|
||||
return false, "Player not found"
|
||||
end
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
function AdminCore.GetAllPlayers(adminPlayer : Player)
|
||||
if RunService:IsServer() then
|
||||
if adminPlayer and AdminCore.CheckIfAdmin(adminPlayer) then
|
||||
return true, GlobalPlayerList
|
||||
else
|
||||
return false, "You don't have permission to use this command."
|
||||
end
|
||||
else
|
||||
return false, "This command can only be used on the server."
|
||||
end
|
||||
end
|
||||
|
||||
return AdminCore
|
||||
10
src/server/Admin/AdminCore.meta.json
Normal file
10
src/server/Admin/AdminCore.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ServerOnly": true
|
||||
}
|
||||
}
|
||||
39
src/server/Admin/AdminUI.luau
Normal file
39
src/server/Admin/AdminUI.luau
Normal file
@ -0,0 +1,39 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
local TextChatService = game:GetService("TextChatService")
|
||||
local MessagingService = game:GetService("MessagingService")
|
||||
local AdminCore = require("./AdminCore")
|
||||
local AdminGui = ServerStorage.Assets.UI:WaitForChild("AdminGui")
|
||||
|
||||
local AdminCount = 0
|
||||
|
||||
local AdminUI = {}
|
||||
|
||||
|
||||
function AdminUI:Init()
|
||||
for _, player in Players:GetPlayers() do
|
||||
if AdminCore.CheckIfAdmin(player) then
|
||||
local adminGui = AdminGui:Clone()
|
||||
adminGui.Parent = player.PlayerGui
|
||||
adminGui.Enabled = false
|
||||
AdminCount += 1
|
||||
end
|
||||
end
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
if AdminCore.CheckIfAdmin(player) then
|
||||
local adminGui = AdminGui:Clone()
|
||||
adminGui.Parent = player.PlayerGui
|
||||
adminGui.Enabled = false
|
||||
AdminCount += 1
|
||||
end
|
||||
end)
|
||||
Players.PlayerRemoving:Connect(function(player)
|
||||
if AdminCore.CheckIfAdmin(player) then
|
||||
AdminCount -= 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return AdminUI
|
||||
10
src/server/Admin/AdminUI.meta.json
Normal file
10
src/server/Admin/AdminUI.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ServerOnly": true
|
||||
}
|
||||
}
|
||||
97
src/server/Data/DataInit.server.luau
Normal file
97
src/server/Data/DataInit.server.luau
Normal file
@ -0,0 +1,97 @@
|
||||
-- Services
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local DataStoreNames = require("./DataStoreNames")
|
||||
|
||||
-- ProfileStore
|
||||
local ProfileStore = require(ServerScriptService.Libraries.ProfileStore)
|
||||
local ValueNames = require("./ValueNames")
|
||||
|
||||
local function GetStoreName()
|
||||
return RunService:IsStudio() and "Test" or "Live"
|
||||
end
|
||||
|
||||
local Template = require(ServerScriptService.Data.Template)
|
||||
local DataManager = require(ServerScriptService.Data.DataManager)
|
||||
|
||||
-- Access profile store
|
||||
local PlayerStore = ProfileStore.New(GetStoreName(), Template)
|
||||
|
||||
|
||||
|
||||
-- Add leaderstats and synchronize player data
|
||||
local function Initialize(player: Player, profile : typeof(PlayerStore:StartSessionAsync()))
|
||||
-- Leaderstats
|
||||
local leaderstats = Instance.new("Folder", player)
|
||||
leaderstats.Name = "leaderstats"
|
||||
|
||||
local Cash = Instance.new("NumberValue", leaderstats)
|
||||
Cash.Name = ValueNames.Cash
|
||||
local InitMoney = DataManager.GetValue(player, ValueNames.Cash) or 0
|
||||
Cash.Value = InitMoney
|
||||
|
||||
DataManager.SyncValue(player,Cash,ValueNames.Cash,DataStoreNames.Cash)
|
||||
--imma see if it works prob not lol
|
||||
local Wins = Instance.new("NumberValue",leaderstats)
|
||||
Wins.Name = ValueNames.Wins
|
||||
DataManager.SyncValue(player,Wins,ValueNames.Wins,DataStoreNames.Wins)
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Creates and stores a profile
|
||||
local function PlayerAdded(player: Player)
|
||||
|
||||
-- Start a new profile session
|
||||
local profile = PlayerStore:StartSessionAsync("Player_" .. player.UserId, {
|
||||
Cancel = function()
|
||||
return player.Parent ~= Players
|
||||
end,
|
||||
})
|
||||
|
||||
-- Sanity check to ensure profile exists
|
||||
if profile ~= nil then
|
||||
|
||||
profile:AddUserId(player.UserId) -- GDPR compliance
|
||||
profile:Reconcile() -- Fill in missing data variables from template
|
||||
|
||||
-- Handles session-locking
|
||||
profile.OnSessionEnd:Connect(function()
|
||||
DataManager.Profiles[player] = nil
|
||||
player:Kick("Data error occured. Please rejoin.")
|
||||
end)
|
||||
|
||||
-- Save profile for later use
|
||||
if player.Parent == Players then
|
||||
|
||||
DataManager.Profiles[player] = profile
|
||||
Initialize(player, profile)
|
||||
|
||||
else
|
||||
profile:EndSession()
|
||||
end
|
||||
|
||||
|
||||
else
|
||||
-- Server shuts down while player is joining
|
||||
player:Kick("Data error occured. Please rejoin.")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Early joiners
|
||||
for _, player in Players:GetPlayers() do
|
||||
task.spawn(PlayerAdded, player)
|
||||
end
|
||||
Players.PlayerAdded:Connect(PlayerAdded)
|
||||
|
||||
Players.PlayerRemoving:Connect(function(player)
|
||||
local profile = DataManager.Profiles[player]
|
||||
if not profile then return end
|
||||
profile:EndSession()
|
||||
DataManager.Profiles[player] = nil
|
||||
end)
|
||||
107
src/server/Data/DataManager.luau
Normal file
107
src/server/Data/DataManager.luau
Normal file
@ -0,0 +1,107 @@
|
||||
local DataManager = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local OrderedDataStore = require("./OrderedDatastoreHandler")
|
||||
|
||||
local ValueNames = require("./ValueNames")
|
||||
local DataStoreNames = require("./DataStoreNames")
|
||||
|
||||
local ProfileStore
|
||||
|
||||
|
||||
|
||||
-- Store profiles from ProfileStore
|
||||
DataManager.Profiles = {}
|
||||
|
||||
local synced = {}
|
||||
|
||||
function DataManager.getKey(player : Player,key : string)
|
||||
return "PLAYER_" .. player.UserId .. "_" .. key
|
||||
end
|
||||
function DataManager.AddValue(player : Player,key : string,num)
|
||||
local value = DataManager.GetValue(player,key)
|
||||
if not value then
|
||||
return
|
||||
end
|
||||
|
||||
DataManager.SetValue(player, key, value + num)
|
||||
end
|
||||
function DataManager.SubValue(player : Player,key : number,num)
|
||||
local value = DataManager.GetValue(player,key)
|
||||
if not value then
|
||||
return
|
||||
end
|
||||
|
||||
DataManager.SetValue(player, key, value - num)
|
||||
end
|
||||
|
||||
function DataManager.GetValue(player : Player,key : string)
|
||||
local profile = DataManager.Profiles[player]
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
return profile.Data[key]
|
||||
end
|
||||
|
||||
function DataManager.SetValue(player : Player,key : string,num)
|
||||
local profile = DataManager.Profiles[player]
|
||||
if not profile then
|
||||
return
|
||||
end
|
||||
|
||||
profile.Data[key] = num
|
||||
|
||||
local keyName = DataManager.getKey(player,key)
|
||||
local s = synced[keyName]
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
|
||||
local value = s.Value
|
||||
if value then
|
||||
value.Value = num
|
||||
end
|
||||
local datastore = s.Datastore
|
||||
if datastore then
|
||||
OrderedDataStore.SaveData(player,datastore,num)
|
||||
end
|
||||
end
|
||||
|
||||
function DataManager.GetTanks(player : Player)
|
||||
|
||||
end
|
||||
|
||||
function DataManager.GetTankData(player : Player,name : string)
|
||||
|
||||
end
|
||||
|
||||
function DataManager.SwitchTank(player : Player,tank : string)
|
||||
|
||||
end
|
||||
|
||||
function DataManager.SwitchTankSkin(player : Player,skinName : string)
|
||||
|
||||
end
|
||||
|
||||
function DataManager.UnlockTank(player : Player,tank : string)
|
||||
|
||||
end
|
||||
|
||||
function DataManager.UnlockTankSkin(player : Player,skinName)
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- yh ill add it later
|
||||
|
||||
function DataManager.SyncValue(player : Player,value : IntValue,key : string,datastore : string)
|
||||
local keyName = DataManager.getKey(player,key)
|
||||
synced[keyName] = {Value = value,Datastore = datastore}
|
||||
|
||||
DataManager.SetValue(player,key,DataManager.GetValue(player,key))
|
||||
end
|
||||
|
||||
--sup
|
||||
return DataManager
|
||||
7
src/server/Data/DataManager.meta.json
Normal file
7
src/server/Data/DataManager.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
src/server/Data/DataStoreNames.luau
Normal file
7
src/server/Data/DataStoreNames.luau
Normal file
@ -0,0 +1,7 @@
|
||||
-- dont change indexes!!!!!!
|
||||
local DataStoreNames = {
|
||||
Cash = "MoneyStore",
|
||||
Wins = "WinStore"
|
||||
}
|
||||
|
||||
return DataStoreNames
|
||||
135
src/server/Data/InventoryService.luau
Normal file
135
src/server/Data/InventoryService.luau
Normal file
@ -0,0 +1,135 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local RemoteFunction = ReplicatedStorage.Remote.InventoryService
|
||||
|
||||
local InventoryService = {}
|
||||
|
||||
local PlayersInventory : {[number] : {}} = {}
|
||||
|
||||
local Tanks: {string} = {}
|
||||
for _, tank in ReplicatedStorage:WaitForChild("Assets"):WaitForChild("Models"):WaitForChild("Tanks"):GetChildren() do
|
||||
if tank.Name ~= "Tank (Testing)" or game:GetService("RunService"):IsStudio() then
|
||||
table.insert(Tanks, tank.Name)
|
||||
end
|
||||
end
|
||||
local Skins: {[typeof(Tanks)] : string} = {}
|
||||
for _, tank in Tanks do
|
||||
Skins[tank] = {}
|
||||
for _, skin in ReplicatedStorage:WaitForChild("Assets"):WaitForChild("Models"):WaitForChild("Tanks"):WaitForChild(tank):GetChildren() do
|
||||
if skin.Name ~= "Base" then
|
||||
table.insert(Skins[tank], skin.Name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function InventoryService.GetModel(tankName : string, skinName : string)
|
||||
local base = ReplicatedStorage:WaitForChild("Assets"):WaitForChild("Models"):WaitForChild("Tanks"):WaitForChild(tankName):FindFirstChild("Base")
|
||||
local skin = ReplicatedStorage:WaitForChild("Assets"):WaitForChild("Models"):WaitForChild("Tanks"):WaitForChild(tankName):FindFirstChild(skinName)
|
||||
if base and skin then
|
||||
return base, skin
|
||||
end
|
||||
end
|
||||
|
||||
function createPlayerInventory(player : Player)
|
||||
print("Creating inventory: ".. player.Name)
|
||||
PlayersInventory[player.UserId] = {}
|
||||
local Loadouts: {[string] : string} = {}
|
||||
local SelectedLoadout: string
|
||||
local PlayerSkins: {[string] : string} = {}
|
||||
|
||||
local Inventory = {}
|
||||
Inventory["SelectedLoadout"] = ""
|
||||
Inventory["Loadouts"] = Loadouts
|
||||
Inventory["Tanks"] = {}
|
||||
Inventory["Skins"] = PlayerSkins
|
||||
PlayersInventory[player.UserId] = Inventory
|
||||
|
||||
if Inventory["Tanks"] == nil or #Inventory["Tanks"] == 0 then
|
||||
Inventory["Tanks"] = {"Tank"}
|
||||
end
|
||||
|
||||
if Inventory["SelectedLoadout"] == "" or Inventory["SelectedLoadout"] == nil or table.find(Inventory["Tanks"], Inventory["SelectedLoadout"]) then
|
||||
print(InventoryService.SelectLoadout(player, Inventory["Tanks"][1]))
|
||||
end
|
||||
|
||||
for _, tank in Inventory["Tanks"] do
|
||||
if Inventory["Skins"][tank] == nil or #Inventory["Skins"][tank] == 0 then
|
||||
Inventory["Skins"][tank] = {"Default"}
|
||||
end
|
||||
end
|
||||
|
||||
if not CheckIfLoadoutExists(player, Inventory["Tanks"][1]) then
|
||||
InventoryService.UpdateLoadout(player, Inventory["SelectedLoadout"], Inventory["Skins"][Inventory["Tanks"][1]][1])
|
||||
end
|
||||
|
||||
return Inventory
|
||||
end
|
||||
|
||||
function InventoryService.GetAllTanks()
|
||||
return Tanks
|
||||
end
|
||||
|
||||
function InventoryService.GetAllSkins()
|
||||
return Skins
|
||||
end
|
||||
|
||||
|
||||
function InventoryService.UpdateLoadout(player : Player, tank : string, skin : string)
|
||||
PlayersInventory[player.UserId]["Loadouts"][tank] = skin
|
||||
return PlayersInventory[player.UserId]["Loadouts"][tank]
|
||||
end
|
||||
|
||||
function InventoryService.SelectLoadout(player : Player, loadout : string)
|
||||
PlayersInventory[player.UserId]["SelectedLoadout"] = loadout
|
||||
return PlayersInventory[player.UserId]["SelectedLoadout"]
|
||||
end
|
||||
|
||||
function CheckIfLoadoutExists(player : Player, tank : string)
|
||||
local Loadouts = PlayersInventory[player.UserId].Loadouts
|
||||
if Loadouts[tank] then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function InventoryService.GetInventory(player : Player)
|
||||
return PlayersInventory[player.UserId]
|
||||
end
|
||||
|
||||
function InventoryService.AllInventories()
|
||||
return PlayersInventory
|
||||
end
|
||||
|
||||
function InventoryService.Init()
|
||||
for _, player in Players:GetPlayers() do
|
||||
createPlayerInventory(player)
|
||||
end
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
createPlayerInventory(player)
|
||||
end)
|
||||
Players.PlayerRemoving:Connect(function(player)
|
||||
PlayersInventory[player.UserId] = nil
|
||||
end)
|
||||
RemoteFunction.OnServerInvoke = function(player, action, ...)
|
||||
local args = {...}
|
||||
if action == "GetInventory" then
|
||||
return InventoryService.GetInventory(player)
|
||||
elseif action == "AllInventories" then
|
||||
return InventoryService.AllInventories()
|
||||
elseif action == "UpdateLoadout" then
|
||||
InventoryService.UpdateLoadout(player, args[1], args[2])
|
||||
elseif action == "SelectLoadout" then
|
||||
return InventoryService.SelectLoadout(player, args[1])
|
||||
elseif action == "GetAllTanks" then
|
||||
return InventoryService.GetAllTanks()
|
||||
elseif action == "GetAllSkins" then
|
||||
return InventoryService.GetAllSkins()
|
||||
elseif action == "GetModel" then
|
||||
return InventoryService.GetModel(args[1], args[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return InventoryService
|
||||
7
src/server/Data/InventoryService.meta.json
Normal file
7
src/server/Data/InventoryService.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
22
src/server/Data/LeaderboardHandler/Settings.luau
Normal file
22
src/server/Data/LeaderboardHandler/Settings.luau
Normal file
@ -0,0 +1,22 @@
|
||||
export type LeaderBoardSettings = {
|
||||
Name : string,
|
||||
UpdateInterval : number
|
||||
}
|
||||
|
||||
local Settings = {
|
||||
|
||||
Cash = {
|
||||
Name = "Richest Players",
|
||||
UpdateInterval = 60,
|
||||
|
||||
},
|
||||
|
||||
Wins = {
|
||||
Name = "Most wins",
|
||||
UpdateInterval = 30,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return Settings
|
||||
131
src/server/Data/LeaderboardHandler/init.luau
Normal file
131
src/server/Data/LeaderboardHandler/init.luau
Normal file
@ -0,0 +1,131 @@
|
||||
local LeaderboardHandler = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
local UI = rAssets.UI
|
||||
|
||||
local LeaderboardTemplate = UI:WaitForChild("LeaderboardTemplate")
|
||||
|
||||
local PlayerService = game:GetService("Players")
|
||||
local OrderedDataStore = require("./OrderedDatastoreHandler")
|
||||
|
||||
local DataStoreNames = require("./DataStoreNames")
|
||||
|
||||
local Settings = require(script.Settings)
|
||||
|
||||
|
||||
local leaderboards = {}
|
||||
|
||||
--ill finish it later cuz im very sleepy rn zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
|
||||
|
||||
function getUserName(UserId)
|
||||
local user
|
||||
|
||||
local success = pcall(function()
|
||||
user = PlayerService:GetNameFromUserIdAsync(UserId)
|
||||
end)
|
||||
if not success then
|
||||
user = "StudioTester_ " .. tostring(UserId)
|
||||
end
|
||||
|
||||
return user
|
||||
end
|
||||
|
||||
function decreaseTimer(data,dt)
|
||||
data.Timer -= dt
|
||||
end
|
||||
|
||||
function updateTimer(model,value)
|
||||
--this is for when we add a gui that tells when it will be refrsehed
|
||||
end
|
||||
|
||||
function LeaderboardHandler.Tick(dt)
|
||||
for name,data in pairs(leaderboards) do
|
||||
local model = data.Model
|
||||
|
||||
decreaseTimer(data,dt)
|
||||
updateTimer(model,data.Timer)
|
||||
|
||||
if data.Timer <= 0 then
|
||||
local currentSettings : Settings.LeaderBoardSettings = Settings[name]
|
||||
|
||||
data.Timer = currentSettings.UpdateInterval
|
||||
|
||||
LeaderboardHandler.UpdateLeaderboard(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function createTemplate(rank,data,parent)
|
||||
local value = data.value
|
||||
local userId = data.key
|
||||
local new = LeaderboardTemplate:Clone()
|
||||
new.Rank.Text = rank
|
||||
|
||||
local name = getUserName(tonumber(userId))
|
||||
new.Username.Text = name
|
||||
new.Value.Text = value
|
||||
new.Parent = parent
|
||||
--print("Parent for template:", parent)
|
||||
end
|
||||
|
||||
local manequin : Model = {}
|
||||
local anim: Animation = {}
|
||||
|
||||
function LeaderboardHandler.UpdateLeaderboard(name)
|
||||
|
||||
local currentLeaderboard = leaderboards[name]
|
||||
local tbl = OrderedDataStore.GetSorted(DataStoreNames[name])
|
||||
|
||||
local scrollingFrame = currentLeaderboard.ScrollingFrame
|
||||
for i,v in pairs(scrollingFrame:GetChildren()) do
|
||||
if v.Name ~= "Titles" and v:IsA("Frame") then
|
||||
v:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
for rank,data in pairs(tbl) do
|
||||
createTemplate(rank,data,currentLeaderboard.ScrollingFrame)
|
||||
end
|
||||
|
||||
|
||||
-- Manequins
|
||||
if not manequin[name] then
|
||||
manequin[name] = PlayerService:CreateHumanoidModelFromUserIdAsync(tbl[1].key)
|
||||
manequin[name].Parent = currentLeaderboard.Model
|
||||
manequin[name]:SetPrimaryPartCFrame(currentLeaderboard.Model:WaitForChild("Board").CFrame * CFrame.new(0,10,0))
|
||||
manequin[name].Name = PlayerService:GetNameFromUserIdAsync(tbl[1].key)
|
||||
manequin[name]:SetAttribute("UserId",tbl[1].key)
|
||||
|
||||
anim[name] = manequin[name].Humanoid:LoadAnimation(ReplicatedStorage.Assets.Animations.Waving)
|
||||
anim[name].Looped = true
|
||||
anim[name]:Play()
|
||||
end
|
||||
manequin[name].Humanoid:ApplyDescription(PlayerService:GetHumanoidDescriptionFromUserIdAsync(tbl[1].key))
|
||||
|
||||
end
|
||||
|
||||
|
||||
function LeaderboardHandler:Init()
|
||||
print(game:GetService("CollectionService"):GetTagged("LEADERBOARD"))
|
||||
local loaded = 0
|
||||
local total = #game:GetService("CollectionService"):GetTagged("LEADERBOARD")
|
||||
|
||||
for _,leaderboard in pairs(game:GetService("CollectionService"):GetTagged("LEADERBOARD")) do
|
||||
leaderboards[leaderboard.Name] = {}
|
||||
local currentLeaderboard = leaderboards[leaderboard.Name]
|
||||
currentLeaderboard.Model = leaderboard
|
||||
currentLeaderboard.ScrollingFrame = leaderboard.Board.SurfaceGui:FindFirstChildOfClass("ScrollingFrame")
|
||||
|
||||
LeaderboardHandler.UpdateLeaderboard(leaderboard.Name)
|
||||
local currentSettings : Settings.LeaderBoardSettings = Settings[leaderboard.Name]
|
||||
|
||||
currentLeaderboard.Timer = currentSettings.UpdateInterval
|
||||
loaded += 1
|
||||
print("Loaded Leaderboard:",leaderboard.Name,"(",loaded,"/",total,")")
|
||||
end
|
||||
|
||||
game:GetService("RunService").Heartbeat:Connect(LeaderboardHandler.Tick)
|
||||
end
|
||||
|
||||
return LeaderboardHandler
|
||||
7
src/server/Data/LeaderboardHandler/init.meta.json
Normal file
7
src/server/Data/LeaderboardHandler/init.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
39
src/server/Data/OrderedDatastoreHandler.luau
Normal file
39
src/server/Data/OrderedDatastoreHandler.luau
Normal file
@ -0,0 +1,39 @@
|
||||
local DatastoreService = game:GetService("DataStoreService")
|
||||
|
||||
local OrderedDatastoreHandler = {}
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
local IsStudio = RunService:IsStudio()
|
||||
|
||||
local offset = IsStudio and "_TEST" or "_REAL"
|
||||
|
||||
function getDataName(name)
|
||||
return name .. offset
|
||||
end
|
||||
|
||||
function getPlayerKeyName(player : Player)
|
||||
return tostring(player.UserId)
|
||||
end
|
||||
|
||||
function OrderedDatastoreHandler.SaveData(player : Player,name,value)
|
||||
--print(name)
|
||||
local dataName = getDataName(name)
|
||||
local datastore = DatastoreService:GetOrderedDataStore(dataName)
|
||||
|
||||
datastore:SetAsync(getPlayerKeyName(player),value)
|
||||
end
|
||||
function OrderedDatastoreHandler.GetSorted(name)
|
||||
--print(name)
|
||||
local dataName = getDataName(name)
|
||||
local datastore = DatastoreService:GetOrderedDataStore(dataName)
|
||||
|
||||
|
||||
local pages = datastore:GetSortedAsync(false,100,nil)
|
||||
|
||||
local top = pages:GetCurrentPage()
|
||||
|
||||
return top
|
||||
end
|
||||
|
||||
|
||||
return OrderedDatastoreHandler
|
||||
12
src/server/Data/Template.luau
Normal file
12
src/server/Data/Template.luau
Normal file
@ -0,0 +1,12 @@
|
||||
return {
|
||||
Money = 50,
|
||||
Wins = 0,
|
||||
|
||||
UnlockedSkins = {},
|
||||
UnlockedTanks = {},
|
||||
UnlockedAccessories = {},
|
||||
|
||||
Loadouts = {},
|
||||
CurrentLoadout = "Default",
|
||||
CurrentTank = "Tank"
|
||||
}
|
||||
6
src/server/Data/ValueNames.luau
Normal file
6
src/server/Data/ValueNames.luau
Normal file
@ -0,0 +1,6 @@
|
||||
local ValueNames = {
|
||||
Cash = "Money",
|
||||
Wins = "Wins"
|
||||
}
|
||||
|
||||
return ValueNames
|
||||
2232
src/server/Libraries/ProfileStore.luau
Normal file
2232
src/server/Libraries/ProfileStore.luau
Normal file
File diff suppressed because it is too large
Load Diff
5
src/server/Libraries/ProfileStore.meta.json
Normal file
5
src/server/Libraries/ProfileStore.meta.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"properties": {
|
||||
"SourceAssetId": 109379033046155.0
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Vex = require(ReplicatedStorage.Vex)
|
||||
|
||||
local Component = require(script.Parent)
|
||||
|
||||
local DestroyableComponent = setmetatable({},Component)
|
||||
|
||||
type self = {
|
||||
model : Model,
|
||||
HitBoxParts: {Part},
|
||||
HitParams: OverlapParams,
|
||||
}
|
||||
|
||||
shared.DESTROYABLE_TAG = "DESTROYABLE"
|
||||
|
||||
export type DestroyableComponent = typeof(setmetatable({}, DestroyableComponent)) & Component.Component
|
||||
|
||||
|
||||
DestroyableComponent.__index = DestroyableComponent
|
||||
|
||||
function DestroyableComponent.new(parentKey,voxelModel : Model,mainModel,data)
|
||||
local self = setmetatable(Component.new(parentKey) :: self,DestroyableComponent)
|
||||
|
||||
mainModel:AddTag(shared.DESTROYABLE_TAG)
|
||||
self.model = voxelModel
|
||||
local v = Vex.new(voxelModel,{voxelSize = .5,maxVoxels = 20000,lifetime = 1,useGreedyMesh = false,anchored = true,weldAdjacent = false})
|
||||
self.VexObject = v
|
||||
v:Destroy()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return DestroyableComponent
|
||||
64
src/server/Modules/Classes/Component/HealthComponent.luau
Normal file
64
src/server/Modules/Classes/Component/HealthComponent.luau
Normal file
@ -0,0 +1,64 @@
|
||||
local Component = require(script.Parent)
|
||||
|
||||
local HealthComponent = setmetatable({},Component)
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local rUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
|
||||
local Signal = require(rUtils.Signal)
|
||||
|
||||
type self = {
|
||||
MaxHealth : number,
|
||||
Health : number,
|
||||
HealthChangedSignal : typeof(Signal.new()),
|
||||
DiedSignal : typeof(Signal.new())
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type HealthComponent = typeof(setmetatable({}, HealthComponent)) & Component.Component
|
||||
|
||||
|
||||
HealthComponent.__index = HealthComponent
|
||||
|
||||
function HealthComponent.new(parentKey,MaxHealth : number)
|
||||
local self = setmetatable(Component.new(parentKey) :: self,HealthComponent)
|
||||
MaxHealth = MaxHealth or 100
|
||||
self.MaxHealth = MaxHealth
|
||||
self.Health = self.MaxHealth
|
||||
self.HealthChangedSignal = Signal.new()
|
||||
self.DiedSignal = Signal.new()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
function HealthComponent:Init()
|
||||
self:SetHealth(self.MaxHealth)
|
||||
end
|
||||
|
||||
function HealthComponent:SetHealth(newHP)
|
||||
local self = self :: HealthComponent
|
||||
local oldHP = self.Health
|
||||
local finalHP = math.round(math.clamp(0,newHP,self.MaxHealth))
|
||||
self.Health = finalHP
|
||||
self.HealthChangedSignal:Fire(oldHP,finalHP)
|
||||
|
||||
if finalHP == 0 then
|
||||
self.DiedSignal:Fire()
|
||||
end
|
||||
end
|
||||
|
||||
function HealthComponent:TakeDamage(num)
|
||||
self:SetHealth(self.Health - num)
|
||||
end
|
||||
function HealthComponent:IsAlive()
|
||||
return self.Health > 0
|
||||
end
|
||||
function HealthComponent:Heal(num)
|
||||
local hp = self.Health
|
||||
self:SetHealth(hp + num)
|
||||
end
|
||||
|
||||
return HealthComponent
|
||||
62
src/server/Modules/Classes/Component/HitboxComponent.luau
Normal file
62
src/server/Modules/Classes/Component/HitboxComponent.luau
Normal file
@ -0,0 +1,62 @@
|
||||
local Component = require(script.Parent)
|
||||
|
||||
local HitboxComponent = setmetatable({},Component)
|
||||
|
||||
type self = {
|
||||
model : Model,
|
||||
HitBoxParts: {Part},
|
||||
HitParams: OverlapParams,
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type HitboxComponent = typeof(setmetatable({}, HitboxComponent)) & Component.Component
|
||||
|
||||
|
||||
HitboxComponent.__index = HitboxComponent
|
||||
|
||||
function HitboxComponent.new(parentKey,model : Model)
|
||||
local self = setmetatable(Component.new(parentKey) :: self,HitboxComponent)
|
||||
self.model = model
|
||||
self:InitHitbox()
|
||||
return self
|
||||
end
|
||||
|
||||
function HitboxComponent:InitHitbox()
|
||||
local model = self.model
|
||||
local hitbox = model:FindFirstChild("Hitbox") or model
|
||||
|
||||
local parts = {}
|
||||
for _, v in ipairs(hitbox:GetChildren()) do
|
||||
if v:IsA("BasePart") then
|
||||
table.insert(parts, v)
|
||||
end
|
||||
end
|
||||
|
||||
local params = OverlapParams.new()
|
||||
params.FilterDescendantsInstances = {model}
|
||||
params.FilterType = Enum.RaycastFilterType.Exclude
|
||||
|
||||
self.HitBoxParts = parts
|
||||
self.HitParams = params
|
||||
end
|
||||
|
||||
|
||||
function HitboxComponent:CollisionCheck(): {Part}
|
||||
local hitModels = {}
|
||||
|
||||
for _, part in ipairs(self.HitBoxParts) do
|
||||
if part:IsA("BasePart") and part.CanQuery then
|
||||
-- Use the OverlapParams stored in self.HitParams
|
||||
local hits = workspace:GetPartsInPart(part, self.HitParams)
|
||||
|
||||
for _, hit in ipairs(hits) do
|
||||
table.insert(hitModels,hit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return hitModels
|
||||
end
|
||||
|
||||
return HitboxComponent
|
||||
@ -0,0 +1,46 @@
|
||||
-- ProjectileComponent.lua
|
||||
local Component = require(script.Parent)
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local sModules = ServerScriptService.Modules
|
||||
local PhysicsProjectileLauncher = require(sModules.Utils.PhysicsProjectileLauncher)
|
||||
|
||||
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local rUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
local TwoDimensionalUtils = require(rUtils.TwoDimensionUtils)
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local DataTypes = require(rData.DataTypes)
|
||||
|
||||
local ProjectileComponent = setmetatable({}, { __index = Component })
|
||||
ProjectileComponent.__index = ProjectileComponent
|
||||
|
||||
export type ProjectileComponent = Component.Component & {
|
||||
Origin: Vector3,
|
||||
SnapToVelocity: Vector3,
|
||||
model: Model,
|
||||
FireData : DataTypes.FireData
|
||||
}
|
||||
|
||||
function ProjectileComponent.new(parentKey: string, model: Model,FireData : DataTypes.FireData ,origin: Vector3?): ProjectileComponent
|
||||
local self = setmetatable(Component.new(parentKey), ProjectileComponent)
|
||||
self.Origin = origin or model:GetPivot().Position
|
||||
self.SnapToVelocity = Vector3.zero
|
||||
self.model = model
|
||||
TwoDimensionalUtils.AddToPosition(model)
|
||||
|
||||
self.FireData = FireData
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ProjectileComponent:Launch()
|
||||
local self = self :: ProjectileComponent
|
||||
|
||||
PhysicsProjectileLauncher.launch(self.model.PrimaryPart,self.FireData,self.SnapToVelocity)
|
||||
end
|
||||
|
||||
return ProjectileComponent
|
||||
15
src/server/Modules/Classes/Component/init.luau
Normal file
15
src/server/Modules/Classes/Component/init.luau
Normal file
@ -0,0 +1,15 @@
|
||||
-- Component.lua
|
||||
local Component = {}
|
||||
Component.__index = Component
|
||||
|
||||
export type Component = {
|
||||
parentKey: string,
|
||||
}
|
||||
|
||||
function Component.new(parentKey: string): Component
|
||||
local self = setmetatable({}, Component)
|
||||
self.parentKey = parentKey
|
||||
return self :: any
|
||||
end
|
||||
|
||||
return Component
|
||||
@ -0,0 +1,74 @@
|
||||
|
||||
local BaseProjectile = require(script.Parent)
|
||||
|
||||
local BouncyGrenade = setmetatable({},BaseProjectile)
|
||||
|
||||
BouncyGrenade.__index = BouncyGrenade
|
||||
|
||||
|
||||
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local sModules = ServerScriptService.Modules
|
||||
local sWeaponUtils = require(sModules.Utils.WeaponUtils)
|
||||
local sBotUtils = require(sModules.Utils.BotUtils)
|
||||
local sClasses = sModules.Classes
|
||||
|
||||
local ComponentM = sClasses.Component
|
||||
local ProjectileComp = require(ComponentM.ProjectileComponent)
|
||||
|
||||
local GameObject = require(script.Parent)
|
||||
|
||||
local PhysicsProjectileLauncher = require(sModules.Utils.PhysicsProjectileLauncher)
|
||||
local SimulatedProjectileLauncher = require(sModules.Utils.SimulatedProjectileLauncher)
|
||||
|
||||
|
||||
--finish this
|
||||
|
||||
function BouncyGrenade.new(userId, model, fireData)
|
||||
local self = setmetatable(BaseProjectile.new(userId, model, fireData) :: BaseProjectile.BaseProjectile, BouncyGrenade)
|
||||
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function BouncyGrenade:OnCreate()
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
|
||||
self.Components.Projectile:Launch()
|
||||
end
|
||||
|
||||
function BouncyGrenade:Tick(dt)
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
|
||||
--print(self.timepassed)
|
||||
|
||||
if self.timepassed >= 3 then
|
||||
self:OnHit(self.model:GetPivot().Position)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function BouncyGrenade:OnHit(pos)
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
|
||||
--print(self.active)
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
local weaponStats = self:GetWeaponData("BouncyGrenade").weaponStats
|
||||
local damage = weaponStats.Damage or 150
|
||||
local radius = weaponStats.Radius
|
||||
|
||||
sWeaponUtils.ExplosionQuery(pos,radius.InnerRadius,radius.OuterRadius,damage)
|
||||
self:_Destroy()
|
||||
end
|
||||
|
||||
function BouncyGrenade:BeforeDestroy()
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
print("kaboom!")
|
||||
end
|
||||
|
||||
return BouncyGrenade
|
||||
@ -0,0 +1,77 @@
|
||||
|
||||
local BaseProjectile = require(script.Parent)
|
||||
|
||||
local Missile = setmetatable({},BaseProjectile)
|
||||
|
||||
Missile.__index = Missile
|
||||
|
||||
|
||||
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local sModules = ServerScriptService.Modules
|
||||
local sWeaponUtils = require(sModules.Utils.WeaponUtils)
|
||||
local sBotUtils = require(sModules.Utils.BotUtils)
|
||||
local sClasses = sModules.Classes
|
||||
|
||||
local ComponentM = sClasses.Component
|
||||
local ProjectileComp = require(ComponentM.ProjectileComponent)
|
||||
|
||||
local GameObject = require(script.Parent)
|
||||
|
||||
local PhysicsProjectileLauncher = require(sModules.Utils.PhysicsProjectileLauncher)
|
||||
local SimulatedProjectileLauncher = require(sModules.Utils.SimulatedProjectileLauncher)
|
||||
|
||||
|
||||
|
||||
|
||||
function Missile.new(userId, model, fireData)
|
||||
local self = setmetatable(BaseProjectile.new(userId, model, fireData) :: BaseProjectile.BaseProjectile, Missile)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Missile:OnCreate()
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
|
||||
self.Components.Projectile:Launch()
|
||||
end
|
||||
|
||||
function Missile:Tick()
|
||||
local hitbox = self.Components.Hitbox
|
||||
local hit = hitbox:CollisionCheck()
|
||||
|
||||
if #hit ~= 0 then
|
||||
print(hit)
|
||||
|
||||
self:OnHit(hit[1],self.model:GetPivot().Position)
|
||||
end
|
||||
|
||||
|
||||
local Hits = self.Components.Projectile
|
||||
end
|
||||
|
||||
function Missile:OnHit(hitPart : Part, pos)
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
|
||||
print(self.active)
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
local model : Model = hitPart.Parent.Parent
|
||||
|
||||
local weaponStats = self:GetWeaponData("Missile").weaponStats
|
||||
local damage = weaponStats.Damage or 150
|
||||
local radius = weaponStats.Radius
|
||||
|
||||
sWeaponUtils.ExplosionQuery(pos,radius.InnerRadius,radius.OuterRadius,damage )
|
||||
self:_Destroy()
|
||||
end
|
||||
|
||||
function Missile:BeforeDestroy()
|
||||
local self = self :: BaseProjectile.BaseProjectile
|
||||
print("kaboom!")
|
||||
end
|
||||
|
||||
return Missile
|
||||
@ -0,0 +1,42 @@
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local sModules = ServerScriptService.Modules
|
||||
|
||||
local sClasses = sModules.Classes
|
||||
|
||||
local ComponentM = sClasses.Component
|
||||
local HitboxComp = require(ComponentM.HitboxComponent)
|
||||
local ProjectileComp = require(ComponentM.ProjectileComponent)
|
||||
|
||||
local GameObject = require(script.Parent)
|
||||
local BaseProjectile = setmetatable({},GameObject)
|
||||
|
||||
|
||||
|
||||
export type BaseProjectile = typeof(setmetatable({}, BaseProjectile)) & GameObject.GameObject
|
||||
|
||||
|
||||
BaseProjectile.__index = BaseProjectile
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
function BaseProjectile.new(userId, model,fireData)
|
||||
local self = setmetatable(GameObject.new(nil,model,userId), BaseProjectile)
|
||||
|
||||
self.ClassName = "BaseProjectile"
|
||||
|
||||
self.Components.Hitbox = HitboxComp.new(self.key,model)
|
||||
self.Components.Projectile = ProjectileComp.new(self.key,model,fireData)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
function BaseProjectile:OnHit(hitPart, hitPosition) end
|
||||
function BaseProjectile:Tick() end
|
||||
function BaseProjectile:OnTurnEnd() print("turn end!") end
|
||||
function BaseProjectile:OnRoundEnd() print("round end!") self:Destroy() end
|
||||
|
||||
|
||||
return BaseProjectile
|
||||
137
src/server/Modules/Classes/GameObject/Bot.luau
Normal file
137
src/server/Modules/Classes/GameObject/Bot.luau
Normal file
@ -0,0 +1,137 @@
|
||||
-- Bot
|
||||
-- Represents a single player-controlled bot in the arena.
|
||||
local GameObject = require(script.Parent)
|
||||
local InventoryService = require(game:GetService("ServerScriptService").Data.InventoryService)
|
||||
|
||||
local Bot = setmetatable({},GameObject)
|
||||
Bot.__index = Bot
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
local rHUD = rAssets.HUD
|
||||
local rModels = rAssets.Models
|
||||
|
||||
local TankFolder = rModels.Tanks
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local BotData = require(rData.BotData)
|
||||
|
||||
|
||||
type self = {
|
||||
accuracy : number,
|
||||
HUD : typeof(rHUD.HUD_TANK)
|
||||
}
|
||||
|
||||
export type Bot = typeof( setmetatable({} :: self, Bot) ) & GameObject.GameObject
|
||||
|
||||
local Component = script.Parent.Parent.Component
|
||||
local HitboxComponent = require(Component.HitboxComponent)
|
||||
local ProjectileComponent = require(Component.ProjectileComponent)
|
||||
local HealthComponent = require(Component.HealthComponent)
|
||||
|
||||
function Bot.new(player : Player, name, character)
|
||||
local self : Bot = setmetatable(GameObject.new(player.UserId,nil,player.UserId) ,Bot)
|
||||
local Inventory = InventoryService.GetInventory(player)
|
||||
local selectedTank = Inventory["SelectedTank"]
|
||||
local selectedSkin = Inventory["Loadouts"][selectedTank]
|
||||
name = selectedTank or "Tank"
|
||||
local skin = selectedSkin or "Default"
|
||||
local bData : BotData.BotData = BotData[name]
|
||||
print(name,bData)
|
||||
|
||||
print(name)
|
||||
self.name = name
|
||||
self.skin = skin
|
||||
self.accuracy = bData.accuracy
|
||||
self.weapons = bData.weapons -- { basic = "Missile", special = "ClusterRocket" }
|
||||
|
||||
self:CreateModel()
|
||||
|
||||
local h = HealthComponent.new(self.key,bData.maxHp)
|
||||
|
||||
|
||||
h.HealthChangedSignal:Connect(function(oldHP,newHP)
|
||||
local diff = oldHP - newHP
|
||||
if oldHP > newHP then
|
||||
print(self.name .. " took " .. diff .. " damage!")
|
||||
else
|
||||
print(self.name .. " healed " .. diff .. " health!")
|
||||
end
|
||||
|
||||
self:DisplayHealth(newHP)
|
||||
end)
|
||||
h.DiedSignal:Connect(function()
|
||||
print("noooo he died!")
|
||||
end)
|
||||
|
||||
self.Components.Health = h
|
||||
|
||||
self.Components.Hitbox = HitboxComponent.new(self.key,self.model)
|
||||
|
||||
self.Components.Health:Init()
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
function Bot:CreateModel()
|
||||
print(self.name)
|
||||
print(TankFolder)
|
||||
local model : Model = TankFolder:FindFirstChild(self.name):FindFirstChild("Base")
|
||||
model = model:Clone()
|
||||
local skin : Model = TankFolder:FindFirstChild(self.name):FindFirstChild(self.skin)
|
||||
skin.Parent = model
|
||||
|
||||
print(self.key)
|
||||
model.Name = self.key
|
||||
model:SetAttribute("Type",self.name)
|
||||
self.model = model
|
||||
self.root = model.PrimaryPart or model.Root
|
||||
|
||||
|
||||
ReplicatedStorage.Assets.Cooldowns:Clone().Parent = model
|
||||
|
||||
|
||||
local HUD_TANK = rHUD.HUD_TANK:Clone()
|
||||
HUD_TANK.Parent = model
|
||||
self.HUD = HUD_TANK
|
||||
|
||||
self:_ResetMass()
|
||||
|
||||
end
|
||||
|
||||
function Bot:DisplayHealth(hp)
|
||||
local self = self :: Bot
|
||||
local Health = self.Components.Health
|
||||
|
||||
|
||||
hp = hp or Health.Health
|
||||
|
||||
local max = Health.MaxHealth
|
||||
local dif = hp / max
|
||||
|
||||
local hud = self.HUD
|
||||
hud.Health.ContainerText.HealthText.Text = hp
|
||||
hud.Health.BlackBar.GreenBar.Size = UDim2.new(dif,0,1,0)
|
||||
end
|
||||
|
||||
function Bot:heal(amount)
|
||||
self.hp = math.min(self.maxHp, self.hp + amount)
|
||||
|
||||
self:DisplayHealth()
|
||||
end
|
||||
|
||||
function Bot:isAlive()
|
||||
return self.hp > 0
|
||||
end
|
||||
|
||||
function Bot:die()
|
||||
self.model:Destroy()
|
||||
|
||||
end
|
||||
|
||||
return Bot
|
||||
@ -0,0 +1,44 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local sModules = ServerScriptService.Modules
|
||||
|
||||
local sClasses = sModules.Classes
|
||||
|
||||
local ComponentM = sClasses.Component
|
||||
local DestroyableComp = require(ComponentM.DestroyableComponent)
|
||||
local HitboxComp = require(ComponentM.HitboxComponent)
|
||||
local ProjectileComp = require(ComponentM.ProjectileComponent)
|
||||
|
||||
local GameObject = require(script.Parent)
|
||||
local DestroyablePlatform = setmetatable({},GameObject)
|
||||
|
||||
|
||||
|
||||
export type DestroyablePlatform = typeof(setmetatable({}, DestroyablePlatform)) & GameObject.GameObject
|
||||
|
||||
|
||||
DestroyablePlatform.__index = DestroyablePlatform
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
function DestroyablePlatform.new(userId, model)
|
||||
local self = setmetatable(GameObject.new(nil,model,userId), DestroyablePlatform)
|
||||
|
||||
self.ClassName = "DestroyablePlatform"
|
||||
|
||||
self.Components.Hitbox = HitboxComp.new(self.key,model)
|
||||
self.Components.Destroyable = DestroyableComp.new(self.key,model,model)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
function DestroyablePlatform:OnHit(hitPart, hitPosition) end
|
||||
function DestroyablePlatform:Tick() end
|
||||
function DestroyablePlatform:OnTurnEnd() print("turn end!") end
|
||||
function DestroyablePlatform:OnRoundEnd() print("round end!") self:Destroy() end
|
||||
|
||||
|
||||
return DestroyablePlatform
|
||||
173
src/server/Modules/Classes/GameObject/init.luau
Normal file
173
src/server/Modules/Classes/GameObject/init.luau
Normal file
@ -0,0 +1,173 @@
|
||||
local GameObject = {}
|
||||
|
||||
local ObjectManager = require("../ObjectManager")
|
||||
local HttpServ = game:GetService("HttpService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
local rBotUtils = require(rUtils.sharedBotUtils)
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local rWeaponData = require(rData.WeaponData)
|
||||
|
||||
|
||||
local Component = script.Parent.Component
|
||||
local HitboxComponent = require(Component.HitboxComponent)
|
||||
local ProjectileComponent = require(Component.ProjectileComponent)
|
||||
local HealthComponent = require(Component.HealthComponent)
|
||||
local DestroyableComponent = require(Component.DestroyableComponent)
|
||||
|
||||
local Janitor = require(ReplicatedStorage:WaitForChild("Janitor"))
|
||||
|
||||
GameObject.__index = GameObject
|
||||
|
||||
type self = {
|
||||
key : string,
|
||||
owner : number?,
|
||||
model : Model?,
|
||||
type : string,
|
||||
active : boolean,
|
||||
|
||||
Janitor : typeof(Janitor.new()),
|
||||
|
||||
Components : {
|
||||
Projectile : ProjectileComponent.ProjectileComponent,
|
||||
Hitbox : HitboxComponent.HitboxComponent,
|
||||
Health : HealthComponent.HealthComponent,
|
||||
Destroyable : DestroyableComponent.DestroyableComponent
|
||||
},
|
||||
|
||||
timepassed : number,
|
||||
tickspassed : number,
|
||||
|
||||
}
|
||||
|
||||
--GameObject(TurnEnd, TurnStart, OnDestroy, Tick) Anything that needs this is a gameobject
|
||||
|
||||
|
||||
export type GameObject = typeof( setmetatable({} :: self, GameObject) )
|
||||
|
||||
function GameObject.new(key : string?,model : Model,userId : number?,active)
|
||||
local self = setmetatable({} :: self,GameObject)
|
||||
|
||||
key = key or HttpServ:GenerateGUID(false)
|
||||
print(key)
|
||||
self.owner = userId
|
||||
self.model = model
|
||||
self.Janitor = Janitor.new()
|
||||
self.key = key
|
||||
self.timepassed = 0
|
||||
self.tickspassed = 0
|
||||
self.active = active
|
||||
|
||||
self.Components = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- DONT OVERWRITE THESE
|
||||
function GameObject:_Init()
|
||||
if self.model then
|
||||
self.model:SetAttribute("key",self.key)
|
||||
end
|
||||
|
||||
self:_ResetMass()
|
||||
self:OnCreate()
|
||||
self:_Store()
|
||||
self.active = true
|
||||
end
|
||||
|
||||
-- DONT OVERWRITE THESE
|
||||
function GameObject:_Store()
|
||||
ObjectManager.Store(self.key,self)
|
||||
end
|
||||
|
||||
-- DONT OVERWRITE THESE
|
||||
function GameObject:_IsOwner(id)
|
||||
if not self.key then
|
||||
return
|
||||
end
|
||||
return self.key == id
|
||||
end
|
||||
|
||||
function GameObject:_ResetMass()
|
||||
local self = self :: GameObject
|
||||
if self.model then
|
||||
local primaryPart = self.model.PrimaryPart
|
||||
for i,v in pairs(self.model:GetDescendants()) do
|
||||
if v:IsA("Part") or v:IsA("MeshPart") or v:IsA("UnionOperation") then
|
||||
if primaryPart == v or v.Parent.Name == "Hitbox" then
|
||||
continue
|
||||
end
|
||||
v.Massless = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- DONT OVERWRITE THESE
|
||||
function GameObject:_BeforeTick(dt)
|
||||
local self = self :: GameObject
|
||||
|
||||
self.timepassed += dt
|
||||
self.tickspassed += 1
|
||||
end
|
||||
-- DONT OVERWRITE THESE
|
||||
function GameObject:_Destroy()
|
||||
local self = self :: GameObject
|
||||
self.active = false
|
||||
pcall(function()
|
||||
self:BeforeDestroy()
|
||||
end)
|
||||
|
||||
pcall(function()
|
||||
if self.model then
|
||||
self.model:Destroy()
|
||||
end
|
||||
self.Janitor:Destroy()
|
||||
ObjectManager._Free(self.key)
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
-----------------------------------------
|
||||
|
||||
-- can be overwritten to get whichever data u want O:
|
||||
|
||||
--if true resolve can resolve early(if all gameobjects agree :o)
|
||||
function GameObject:ShouldEnd()
|
||||
if self.model then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function GameObject:GetWeaponData(name) : rWeaponData.WeaponData
|
||||
return rWeaponData[name]
|
||||
end
|
||||
|
||||
function GameObject:OnHit()
|
||||
local self = self :: GameObject
|
||||
|
||||
end
|
||||
|
||||
function GameObject:OnTurnEnd()
|
||||
local self = self :: GameObject
|
||||
|
||||
end
|
||||
|
||||
function GameObject:Tick(dt)
|
||||
|
||||
end
|
||||
|
||||
function GameObject:OnCreate()
|
||||
local self = self :: GameObject
|
||||
|
||||
end
|
||||
--Before Destroying
|
||||
function GameObject:BeforeDestroy()
|
||||
local self = self :: GameObject
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
return GameObject
|
||||
378
src/server/Modules/Classes/Round.luau
Normal file
378
src/server/Modules/Classes/Round.luau
Normal file
@ -0,0 +1,378 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
|
||||
local Data = ReplicatedStorage.Data
|
||||
|
||||
local BotData = require(Data.BotData)
|
||||
local DataTypes = require(Data.DataTypes)
|
||||
local GameState = require(Data.GameState)
|
||||
|
||||
local sData = ServerScriptService.Data
|
||||
local DataManager = require(sData.DataManager)
|
||||
local ValueNames = require(sData.ValueNames)
|
||||
|
||||
local sModules = ServerScriptService.Modules
|
||||
local Bot = require(sModules.Classes.GameObject.Bot)
|
||||
local VotingHandlerServer = require(sModules.VotingHandlerServer)
|
||||
local TurnManager = require(sModules.TurnManager)
|
||||
local ObjectManager = require(sModules.ObjectManager)
|
||||
|
||||
local MapManager = require(sModules.Map.MapManager)
|
||||
|
||||
|
||||
local FerrUtils = require(ReplicatedStorage.Shared.SharedUtils.FerrUtils)
|
||||
local rBotUtils = require(ReplicatedStorage.Shared.SharedUtils.sharedBotUtils)
|
||||
|
||||
local BotUtils = require(sModules.Utils.BotUtils)
|
||||
|
||||
local Remote = ReplicatedStorage.Remote
|
||||
|
||||
local rev_statusRemoteEvent = Remote.StatusRemoteEvent
|
||||
local rev_timerRemoteEvent = Remote.TimerRemoteEvent
|
||||
local rev_UpdateGameState = Remote.UpdateGameState
|
||||
|
||||
local Round = {}
|
||||
Round.__index = Round
|
||||
|
||||
function getPlaying() : {Player}
|
||||
return game:GetService("CollectionService"):GetTagged("PLAYING")
|
||||
end
|
||||
|
||||
function Round.new()
|
||||
local self = setmetatable({}, Round)
|
||||
if game:GetService("RunService"):IsStudio() then
|
||||
self.IntermissionTime = 1
|
||||
else
|
||||
self.IntermissionTime = 1
|
||||
end
|
||||
|
||||
self.MapVotingTime = 100
|
||||
self.GraceTime = 1
|
||||
self.DecisionTime = 15
|
||||
self.Status = ""
|
||||
self.RoundResults = {}
|
||||
|
||||
self:ChangeGameState(GameState.LOBBY)
|
||||
self.Timer = 0
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Round:ChangeStatus(newText : DataStoreListingPages)
|
||||
self.Status = newText
|
||||
self:UpdateState()
|
||||
end
|
||||
|
||||
function Round:FireTo(remote,players,...)
|
||||
players = players or game.Players:GetPlayers()
|
||||
for i,v in pairs(players) do
|
||||
remote:FireClient(v,...)
|
||||
end
|
||||
end
|
||||
|
||||
function Round:ChangeGameState(newState : number)
|
||||
self.GameState = newState
|
||||
shared.Phase = newState
|
||||
self:FireTo(rev_UpdateGameState,getPlaying(),newState)
|
||||
end
|
||||
|
||||
|
||||
|
||||
function Round:UpdateState()
|
||||
rev_statusRemoteEvent:FireAllClients(self.Status .. ": " .. self.Timer)
|
||||
end
|
||||
|
||||
function Round:EnoughPlayers()
|
||||
return #Players:GetPlayers() >= 2 or game:GetService("RunService"):IsStudio() and script:GetAttribute("StudioEnoughPlayersOverride")
|
||||
end
|
||||
|
||||
function Round:AllPlayersReady()
|
||||
return #getPlaying() == TurnManager.GetReadyPlayers()
|
||||
end
|
||||
|
||||
function Round:WaitFor(sec : number,condition,...)
|
||||
for index = sec,0,-1 do
|
||||
self.Timer = index
|
||||
self:UpdateState()
|
||||
task.wait(.5)
|
||||
if self.Break then
|
||||
self.Break = false
|
||||
self.Timer = 0
|
||||
self:UpdateState()
|
||||
break
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Round:Halt()
|
||||
repeat task.wait() until self.Break == false
|
||||
end
|
||||
|
||||
function Round:Intermission()
|
||||
|
||||
self:ChangeGameState(GameState.LOBBY)
|
||||
self:ChangeStatus("Intermission")
|
||||
self:WaitFor(self.IntermissionTime)
|
||||
end
|
||||
function Round:ChooseMap()
|
||||
workspace.Map:ClearAllChildren()
|
||||
|
||||
|
||||
while task.wait() do
|
||||
|
||||
self:ChangeStatus("Choose Map")
|
||||
VotingHandlerServer.StartVoting(1,"MapData",workspace.Lobby["Map Voting Stands"]:GetChildren())
|
||||
self:WaitFor(self.MapVotingTime)
|
||||
local winnerIndex,winnerData : DataTypes.VoteOptionData = VotingHandlerServer.EndVoting()
|
||||
print(winnerIndex)
|
||||
print(winnerData)
|
||||
|
||||
MapManager.SpawnMap(winnerData.Name)
|
||||
break
|
||||
end
|
||||
|
||||
self:ChangeStatus("Loading map...")
|
||||
self:WaitFor(MapManager.MAP_LOAD_TIME)
|
||||
end
|
||||
|
||||
function Round:BringPlayers()
|
||||
local pick = "PlasmaBot"
|
||||
|
||||
|
||||
|
||||
for _,plr in pairs(game.Players:GetPlayers()) do
|
||||
local char = plr.Character
|
||||
if not char then
|
||||
continue
|
||||
end
|
||||
char:PivotTo(workspace.BoxOfDoom.PrimaryPart.CFrame)
|
||||
|
||||
local newBot = Bot.new(plr,pick,char)
|
||||
newBot:_Init()
|
||||
plr:AddTag("PLAYING")
|
||||
end
|
||||
|
||||
BotUtils.SpawnBots(getPlaying())
|
||||
|
||||
end
|
||||
|
||||
Round.END_TYPES = {
|
||||
WIN = 1,
|
||||
DRAW = 2,
|
||||
}
|
||||
|
||||
function Round:ShouldRoundEnd()
|
||||
local max = #getPlaying()
|
||||
|
||||
if game:GetService("RunService"):IsStudio() then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
|
||||
local alive = {}
|
||||
|
||||
for i,id in pairs(#getPlaying()) do
|
||||
local b = ObjectManager.Get(id)
|
||||
if b.Components.Health:IsAlive() then
|
||||
table.insert(alive,id)
|
||||
else
|
||||
ObjectManager.Destroy(id)
|
||||
end
|
||||
end
|
||||
|
||||
if #alive == 0 then
|
||||
return Round.END_TYPES.DRAW
|
||||
end
|
||||
|
||||
if #alive == 1 then
|
||||
return Round.END_TYPES.WIN,alive[1]
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
function Round:GracePeriod()
|
||||
self:ChangeStatus("Grace period")
|
||||
self:ChangeGameState(GameState.GRACE)
|
||||
self:WaitFor(self.GraceTime)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function Round:TryStopEarlyAim()
|
||||
task.spawn(function()
|
||||
repeat task.wait() until self.GameState == GameState.RESOLVING or self:AllPlayersReady()
|
||||
if self.GameState ~= GameState.RESOLVING then
|
||||
self.Break = true
|
||||
end
|
||||
self:Halt()
|
||||
end)
|
||||
end
|
||||
|
||||
function Round:StartAimPhase()
|
||||
--for i,v in pairs(workspace.Playing:GetChildren()) do
|
||||
-- v.PrimaryPart.Anchored = true
|
||||
--end
|
||||
self:ChangeStatus("Decision making")
|
||||
self:ChangeGameState(GameState.AIMING)
|
||||
|
||||
for i,plr in pairs(getPlaying()) do
|
||||
local primary = rBotUtils.GetBotRoot(plr.UserId)
|
||||
local rot = primary.Orientation
|
||||
primary.Orientation = Vector3.new(0,0,rot.Z)
|
||||
end
|
||||
|
||||
TurnManager.collectInputs()
|
||||
|
||||
self:TryStopEarlyAim()
|
||||
|
||||
|
||||
self:WaitFor(self.DecisionTime)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function Round:ShouldStopEarlyResolve()
|
||||
for _,gameObject in pairs(ObjectManager.GameObject) do
|
||||
if not gameObject.model or not gameObject.model.Parent then
|
||||
continue
|
||||
end
|
||||
local vel = gameObject.model.PrimaryPart.AssemblyLinearVelocity
|
||||
|
||||
local horizontalVel = Vector3.new(vel.X, 0, vel.Z).Magnitude
|
||||
local verticalVel = math.abs(vel.Y)
|
||||
if horizontalVel > 0.1 or verticalVel > 0.5 or not rBotUtils.IsObjectOnFloor(gameObject.model) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
return true
|
||||
end
|
||||
function Round:TryStopEarlyResolve()
|
||||
task.spawn(function()
|
||||
local stableTime = 0
|
||||
print(self.GameState)
|
||||
while self.GameState == GameState.RESOLVING do
|
||||
task.wait(0.1)
|
||||
|
||||
if self:ShouldStopEarlyResolve() then
|
||||
stableTime += 0.1
|
||||
if stableTime >= 0.5 then -- must be stable for 0.5s
|
||||
self.Break = true
|
||||
break
|
||||
end
|
||||
else
|
||||
stableTime = 0
|
||||
end
|
||||
end
|
||||
|
||||
self:Halt()
|
||||
end)
|
||||
end
|
||||
|
||||
function Round:StartResolvePhase()
|
||||
self:ChangeGameState(GameState.RESOLVING)
|
||||
|
||||
self:TryStopEarlyResolve()
|
||||
|
||||
for _, v : Model in pairs(workspace.Bots:GetChildren()) do
|
||||
|
||||
local root = v.PrimaryPart
|
||||
root:SetNetworkOwner(nil)
|
||||
v:PivotTo(v:GetPivot())
|
||||
|
||||
root.Velocity = Vector3.zero
|
||||
root.Anchored = false
|
||||
end
|
||||
TurnManager.resolve()
|
||||
|
||||
self:ChangeStatus("Resolve")
|
||||
self:WaitFor(10)
|
||||
end
|
||||
|
||||
function Round:Start()
|
||||
|
||||
local endType,winner = nil,nil
|
||||
|
||||
while task.wait() do
|
||||
|
||||
endType,winner = self:ShouldRoundEnd()
|
||||
|
||||
if endType then
|
||||
return endType,winner
|
||||
end
|
||||
|
||||
|
||||
self:StartAimPhase()
|
||||
self:StartResolvePhase()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function Round:Results(endtype,winner : number)
|
||||
|
||||
for i,v in pairs(getPlaying()) do
|
||||
v:RemoveTag("PLAYING")
|
||||
end
|
||||
|
||||
|
||||
ObjectManager.DestroyAll()
|
||||
MapManager.DestroyMap()
|
||||
|
||||
local text
|
||||
|
||||
if endtype == Round.END_TYPES.WIN then
|
||||
local winnerName
|
||||
pcall(function()
|
||||
winnerName = game.Players:GetNameFromUserIdAsync(winner)
|
||||
end)
|
||||
winnerName = winnerName or "Studio guy"
|
||||
|
||||
text = winnerName .. " has won!"
|
||||
pcall(function()
|
||||
DataManager.AddValue(game.Players:GetPlayerByUserId(winner),ValueNames.Wins,1)
|
||||
end)
|
||||
else
|
||||
text = "It's a draw!"
|
||||
end
|
||||
for i,v in pairs(game.Players:GetPlayers()) do
|
||||
v:LoadCharacterAsync()
|
||||
end
|
||||
self:ChangeGameState(GameState.RESULTS)
|
||||
self:ChangeStatus(text)
|
||||
self:WaitFor(5)
|
||||
|
||||
end
|
||||
|
||||
function Round:Initiate()
|
||||
|
||||
while true do
|
||||
local result = self:EnoughPlayers()
|
||||
if not result then
|
||||
self:ChangeStatus("Not enough players")
|
||||
return
|
||||
end
|
||||
|
||||
self:Intermission()
|
||||
self:ChooseMap()
|
||||
self:BringPlayers()
|
||||
|
||||
self:GracePeriod()
|
||||
|
||||
local endType,winner = self:Start()
|
||||
|
||||
self:Results(endType,winner)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
return Round
|
||||
5
src/server/Modules/Classes/Round.meta.json
Normal file
5
src/server/Modules/Classes/Round.meta.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"attributes": {
|
||||
"StudioEnoughPlayersOverride": false
|
||||
}
|
||||
}
|
||||
110
src/server/Modules/Classes/Weapon.luau
Normal file
110
src/server/Modules/Classes/Weapon.luau
Normal file
@ -0,0 +1,110 @@
|
||||
-- Weapon
|
||||
-- All weapon fire logic lives here as plain functions.
|
||||
-- Add a new weapon by adding a new entry to the dispatch table below.
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
local rModels = rAssets.Models
|
||||
local rWeaponModels = rModels.WeaponModels
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local weaponData = require(rData.WeaponData)
|
||||
local DataTypes = require(rData.DataTypes)
|
||||
|
||||
local rShared = ReplicatedStorage.Shared
|
||||
local rBotUtils = require(rShared.SharedUtils.sharedBotUtils)
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local sModules = ServerScriptService.Modules
|
||||
local PhysicsProjectileLauncher = require(sModules.Utils.PhysicsProjectileLauncher)
|
||||
local SimulatedProjectileLauncher = require(sModules.Utils.SimulatedProjectileLauncher)
|
||||
|
||||
|
||||
local FORWARD_AXIS = Vector3.new(1, 0, 0)
|
||||
|
||||
local function getLaunchDirection(angle: number)
|
||||
local horizontal = math.cos(angle)
|
||||
local vertical = math.sin(angle)
|
||||
|
||||
return (FORWARD_AXIS * horizontal + Vector3.new(0, 1, 0) * vertical).Unit
|
||||
end
|
||||
|
||||
--finish hp system, tryresolveearlystop func, etc
|
||||
|
||||
local Weapon = {}
|
||||
|
||||
-- Dispatch table: weaponName -> fire function
|
||||
-- Each function receives (bot, angle, power) and is responsible for
|
||||
-- spawning the projectile and applying damage on hit.
|
||||
|
||||
local GameObject = sModules.Classes.GameObject
|
||||
|
||||
local Missile = require(GameObject.BaseProjectile.Missile)
|
||||
local BouncyGrenade = require(GameObject.BaseProjectile.BouncyGrenade)
|
||||
|
||||
local weapons = {
|
||||
|
||||
Missile = function(UserId,FireData : DataTypes.FireData)
|
||||
local bot = rBotUtils.FindBotModel(UserId)
|
||||
|
||||
local model = rWeaponModels.Missile:Clone()
|
||||
model.Parent = workspace
|
||||
model:PivotTo(CFrame.new(rBotUtils.GetShootPos(UserId)))
|
||||
|
||||
local m = Missile.new(UserId,model,FireData)
|
||||
m:_Init()
|
||||
end,
|
||||
|
||||
Move = function(UserId,FireData)
|
||||
local bot = rBotUtils.FindBotModel(UserId)
|
||||
local origin = rBotUtils.GetBotPosition(UserId)
|
||||
|
||||
|
||||
PhysicsProjectileLauncher.launch(bot.PrimaryPart,FireData)
|
||||
|
||||
|
||||
end,
|
||||
|
||||
ClusterRocket = function(UserId,FireData)
|
||||
-- TODO: spawn large rocket
|
||||
-- TODO: at apex split into 5 smaller missiles
|
||||
end,
|
||||
|
||||
BouncyGrenade = function(UserId,FireData)
|
||||
local bot = rBotUtils.FindBotModel(UserId)
|
||||
|
||||
local model = rWeaponModels.BouncyGrenade:Clone()
|
||||
model.Parent = workspace
|
||||
model:PivotTo(CFrame.new(rBotUtils.GetShootPos(UserId)))
|
||||
print(model)
|
||||
|
||||
local m = BouncyGrenade.new(UserId,model,FireData)
|
||||
m:_Init()
|
||||
end,
|
||||
|
||||
ReflectorShield = function(UserId,FireData)
|
||||
-- TODO: spawn shield Part in aimed direction
|
||||
-- TODO: on contact with projectile/bot, reflect it back
|
||||
end,
|
||||
|
||||
}
|
||||
|
||||
function Weapon.fire(weaponName,UserId,FireData : DataTypes.FireData)
|
||||
local fn = weapons[weaponName]
|
||||
if not fn then
|
||||
warn("Weapon not found: " .. tostring(weaponName))
|
||||
return
|
||||
end
|
||||
local name = rBotUtils.GetWeaponName(UserId,FireData.weapon)
|
||||
local wData : weaponData.WeaponData = weaponData[name]
|
||||
|
||||
if wData.preview.type == "projectile" then
|
||||
local dir = getLaunchDirection(FireData.angle)
|
||||
rBotUtils.RotateTurret(UserId,dir)
|
||||
end
|
||||
|
||||
fn(UserId,FireData)
|
||||
end
|
||||
|
||||
return Weapon
|
||||
28
src/server/Modules/Map/MapManager/SyncHandler.luau
Normal file
28
src/server/Modules/Map/MapManager/SyncHandler.luau
Normal file
@ -0,0 +1,28 @@
|
||||
local SyncHandler = {}
|
||||
|
||||
local CurrentSyncPart : Part = nil
|
||||
local SyncPartOrientation = Vector3.new(0, -90, 0)
|
||||
|
||||
function createSyncPart(m : Model)
|
||||
local SyncPart = Instance.new("Part")
|
||||
SyncPart.Name = "SyncPart"
|
||||
SyncPart.Parent = m
|
||||
return SyncPart
|
||||
end
|
||||
|
||||
function SyncHandler.AddSyncPart(m : Model)
|
||||
local SyncPart : Part = m:WaitForChild("SyncPart",5)
|
||||
if not SyncPart then
|
||||
SyncPart = createSyncPart(m)
|
||||
end
|
||||
SyncPart:ClearAllChildren()
|
||||
SyncPart.Orientation = SyncPartOrientation
|
||||
SyncPart.Position = m:GetPivot().Position
|
||||
local newAttachment = Instance.new("Attachment")
|
||||
newAttachment.Parent = SyncPart
|
||||
newAttachment.Name = "PlaneAttachment"
|
||||
end
|
||||
|
||||
|
||||
|
||||
return SyncHandler
|
||||
55
src/server/Modules/Map/MapManager/init.luau
Normal file
55
src/server/Modules/Map/MapManager/init.luau
Normal file
@ -0,0 +1,55 @@
|
||||
local MapManager = {}
|
||||
|
||||
local SynchHandler = require(script.SyncHandler)
|
||||
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
local sAssets = ServerStorage.Assets
|
||||
local sMaps = sAssets.Maps
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local sModules = ServerScriptService.Modules
|
||||
|
||||
local DestroyablePlatform = require(sModules.Classes.GameObject.DestroyablePlatform)
|
||||
|
||||
MapManager.MAP_LOAD_TIME = 5
|
||||
|
||||
MapManager.Structures = {}
|
||||
|
||||
type Map = typeof(sMaps.LayoutMap1)
|
||||
|
||||
function MapManager.SpawnMap(name : string)
|
||||
name = name or "LayoutMap1"
|
||||
local map : Map = sMaps:FindFirstChild(name)
|
||||
if not map then
|
||||
map = sMaps:FindFirstChildOfClass("Model")
|
||||
end
|
||||
|
||||
map.ModelStreamingMode = Enum.ModelStreamingMode.Atomic
|
||||
map = map:Clone()
|
||||
|
||||
map.Parent = workspace.Map
|
||||
|
||||
map:SetAttribute("MapName",map.Name)
|
||||
map.Name = "Map"
|
||||
|
||||
SynchHandler.AddSyncPart(map)
|
||||
SetupPlatforms(map)
|
||||
end
|
||||
|
||||
function SetupPlatforms(map : Map)
|
||||
for _,v : Model in pairs(map.DestroyablePlatforms:GetChildren()) do
|
||||
task.wait()
|
||||
local newPlat = DestroyablePlatform.new(nil,v)
|
||||
newPlat:_Init()
|
||||
end
|
||||
end
|
||||
|
||||
function MapManager.GetMap()
|
||||
return workspace.Map:FindFirstChildOfClass("Model")
|
||||
end
|
||||
|
||||
function MapManager.DestroyMap()
|
||||
workspace.Map:ClearAllChildren()
|
||||
end
|
||||
|
||||
return MapManager
|
||||
73
src/server/Modules/ObjectManager.luau
Normal file
73
src/server/Modules/ObjectManager.luau
Normal file
@ -0,0 +1,73 @@
|
||||
local ObjectManager = {}
|
||||
|
||||
ObjectManager.GameObject = {}
|
||||
|
||||
function ObjectManager.Get(key)
|
||||
return ObjectManager.GameObject[key]
|
||||
end
|
||||
function ObjectManager.DestroyAll()
|
||||
local keys = {}
|
||||
|
||||
for i,v in pairs(ObjectManager.GameObject) do
|
||||
table.insert(keys,i)
|
||||
end
|
||||
|
||||
for i,v in pairs(keys) do
|
||||
ObjectManager.Destroy(v)
|
||||
end
|
||||
end
|
||||
--Destroys and cleans up object, use this
|
||||
function ObjectManager.Destroy(key)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
local g = ObjectManager.GameObject[key]
|
||||
if not g then
|
||||
return
|
||||
end
|
||||
g:_Destroy()
|
||||
end
|
||||
--Free doesn't trigger GameObject:_Destroy, beware of memory leaks
|
||||
function ObjectManager._Free(key)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
ObjectManager.GameObject[key] = nil
|
||||
end
|
||||
function ObjectManager.Store(key,object)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
ObjectManager.GameObject[key] = object
|
||||
end
|
||||
|
||||
function ObjectManager.GetKeyByModel(model : Model)
|
||||
if not model then
|
||||
return
|
||||
end
|
||||
|
||||
return model:GetAttribute("key")
|
||||
end
|
||||
|
||||
function ObjectManager.GetObjectByModel(model : Model)
|
||||
local key =ObjectManager.GetKeyByModel(model)
|
||||
|
||||
local obj = ObjectManager.Get(key)
|
||||
return obj
|
||||
end
|
||||
|
||||
function ObjectManager.QueryComponents(component : string)
|
||||
local tbl = {}
|
||||
for i,v in pairs(ObjectManager.GameObject) do
|
||||
if v.Components[component] then
|
||||
tbl[i] = v
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
function ObjectManager.FindObject(child)
|
||||
|
||||
end
|
||||
|
||||
return ObjectManager
|
||||
18
src/server/Modules/RoundManager.luau
Normal file
18
src/server/Modules/RoundManager.luau
Normal file
@ -0,0 +1,18 @@
|
||||
local RoundManager = {}
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Round = require(ServerScriptService.Modules.Classes.Round)
|
||||
local newRound = Round.new()
|
||||
|
||||
RoundManager.Round = newRound
|
||||
|
||||
function RoundManager:Init()
|
||||
task.spawn(function()
|
||||
newRound:Initiate()
|
||||
end)
|
||||
end
|
||||
|
||||
return RoundManager
|
||||
|
||||
7
src/server/Modules/RoundManager.meta.json
Normal file
7
src/server/Modules/RoundManager.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
119
src/server/Modules/TurnManager.luau
Normal file
119
src/server/Modules/TurnManager.luau
Normal file
@ -0,0 +1,119 @@
|
||||
local TurnManager = {}
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
local Maps = ServerStorage.Assets.Maps
|
||||
|
||||
local Data = ReplicatedStorage.Data
|
||||
|
||||
local BotData = require(Data.BotData)
|
||||
local WeaponData = require(Data.WeaponData)
|
||||
local DataTypes = require(Data.DataTypes)
|
||||
local GameState = require(Data.GameState)
|
||||
|
||||
local serverModules = ServerScriptService.Modules
|
||||
local Weapon = require(serverModules.Classes.Weapon)
|
||||
local ObjectManager = require(serverModules.ObjectManager)
|
||||
|
||||
local FerrUtils = require(ReplicatedStorage.Shared.SharedUtils.FerrUtils)
|
||||
local sBotUtils = require(ReplicatedStorage.Shared.SharedUtils.sharedBotUtils)
|
||||
|
||||
local Remote = ReplicatedStorage.Remote
|
||||
|
||||
local rev_SubmitAction = Remote.SubmitAction
|
||||
local rfn_GetLastSecondInput = Remote.GetLastSecondInput
|
||||
|
||||
local TURN_DURATION = 10
|
||||
|
||||
local inputs = {}
|
||||
|
||||
|
||||
function TurnManager.collectInputs(players)
|
||||
inputs = {}
|
||||
end
|
||||
|
||||
|
||||
function TurnManager.Tick(dt)
|
||||
for i,v in pairs(ObjectManager.GameObject) do
|
||||
if not v.active then
|
||||
continue
|
||||
end
|
||||
v:_BeforeTick(dt)
|
||||
v:Tick(dt)
|
||||
end
|
||||
end
|
||||
|
||||
function getLastSecondInput()
|
||||
|
||||
|
||||
local startTime = os.time()
|
||||
local timeout = 3
|
||||
|
||||
local playersRecieved = 0
|
||||
|
||||
local playerTable = game.Players:GetPlayers()
|
||||
|
||||
local max = #playerTable - FerrUtils.LenDict(inputs)
|
||||
|
||||
for i,v in pairs(playerTable) do
|
||||
if inputs[v.UserId] then
|
||||
continue
|
||||
end
|
||||
task.spawn(function()
|
||||
local data = rfn_GetLastSecondInput:InvokeClient(v)
|
||||
playersRecieved += 1
|
||||
if data then
|
||||
submitActionEvent(v,data)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
repeat task.wait() until os.time() - startTime > 3 or playersRecieved == max
|
||||
end
|
||||
|
||||
function TurnManager.GetReadyPlayers()
|
||||
return FerrUtils.LenDict(inputs)
|
||||
end
|
||||
|
||||
function TurnManager.resolve()
|
||||
|
||||
getLastSecondInput()
|
||||
|
||||
for userId,data in pairs(inputs) do
|
||||
task.spawn(function()
|
||||
local bot = sBotUtils.FindBotModel(userId)
|
||||
local bData = sBotUtils.GetBotData(userId)
|
||||
local weaponName = bData.weapons[data.weapon]
|
||||
|
||||
if not weaponName then
|
||||
weaponName = data.weapon
|
||||
end
|
||||
|
||||
|
||||
Weapon.fire(weaponName,userId,data)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
function submitActionEvent(plr : Player,data)
|
||||
local userId = plr.UserId
|
||||
|
||||
if inputs[userId] then
|
||||
return
|
||||
end
|
||||
|
||||
inputs[userId] = data
|
||||
end
|
||||
|
||||
function TurnManager:Init()
|
||||
rev_SubmitAction.OnServerEvent:Connect(submitActionEvent)
|
||||
|
||||
game:GetService("RunService").Heartbeat:Connect(TurnManager.Tick)
|
||||
end
|
||||
|
||||
return TurnManager
|
||||
7
src/server/Modules/TurnManager.meta.json
Normal file
7
src/server/Modules/TurnManager.meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
56
src/server/Modules/Utils/BotUtils.luau
Normal file
56
src/server/Modules/Utils/BotUtils.luau
Normal file
@ -0,0 +1,56 @@
|
||||
local BotUtils = {}
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local sModules = ServerScriptService.Modules
|
||||
|
||||
local ObjectManager = require("../ObjectManager")
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local sUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
local TwoDimensionUtils = require(sUtils.TwoDimensionUtils)
|
||||
|
||||
local wBots = workspace.Bots
|
||||
|
||||
function BotUtils.GetBotByModel(model : Model)
|
||||
local userId = tonumber(model.Name)
|
||||
return ObjectManager.Get(userId)
|
||||
end
|
||||
|
||||
|
||||
|
||||
function BotUtils.SpawnBots(ids : {number})
|
||||
local Map = workspace.Map:WaitForChild("Map")
|
||||
|
||||
local spawns = Map.Spawns:GetChildren()
|
||||
|
||||
for i,plr in pairs(ids) do
|
||||
local id = plr.UserId
|
||||
local b = ObjectManager.Get(id)
|
||||
b:DisplayHealth()
|
||||
local model : Model = b.model
|
||||
|
||||
model.Parent = workspace.Bots
|
||||
|
||||
local pick = math.random(1,#spawns)
|
||||
local pos = spawns[pick].Position
|
||||
table.remove(spawns,pick)
|
||||
|
||||
model:MoveTo(pos)
|
||||
|
||||
for i,v in pairs(model:GetDescendants()) do
|
||||
if v:IsA("Part") or v:IsA("MeshPart") then
|
||||
task.spawn(function()
|
||||
v:SetNetworkOwner()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
TwoDimensionUtils.AddToPosition(model)
|
||||
TwoDimensionUtils.AddToOrientation(model)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return BotUtils
|
||||
|
||||
49
src/server/Modules/Utils/PhysicsProjectileLauncher.luau
Normal file
49
src/server/Modules/Utils/PhysicsProjectileLauncher.luau
Normal file
@ -0,0 +1,49 @@
|
||||
local PhysicsProjectileLauncher = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
local rModels = rAssets.Models
|
||||
local rWeaponModels = rModels.WeaponModels
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local DataTypes = require(rData.DataTypes)
|
||||
|
||||
local rModules = ReplicatedStorage.Shared
|
||||
local rBotUtils = require(rModules.SharedUtils.sharedBotUtils)
|
||||
local rPhysicsUtils = require(rModules.SharedUtils.PhysicsUtils)
|
||||
|
||||
-- snapToVelocity: boolean (true = face movement direction instantly)
|
||||
function PhysicsProjectileLauncher.launch(part : Part, data : DataTypes.FireData, snapToVelocity : boolean)
|
||||
|
||||
part:SetNetworkOwner(nil)
|
||||
|
||||
-- Calculate velocity
|
||||
local velocity = rPhysicsUtils.CalculateVelocity(data.power, data.angle)
|
||||
part.AssemblyLinearVelocity = velocity
|
||||
|
||||
-- If snapping is disabled, just return
|
||||
if not snapToVelocity then
|
||||
return part
|
||||
end
|
||||
|
||||
-- Snap + keep updating every frame
|
||||
local connection
|
||||
connection = RunService.Heartbeat:Connect(function()
|
||||
if not part or not part.Parent then
|
||||
if connection then connection:Disconnect() end
|
||||
return
|
||||
end
|
||||
|
||||
local vel = part.AssemblyLinearVelocity
|
||||
|
||||
if vel.Magnitude > 0.1 then
|
||||
part.CFrame = CFrame.lookAt(part.Position, part.Position + vel)
|
||||
end
|
||||
end)
|
||||
|
||||
return part
|
||||
end
|
||||
|
||||
return PhysicsProjectileLauncher
|
||||
103
src/server/Modules/Utils/SimulatedProjectileLauncher.luau
Normal file
103
src/server/Modules/Utils/SimulatedProjectileLauncher.luau
Normal file
@ -0,0 +1,103 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local workspace = game:GetService("Workspace")
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
local rModels = rAssets.Models
|
||||
local rWeaponModels = rModels.WeaponModels
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local DataTypes = require(rData.DataTypes)
|
||||
|
||||
local SimulatedProjectileLauncher = {}
|
||||
|
||||
local FORWARD_AXIS = Vector3.new(1, 0, 0)
|
||||
|
||||
local currentProjectiles = {}
|
||||
|
||||
|
||||
function SimulatedProjectileLauncher.launch(projectile,data : DataTypes.FireData)
|
||||
|
||||
local origin = data.origin
|
||||
local angle = data.angle
|
||||
local power = data.power
|
||||
|
||||
if not origin or not projectile then return end
|
||||
|
||||
local speed = power * 100
|
||||
|
||||
local horizSpeed = math.cos(angle) * speed
|
||||
local vertSpeed = math.sin(angle) * speed
|
||||
|
||||
local startTime = tick()
|
||||
|
||||
currentProjectiles[projectile] = {
|
||||
origin = origin,
|
||||
angle = angle,
|
||||
power = power,
|
||||
speed = speed,
|
||||
startTime = startTime,
|
||||
horizSpeed = horizSpeed,
|
||||
vertSpeed = vertSpeed
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
function SimulatedProjectileLauncher.StopProjectile(projectile)
|
||||
currentProjectiles[projectile] = nil
|
||||
end
|
||||
|
||||
function tickProjectile(projectile, data,g)
|
||||
|
||||
local startTime = data.startTime
|
||||
local origin = data.origin
|
||||
local horizSpeed = data.horizSpeed
|
||||
local vertSpeed = data.vertSpeed
|
||||
|
||||
local t = tick() - startTime
|
||||
|
||||
local pos = origin
|
||||
+ FORWARD_AXIS * (horizSpeed * t)
|
||||
+ Vector3.new(0,1,0) * (vertSpeed * t - 0.5 * g * t * t)
|
||||
|
||||
-- move projectile
|
||||
if projectile:IsA("Model") then
|
||||
projectile:PivotTo(CFrame.new(pos))
|
||||
else
|
||||
projectile.Position = pos
|
||||
end
|
||||
|
||||
-- OPTIONAL: rotate to face velocity direction
|
||||
local vel = Vector3.new(
|
||||
horizSpeed,
|
||||
vertSpeed - g * t,
|
||||
0
|
||||
)
|
||||
|
||||
if vel.Magnitude > 0 then
|
||||
local cf = CFrame.lookAt(pos, pos + vel.Unit)
|
||||
|
||||
if projectile:IsA("Model") then
|
||||
projectile:PivotTo(cf)
|
||||
else
|
||||
projectile.CFrame = cf
|
||||
end
|
||||
end
|
||||
|
||||
-- OPTIONAL: stop when below ground
|
||||
--if pos.Y < 0 then
|
||||
-- connection:Disconnect()
|
||||
--end
|
||||
end
|
||||
|
||||
function SimulatedProjectileLauncher:Init()
|
||||
RunService.Heartbeat:Connect(function()
|
||||
local g = workspace.Gravity
|
||||
|
||||
for projectile,data in pairs(currentProjectiles) do
|
||||
tickProjectile(projectile,data,g)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return SimulatedProjectileLauncher
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
}
|
||||
}
|
||||
156
src/server/Modules/Utils/WeaponUtils.luau
Normal file
156
src/server/Modules/Utils/WeaponUtils.luau
Normal file
@ -0,0 +1,156 @@
|
||||
local WeaponUtils = {}
|
||||
|
||||
local rBotUtils = require(game.ReplicatedStorage.Shared.SharedUtils.sharedBotUtils)
|
||||
local BotUtils = require("./BotUtils")
|
||||
|
||||
--////////////////////////////////////////////////////////
|
||||
-- CONFIG
|
||||
--////////////////////////////////////////////////////////
|
||||
|
||||
local VOXEL_SIZE = 0.5
|
||||
local MAX_VOXELS_PER_EXPLOSION = 1500
|
||||
|
||||
local MATERIAL_RESISTANCE = {
|
||||
[Enum.Material.Concrete] = 0.75,
|
||||
[Enum.Material.Slate] = 0.65,
|
||||
[Enum.Material.Wood] = 0.35,
|
||||
[Enum.Material.Plastic] = 0.2,
|
||||
Default = 0.5
|
||||
}
|
||||
|
||||
local overlapParams = OverlapParams.new()
|
||||
overlapParams.FilterType = Enum.RaycastFilterType.Include
|
||||
|
||||
local function getVoxelFolder()
|
||||
return workspace:FindFirstChild("VoxelDebris")
|
||||
end
|
||||
|
||||
local function snap(v)
|
||||
return math.floor(v / VOXEL_SIZE + 0.5)
|
||||
end
|
||||
|
||||
local DEBUG = true
|
||||
local DEBUG_LIFETIME = 0.35
|
||||
|
||||
local function spawnDebugSphere(position: Vector3, radius: number, color: Color3)
|
||||
local sphere = Instance.new("Part")
|
||||
sphere.Shape = Enum.PartType.Ball
|
||||
sphere.Anchored = true
|
||||
sphere.CanCollide = false
|
||||
sphere.Material = Enum.Material.Neon
|
||||
sphere.Transparency = 0.75
|
||||
sphere.Color = color
|
||||
sphere.Size = Vector3.new(radius * 2, radius * 2, radius * 2)
|
||||
sphere.Position = position
|
||||
sphere.Parent = workspace
|
||||
|
||||
game:GetService("Debris"):AddItem(sphere, DEBUG_LIFETIME)
|
||||
end
|
||||
|
||||
--////////////////////////////////////////////////////////
|
||||
-- EXPLOSION (2 PHASE COLUMN SYSTEM)
|
||||
--////////////////////////////////////////////////////////
|
||||
|
||||
function WeaponUtils.ExplosionQuery(origin: Vector3, innerRadius: number, outerRadius: number, maxDamage: number)
|
||||
--------------------------------------------------------
|
||||
-- BOT DAMAGE
|
||||
--------------------------------------------------------
|
||||
for _, model in pairs(workspace.Bots:GetChildren()) do
|
||||
local bot = BotUtils.GetBotByModel(model)
|
||||
if not bot then continue end
|
||||
|
||||
local position = rBotUtils.GetBotPosition(bot.key)
|
||||
local damage = WeaponUtils.CalculateExplosionDamage(origin, position, outerRadius, maxDamage)
|
||||
|
||||
if damage > 0 then
|
||||
bot.Components.Health:TakeDamage(damage)
|
||||
end
|
||||
end
|
||||
if DEBUG then
|
||||
spawnDebugSphere(origin, outerRadius, Color3.fromRGB(255, 60, 60)) -- red outer
|
||||
spawnDebugSphere(origin, innerRadius, Color3.fromRGB(255, 140, 0)) -- orange inner
|
||||
end
|
||||
|
||||
local folder = getVoxelFolder()
|
||||
if not folder then return end
|
||||
|
||||
overlapParams.FilterDescendantsInstances = {folder}
|
||||
|
||||
local parts = workspace:GetPartBoundsInRadius(origin, innerRadius, overlapParams)
|
||||
if #parts == 0 then return end
|
||||
|
||||
local radiusSq = innerRadius * innerRadius
|
||||
|
||||
--------------------------------------------------------
|
||||
-- PHASE 1: FIND VALID VOXELS (SEEDS)
|
||||
--------------------------------------------------------
|
||||
local validColumns = {}
|
||||
local processed = 0
|
||||
|
||||
for i = 1, #parts do
|
||||
if processed > MAX_VOXELS_PER_EXPLOSION then break end
|
||||
|
||||
local part = parts[i]
|
||||
if not part:IsA("BasePart") then continue end
|
||||
|
||||
local offset = part.Position - origin
|
||||
local distSq = offset:Dot(offset)
|
||||
|
||||
if distSq > radiusSq then continue end
|
||||
|
||||
local dist = math.sqrt(distSq)
|
||||
local falloff = 1 - (dist / innerRadius)
|
||||
local power = falloff * falloff
|
||||
|
||||
local resistance = MATERIAL_RESISTANCE[part.Material] or MATERIAL_RESISTANCE.Default
|
||||
|
||||
if power > resistance then
|
||||
-- STORE COLUMN KEY (X + Y ONLY)
|
||||
local gx = snap(part.Position.X)
|
||||
local gy = snap(part.Position.Y)
|
||||
|
||||
local key = gx .. "," .. gy
|
||||
validColumns[key] = true
|
||||
end
|
||||
|
||||
processed += 1
|
||||
end
|
||||
|
||||
--------------------------------------------------------
|
||||
-- PHASE 2: DESTROY FULL Z COLUMNS (NO CHECKS)
|
||||
--------------------------------------------------------
|
||||
for i = 1, #parts do
|
||||
local part = parts[i]
|
||||
if not part:IsA("BasePart") then continue end
|
||||
|
||||
local gx = snap(part.Position.X)
|
||||
local gy = snap(part.Position.Y)
|
||||
|
||||
local key = gx .. "," .. gy
|
||||
|
||||
if validColumns[key] then
|
||||
part:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
--////////////////////////////////////////////////////////
|
||||
-- DAMAGE
|
||||
--////////////////////////////////////////////////////////
|
||||
|
||||
function WeaponUtils.CalculateExplosionDamage(origin: Vector3, position: Vector3, radius: number, maxDamage: number)
|
||||
local offset = position - origin
|
||||
local distSq = offset:Dot(offset)
|
||||
|
||||
if distSq >= radius * radius then return 0 end
|
||||
|
||||
local dist = math.sqrt(distSq)
|
||||
local normalized = dist / radius
|
||||
|
||||
local damage = maxDamage * (1 - normalized)^2
|
||||
return math.clamp(damage, 0, maxDamage)
|
||||
end
|
||||
|
||||
return WeaponUtils
|
||||
201
src/server/Modules/VotingHandlerServer.luau
Normal file
201
src/server/Modules/VotingHandlerServer.luau
Normal file
@ -0,0 +1,201 @@
|
||||
local VotingHandlerServer = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Data = ReplicatedStorage:WaitForChild("Data")
|
||||
local OptionsData = require(Data:WaitForChild("OptionsData"))
|
||||
|
||||
local Remote = ReplicatedStorage:WaitForChild("Remote")
|
||||
local rev_Vote = Remote:WaitForChild("Vote")
|
||||
local rev_StartVote = Remote:WaitForChild("StartVote")
|
||||
local rev_EndVote = Remote:WaitForChild("EndVote")
|
||||
|
||||
local alreadyVoted = {} -- userId -> option
|
||||
local voteCounts = {} -- option -> count
|
||||
|
||||
local currentOptions = {}
|
||||
local moduleData = nil
|
||||
|
||||
local activeConnections = {}
|
||||
local touchDebounce = {}
|
||||
|
||||
-- cleanup
|
||||
local function cleanupConnections()
|
||||
for _, conn in ipairs(activeConnections) do
|
||||
if conn then conn:Disconnect() end
|
||||
end
|
||||
table.clear(activeConnections)
|
||||
end
|
||||
|
||||
-- pick random KEYS from dictionary
|
||||
local function pickRandomOptions(source, count)
|
||||
local keys = {}
|
||||
|
||||
for key in pairs(source) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
|
||||
local selected = {}
|
||||
|
||||
for i = 1, math.min(count, #keys) do
|
||||
local index = math.random(1, #keys)
|
||||
table.insert(selected, keys[index])
|
||||
table.remove(keys, index)
|
||||
end
|
||||
|
||||
return selected
|
||||
end
|
||||
|
||||
-- results
|
||||
local function calculateResults()
|
||||
local results = {}
|
||||
|
||||
for _, option in ipairs(currentOptions) do
|
||||
results[option] = voteCounts[option] or 0
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
-- update stands
|
||||
local function updateVotingStands(votingStands, results)
|
||||
if not votingStands then return end
|
||||
|
||||
for index, stand in ipairs(votingStands) do
|
||||
local option = currentOptions[index]
|
||||
if not option then continue end
|
||||
|
||||
local data = moduleData[option]
|
||||
|
||||
local gui = stand:FindFirstChild("IndicatorPart")
|
||||
if gui then gui = gui:FindFirstChild("VoteGui") end
|
||||
if not gui then continue end
|
||||
|
||||
local votesLabel = gui:FindFirstChild("Votes")
|
||||
local mapLabel = gui:FindFirstChild("MapName")
|
||||
|
||||
if mapLabel then
|
||||
mapLabel.Text = (data and data.DisplayName) or option
|
||||
end
|
||||
|
||||
if votesLabel then
|
||||
votesLabel.Text = tostring(results[option] or 0)
|
||||
end
|
||||
|
||||
local imagePart = stand:FindFirstChild("MapImagePart")
|
||||
if imagePart and imagePart:FindFirstChild("ImageGui") then
|
||||
local img = imagePart.ImageGui:FindFirstChild("Picture")
|
||||
if img then
|
||||
img.Image = (data and data.DisplayImage) or ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function broadcast(votingStands)
|
||||
local results = calculateResults()
|
||||
rev_Vote:FireAllClients(results)
|
||||
updateVotingStands(votingStands, results)
|
||||
end
|
||||
|
||||
-- voting logic
|
||||
local function registerVote(player, option, votingStands)
|
||||
if not player or not option then return end
|
||||
|
||||
voteCounts[option] = voteCounts[option] or 0
|
||||
local userId = player.UserId
|
||||
|
||||
if alreadyVoted[userId] == option then
|
||||
return
|
||||
end
|
||||
|
||||
local previous = alreadyVoted[userId]
|
||||
if previous and voteCounts[previous] then
|
||||
voteCounts[previous] -= 1
|
||||
end
|
||||
|
||||
alreadyVoted[userId] = option
|
||||
voteCounts[option] += 1
|
||||
|
||||
broadcast(votingStands)
|
||||
end
|
||||
|
||||
-- START
|
||||
function VotingHandlerServer.StartVoting(optionCount, moduleKey, votingStands)
|
||||
cleanupConnections()
|
||||
|
||||
alreadyVoted = {}
|
||||
voteCounts = {}
|
||||
touchDebounce = {}
|
||||
|
||||
moduleData = OptionsData.Get(moduleKey)
|
||||
if not moduleData then
|
||||
warn("No moduleData for:", moduleKey)
|
||||
return
|
||||
end
|
||||
|
||||
currentOptions = pickRandomOptions(moduleData, optionCount)
|
||||
|
||||
-- remote voting
|
||||
table.insert(activeConnections,
|
||||
rev_Vote.OnServerEvent:Connect(function(player, option)
|
||||
registerVote(player, option, votingStands)
|
||||
end)
|
||||
)
|
||||
|
||||
-- stand voting
|
||||
if votingStands then
|
||||
for index, stand in ipairs(votingStands) do
|
||||
local option = currentOptions[index]
|
||||
if option then
|
||||
local conn = stand.ToVotePart.Touched:Connect(function(hit)
|
||||
local player = Players:GetPlayerFromCharacter(hit.Parent)
|
||||
if not player then return end
|
||||
|
||||
local userId = player.UserId
|
||||
if touchDebounce[userId] then return end
|
||||
|
||||
touchDebounce[userId] = true
|
||||
registerVote(player, option, votingStands)
|
||||
|
||||
task.delay(0.5, function()
|
||||
touchDebounce[userId] = nil
|
||||
end)
|
||||
end)
|
||||
|
||||
table.insert(activeConnections, conn)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
updateVotingStands(votingStands, calculateResults())
|
||||
rev_StartVote:FireAllClients(currentOptions, moduleKey)
|
||||
end
|
||||
|
||||
-- END
|
||||
function VotingHandlerServer.EndVoting()
|
||||
cleanupConnections()
|
||||
|
||||
rev_EndVote:FireAllClients()
|
||||
|
||||
local results = calculateResults()
|
||||
|
||||
local winner
|
||||
local highest = -1
|
||||
|
||||
for option, votes in pairs(results) do
|
||||
if votes > highest then
|
||||
highest = votes
|
||||
winner = option
|
||||
end
|
||||
end
|
||||
|
||||
if not winner and #currentOptions > 0 then
|
||||
winner = currentOptions[math.random(1, #currentOptions)]
|
||||
end
|
||||
|
||||
return winner, moduleData and moduleData[winner]
|
||||
end
|
||||
|
||||
return VotingHandlerServer
|
||||
15
src/server/Modules/WeldHandler.luau
Normal file
15
src/server/Modules/WeldHandler.luau
Normal file
@ -0,0 +1,15 @@
|
||||
local WeldHandler = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rAssets = ReplicatedStorage.Assets
|
||||
|
||||
local rModules = ReplicatedStorage.Shared
|
||||
local rUtils = rModules.SharedUtils
|
||||
|
||||
local WeldModule = require(rUtils.WeldModule)
|
||||
|
||||
function WeldHandler:Start()
|
||||
|
||||
end
|
||||
|
||||
return WeldHandler
|
||||
15
src/server/Start.server.luau
Normal file
15
src/server/Start.server.luau
Normal file
@ -0,0 +1,15 @@
|
||||
--!strict
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ModuleLoader = require(ReplicatedStorage.Shared.ModuleLoader)
|
||||
|
||||
-- you can optionally modify settings (also change it via the attributes on ModuleLoader)
|
||||
ModuleLoader.ChangeSettings({
|
||||
FOLDER_SEARCH_DEPTH = 1,
|
||||
YIELD_THRESHOLD = 10,
|
||||
VERBOSE_LOADING = false,
|
||||
WAIT_FOR_SERVER = true,
|
||||
})
|
||||
-- pass any containers for your custom services to the Start() function
|
||||
ModuleLoader.Start(script)
|
||||
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
3
src/server/leaderstats/Cash.model.json
Normal file
3
src/server/leaderstats/Cash.model.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"className": "IntValue"
|
||||
}
|
||||
17
src/shared/client/Chat.luau
Normal file
17
src/shared/client/Chat.luau
Normal file
@ -0,0 +1,17 @@
|
||||
local Message = game:GetService('ReplicatedStorage').Remote.Message
|
||||
local TextChatService = game:GetService("TextChatService")
|
||||
local Chat = {}
|
||||
|
||||
function Chat:Init()
|
||||
Message.OnClientEvent:Connect(function(message)
|
||||
|
||||
local TextChatService = game:GetService("TextChatService")
|
||||
local TextChannels = TextChatService:WaitForChild("TextChannels")
|
||||
|
||||
|
||||
TextChannels.RBXSystem:DisplaySystemMessage(message)
|
||||
|
||||
end)
|
||||
end
|
||||
|
||||
return Chat
|
||||
11
src/shared/client/Chat.meta.json
Normal file
11
src/shared/client/Chat.meta.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true,
|
||||
"ClientWaitForServer": true
|
||||
}
|
||||
}
|
||||
79
src/shared/client/ClientController.luau
Normal file
79
src/shared/client/ClientController.luau
Normal file
@ -0,0 +1,79 @@
|
||||
local ClientController = {}
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local localPlayer = Players.LocalPlayer
|
||||
|
||||
local Data = ReplicatedStorage.Data -- server modules (read-only on client)
|
||||
local GameStates = require(Data.GameState)
|
||||
|
||||
local ClientModules = script.Parent.Modules
|
||||
|
||||
local AimRenderer = require(ClientModules.AimRenderer)
|
||||
local InputHandler = require(ClientModules.InputHandler)
|
||||
local BotSelectUI = require(ClientModules.BotSelectUI)
|
||||
local CameraController = require(ClientModules.CameraController)
|
||||
local BotAbilityUI = require(ClientModules.BotAbilityUI)
|
||||
|
||||
local Remote = ReplicatedStorage.Remote
|
||||
local rev_SubmitAction = Remote.SubmitAction
|
||||
local rev_UpdateGameState = Remote.UpdateGameState
|
||||
local rev_HpUpdated = Remote.HpUpdated
|
||||
local rev_BotDied = Remote.BotDied
|
||||
|
||||
--finish later brochacho chip
|
||||
local turns = 1
|
||||
|
||||
local PLAYING = "PLAYING"
|
||||
|
||||
function ClientController:Init()
|
||||
|
||||
-- React to server phase changes
|
||||
rev_UpdateGameState.OnClientEvent:Connect(function(phase)
|
||||
shared.Phase = phase
|
||||
if phase == GameStates.LOBBY then
|
||||
|
||||
turns = 1
|
||||
CameraController.ResetCamera()
|
||||
elseif phase == GameStates.GRACE then
|
||||
|
||||
CameraController.WideMapView()
|
||||
BotAbilityUI.NewAbilities()
|
||||
elseif phase == GameStates.AIMING then
|
||||
|
||||
AimRenderer.UnHighlight()
|
||||
|
||||
InputHandler.enable(function(input)
|
||||
-- Player confirmed their action — send to server
|
||||
rev_SubmitAction:FireServer(input)
|
||||
InputHandler.disable()
|
||||
BotAbilityUI.DisableAll()
|
||||
AimRenderer.Highlight()
|
||||
|
||||
end,turns)
|
||||
|
||||
elseif phase == GameStates.RESOLVING then
|
||||
|
||||
turns += 1
|
||||
BotAbilityUI.HideGUI()
|
||||
InputHandler.disable()
|
||||
AimRenderer.hide()
|
||||
|
||||
elseif phase == "Results" then
|
||||
if localPlayer:HasTag(PLAYING) then
|
||||
BotAbilityUI.HideGUI()
|
||||
CameraController.ResetCamera()
|
||||
end
|
||||
|
||||
|
||||
-- TODO: show win screen
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
return ClientController
|
||||
10
src/shared/client/ClientController.meta.json
Normal file
10
src/shared/client/ClientController.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
57
src/shared/client/ClientUtils/GetCharacter.luau
Normal file
57
src/shared/client/ClientUtils/GetCharacter.luau
Normal file
@ -0,0 +1,57 @@
|
||||
local GetCharacter = {}
|
||||
|
||||
local localPlayer = game.Players.LocalPlayer
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
|
||||
local childAddedEvent
|
||||
local otherEvents = {}
|
||||
|
||||
function CharacterAdded(char : Model)
|
||||
|
||||
if childAddedEvent then
|
||||
childAddedEvent:Disconnect()
|
||||
childAddedEvent = nil
|
||||
end
|
||||
if otherEvents then
|
||||
for i,v in pairs(otherEvents) do
|
||||
v:Disconnect()
|
||||
end
|
||||
table.clear(otherEvents)
|
||||
end
|
||||
shared.Character = char
|
||||
shared.Head = char:WaitForChild("Head")
|
||||
shared.Humanoid = char:WaitForChild("Humanoid")
|
||||
shared.HumanoidRootPart = char:WaitForChild("HumanoidRootPart")
|
||||
|
||||
--print(shared.HumanoidRootPart)
|
||||
|
||||
for i,v in pairs(char:GetDescendants()) do
|
||||
--hidePart(v)
|
||||
end
|
||||
childAddedEvent = char.DescendantAdded:Connect(function(bodyPart)
|
||||
--hidePart(bodyPart)
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
function hidePart(bodyPart)
|
||||
if (bodyPart:IsA('BasePart')) then
|
||||
local event = bodyPart:GetPropertyChangedSignal('LocalTransparencyModifier'):Connect(function()
|
||||
bodyPart.LocalTransparencyModifier = 1
|
||||
end)
|
||||
|
||||
bodyPart.LocalTransparencyModifier = 1
|
||||
|
||||
table.insert(otherEvents,event)
|
||||
end
|
||||
end
|
||||
|
||||
function GetCharacter:Start()
|
||||
CharacterAdded(localPlayer.Character or localPlayer.CharacterAdded:Wait())
|
||||
localPlayer.CharacterAdded:Connect(CharacterAdded)
|
||||
|
||||
end
|
||||
|
||||
return GetCharacter
|
||||
10
src/shared/client/ClientUtils/GetCharacter.meta.json
Normal file
10
src/shared/client/ClientUtils/GetCharacter.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
25
src/shared/client/ClientUtils/GetProfilePic.luau
Normal file
25
src/shared/client/ClientUtils/GetProfilePic.luau
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
local ProfilePictureCache = {}
|
||||
|
||||
local thumbType = Enum.ThumbnailType.HeadShot
|
||||
local thumbSize = Enum.ThumbnailSize.Size420x420
|
||||
|
||||
local PLACEHOLDER_IMAGE = "rbxassetid://13528140280"
|
||||
|
||||
return function(userID : number) : string
|
||||
local cachedImage = ProfilePictureCache[userID]
|
||||
|
||||
if cachedImage then
|
||||
return cachedImage
|
||||
else
|
||||
|
||||
local content, isReady = game.Players:GetUserThumbnailAsync(userID, thumbType, thumbSize)
|
||||
|
||||
if isReady and content then
|
||||
ProfilePictureCache[userID] = content
|
||||
return content
|
||||
end
|
||||
end
|
||||
return PLACEHOLDER_IMAGE
|
||||
end
|
||||
|
||||
43
src/shared/client/Garage.luau
Normal file
43
src/shared/client/Garage.luau
Normal file
@ -0,0 +1,43 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local player = Players.LocalPlayer
|
||||
local Garage = workspace.Garage
|
||||
local TankFolder = ReplicatedStorage:WaitForChild("Assets"):WaitForChild("Models"):WaitForChild("Tanks")
|
||||
|
||||
if not game:GetService("RunService"):IsStudio() then
|
||||
TankFolder:WaitForChild("Tank (Testing)"):Destroy()
|
||||
end
|
||||
|
||||
local SelectedTank = player:WaitForChild("SelectedTank")
|
||||
local SelectedSkin = player:WaitForChild("SelectedSkin")
|
||||
local TankDisplay = Garage:WaitForChild("TankDisplay")
|
||||
|
||||
local garage = {}
|
||||
|
||||
local function updateTank()
|
||||
local tankName = SelectedTank.Value
|
||||
local skinName = SelectedSkin.Value
|
||||
local tankModel = TankFolder:FindFirstChild(tankName)
|
||||
if tankModel then
|
||||
local tankBase = tankModel:FindFirstChild("Base")
|
||||
if tankModel and tankBase then
|
||||
local skin = tankModel:FindFirstChild(skinName) or tankModel:FindFirstChild("Default")
|
||||
if skin then
|
||||
TankDisplay:ClearAllChildren()
|
||||
local Tank = tankBase:Clone()
|
||||
Tank.Parent = TankDisplay
|
||||
skin = skin:Clone()
|
||||
skin.Parent = Tank
|
||||
Tank:PivotTo(TankDisplay.CFrame)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function garage:Init()
|
||||
SelectedTank:GetPropertyChangedSignal("Value"):Connect(updateTank)
|
||||
SelectedSkin:GetPropertyChangedSignal("Value"):Connect(updateTank)
|
||||
updateTank()
|
||||
end
|
||||
|
||||
return garage
|
||||
10
src/shared/client/Garage.meta.json
Normal file
10
src/shared/client/Garage.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
61
src/shared/client/LoadingScreen.luau
Normal file
61
src/shared/client/LoadingScreen.luau
Normal file
@ -0,0 +1,61 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local ReplicatedFirst = game:GetService("ReplicatedFirst")
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local LocalPlayer = game:GetService("Players").LocalPlayer
|
||||
local LoadingUi = LocalPlayer.PlayerGui:WaitForChild("LoadingScreen")
|
||||
local LoadingBar = LoadingUi.Frame:WaitForChild("Bar"):WaitForChild("LoadingBar")
|
||||
local LoadingText = LoadingUi.Frame:WaitForChild("LoadingText")
|
||||
local LoadingBarFill = LoadingUi.Frame:WaitForChild("Bar"):WaitForChild("LoadingBarFill")
|
||||
|
||||
|
||||
local LoadingScreen = {}
|
||||
|
||||
function LoadingScreen:UpdateProgress(progress)
|
||||
progress = math.clamp(progress, 0, 1)
|
||||
|
||||
TweenService:Create(LoadingBarFill, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
|
||||
Size = UDim2.new(progress, 0, 1, 0)
|
||||
}):Play()
|
||||
|
||||
LoadingText.Text = "Loading " .. math.floor(progress * 100) .. "%"
|
||||
end
|
||||
|
||||
function LoadingScreen:StartLoading()
|
||||
|
||||
local AllAssets = {}
|
||||
for _, Asset in pairs(workspace:GetDescendants()) do
|
||||
if Asset:IsA("Sound") or Asset:IsA("ImageLabel") or Asset:IsA("ImageButton") or Asset:IsA("Texture") or Asset:IsA("Decal") or Asset:IsA("Animation") then
|
||||
table.insert(AllAssets, Asset)
|
||||
end
|
||||
|
||||
end
|
||||
local TotalAssets = #AllAssets
|
||||
local LoadedAssets = 0
|
||||
|
||||
for _, Asset in pairs(AllAssets) do
|
||||
ContentProvider:PreloadAsync({Asset})
|
||||
task.wait(0.01)
|
||||
LoadedAssets += 1
|
||||
LoadingScreen:UpdateProgress(LoadedAssets / TotalAssets)
|
||||
end
|
||||
|
||||
LoadingUi.Enabled = false
|
||||
LocalPlayer:AddTag("Loaded")
|
||||
end
|
||||
|
||||
function LoadingScreen:Init()
|
||||
if RunService:IsStudio() then
|
||||
if not script:GetAttribute("StudioLoading") then
|
||||
return
|
||||
end
|
||||
end
|
||||
ReplicatedFirst:RemoveDefaultLoadingScreen()
|
||||
LoadingUi.Enabled = true
|
||||
|
||||
|
||||
task.spawn(LoadingScreen:StartLoading())
|
||||
end
|
||||
|
||||
return LoadingScreen
|
||||
12
src/shared/client/LoadingScreen.meta.json
Normal file
12
src/shared/client/LoadingScreen.meta.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true,
|
||||
"LoaderPriority": 99.0,
|
||||
"StudioLoading": false
|
||||
}
|
||||
}
|
||||
266
src/shared/client/Modules/AimRenderer.luau
Normal file
266
src/shared/client/Modules/AimRenderer.luau
Normal file
@ -0,0 +1,266 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local AimRenderer = {}
|
||||
|
||||
local PREVIEW_STEPS = 30
|
||||
local DOT_SIZE = 0.3
|
||||
local DT = 0.1 -- seconds per dot — same time axis as PhysicsProjectileLauncher
|
||||
|
||||
local previewParts : {Part} = {}
|
||||
|
||||
local active = false
|
||||
local angle : number = 0
|
||||
local power : number = 0 -- always pre-scaled by accuracy before arriving here
|
||||
local origin : Vector3? = nil
|
||||
local moveTurret = true
|
||||
|
||||
local FORWARD_AXIS = Vector3.new(1, 0, 0)
|
||||
|
||||
local rAssets = ReplicatedStorage:WaitForChild("Assets")
|
||||
local rModels = rAssets:WaitForChild("Models")
|
||||
local rWeaponModels = rModels:WaitForChild("PreviewModels")
|
||||
|
||||
local rData = ReplicatedStorage.Data
|
||||
local rWeaponData = require(rData.WeaponData)
|
||||
local rBotData = require(rData.BotData)
|
||||
|
||||
local rUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
local rBotUtils = require(rUtils.sharedBotUtils)
|
||||
|
||||
local prevModel : Model? = nil
|
||||
local localUserId : number? = nil
|
||||
|
||||
local wData = nil -- WeaponData entry for current weapon
|
||||
local bData = nil -- BotData entry for local bot
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Preview Model
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function AimRenderer.CreatePreview(name : string)
|
||||
if prevModel and prevModel.Name == name then return end
|
||||
if prevModel then prevModel:Destroy() end
|
||||
|
||||
name = name or "Missile"
|
||||
local template = rWeaponModels:FindFirstChild(name)
|
||||
or rWeaponModels:FindFirstChild("Missile")
|
||||
if not template then return end
|
||||
|
||||
prevModel = template:Clone()
|
||||
prevModel.Parent = workspace
|
||||
|
||||
if not prevModel.PrimaryPart then
|
||||
prevModel.PrimaryPart = prevModel:FindFirstChildWhichIsA("BasePart")
|
||||
end
|
||||
end
|
||||
|
||||
function AimRenderer.MovePreview(cframe : CFrame)
|
||||
if not prevModel then return end
|
||||
|
||||
if prevModel:GetAttribute("NO_ROT") then
|
||||
cframe = CFrame.new(cframe.Position)
|
||||
end
|
||||
|
||||
local offset = prevModel:GetAttribute("OFFSET") or CFrame.new()
|
||||
|
||||
if wData and wData.preview and wData.preview.type == "move" then
|
||||
cframe = CFrame.new(cframe.Position)
|
||||
local x, y, z = rBotUtils.FindBotModel(localUserId):GetPivot():ToOrientation()
|
||||
offset = CFrame.Angles(x, y, z)
|
||||
end
|
||||
|
||||
prevModel:PivotTo(cframe * offset)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Dots
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local function createDots()
|
||||
local dotFolder = workspace:WaitForChild("Dot")
|
||||
for i = 1, PREVIEW_STEPS do
|
||||
local dot = Instance.new("Part")
|
||||
dot.Size = Vector3.new(DOT_SIZE, DOT_SIZE, DOT_SIZE)
|
||||
dot.Shape = Enum.PartType.Ball
|
||||
dot.Anchored = true
|
||||
dot.CanCollide = false
|
||||
dot.CastShadow = false
|
||||
dot.Material = Enum.Material.Neon
|
||||
dot.Color = Color3.fromRGB(255, 255, 255)
|
||||
dot.CFrame = CFrame.new(0, -1000, 0)
|
||||
dot.Parent = dotFolder
|
||||
previewParts[i] = dot
|
||||
end
|
||||
end
|
||||
|
||||
local function parkDot(dot : Part)
|
||||
dot.CFrame = CFrame.new(0, -1000, 0)
|
||||
dot.Transparency = 1
|
||||
end
|
||||
|
||||
local function parkAllDots()
|
||||
for _, dot in previewParts do
|
||||
parkDot(dot)
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Trajectory
|
||||
-- Uses the exact same formula as PhysicsUtils.CalculateVelocity on the server.
|
||||
-- power arrives already scaled by accuracy from InputHandler — no extra scaling here.
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local function moveDots()
|
||||
if not origin or not wData or not bData then return end
|
||||
|
||||
-- accuracy controls how many dots are visible (shorter preview = less certain)
|
||||
local accuracy = math.clamp(bData.accuracy or 1, 0.1, 1)
|
||||
local visibleSteps = math.floor(PREVIEW_STEPS * accuracy)
|
||||
|
||||
-- EXACT same formula as PhysicsUtils.CalculateVelocity + gravity simulation
|
||||
-- power is already accuracy-scaled — do NOT scale again here
|
||||
local speed = power * 100
|
||||
local g = workspace.Gravity
|
||||
local horizSpeed = math.cos(angle) * speed
|
||||
local vertSpeed = math.sin(angle) * speed
|
||||
|
||||
local firstPos : Vector3? = nil
|
||||
local lastPos : Vector3? = nil
|
||||
local prevPos : Vector3? = nil
|
||||
|
||||
for i = 1, PREVIEW_STEPS do
|
||||
local dot = previewParts[i]
|
||||
if not dot then continue end
|
||||
|
||||
-- fade out near the end of the visible range instead of hard cutoff
|
||||
local visibility = (visibleSteps - i) / 5
|
||||
|
||||
if visibility <= 0 then
|
||||
parkDot(dot)
|
||||
continue
|
||||
end
|
||||
|
||||
local t = i * DT
|
||||
local worldPos = origin
|
||||
+ FORWARD_AXIS * (horizSpeed * t)
|
||||
+ Vector3.new(0,1,0) * (vertSpeed * t - 0.5 * g * t * t)
|
||||
|
||||
dot.CFrame = CFrame.new(worldPos)
|
||||
dot.Transparency = 1 - math.clamp(visibility, 0, 1)
|
||||
|
||||
if i == 1 then firstPos = worldPos end
|
||||
prevPos = lastPos
|
||||
lastPos = worldPos
|
||||
end
|
||||
|
||||
-- rotate turret to match launch direction
|
||||
if moveTurret and firstPos and origin and wData.preview and wData.preview.type == "projectile" then
|
||||
local dir = firstPos - origin
|
||||
if dir.Magnitude > 0.001 then
|
||||
rBotUtils.RotateTurret(localUserId, dir.Unit)
|
||||
end
|
||||
end
|
||||
|
||||
-- move preview model to tip of arc
|
||||
if lastPos and prevPos then
|
||||
local dir = (lastPos - prevPos).Unit
|
||||
AimRenderer.MovePreview(CFrame.lookAt(lastPos, lastPos + dir))
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Public API
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
-- power arrives pre-scaled by accuracy from InputHandler.
|
||||
-- AimRenderer never scales power itself — single source of truth.
|
||||
function AimRenderer.update(newAngle : number?, newPower : number?, newBot : boolean?, newWeaponName : string?)
|
||||
if newAngle ~= nil then
|
||||
angle = newAngle
|
||||
end
|
||||
|
||||
if newPower ~= nil then
|
||||
power = newPower -- already accuracy-scaled, do NOT multiply again
|
||||
end
|
||||
|
||||
if newBot then
|
||||
bData = rBotUtils.GetBotData(localUserId)
|
||||
end
|
||||
|
||||
if newWeaponName then
|
||||
wData = rWeaponData[newWeaponName]
|
||||
end
|
||||
end
|
||||
|
||||
function AimRenderer.setForwardAxis(axis : Vector3)
|
||||
FORWARD_AXIS = axis
|
||||
end
|
||||
|
||||
function AimRenderer.enable()
|
||||
active = true
|
||||
end
|
||||
|
||||
function AimRenderer.hide()
|
||||
active = false
|
||||
parkAllDots()
|
||||
if prevModel then
|
||||
prevModel:PivotTo(CFrame.new(0, -1000, 0))
|
||||
end
|
||||
end
|
||||
|
||||
function AimRenderer.Highlight()
|
||||
for _, dot in previewParts do
|
||||
dot.Color = Color3.fromRGB(0, 255, 0)
|
||||
end
|
||||
end
|
||||
|
||||
function AimRenderer.UnHighlight()
|
||||
for _, dot in previewParts do
|
||||
dot.Color = Color3.fromRGB(255, 255, 255)
|
||||
end
|
||||
end
|
||||
|
||||
function AimRenderer.destroy()
|
||||
active = false
|
||||
for _, dot in previewParts do
|
||||
dot:Destroy()
|
||||
end
|
||||
previewParts = {}
|
||||
origin = nil
|
||||
if prevModel then
|
||||
prevModel:Destroy()
|
||||
prevModel = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Init
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function AimRenderer:Init()
|
||||
createDots()
|
||||
localUserId = game.Players.LocalPlayer.UserId
|
||||
|
||||
RunService.RenderStepped:Connect(function()
|
||||
if not active then
|
||||
parkAllDots()
|
||||
return
|
||||
end
|
||||
|
||||
if not wData or not bData then return end
|
||||
|
||||
-- refresh origin every frame so it tracks the barrel tip if the bot moves
|
||||
if wData.preview and wData.preview.type == "projectile" then
|
||||
origin = rBotUtils.GetShootPos(localUserId)
|
||||
else
|
||||
origin = rBotUtils.GetBotPosition(localUserId)
|
||||
end
|
||||
|
||||
if origin then
|
||||
moveDots()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return AimRenderer
|
||||
10
src/shared/client/Modules/AimRenderer.meta.json
Normal file
10
src/shared/client/Modules/AimRenderer.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
208
src/shared/client/Modules/BotAbilityUI.luau
Normal file
208
src/shared/client/Modules/BotAbilityUI.luau
Normal file
@ -0,0 +1,208 @@
|
||||
local BotAbilityUI = {}
|
||||
|
||||
local localPlayer = game.Players.LocalPlayer
|
||||
local PlayerGui = localPlayer.PlayerGui
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local sUtils = ReplicatedStorage.Shared.SharedUtils
|
||||
local sBotUtils = require(sUtils.sharedBotUtils)
|
||||
|
||||
local AbilityGui = PlayerGui:WaitForChild("AbilityGui")
|
||||
local AbilitiesFolder = AbilityGui:WaitForChild("Abilities")
|
||||
local Container = AbilityGui:WaitForChild("Container")
|
||||
local ConfirmAction = Container:WaitForChild("ConfirmAction")
|
||||
|
||||
local currentAbilities = nil
|
||||
local updateCooldownEvent : {RBXScriptConnection} = {}
|
||||
local selectedAbility : Frame? = nil
|
||||
|
||||
local PLACEHOLDER_IMAGE = "rbxassetid:/77459325511400"
|
||||
local COOLDOWN_LABEL_NAME = "CooldownLabel"
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Private
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local function toggle(transp : number)
|
||||
ConfirmAction.BackgroundTransparency = transp
|
||||
ConfirmAction.Target.ImageTransparency = transp
|
||||
ConfirmAction.Active = transp == 0
|
||||
|
||||
BotAbilityUI.UpdateAllCooldowns()
|
||||
end
|
||||
|
||||
local function refreshCooldownEvents()
|
||||
for _, conn in pairs(updateCooldownEvent) do
|
||||
conn:Disconnect()
|
||||
end
|
||||
table.clear(updateCooldownEvent)
|
||||
|
||||
local cooldowns = sBotUtils.GetCooldowns(localPlayer.UserId)
|
||||
|
||||
for _, v : IntValue in pairs(cooldowns:GetChildren()) do
|
||||
local weaponType = v.Name
|
||||
local cAbility = AbilitiesFolder:FindFirstChild(weaponType)
|
||||
if not cAbility then continue end
|
||||
|
||||
BotAbilityUI.UpdateCooldown(cAbility)
|
||||
|
||||
local conn = v:GetPropertyChangedSignal("Value"):Connect(function()
|
||||
BotAbilityUI.UpdateCooldown(cAbility)
|
||||
end)
|
||||
table.insert(updateCooldownEvent, conn)
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Queries
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.GetAbility(weaponType : string) : Frame?
|
||||
return AbilitiesFolder:FindFirstChild(weaponType)
|
||||
end
|
||||
|
||||
function BotAbilityUI.GetAbilities() : {Frame}
|
||||
local tbl = {}
|
||||
for _, v in pairs(AbilitiesFolder:GetChildren()) do
|
||||
if v:IsA("Frame") then
|
||||
table.insert(tbl, v)
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
function BotAbilityUI.GetSelected() : Frame?
|
||||
return selectedAbility
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Visibility
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.ShowGUI()
|
||||
AbilityGui.Enabled = true
|
||||
end
|
||||
|
||||
function BotAbilityUI.HideGUI()
|
||||
AbilityGui.Enabled = false
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Confirm button
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.EnableConfirm()
|
||||
toggle(0)
|
||||
end
|
||||
|
||||
function BotAbilityUI.DisableConfirm()
|
||||
toggle(0.5)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Selection
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.HighlightAbility(cAbility : Frame)
|
||||
cAbility.BackgroundTransparency = 0.5
|
||||
end
|
||||
|
||||
function BotAbilityUI.UnHighlightAbility(cAbility : Frame)
|
||||
cAbility.BackgroundTransparency = 0
|
||||
end
|
||||
|
||||
function BotAbilityUI.UnSelectAll()
|
||||
for _, v in pairs(BotAbilityUI.GetAbilities()) do
|
||||
BotAbilityUI.UnHighlightAbility(v)
|
||||
end
|
||||
selectedAbility = nil
|
||||
end
|
||||
|
||||
function BotAbilityUI.SelectAbility(cAbility : Frame)
|
||||
|
||||
if cAbility:GetAttribute("Disabled") then return end
|
||||
|
||||
BotAbilityUI.UnSelectAll()
|
||||
BotAbilityUI.HighlightAbility(cAbility)
|
||||
selectedAbility = cAbility
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Enabled / disabled state
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.EnableAbility(cAbility : Frame)
|
||||
cAbility:SetAttribute("Disabled", false)
|
||||
cAbility.AbilityButton.ImageTransparency = 0
|
||||
end
|
||||
|
||||
function BotAbilityUI.DisableAll()
|
||||
for i,v in pairs(BotAbilityUI.GetAbilities()) do
|
||||
BotAbilityUI.DisableAbility(v)
|
||||
end
|
||||
end
|
||||
|
||||
function BotAbilityUI.DisableAbility(cAbility : Frame)
|
||||
cAbility:SetAttribute("Disabled", true)
|
||||
cAbility.AbilityButton.ImageTransparency = 0.5
|
||||
-- deselect if this ability was selected
|
||||
if selectedAbility == cAbility then
|
||||
BotAbilityUI.UnSelectAll()
|
||||
end
|
||||
end
|
||||
|
||||
function BotAbilityUI.UpdateAbility(cAbility : Frame)
|
||||
local canUse = sBotUtils.CanUseAbility(localPlayer.UserId, cAbility.Name)
|
||||
if canUse then
|
||||
BotAbilityUI.EnableAbility(cAbility)
|
||||
else
|
||||
BotAbilityUI.DisableAbility(cAbility)
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Cooldown display
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.UpdateCooldown(cAbility : Frame)
|
||||
BotAbilityUI.UpdateAbility(cAbility)
|
||||
|
||||
local cooldownLabel : TextLabel? = cAbility:FindFirstChild(COOLDOWN_LABEL_NAME)
|
||||
if not cooldownLabel then return end
|
||||
|
||||
local cooldowns = sBotUtils.GetCooldowns(localPlayer.UserId)
|
||||
local cooldownValue : IntValue? = cooldowns:FindFirstChild(cAbility.Name)
|
||||
|
||||
if not cooldownValue or cooldownValue.Value <= 0 then
|
||||
cooldownLabel.Text = ""
|
||||
cooldownLabel.Visible = false
|
||||
else
|
||||
cooldownLabel.Text = tostring(cooldownValue.Value)
|
||||
cooldownLabel.Visible = true
|
||||
end
|
||||
end
|
||||
|
||||
function BotAbilityUI.UpdateAllCooldowns()
|
||||
for _, v in pairs(BotAbilityUI.GetAbilities()) do
|
||||
BotAbilityUI.UpdateCooldown(v)
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- New abilities (called when bot changes)
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function BotAbilityUI.NewAbilities()
|
||||
currentAbilities = sBotUtils.GetWeaponsData(localPlayer.UserId)
|
||||
|
||||
for weaponType, weaponData in pairs(currentAbilities) do
|
||||
local cAbility = AbilitiesFolder:FindFirstChild(weaponType)
|
||||
if not cAbility then continue end
|
||||
local image = weaponData.DisplayImage or PLACEHOLDER_IMAGE
|
||||
cAbility.AbilityButton.Image = image
|
||||
end
|
||||
|
||||
refreshCooldownEvents()
|
||||
end
|
||||
|
||||
return BotAbilityUI
|
||||
39
src/shared/client/Modules/BotSelectUI.luau
Normal file
39
src/shared/client/Modules/BotSelectUI.luau
Normal file
@ -0,0 +1,39 @@
|
||||
-- BotSelectUI (client only)
|
||||
-- Shows the bot selection screen before a round starts.
|
||||
-- Calls onSelect(botName) when the player confirms their choice.
|
||||
|
||||
local BotSelectUI = {}
|
||||
|
||||
-- TODO: reference your ScreenGui here
|
||||
-- local gui = playerGui:WaitForChild("BotSelectScreen")
|
||||
|
||||
local selected = nil
|
||||
local onSelect = nil
|
||||
|
||||
function BotSelectUI.show(selectCallback)
|
||||
onSelect = selectCallback
|
||||
selected = nil
|
||||
-- TODO: make gui visible
|
||||
-- TODO: populate the 3 bot cards from BotData (name, hp, weight, accuracy, special description)
|
||||
end
|
||||
|
||||
function BotSelectUI.hide()
|
||||
-- TODO: make gui invisible
|
||||
end
|
||||
|
||||
-- Called by each bot card's select button
|
||||
function BotSelectUI.pickBot(botName)
|
||||
selected = botName
|
||||
-- TODO: highlight the selected card, dim the others
|
||||
end
|
||||
|
||||
-- Called by the Confirm button
|
||||
function BotSelectUI.confirm()
|
||||
if not selected then return end -- nothing picked yet
|
||||
if onSelect then
|
||||
onSelect(selected)
|
||||
end
|
||||
BotSelectUI.hide()
|
||||
end
|
||||
|
||||
return BotSelectUI
|
||||
124
src/shared/client/Modules/CameraController.luau
Normal file
124
src/shared/client/Modules/CameraController.luau
Normal file
@ -0,0 +1,124 @@
|
||||
local CameraController = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
|
||||
|
||||
local camera = workspace.Camera
|
||||
|
||||
local MAX_DIST = 40
|
||||
|
||||
local canMove = false
|
||||
|
||||
local cameraPart
|
||||
|
||||
function CameraController.WideMapView()
|
||||
local map = workspace.Map:WaitForChild("Map")
|
||||
if not map then
|
||||
print("lol")
|
||||
return
|
||||
end
|
||||
cameraPart = map:WaitForChild("CameraPart")
|
||||
if not cameraPart then
|
||||
print("lolipop")
|
||||
return
|
||||
end
|
||||
camera.CameraType = Enum.CameraType.Scriptable
|
||||
camera.CFrame = cameraPart.CFrame
|
||||
canMove = true
|
||||
end
|
||||
|
||||
local state = {
|
||||
X_Increment = 0,
|
||||
Y_Increment = 0,
|
||||
Z_Increment = 0
|
||||
}
|
||||
local SPEED = 30
|
||||
|
||||
local axis = {"X","Y","Z"}
|
||||
|
||||
function update(dt)
|
||||
if not canMove then
|
||||
return
|
||||
end
|
||||
local frame = camera.CFrame
|
||||
|
||||
local movement = Vector3.new(
|
||||
state.X_Increment * dt * SPEED,
|
||||
state.Y_Increment * dt * SPEED,
|
||||
state.Z_Increment * dt * SPEED
|
||||
)
|
||||
|
||||
local newPos = frame.Position + movement
|
||||
local x, y, z = frame:ToOrientation()
|
||||
|
||||
for i,v in pairs(axis) do
|
||||
if math.abs(newPos[v] - cameraPart.Position[v]) > MAX_DIST then
|
||||
return
|
||||
end
|
||||
end
|
||||
camera.CFrame = CFrame.new(newPos) * CFrame.Angles(x, y, z)
|
||||
|
||||
end
|
||||
|
||||
local uis = game:GetService("UserInputService")
|
||||
|
||||
function InputBegan(input : InputObject)
|
||||
if input.KeyCode == Enum.KeyCode.A then
|
||||
state.X_Increment = 1
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.D then
|
||||
state.X_Increment = -1
|
||||
end
|
||||
|
||||
-- Y axis (was W/S, now Q/E)
|
||||
if input.KeyCode == Enum.KeyCode.Q then
|
||||
state.Y_Increment = -1
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.E then
|
||||
state.Y_Increment = 1
|
||||
end
|
||||
|
||||
-- Z axis (new: W/S)
|
||||
if input.KeyCode == Enum.KeyCode.W then
|
||||
state.Z_Increment = 1
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.S then
|
||||
state.Z_Increment = -1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function InputEnded(input : InputObject)
|
||||
if input.KeyCode == Enum.KeyCode.A or input.KeyCode == Enum.KeyCode.D then
|
||||
state.X_Increment = 0
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.Q or input.KeyCode == Enum.KeyCode.E then
|
||||
state.Y_Increment = 0
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.W or input.KeyCode == Enum.KeyCode.S then
|
||||
state.Z_Increment = 0
|
||||
end
|
||||
end
|
||||
|
||||
function CameraController.ResetCamera()
|
||||
camera.CameraType = Enum.CameraType.Custom
|
||||
camera.CameraSubject = game:GetService("Players").LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
|
||||
canMove = false
|
||||
end
|
||||
|
||||
|
||||
|
||||
function CameraController:Init()
|
||||
uis.InputBegan:Connect(InputBegan)
|
||||
uis.InputEnded:Connect(InputEnded)
|
||||
|
||||
game:GetService("RunService").Heartbeat:Connect(update)
|
||||
end
|
||||
|
||||
return CameraController
|
||||
10
src/shared/client/Modules/CameraController.meta.json
Normal file
10
src/shared/client/Modules/CameraController.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
22
src/shared/client/Modules/GarageUIController.luau
Normal file
22
src/shared/client/Modules/GarageUIController.luau
Normal file
@ -0,0 +1,22 @@
|
||||
local GarageUIController = {}
|
||||
|
||||
local localPlayer = game.Players.LocalPlayer
|
||||
|
||||
local PlayerGui = localPlayer.PlayerGui
|
||||
local GarageGui = PlayerGui:WaitForChild("GarageGui")
|
||||
|
||||
function GarageUIController.OpenGUI()
|
||||
|
||||
end
|
||||
|
||||
function GarageUIController.CloseGUI()
|
||||
|
||||
end
|
||||
|
||||
|
||||
function GarageUIController:Init()
|
||||
|
||||
end
|
||||
|
||||
|
||||
return GarageUIController
|
||||
263
src/shared/client/Modules/InputHandler.luau
Normal file
263
src/shared/client/Modules/InputHandler.luau
Normal file
@ -0,0 +1,263 @@
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local localPlayer = Players.LocalPlayer
|
||||
|
||||
local Remote = ReplicatedStorage.Remote
|
||||
local rfn_GetLastSecondInput = Remote.GetLastSecondInput
|
||||
|
||||
local DataTypes = require(ReplicatedStorage.Data.DataTypes)
|
||||
local sBotUtils = require(ReplicatedStorage.Shared.SharedUtils.sharedBotUtils)
|
||||
|
||||
local AimRenderer = require(script.Parent.AimRenderer)
|
||||
local BotAbilityUI = require(script.Parent.BotAbilityUI)
|
||||
|
||||
local InputHandler = {}
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- State
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local state = {
|
||||
active = false,
|
||||
moving = false,
|
||||
hasMoved = false,
|
||||
|
||||
currentAbility = "Move",
|
||||
abilityName = "Move",
|
||||
currentAngle = 0,
|
||||
currentPower = 0.5,
|
||||
|
||||
dragStartPos = nil,
|
||||
|
||||
botOrigin = nil,
|
||||
accuracy = 1,
|
||||
|
||||
onConfirm = nil,
|
||||
}
|
||||
|
||||
local MAX_DRAG_DISTANCE = 250
|
||||
local MIN_POWER = 0.01
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Helpers
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local function setBotData()
|
||||
state.botOrigin = sBotUtils.GetBotPosition(localPlayer.UserId)
|
||||
local botData = sBotUtils.GetBotData(localPlayer.UserId)
|
||||
-- read real accuracy from BotData, default 1 if missing
|
||||
state.accuracy = (botData and botData.accuracy) or 1
|
||||
end
|
||||
|
||||
-- Power sent to server = raw drag power scaled by accuracy.
|
||||
-- This must match what AimRenderer previews.
|
||||
local function buildFireData()
|
||||
return {
|
||||
weapon = state.currentAbility,
|
||||
angle = state.currentAngle,
|
||||
power = state.currentPower * state.accuracy, -- scaled, matches preview
|
||||
specialArgs = {},
|
||||
}
|
||||
end
|
||||
|
||||
local function canConfirm()
|
||||
return state.currentPower >= MIN_POWER
|
||||
end
|
||||
|
||||
-- AimRenderer always receives already-scaled power so there is
|
||||
-- only ONE place doing the accuracy multiplication (here).
|
||||
local function updateAim()
|
||||
AimRenderer.update(
|
||||
state.currentAngle,
|
||||
state.currentPower * state.accuracy -- scaled once, here
|
||||
)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Ability selection
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function InputHandler.setAbility(frame, force)
|
||||
if not state.active or frame:GetAttribute("Disabled") then return end
|
||||
|
||||
local abilityType = frame.Name
|
||||
|
||||
if not force and abilityType == state.currentAbility then
|
||||
state.currentAbility = nil
|
||||
BotAbilityUI.UnSelectAll()
|
||||
AimRenderer.hide()
|
||||
return
|
||||
end
|
||||
|
||||
state.hasMoved = true
|
||||
state.currentAbility = abilityType
|
||||
state.abilityName = sBotUtils.GetWeaponName(localPlayer.UserId, abilityType)
|
||||
|
||||
AimRenderer.enable()
|
||||
AimRenderer.CreatePreview(state.abilityName)
|
||||
-- pass newBot=true and weapon name so AimRenderer refreshes both caches
|
||||
AimRenderer.update(nil, nil, true, state.abilityName)
|
||||
|
||||
setBotData()
|
||||
BotAbilityUI.SelectAbility(frame)
|
||||
updateAim()
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Drag logic
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
local function startDrag(input)
|
||||
state.moving = true
|
||||
|
||||
local mousePos = Vector2.new(input.Position.X, input.Position.Y)
|
||||
|
||||
if state.currentPower > 0 then
|
||||
local distance = state.currentPower ^ (1 / 1.5) * MAX_DRAG_DISTANCE
|
||||
local offset = Vector2.new(
|
||||
-math.cos(state.currentAngle) * distance,
|
||||
-math.sin(state.currentAngle) * distance
|
||||
)
|
||||
state.dragStartPos = mousePos - offset
|
||||
else
|
||||
state.dragStartPos = mousePos
|
||||
end
|
||||
end
|
||||
|
||||
local function endDrag()
|
||||
state.moving = false
|
||||
state.dragStartPos = nil
|
||||
end
|
||||
|
||||
local function updateDrag(input)
|
||||
if not state.active or not state.moving or not state.dragStartPos then return end
|
||||
|
||||
state.hasMoved = true
|
||||
|
||||
local pos = Vector2.new(input.Position.X, input.Position.Y)
|
||||
local drag = pos - state.dragStartPos
|
||||
local distance = drag.Magnitude
|
||||
|
||||
state.currentAngle = math.atan2(-drag.Y, -drag.X)
|
||||
state.currentPower = math.clamp(distance / MAX_DRAG_DISTANCE, 0, 1) ^ 1.5
|
||||
|
||||
updateAim()
|
||||
|
||||
if canConfirm() then
|
||||
BotAbilityUI.EnableConfirm()
|
||||
else
|
||||
BotAbilityUI.DisableConfirm()
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Enable / Disable
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function InputHandler.enable(confirmCallback, turnIndex)
|
||||
state.active = true
|
||||
state.onConfirm = confirmCallback
|
||||
state.moving = false
|
||||
state.dragStartPos = nil
|
||||
state.hasMoved = false
|
||||
|
||||
setBotData()
|
||||
|
||||
BotAbilityUI.ShowGUI()
|
||||
BotAbilityUI.UnSelectAll()
|
||||
BotAbilityUI.UpdateAllCooldowns()
|
||||
|
||||
local ability = (turnIndex == 1)
|
||||
and BotAbilityUI.GetAbility("Missile")
|
||||
or BotAbilityUI.GetAbility(state.currentAbility, true)
|
||||
|
||||
InputHandler.setAbility(ability)
|
||||
end
|
||||
|
||||
function InputHandler.disable()
|
||||
state.active = false
|
||||
state.onConfirm = nil
|
||||
state.moving = false
|
||||
state.dragStartPos = nil
|
||||
|
||||
BotAbilityUI.UnSelectAll()
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Confirm
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function InputHandler.confirm()
|
||||
if not state.active or not state.onConfirm or not canConfirm() then return end
|
||||
state.onConfirm(buildFireData())
|
||||
InputHandler.disable()
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Input bindings
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
UserInputService.InputBegan:Connect(function(input, gp)
|
||||
if not state.active or gp then return end
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1
|
||||
or input.UserInputType == Enum.UserInputType.Touch then
|
||||
startDrag(input)
|
||||
end
|
||||
end)
|
||||
|
||||
UserInputService.InputChanged:Connect(function(input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseMovement
|
||||
or input.UserInputType == Enum.UserInputType.Touch then
|
||||
updateDrag(input)
|
||||
end
|
||||
end)
|
||||
|
||||
UserInputService.InputEnded:Connect(function(input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1
|
||||
or input.UserInputType == Enum.UserInputType.Touch then
|
||||
endDrag()
|
||||
end
|
||||
end)
|
||||
|
||||
-- ─────────────────────────────────────────
|
||||
-- Init
|
||||
-- ─────────────────────────────────────────
|
||||
|
||||
function InputHandler:Init()
|
||||
for i, v in pairs(BotAbilityUI.GetAbilities()) do
|
||||
v:WaitForChild("AbilityButton").Activated:Connect(function()
|
||||
InputHandler.setAbility(v)
|
||||
end)
|
||||
|
||||
UserInputService.InputBegan:Connect(function(inputObject, processedEvent)
|
||||
if processedEvent then return end
|
||||
if (inputObject.KeyCode.Value - 48) == i then
|
||||
InputHandler.setAbility(v)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local confirmBtn = localPlayer.PlayerGui
|
||||
:WaitForChild("AbilityGui")
|
||||
:WaitForChild("Container")
|
||||
:WaitForChild("ConfirmAction")
|
||||
|
||||
confirmBtn.Activated:Connect(InputHandler.confirm)
|
||||
|
||||
UserInputService.InputBegan:Connect(function(inputObject, processedEvent)
|
||||
if processedEvent then return end
|
||||
if inputObject.KeyCode.Name == "F" then
|
||||
InputHandler.confirm()
|
||||
end
|
||||
end)
|
||||
|
||||
rfn_GetLastSecondInput.OnClientInvoke = function()
|
||||
if state.hasMoved and state.currentAbility then
|
||||
return buildFireData()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return InputHandler
|
||||
10
src/shared/client/Modules/InputHandler.meta.json
Normal file
10
src/shared/client/Modules/InputHandler.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
24
src/shared/client/Modules/RoundClient.luau
Normal file
24
src/shared/client/Modules/RoundClient.luau
Normal file
@ -0,0 +1,24 @@
|
||||
local RoundClient = {}
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local statusRemoteEvent = ReplicatedStorage.Remote.StatusRemoteEvent
|
||||
local timerRemoteEvent = ReplicatedStorage.Remote.TimerRemoteEvent
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local playerGui = player.PlayerGui
|
||||
local statusGui = playerGui:WaitForChild("StatusGui")
|
||||
local container = statusGui.MainFrame.Container
|
||||
|
||||
local function onStatusRemoteEvent(status: string)
|
||||
--print(status)
|
||||
container.Status.Text = status
|
||||
end
|
||||
|
||||
|
||||
function RoundClient:Init()
|
||||
statusRemoteEvent.OnClientEvent:Connect(onStatusRemoteEvent)
|
||||
end
|
||||
|
||||
return RoundClient
|
||||
10
src/shared/client/Modules/RoundClient.meta.json
Normal file
10
src/shared/client/Modules/RoundClient.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
130
src/shared/client/Modules/VotingHandlerClient.luau
Normal file
130
src/shared/client/Modules/VotingHandlerClient.luau
Normal file
@ -0,0 +1,130 @@
|
||||
local VotingHandlerClient = {}
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Data = ReplicatedStorage:WaitForChild("Data")
|
||||
local OptionsData = require(Data:WaitForChild("OptionsData"))
|
||||
|
||||
local Remote = ReplicatedStorage:WaitForChild("Remote")
|
||||
|
||||
local rev_Vote = Remote:WaitForChild("Vote")
|
||||
local rev_StartVote = Remote:WaitForChild("StartVote")
|
||||
local rev_EndVote = Remote:WaitForChild("EndVote")
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local gui = player:WaitForChild("PlayerGui"):WaitForChild("VotingGui")
|
||||
local frame = gui:WaitForChild("VotingFrame")
|
||||
local optionsFolder = frame:WaitForChild("Options")
|
||||
|
||||
local voted = nil
|
||||
local connections = {}
|
||||
|
||||
local NORMAL = Color3.fromRGB(0,0,0)
|
||||
local HIGHLIGHT = Color3.fromRGB(0,255,255)
|
||||
|
||||
local function clearConnections()
|
||||
for _, c in ipairs(connections) do
|
||||
c:Disconnect()
|
||||
end
|
||||
table.clear(connections)
|
||||
end
|
||||
|
||||
local function getButtons()
|
||||
local buttons = {}
|
||||
|
||||
for _, v in ipairs(optionsFolder:GetChildren()) do
|
||||
if v:IsA("TextButton") then
|
||||
table.insert(buttons, v)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(buttons, function(a,b)
|
||||
return a.LayoutOrder < b.LayoutOrder
|
||||
end)
|
||||
|
||||
return buttons
|
||||
end
|
||||
|
||||
local function vote(option)
|
||||
if option == voted then return end
|
||||
|
||||
if voted then
|
||||
local old = optionsFolder:FindFirstChild(voted)
|
||||
if old and old:FindFirstChild("OptionTitle") then
|
||||
old.OptionTitle.TextColor3 = NORMAL
|
||||
end
|
||||
end
|
||||
|
||||
local new = optionsFolder:FindFirstChild(option)
|
||||
if new and new:FindFirstChild("OptionTitle") then
|
||||
new.OptionTitle.TextColor3 = HIGHLIGHT
|
||||
end
|
||||
|
||||
voted = option
|
||||
rev_Vote:FireServer(option)
|
||||
end
|
||||
|
||||
local function refresh(results)
|
||||
for option, count in pairs(results) do
|
||||
local btn = optionsFolder:FindFirstChild(option)
|
||||
if btn and btn:FindFirstChild("VoteAmount") then
|
||||
btn.VoteAmount.Text = count .. " Votes"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function start(options, moduleKey)
|
||||
clearConnections()
|
||||
voted = nil
|
||||
|
||||
local data = OptionsData.Get(moduleKey)
|
||||
if not data then return end
|
||||
|
||||
local buttons = getButtons()
|
||||
|
||||
for i = 1, math.min(#options, #buttons) do
|
||||
local option = options[i]
|
||||
local button = buttons[i]
|
||||
local optionData = data[option]
|
||||
|
||||
if optionData then
|
||||
button.Name = option
|
||||
button.OptionTitle.Text = optionData.DisplayName
|
||||
button.VoteAmount.Text = "0 Votes"
|
||||
button.Visible = true
|
||||
|
||||
local conn = button.Activated:Connect(function()
|
||||
vote(option)
|
||||
end)
|
||||
|
||||
table.insert(connections, conn)
|
||||
end
|
||||
end
|
||||
|
||||
gui.Enabled = true
|
||||
end
|
||||
|
||||
local function finish()
|
||||
voted = nil
|
||||
|
||||
for _, v in ipairs(optionsFolder:GetChildren()) do
|
||||
if v:IsA("TextButton") then
|
||||
v.Visible = false
|
||||
if v:FindFirstChild("OptionTitle") then
|
||||
v.OptionTitle.TextColor3 = NORMAL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
gui.Enabled = false
|
||||
clearConnections()
|
||||
end
|
||||
|
||||
function VotingHandlerClient:Init()
|
||||
rev_StartVote.OnClientEvent:Connect(start)
|
||||
rev_EndVote.OnClientEvent:Connect(finish)
|
||||
rev_Vote.OnClientEvent:Connect(refresh)
|
||||
end
|
||||
|
||||
return VotingHandlerClient
|
||||
10
src/shared/client/Modules/VotingHandlerClient.meta.json
Normal file
10
src/shared/client/Modules/VotingHandlerClient.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
3
src/shared/client/Modules/clientBotUtils.luau
Normal file
3
src/shared/client/Modules/clientBotUtils.luau
Normal file
@ -0,0 +1,3 @@
|
||||
local module = {}
|
||||
|
||||
return module
|
||||
111
src/shared/client/UIUpdater.luau
Normal file
111
src/shared/client/UIUpdater.luau
Normal file
@ -0,0 +1,111 @@
|
||||
|
||||
|
||||
local LocalPlayer = game:GetService("Players").LocalPlayer
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local Values = LocalPlayer:WaitForChild("leaderstats")
|
||||
local Money = Values:WaitForChild("Money")
|
||||
|
||||
local UI = LocalPlayer.PlayerGui:WaitForChild("MoneyGui")
|
||||
local MoneyLabel = UI:WaitForChild("Container"):WaitForChild("Indicator")
|
||||
|
||||
local UiUpdater = {}
|
||||
|
||||
-- Track the currently displayed value
|
||||
local currentDisplayedValue = Money.Value
|
||||
local animationConnection = nil
|
||||
|
||||
function formatMoney(value)
|
||||
local absValue = math.abs(value)
|
||||
local sign = value < 0 and "-" or ""
|
||||
|
||||
if absValue < 1000 then
|
||||
return sign .. tostring(absValue)
|
||||
end
|
||||
|
||||
local suffixes = {"", "K", "M", "B", "T"}
|
||||
local suffixIndex = 1
|
||||
local remaining = absValue
|
||||
|
||||
while remaining >= 1000 and suffixIndex < #suffixes do
|
||||
remaining = remaining / 1000
|
||||
suffixIndex = suffixIndex + 1
|
||||
end
|
||||
|
||||
local suffix = suffixes[suffixIndex]
|
||||
|
||||
local rounded = math.round(remaining * 100) / 100
|
||||
local wholePart = math.floor(rounded)
|
||||
local decimalPart = math.floor((rounded - wholePart) * 100 + 0.5)
|
||||
|
||||
if decimalPart >= 100 then
|
||||
wholePart = wholePart + 1
|
||||
decimalPart = 0
|
||||
end
|
||||
|
||||
local decimalStr = string.format("%02d", decimalPart)
|
||||
|
||||
return sign .. wholePart .. "," .. decimalStr .. suffix
|
||||
end
|
||||
|
||||
function UiUpdater:Init()
|
||||
|
||||
MoneyLabel.Text = "$" .. formatMoney(Money.Value)
|
||||
|
||||
-- Hover to show raw value
|
||||
local isHovering = false
|
||||
|
||||
MoneyLabel.MouseEnter:Connect(function()
|
||||
isHovering = true
|
||||
MoneyLabel.Text = "$" .. tostring(Money.Value)
|
||||
end)
|
||||
|
||||
MoneyLabel.MouseLeave:Connect(function()
|
||||
isHovering = false
|
||||
MoneyLabel.Text = "$" .. formatMoney(currentDisplayedValue)
|
||||
end)
|
||||
|
||||
Money:GetPropertyChangedSignal("Value"):Connect(function()
|
||||
local targetValue = Money.Value
|
||||
local startValue = currentDisplayedValue
|
||||
local difference = targetValue - startValue
|
||||
|
||||
-- Cancel any existing animation
|
||||
if animationConnection then
|
||||
animationConnection:Disconnect()
|
||||
end
|
||||
|
||||
-- Animation duration based on difference (faster for smaller changes)
|
||||
local duration = math.clamp(math.abs(difference) * 0.001, 0.3, 1.5)
|
||||
local startTime = tick()
|
||||
local endTime = startTime + duration
|
||||
|
||||
animationConnection = game:GetService("RunService").RenderStepped:Connect(function()
|
||||
local now = tick()
|
||||
|
||||
if now >= endTime then
|
||||
-- Animation complete
|
||||
currentDisplayedValue = targetValue
|
||||
if isHovering then
|
||||
MoneyLabel.Text = "$" .. tostring(targetValue)
|
||||
else
|
||||
MoneyLabel.Text = "$" .. formatMoney(targetValue)
|
||||
end
|
||||
animationConnection:Disconnect()
|
||||
animationConnection = nil
|
||||
else
|
||||
-- Interpolate value with easing
|
||||
local progress = (now - startTime) / duration
|
||||
-- Use ease-out quad for smooth deceleration
|
||||
local easedProgress = 1 - (1 - progress) * (1 - progress)
|
||||
currentDisplayedValue = startValue + difference * easedProgress
|
||||
if isHovering then
|
||||
MoneyLabel.Text = "$" .. tostring(math.floor(currentDisplayedValue))
|
||||
else
|
||||
MoneyLabel.Text = "$" .. formatMoney(math.floor(currentDisplayedValue))
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return UiUpdater
|
||||
10
src/shared/client/UIUpdater.meta.json
Normal file
10
src/shared/client/UIUpdater.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"LOAD_MODULE"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"ClientOnly": true
|
||||
}
|
||||
}
|
||||
50
src/shared/data/BotData.luau
Normal file
50
src/shared/data/BotData.luau
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
export type BotData = {
|
||||
name : string,
|
||||
maxHp : string,
|
||||
weight : number,
|
||||
accuracy : number,
|
||||
weapons : {
|
||||
basic : string,
|
||||
special : string
|
||||
}
|
||||
}
|
||||
|
||||
local BotData = {
|
||||
|
||||
Tank = {
|
||||
name = "Tank",
|
||||
maxHp = 200,
|
||||
weight = 10,
|
||||
accuracy = 0.7,
|
||||
weapons = {
|
||||
basic = "ClusterRocket",
|
||||
special = "BouncyGrenade",
|
||||
},
|
||||
},
|
||||
|
||||
PlasmaBot = {
|
||||
name = "PlasmaBot",
|
||||
maxHp = 200,
|
||||
weight = 10,
|
||||
accuracy = 0.7,
|
||||
weapons = {
|
||||
basic = "ClusterRocket",
|
||||
special = "BouncyGrenade",
|
||||
},
|
||||
},
|
||||
|
||||
Defender = {
|
||||
name = "Defender",
|
||||
maxHp = 160,
|
||||
weight = 7,
|
||||
accuracy = 0.7,
|
||||
weapons = {
|
||||
basic = "Missile",
|
||||
special = "ReflectorShield",
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
return BotData
|
||||
22
src/shared/data/DataTypes.luau
Normal file
22
src/shared/data/DataTypes.luau
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
export type FireData = {
|
||||
weapon : string,
|
||||
angle : number,
|
||||
power : number,
|
||||
origin : Vector3,
|
||||
|
||||
specialArgs : {any}
|
||||
}
|
||||
|
||||
export type VoteOptionData = {
|
||||
Name : string,
|
||||
DisplayName : string,
|
||||
DisplayImage : string
|
||||
}
|
||||
|
||||
export type Loadout = {
|
||||
Skin : string,
|
||||
HeadAccessory : string?,
|
||||
}
|
||||
|
||||
return {}
|
||||
11
src/shared/data/GameState.luau
Normal file
11
src/shared/data/GameState.luau
Normal file
@ -0,0 +1,11 @@
|
||||
local GameState = {
|
||||
LOBBY = 1,
|
||||
GRACE = 2,
|
||||
AIMING = 3,
|
||||
RESOLVING = 4,
|
||||
RESULTS = 5,
|
||||
}
|
||||
|
||||
|
||||
|
||||
return GameState
|
||||
13
src/shared/data/OptionsData/MapData.luau
Normal file
13
src/shared/data/OptionsData/MapData.luau
Normal file
@ -0,0 +1,13 @@
|
||||
local DataTypes = require("../DataTypes")
|
||||
|
||||
local MapsData : {DataTypes.VoteOptionData} = {
|
||||
|
||||
LayoutMap1 = {
|
||||
Name = "LayoutMap1",
|
||||
DisplayName = "Test Map",
|
||||
DisplayImage = "rbxassetid://13979677676"
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
return MapsData
|
||||
38
src/shared/data/OptionsData/init.luau
Normal file
38
src/shared/data/OptionsData/init.luau
Normal file
@ -0,0 +1,38 @@
|
||||
local ModuleLoader = {}
|
||||
|
||||
local DataTypes = require(game.ReplicatedStorage.Data.DataTypes)
|
||||
|
||||
ModuleLoader.cachedModules = {}
|
||||
local runService = game:GetService("RunService")
|
||||
|
||||
local printStatement = "Server"
|
||||
if runService:IsClient() then
|
||||
printStatement = "Client"
|
||||
end
|
||||
|
||||
function ModuleLoader.Load(Modules : {ModuleScript})
|
||||
for i,v in pairs(Modules) do
|
||||
if v.ClassName ~= "ModuleScript" then continue end
|
||||
local success,ErrorMsg = pcall(function()
|
||||
local requiredMod = require(v)
|
||||
ModuleLoader.cachedModules[v.Name] = requiredMod
|
||||
end)
|
||||
|
||||
if not success and ErrorMsg then
|
||||
warn("FAILED TO LOAD ".. v.Name .. ":" .. ErrorMsg)
|
||||
end
|
||||
end
|
||||
print("Loaded all modules in " .. printStatement)
|
||||
end
|
||||
|
||||
function ModuleLoader.Get(ModuleName : string) : {DataTypes.VoteOptionData}
|
||||
if not ModuleLoader.cachedModules[ModuleName] then
|
||||
warn("didnt find " .. ModuleName .. " module ._.")
|
||||
end
|
||||
return ModuleLoader.cachedModules[ModuleName]
|
||||
end
|
||||
|
||||
ModuleLoader.Load(script:GetDescendants())
|
||||
|
||||
|
||||
return ModuleLoader
|
||||
33
src/shared/data/ProjectileData.luau
Normal file
33
src/shared/data/ProjectileData.luau
Normal file
@ -0,0 +1,33 @@
|
||||
export type ProjectileData = {
|
||||
speed : number,
|
||||
mass : number,
|
||||
drag : number,
|
||||
maxRange : number
|
||||
}
|
||||
|
||||
|
||||
local ProjectileData = {
|
||||
Missile = {
|
||||
speed = 80,
|
||||
mass = 1,
|
||||
drag = 0.02,
|
||||
maxRange = 300,
|
||||
},
|
||||
|
||||
ClusterRocket = {
|
||||
speed = 60,
|
||||
mass = 0.4,
|
||||
drag = 0.05,
|
||||
maxRange = 150,
|
||||
},
|
||||
|
||||
ClusterShard = {
|
||||
speed = 60,
|
||||
mass = 0.4,
|
||||
drag = 0.05,
|
||||
maxRange = 150,
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectileData
|
||||
|
||||
78
src/shared/data/WeaponData.luau
Normal file
78
src/shared/data/WeaponData.luau
Normal file
@ -0,0 +1,78 @@
|
||||
export type WeaponData = {
|
||||
cooldownTurns : number,
|
||||
preview : {
|
||||
DotsVisible : boolean?, -- if dots are visible, only icon appears
|
||||
GoThroughWalls : boolean?, -- if dots still appear after collision with a wall
|
||||
BounceForwardStrentgh : number?, -- icon bounce forward strentgh
|
||||
|
||||
type : string, -- type of thing the icon preview shows
|
||||
name : string -- name of the model of icon preview
|
||||
},
|
||||
weaponStats : {
|
||||
-- could be anything, this is just for weapon functions and descriptions to use
|
||||
Damage : number?,
|
||||
Radius : {
|
||||
InnerRadius : number,
|
||||
OuterRadius : number
|
||||
}?
|
||||
|
||||
},
|
||||
DisplayImage : string? -- for the lovely ui ty mr top hat man :)
|
||||
}
|
||||
|
||||
local WeaponData = {
|
||||
|
||||
ClusterRocket = {
|
||||
preview = {
|
||||
type = "projectile",
|
||||
name = "ClusterRocket"
|
||||
},
|
||||
weaponStats = {
|
||||
Damage = 20
|
||||
}
|
||||
-- spawns: "ClusterRocket" projectile, which itself spawns 5x "ClusterShard"
|
||||
},
|
||||
|
||||
BouncyGrenade = {
|
||||
preview = {
|
||||
type = "projectile",
|
||||
name = "BouncyGrenade",
|
||||
},
|
||||
weaponStats = {
|
||||
Damage = 65,
|
||||
Radius = {
|
||||
InnerRadius = 5,
|
||||
OuterRadius = 35,
|
||||
}
|
||||
},
|
||||
DisplayImage = "rbxassetid://11337954346"
|
||||
},
|
||||
|
||||
Missile = {
|
||||
preview = {
|
||||
type = "projectile",
|
||||
name = "Missile", -- lookup key into ProjectileData
|
||||
},
|
||||
weaponStats = {
|
||||
Damage = 30,
|
||||
Radius = {
|
||||
InnerRadius = 5,
|
||||
OuterRadius = 15,
|
||||
}
|
||||
|
||||
},
|
||||
DisplayImage = "rbxassetid://7357046479"
|
||||
},
|
||||
|
||||
Move = {
|
||||
preview = {
|
||||
type = "move",
|
||||
},
|
||||
DisplayImage = "rbxassetid://12438218791"
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
return WeaponData
|
||||
161
src/shared/janitor/Promise.luau
Normal file
161
src/shared/janitor/Promise.luau
Normal file
@ -0,0 +1,161 @@
|
||||
--!strict
|
||||
-- Partial types for Promise
|
||||
|
||||
local Packages = script.Parent.Packages
|
||||
local Promise: any = if Packages:FindFirstChild("Promise") then require(Packages.Promise) else nil
|
||||
|
||||
export type Status = "Started" | "Resolved" | "Rejected" | "Cancelled"
|
||||
export type ErrorKind = "ExecutionError" | "AlreadyCancelled" | "NotResolvedInTime" | "TimedOut"
|
||||
|
||||
type ErrorStaticAndShared = {
|
||||
Kind: {
|
||||
ExecutionError: "ExecutionError",
|
||||
AlreadyCancelled: "AlreadyCancelled",
|
||||
NotResolvedInTime: "NotResolvedInTime",
|
||||
TimedOut: "TimedOut",
|
||||
},
|
||||
}
|
||||
type ErrorOptions = {
|
||||
error: string,
|
||||
trace: string?,
|
||||
context: string?,
|
||||
kind: ErrorKind,
|
||||
}
|
||||
|
||||
export type Error = typeof(setmetatable(
|
||||
{} :: ErrorStaticAndShared & {
|
||||
error: string,
|
||||
trace: string?,
|
||||
context: string?,
|
||||
kind: ErrorKind,
|
||||
parent: Error?,
|
||||
createdTick: number,
|
||||
createdTrace: string,
|
||||
|
||||
extend: (self: Error, options: ErrorOptions?) -> Error,
|
||||
getErrorChain: (self: Error) -> {Error},
|
||||
},
|
||||
{} :: {__tostring: (self: Error) -> string}
|
||||
))
|
||||
type ErrorStatic = ErrorStaticAndShared & {
|
||||
new: (options: ErrorOptions?, parent: Error?) -> Error,
|
||||
is: (anything: any) -> boolean,
|
||||
isKind: (anything: any, kind: ErrorKind) -> boolean,
|
||||
}
|
||||
|
||||
export type Promise = {
|
||||
andThen: (
|
||||
self: Promise,
|
||||
successHandler: (...any) -> ...any,
|
||||
failureHandler: ((...any) -> ...any)?
|
||||
) -> Promise,
|
||||
andThenCall: <TArgs...>(self: Promise, callback: (TArgs...) -> ...any, TArgs...) -> any,
|
||||
andThenReturn: (self: Promise, ...any) -> Promise,
|
||||
|
||||
await: (self: Promise) -> (boolean, ...any),
|
||||
awaitStatus: (self: Promise) -> (Status, ...any),
|
||||
|
||||
cancel: (self: Promise) -> (),
|
||||
catch: (self: Promise, failureHandler: (...any) -> ...any) -> Promise,
|
||||
expect: (self: Promise) -> ...any,
|
||||
|
||||
finally: (self: Promise, finallyHandler: (status: Status) -> ...any) -> Promise,
|
||||
finallyCall: <TArgs...>(self: Promise, callback: (TArgs...) -> ...any, TArgs...) -> Promise,
|
||||
finallyReturn: (self: Promise, ...any) -> Promise,
|
||||
|
||||
getStatus: (self: Promise) -> Status,
|
||||
now: (self: Promise, rejectionValue: any?) -> Promise,
|
||||
tap: (self: Promise, tapHandler: (...any) -> ...any) -> Promise,
|
||||
timeout: (self: Promise, seconds: number, rejectionValue: any?) -> Promise,
|
||||
}
|
||||
export type TypedPromise<T...> = {
|
||||
andThen: (self: Promise, successHandler: (T...) -> ...any, failureHandler: ((...any) -> ...any)?) -> Promise,
|
||||
andThenCall: <TArgs...>(self: Promise, callback: (TArgs...) -> ...any, TArgs...) -> Promise,
|
||||
andThenReturn: (self: Promise, ...any) -> Promise,
|
||||
|
||||
await: (self: Promise) -> (boolean, T...),
|
||||
awaitStatus: (self: Promise) -> (Status, T...),
|
||||
|
||||
cancel: (self: Promise) -> (),
|
||||
catch: (self: Promise, failureHandler: (...any) -> ...any) -> Promise,
|
||||
expect: (self: Promise) -> T...,
|
||||
|
||||
finally: (self: Promise, finallyHandler: (status: Status) -> ...any) -> Promise,
|
||||
finallyCall: <TArgs...>(self: Promise, callback: (TArgs...) -> ...any, TArgs...) -> Promise,
|
||||
finallyReturn: (self: Promise, ...any) -> Promise,
|
||||
|
||||
getStatus: (self: Promise) -> Status,
|
||||
now: (self: Promise, rejectionValue: any?) -> Promise,
|
||||
tap: (self: Promise, tapHandler: (T...) -> ...any) -> Promise,
|
||||
timeout: (self: Promise, seconds: number, rejectionValue: any?) -> TypedPromise<T...>,
|
||||
}
|
||||
|
||||
type Signal<T...> = {
|
||||
Connect: (self: Signal<T...>, callback: (T...) -> ...any) -> SignalConnection,
|
||||
}
|
||||
|
||||
type SignalConnection = {
|
||||
Disconnect: (self: SignalConnection) -> ...any,
|
||||
[any]: any,
|
||||
}
|
||||
export type PromiseStatic = {
|
||||
Error: ErrorStatic,
|
||||
Status: {
|
||||
Started: "Started",
|
||||
Resolved: "Resolved",
|
||||
Rejected: "Rejected",
|
||||
Cancelled: "Cancelled",
|
||||
},
|
||||
|
||||
all: <T>(promises: {TypedPromise<T>}) -> TypedPromise<{T}>,
|
||||
allSettled: <T>(promise: {TypedPromise<T>}) -> TypedPromise<{Status}>,
|
||||
any: <T>(promise: {TypedPromise<T>}) -> TypedPromise<T>,
|
||||
defer: <TReturn...>(
|
||||
executor: (
|
||||
resolve: (TReturn...) -> (),
|
||||
reject: (...any) -> (),
|
||||
onCancel: (abortHandler: (() -> ())?) -> boolean
|
||||
) -> ()
|
||||
) -> TypedPromise<TReturn...>,
|
||||
delay: (seconds: number) -> TypedPromise<number>,
|
||||
each: <T, TReturn>(
|
||||
list: {T | TypedPromise<T>},
|
||||
predicate: (value: T, index: number) -> TReturn | TypedPromise<TReturn>
|
||||
) -> TypedPromise<{TReturn}>,
|
||||
fold: <T, TReturn>(
|
||||
list: {T | TypedPromise<T>},
|
||||
reducer: (accumulator: TReturn, value: T, index: number) -> TReturn | TypedPromise<TReturn>
|
||||
) -> TypedPromise<TReturn>,
|
||||
fromEvent: <TReturn...>(
|
||||
event: Signal<TReturn...>,
|
||||
predicate: ((TReturn...) -> boolean)?
|
||||
) -> TypedPromise<TReturn...>,
|
||||
is: (object: any) -> boolean,
|
||||
new: <TReturn...>(
|
||||
executor: (
|
||||
resolve: (TReturn...) -> (),
|
||||
reject: (...any) -> (),
|
||||
onCancel: (abortHandler: (() -> ())?) -> boolean
|
||||
) -> ()
|
||||
) -> TypedPromise<TReturn...>,
|
||||
onUnhandledRejection: (callback: (promise: TypedPromise<any>, ...any) -> ()) -> () -> (),
|
||||
promisify: <TArgs..., TReturn...>(callback: (TArgs...) -> TReturn...) -> (TArgs...) -> TypedPromise<TReturn...>,
|
||||
race: <T>(promises: {TypedPromise<T>}) -> TypedPromise<T>,
|
||||
reject: (...any) -> TypedPromise<...any>,
|
||||
resolve: <TReturn...>(TReturn...) -> TypedPromise<TReturn...>,
|
||||
retry: <TArgs..., TReturn...>(
|
||||
callback: (TArgs...) -> TypedPromise<TReturn...>,
|
||||
times: number,
|
||||
TArgs...
|
||||
) -> TypedPromise<TReturn...>,
|
||||
retryWithDelay: <TArgs..., TReturn...>(
|
||||
callback: (TArgs...) -> TypedPromise<TReturn...>,
|
||||
times: number,
|
||||
seconds: number,
|
||||
TArgs...
|
||||
) -> TypedPromise<TReturn...>,
|
||||
some: <T>(promise: {TypedPromise<T>}, count: number) -> TypedPromise<{T}>,
|
||||
try: <TArgs..., TReturn...>(callback: (TArgs...) -> TReturn..., TArgs...) -> TypedPromise<TReturn...>,
|
||||
}
|
||||
|
||||
return Promise :: PromiseStatic?
|
||||
BIN
src/shared/shared/ModuleLoader/ActorForClient.rbxm
Normal file
BIN
src/shared/shared/ModuleLoader/ActorForClient.rbxm
Normal file
Binary file not shown.
BIN
src/shared/shared/ModuleLoader/ActorForServer.rbxm
Normal file
BIN
src/shared/shared/ModuleLoader/ActorForServer.rbxm
Normal file
Binary file not shown.
83
src/shared/shared/ModuleLoader/ParallelModuleLoader.luau
Normal file
83
src/shared/shared/ModuleLoader/ParallelModuleLoader.luau
Normal file
@ -0,0 +1,83 @@
|
||||
--!strict
|
||||
--@author: crusherfire
|
||||
--@date: 5/8/24
|
||||
--[[@description:
|
||||
General code for parallel scripts.
|
||||
]]
|
||||
-- Since this module will be required by scripts in separate actors, these variables won't be shared!
|
||||
local require = require
|
||||
local requiredModule
|
||||
local function onRequireModule(script: BaseScript, module: ModuleScript)
|
||||
local success, result = pcall(function()
|
||||
return require(module)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
warn("Parallel module errored!\n", result)
|
||||
script:SetAttribute("Errored", true)
|
||||
script:SetAttribute("Required", true)
|
||||
return
|
||||
end
|
||||
requiredModule = result
|
||||
script:SetAttribute("Required", true)
|
||||
end
|
||||
|
||||
local function onInitModule(script: BaseScript)
|
||||
if script:GetAttribute("Errored") then
|
||||
warn("Unable to init errored module!")
|
||||
script:SetAttribute("Initialized", true)
|
||||
return
|
||||
end
|
||||
if not requiredModule then
|
||||
warn("Told to load module that does not exist!")
|
||||
script:SetAttribute("Initialized", true)
|
||||
return
|
||||
end
|
||||
if not requiredModule.Init then
|
||||
script:SetAttribute("Initialized", true)
|
||||
return
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
requiredModule:Init()
|
||||
end)
|
||||
|
||||
if not success then
|
||||
warn("Parallel module errored!\n", result)
|
||||
script:SetAttribute("Errored", true)
|
||||
script:SetAttribute("Initialized", true)
|
||||
return
|
||||
end
|
||||
script:SetAttribute("Initialized", true)
|
||||
end
|
||||
|
||||
local function onStartModule(script: BaseScript)
|
||||
if script:GetAttribute("Errored") then
|
||||
warn("Unable to start errored module!")
|
||||
script:SetAttribute("Started", true)
|
||||
return
|
||||
end
|
||||
if not requiredModule then
|
||||
warn("Told to start module that does not exist!")
|
||||
script:SetAttribute("Started", true)
|
||||
return
|
||||
end
|
||||
if not requiredModule.Start then
|
||||
script:SetAttribute("Started", true)
|
||||
return
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
requiredModule:Start()
|
||||
end)
|
||||
|
||||
if not success then
|
||||
warn("Parallel module errored!\n", result)
|
||||
script:SetAttribute("Errored", true)
|
||||
script:SetAttribute("Started", true)
|
||||
return
|
||||
end
|
||||
script:SetAttribute("Started", true)
|
||||
end
|
||||
|
||||
return { onRequireModule = onRequireModule, onInitModule = onInitModule, onStartModule = onStartModule }
|
||||
10
src/shared/shared/ModuleLoader/RelocatedTemplate.luau
Normal file
10
src/shared/shared/ModuleLoader/RelocatedTemplate.luau
Normal file
@ -0,0 +1,10 @@
|
||||
--!strict
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local RELOCATED_FOLDER = ServerScriptService:FindFirstChild("RELOCATED_MODULES")
|
||||
assert(RELOCATED_FOLDER, "ServerScriptService missing 'RELOCATE_MODULES' folder")
|
||||
|
||||
local module = RELOCATED_FOLDER:FindFirstChild(script.Name)
|
||||
assert(module, `RELOCATED_MODULES folder missing module '{script.Name}'`)
|
||||
|
||||
return require(module) :: any
|
||||
759
src/shared/shared/ModuleLoader/init.luau
Normal file
759
src/shared/shared/ModuleLoader/init.luau
Normal file
@ -0,0 +1,759 @@
|
||||
--!strict
|
||||
--[[
|
||||
----
|
||||
crusherfire's Module Loader!
|
||||
08/05/2025
|
||||
----
|
||||
|
||||
-- FEATURES --
|
||||
"LoaderPriority" number attribute:
|
||||
Set this attribute on a ModuleScript to modify the loading priority. Larger number == higher priority.
|
||||
|
||||
"RelocateToServerScriptService" boolean attribute:
|
||||
Relocates a module to ServerScriptService and leave a pointer in its place to hide server code from clients.
|
||||
This should only be performed on server-only modules that you want to organize within the same containers in Studio.
|
||||
|
||||
"ClientOnly" or "ServerOnly" boolean attributes:
|
||||
Allows you to restrict a module from being loaded on a specific run context.
|
||||
|
||||
"Parallel" boolean attribute:
|
||||
Requires the module from another script within its own actor for executing code in parallel.
|
||||
|
||||
"IgnoreLoader" boolean attribute:
|
||||
Allows you to prevent a module from being loaded.
|
||||
|
||||
Supports CollectionService tags by placing the tag name (defined by LoaderTag attribute) on ModuleScript instances.
|
||||
|
||||
-- LOADER ATTRIBUTE SETTINGS --
|
||||
ClientWaitForServer: Client avoids starting the module loading process until the server finishes.
|
||||
FolderSearchDepth: How deep the loader will search for modules to load in a folder. '1' represents the direct children.
|
||||
LoaderTag: Tag to be set on modules you want to load that are not within loaded containers.
|
||||
VerboseLoading: If messages should be output that document the loading process
|
||||
YieldThreshold: How long to wait (in seconds) before displaying warning messages when a particular module is yielidng for too long
|
||||
UseCollectionService: If the loader should search for modules to load based on LoaderTag
|
||||
|
||||
-- DEFAULT FILTERING BEHAVIOR --
|
||||
Respects "ClientOnly", "ServerOnly", and "IgnoreLoader" attributes.
|
||||
|
||||
Modules that are not a direct child of a given container or whose ancestry are not folders
|
||||
that lead back to a container will not be loaded when the FolderSearchDepth is a larger value.
|
||||
|
||||
NOTE:
|
||||
If you do not like this default filtering behavior, you can pass your own filtering predicate to the StartCustom() function
|
||||
and define your own behavior. Otherwise, use the Start() function for the default behavior!
|
||||
--------------
|
||||
]]
|
||||
|
||||
-----------------------------
|
||||
-- SERVICES --
|
||||
-----------------------------
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
local CollectionService = game:GetService("CollectionService")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
-----------------------------
|
||||
-- VARIABLES --
|
||||
-----------------------------
|
||||
local parallelModuleLoader = script.ParallelModuleLoader
|
||||
local actorForServer: Actor? = script:FindFirstChild("ActorForServer") :: any
|
||||
local actorForClient: Actor? = script:FindFirstChild("ActorForClient") :: any
|
||||
local isClient = RunService:IsClient()
|
||||
local require = require
|
||||
local loadedEvent: RemoteEvent
|
||||
if isClient then
|
||||
loadedEvent = script:WaitForChild("LoadedEvent")
|
||||
else
|
||||
loadedEvent = Instance.new("RemoteEvent")
|
||||
loadedEvent.Name = "LoadedEvent"
|
||||
loadedEvent.Parent = script
|
||||
end
|
||||
|
||||
local started = false
|
||||
|
||||
local tracker = {
|
||||
Load = {} :: { [ModuleScript]: any },
|
||||
Init = {} :: { [ModuleScript]: boolean },
|
||||
Start = {} :: { [ModuleScript]: boolean }
|
||||
}
|
||||
|
||||
local trackerForActors = {
|
||||
Load = {} :: { [ModuleScript]: Actor },
|
||||
Init = {},
|
||||
Start = {}
|
||||
}
|
||||
|
||||
export type LoaderSettings = {
|
||||
FOLDER_SEARCH_DEPTH: number?,
|
||||
YIELD_THRESHOLD: number?,
|
||||
VERBOSE_LOADING: boolean?,
|
||||
WAIT_FOR_SERVER: boolean?,
|
||||
USE_COLLECTION_SERVICE: boolean?,
|
||||
}
|
||||
|
||||
export type KeepModulePredicate = (container: Instance, module: ModuleScript) -> (boolean)
|
||||
|
||||
-- CONSTANTS --
|
||||
local SETTINGS: LoaderSettings = {
|
||||
FOLDER_SEARCH_DEPTH = script:GetAttribute("FolderSearchDepth"),
|
||||
YIELD_THRESHOLD = script:GetAttribute("YieldThreshold"), -- how long until the module starts warning for a module that is taking too long
|
||||
VERBOSE_LOADING = script:GetAttribute("VerboseLoading"),
|
||||
WAIT_FOR_SERVER = script:GetAttribute("ClientWaitForServer"),
|
||||
USE_COLLECTION_SERVICE = script:GetAttribute("UseCollectionService"),
|
||||
}
|
||||
|
||||
local PRINT_IDENTIFIER = if isClient then "[C]" else "[S]"
|
||||
local LOADED_IDENTIFIER = if isClient then "Client" else "Server"
|
||||
local ACTOR_PARENT = if isClient then Players.LocalPlayer.PlayerScripts else game:GetService("ServerScriptService")
|
||||
local TAG = script:GetAttribute("LoaderTag")
|
||||
local RELOCATED_MODULES do
|
||||
if RunService:IsServer() then
|
||||
RELOCATED_MODULES = Instance.new("Folder")
|
||||
RELOCATED_MODULES.Name = "RELOCATED_MODULES"
|
||||
RELOCATED_MODULES.Parent = ServerScriptService
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------
|
||||
-- PRIVATE FUNCTIONS --
|
||||
-----------------------------
|
||||
|
||||
-- <strong><code>!YIELDS!</code></strong>
|
||||
local function waitForEither<Func, T...>(eventYes: RBXScriptSignal, eventNo: RBXScriptSignal): boolean
|
||||
local thread = coroutine.running()
|
||||
|
||||
local connection1: any = nil
|
||||
local connection2: any = nil
|
||||
|
||||
connection1 = eventYes:Once(function(...)
|
||||
if connection1 == nil then
|
||||
return
|
||||
end
|
||||
|
||||
connection1:Disconnect()
|
||||
connection2:Disconnect()
|
||||
connection1 = nil
|
||||
connection2 = nil
|
||||
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
task.spawn(thread, true, ...)
|
||||
end
|
||||
end)
|
||||
|
||||
connection2 = eventNo:Once(function(...)
|
||||
if connection2 == nil then
|
||||
return
|
||||
end
|
||||
|
||||
connection1:Disconnect()
|
||||
connection2:Disconnect()
|
||||
connection1 = nil
|
||||
connection2 = nil
|
||||
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
task.spawn(thread, false, ...)
|
||||
end
|
||||
end)
|
||||
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
local function copy<T>(t: T, deep: boolean?): T
|
||||
if not deep then
|
||||
return (table.clone(t :: any) :: any) :: T
|
||||
end
|
||||
local function deepCopy(object: any)
|
||||
assert(typeof(object) == "table", "Expected table for deepCopy!")
|
||||
-- Returns a deep copy of the provided table.
|
||||
local newObject = setmetatable({}, getmetatable(object)) -- Clone metaData
|
||||
|
||||
for index: any, value: any in object do
|
||||
if typeof(value) == "table" then
|
||||
newObject[index] = deepCopy(value)
|
||||
continue
|
||||
end
|
||||
|
||||
newObject[index] = value
|
||||
end
|
||||
|
||||
return newObject
|
||||
end
|
||||
return deepCopy(t :: any) :: T
|
||||
end
|
||||
|
||||
local function reconcile<S, T>(src: S, template: T): S & T
|
||||
assert(type(src) == "table", "First argument must be a table")
|
||||
assert(type(template) == "table", "Second argument must be a table")
|
||||
|
||||
local tbl = table.clone(src)
|
||||
|
||||
for k, v in template do
|
||||
local sv = src[k]
|
||||
if sv == nil then
|
||||
if type(v) == "table" then
|
||||
tbl[k] = copy(v, true)
|
||||
else
|
||||
tbl[k] = v
|
||||
end
|
||||
elseif type(sv) == "table" then
|
||||
if type(v) == "table" then
|
||||
tbl[k] = reconcile(sv, v)
|
||||
else
|
||||
tbl[k] = copy(sv, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return (tbl :: any) :: S & T
|
||||
end
|
||||
|
||||
-- Returns a new array that is the result of array1 and array2
|
||||
local function mergeArrays(array1: {[number]: any}, array2: {[number]: any})
|
||||
local length = #array2
|
||||
local newArray = table.clone(array2)
|
||||
for i, v in ipairs(array1) do
|
||||
newArray[length + i] = v
|
||||
end
|
||||
return newArray
|
||||
end
|
||||
|
||||
local function filter<T>(t: { T }, predicate: (T, any, { T }) -> boolean): { T }
|
||||
assert(type(t) == "table", "First argument must be a table")
|
||||
assert(type(predicate) == "function", "Second argument must be a function")
|
||||
local newT = table.create(#t)
|
||||
if #t > 0 then
|
||||
local n = 0
|
||||
for i, v in t do
|
||||
if predicate(v, i, t) then
|
||||
n += 1
|
||||
newT[n] = v
|
||||
end
|
||||
end
|
||||
else
|
||||
for k, v in t do
|
||||
if predicate(v, k, t) then
|
||||
newT[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return newT
|
||||
end
|
||||
|
||||
-- Returns the 'depth' of <code>descendant</code> in the child hierarchy of <code>root</code>.
|
||||
-- If the descendant is not found in <code>root</code>, then this function will return 0.
|
||||
local function getDepthInHierarchy(descendant: Instance, root: Instance): number
|
||||
local depth = 0
|
||||
local current: Instance? = descendant
|
||||
while current and current ~= root do
|
||||
current = current.Parent
|
||||
depth += 1
|
||||
end
|
||||
if not current then
|
||||
depth = 0
|
||||
end
|
||||
return depth
|
||||
end
|
||||
|
||||
local function findAllFromClass(class: string, searchIn: Instance, searchDepth: number?): { any }
|
||||
assert(class and typeof(class) == "string", "class is invalid or nil")
|
||||
assert(searchIn and typeof(searchIn) == "Instance", "searchIn is invalid or nil")
|
||||
|
||||
local foundObjects = {}
|
||||
|
||||
if searchDepth then
|
||||
for _, object in pairs(searchIn:GetDescendants()) do
|
||||
if object:IsA(class) and getDepthInHierarchy(object, searchIn) <= searchDepth then
|
||||
table.insert(foundObjects, object)
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, object in pairs(searchIn:GetDescendants()) do
|
||||
if object:IsA(class) then
|
||||
table.insert(foundObjects, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return foundObjects
|
||||
end
|
||||
|
||||
local function keepModule(container: Instance, module: ModuleScript): boolean
|
||||
if module:GetAttribute("ClientOnly") and RunService:IsServer() then
|
||||
return false
|
||||
elseif module:GetAttribute("ServerOnly") and RunService:IsClient() then
|
||||
return false
|
||||
elseif module:GetAttribute("IgnoreLoader") then
|
||||
return false
|
||||
end
|
||||
local ancestor = module.Parent
|
||||
while ancestor do
|
||||
if ancestor == container then
|
||||
-- The ancestry should eventually lead to the container (if ancestors were always folders)
|
||||
return true
|
||||
elseif not ancestor:IsA("Folder") then
|
||||
return false
|
||||
end
|
||||
ancestor = ancestor.Parent
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function newPrint(...)
|
||||
print(PRINT_IDENTIFIER, ...)
|
||||
end
|
||||
|
||||
local function newWarn(...)
|
||||
warn(PRINT_IDENTIFIER, ...)
|
||||
end
|
||||
|
||||
local function loadModule(module: ModuleScript)
|
||||
-- attempts to relocate the module, if eligible
|
||||
local function attemptRelocate(module: ModuleScript)
|
||||
if RunService:IsClient() then
|
||||
return
|
||||
end
|
||||
if not module:GetAttribute("RelocateToServerScriptService") then
|
||||
return
|
||||
end
|
||||
if module:IsDescendantOf(ServerScriptService) then
|
||||
warn(`RelocateToServerScriptService attribute is enabled on module '{module:GetFullName()}' that's already in ServerScriptService`)
|
||||
return
|
||||
end
|
||||
local clone = script.RelocatedTemplate:Clone()
|
||||
clone.Name = module.Name
|
||||
clone:SetAttribute("ServerOnly", true)
|
||||
clone.Parent = module.Parent
|
||||
module.Parent = RELOCATED_MODULES
|
||||
end
|
||||
|
||||
if module:GetAttribute("Parallel") then
|
||||
local actorTemplate = if isClient then actorForClient else actorForServer
|
||||
|
||||
if actorTemplate == nil then
|
||||
newWarn(`Parallel module {module.Name} requested but no Actor template is configured - loading normally`)
|
||||
else
|
||||
-- This module needs to be run in parallel, so create new actor and script.
|
||||
local newActorSystem = actorTemplate:Clone()
|
||||
local loaderClone = parallelModuleLoader:Clone()
|
||||
loaderClone.Parent = newActorSystem
|
||||
local actorScript: BaseScript = newActorSystem:FindFirstChildWhichIsA("BaseScript") :: any
|
||||
|
||||
actorScript.Enabled = true
|
||||
actorScript.Name = `Required{module.Name}`
|
||||
newActorSystem.Parent = ACTOR_PARENT
|
||||
|
||||
if not actorScript:GetAttribute("Loaded") then
|
||||
actorScript:GetAttributeChangedSignal("Loaded"):Wait()
|
||||
end
|
||||
|
||||
newActorSystem:SendMessage("RequireModule", module)
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Loading PARALLEL module '%s'"):format(module.Name))
|
||||
end
|
||||
|
||||
local startTime = tick()
|
||||
if not actorScript:GetAttribute("Required") then
|
||||
actorScript:GetAttributeChangedSignal("Required"):Wait()
|
||||
end
|
||||
local endTime = tick()
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then
|
||||
newPrint(`>> Loaded PARALLEL module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif actorScript:GetAttribute("Errored") then
|
||||
newWarn(
|
||||
`>> Failed to load PARALLEL module {module.Name}`,
|
||||
("(took %.3f seconds)"):format(endTime - startTime)
|
||||
)
|
||||
end
|
||||
|
||||
-- relocate after loading to maintain relative paths within modules
|
||||
attemptRelocate(module)
|
||||
|
||||
trackerForActors.Load[module] = newActorSystem
|
||||
tracker.Load[module] = true
|
||||
tracker.Init[module] = true
|
||||
tracker.Start[module] = true
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Loading module '%s'"):format(module.Name))
|
||||
end
|
||||
local mainThread = coroutine.running()
|
||||
local startTime = tick()
|
||||
local endTime
|
||||
local executionSuccess, errMsg = false, ""
|
||||
local thread: thread = task.spawn(function()
|
||||
debug.setmemorycategory(`Module::{module.Name}`)
|
||||
local success, result = xpcall(function()
|
||||
return require(module)
|
||||
end, debug.traceback)
|
||||
debug.resetmemorycategory()
|
||||
if success then
|
||||
tracker.Load[module] = result
|
||||
if result.Init then
|
||||
tracker.Init[module] = false
|
||||
end
|
||||
if result.Start then
|
||||
tracker.Start[module] = false
|
||||
end
|
||||
executionSuccess = true
|
||||
|
||||
-- relocate after loading to maintain relative paths within modules
|
||||
attemptRelocate(module)
|
||||
else
|
||||
errMsg = result
|
||||
end
|
||||
endTime = tick()
|
||||
if coroutine.status(mainThread) == "suspended" then
|
||||
task.spawn(mainThread)
|
||||
end
|
||||
end)
|
||||
if not endTime then
|
||||
endTime = tick()
|
||||
end
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
local loopThread = task.spawn(function()
|
||||
task.wait(SETTINGS.YIELD_THRESHOLD)
|
||||
while true do
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
newWarn(`>> Loading Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime))
|
||||
end
|
||||
task.wait(5)
|
||||
end
|
||||
end)
|
||||
coroutine.yield()
|
||||
if coroutine.status(loopThread) ~= "dead" then
|
||||
task.cancel(loopThread)
|
||||
end
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and executionSuccess then
|
||||
newPrint(`>> Loaded module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif not executionSuccess then
|
||||
newWarn(`>> Failed to load module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg))
|
||||
end
|
||||
end
|
||||
|
||||
local function initializeModule(loadedModule, module: ModuleScript)
|
||||
if trackerForActors.Load[module] then
|
||||
local actorScript: BaseScript = trackerForActors.Load[module]:FindFirstChildWhichIsA("BaseScript") :: any
|
||||
trackerForActors.Load[module]:SendMessage("InitModule")
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Initializing PARALLEL module '%s'"):format(actorScript.Name))
|
||||
end
|
||||
|
||||
local startTime = tick()
|
||||
if not actorScript:GetAttribute("Initialized") then
|
||||
actorScript:GetAttributeChangedSignal("Initialized"):Wait()
|
||||
end
|
||||
local endTime = tick()
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then
|
||||
newPrint(`>> Initialized PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif actorScript:GetAttribute("Errored") then
|
||||
newWarn(`>> Failed to init PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if not loadedModule.Init then
|
||||
return
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Initializing module '%s'"):format(module.Name))
|
||||
end
|
||||
local mainThread = coroutine.running()
|
||||
local startTime = tick()
|
||||
local endTime
|
||||
local executionSuccess, errMsg = false, ""
|
||||
local thread: thread = task.spawn(function()
|
||||
local success, err = xpcall(function()
|
||||
loadedModule:Init()
|
||||
end, function(err)
|
||||
return `{err}\n{debug.traceback()}`
|
||||
end)
|
||||
executionSuccess = success
|
||||
if success then
|
||||
tracker.Init[module] = true
|
||||
else
|
||||
errMsg = err
|
||||
end
|
||||
endTime = tick()
|
||||
if coroutine.status(mainThread) == "suspended" then
|
||||
task.spawn(mainThread)
|
||||
end
|
||||
end)
|
||||
if not endTime then
|
||||
endTime = tick()
|
||||
end
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
local loopThread = task.spawn(function()
|
||||
task.wait(SETTINGS.YIELD_THRESHOLD)
|
||||
while true do
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
newWarn(`>> :Init() for Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime))
|
||||
end
|
||||
task.wait(5)
|
||||
end
|
||||
end)
|
||||
coroutine.yield()
|
||||
if coroutine.status(loopThread) ~= "dead" then
|
||||
task.cancel(loopThread)
|
||||
end
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and executionSuccess then
|
||||
newPrint(`>> Initialized module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif not executionSuccess then
|
||||
newWarn(`>> Failed to init module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg))
|
||||
end
|
||||
end
|
||||
|
||||
local function startModule(loadedModule, module: ModuleScript)
|
||||
if trackerForActors.Load[module] then
|
||||
local actorScript: BaseScript = trackerForActors.Load[module]:FindFirstChildWhichIsA("BaseScript") :: any
|
||||
trackerForActors.Load[module]:SendMessage("StartModule")
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Starting PARALLEL module '%s'"):format(actorScript.Name))
|
||||
end
|
||||
|
||||
local startTime = tick()
|
||||
if not actorScript:GetAttribute("Started") then
|
||||
actorScript:GetAttributeChangedSignal("Started"):Wait()
|
||||
end
|
||||
local endTime = tick()
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and not actorScript:GetAttribute("Errored") then
|
||||
newPrint(`>> Started PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif actorScript:GetAttribute("Errored") then
|
||||
newWarn(`>> Failed to start PARALLEL module {actorScript.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if not loadedModule.Start then
|
||||
return
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newPrint(("Starting module '%s'"):format(module.Name))
|
||||
end
|
||||
local mainThread = coroutine.running()
|
||||
local startTime = tick()
|
||||
local endTime
|
||||
local executionSuccess, errMsg = false, ""
|
||||
local thread: thread = task.spawn(function()
|
||||
local success, err = xpcall(function()
|
||||
loadedModule:Start()
|
||||
end, function(err)
|
||||
return `{err}\n{debug.traceback()}`
|
||||
end)
|
||||
executionSuccess = success
|
||||
if success then
|
||||
tracker.Start[module] = true
|
||||
else
|
||||
errMsg = err
|
||||
end
|
||||
endTime = tick()
|
||||
if coroutine.status(mainThread) == "suspended" then
|
||||
task.spawn(mainThread)
|
||||
end
|
||||
end)
|
||||
if not endTime then
|
||||
endTime = tick()
|
||||
end
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
local loopThread = task.spawn(function()
|
||||
task.wait(SETTINGS.YIELD_THRESHOLD)
|
||||
while true do
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
newWarn(`>> :Start() for Module '{module.Name}' is taking a while!`, ("(%.3f seconds elapsed)"):format(tick() - startTime))
|
||||
end
|
||||
task.wait(5)
|
||||
end
|
||||
end)
|
||||
coroutine.yield()
|
||||
if coroutine.status(loopThread) ~= "dead" then
|
||||
task.cancel(loopThread)
|
||||
end
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING and executionSuccess then
|
||||
newPrint(`>> Started module {module.Name}`, ("(took %.3f seconds)"):format(endTime - startTime))
|
||||
elseif not executionSuccess then
|
||||
newWarn(`>> Failed to start module {module.Name}`, ("(took %.3f seconds)\n%s"):format(endTime - startTime, errMsg))
|
||||
end
|
||||
end
|
||||
|
||||
-- Gets all modules to be loaded in order.
|
||||
local function getModules(containers: { Instance }): { ModuleScript }
|
||||
local totalModules = {}
|
||||
for _, container in ipairs(containers) do
|
||||
local modules = findAllFromClass("ModuleScript", container, SETTINGS.FOLDER_SEARCH_DEPTH)
|
||||
modules = filter(modules, function(module)
|
||||
return keepModule(container, module)
|
||||
end)
|
||||
totalModules = mergeArrays(totalModules, modules)
|
||||
end
|
||||
if SETTINGS.USE_COLLECTION_SERVICE and TAG ~= "" then
|
||||
for _, module in CollectionService:GetTagged(TAG) do
|
||||
if not module:IsA("ModuleScript") then
|
||||
warn(`item: {module} with tag: {TAG} is not a module script!`)
|
||||
continue
|
||||
end
|
||||
if not keepModule(module.Parent, module) then
|
||||
continue
|
||||
end
|
||||
if table.find(totalModules, module) then
|
||||
continue
|
||||
end
|
||||
table.insert(totalModules, module)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(totalModules, function(a, b)
|
||||
local aPriority = a:GetAttribute("LoaderPriority") or 0
|
||||
local bPriority = b:GetAttribute("LoaderPriority") or 0
|
||||
|
||||
return aPriority > bPriority
|
||||
end)
|
||||
return totalModules
|
||||
end
|
||||
|
||||
-----------------------------
|
||||
-- MAIN --
|
||||
-----------------------------
|
||||
|
||||
--[[
|
||||
Starts the loader with the default module filtering behavior.
|
||||
]]
|
||||
local function start(...: Instance)
|
||||
assert(not started, "attempt to start module loader more than once")
|
||||
started = true
|
||||
local containers = {...}
|
||||
if isClient and SETTINGS.WAIT_FOR_SERVER and not workspace:GetAttribute("ServerLoaded") then
|
||||
workspace:GetAttributeChangedSignal("ServerLoaded"):Wait()
|
||||
end
|
||||
|
||||
if SETTINGS.VERBOSE_LOADING then
|
||||
newWarn("=== LOADING MODULES ===")
|
||||
local modules = getModules(containers)
|
||||
for _, module in modules do
|
||||
loadModule(module)
|
||||
end
|
||||
|
||||
newWarn("=== INITIALIZING MODULES ===")
|
||||
for _, module in modules do
|
||||
if not tracker.Load[module] then
|
||||
continue
|
||||
end
|
||||
initializeModule(tracker.Load[module], module)
|
||||
end
|
||||
|
||||
newWarn("=== STARTING MODULES ===")
|
||||
for _, module in modules do
|
||||
if not tracker.Load[module] then
|
||||
continue
|
||||
end
|
||||
startModule(tracker.Load[module], module)
|
||||
end
|
||||
|
||||
newWarn("=== LOADING FINISHED ===")
|
||||
else
|
||||
local modules = getModules(containers)
|
||||
for _, module in modules do
|
||||
loadModule(module)
|
||||
end
|
||||
for _, module in modules do
|
||||
if not tracker.Load[module] then
|
||||
continue
|
||||
end
|
||||
initializeModule(tracker.Load[module], module)
|
||||
end
|
||||
for _, module in modules do
|
||||
if not tracker.Load[module] then
|
||||
continue
|
||||
end
|
||||
startModule(tracker.Load[module], module)
|
||||
end
|
||||
end
|
||||
|
||||
workspace:SetAttribute(`{LOADED_IDENTIFIER}LoadedTimestamp`, workspace:GetServerTimeNow())
|
||||
workspace:SetAttribute(`{LOADED_IDENTIFIER}Loaded`, true)
|
||||
if RunService:IsClient() then
|
||||
loadedEvent:FireServer()
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Starts the loader with your own custom module filtering behavior for determining what modules should be loaded.
|
||||
]]
|
||||
local function startCustom(shouldKeep: KeepModulePredicate, ...: Instance)
|
||||
keepModule = shouldKeep
|
||||
start(...)
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns if the client finished loading, initializing, and starting all modules.
|
||||
]]
|
||||
local function isClientLoaded(player: Player): boolean
|
||||
return player:GetAttribute("_ModulesLoaded") == true
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns if the server finished loading, initializing, and starting all modules.
|
||||
]]
|
||||
local function isServerLoaded(): boolean
|
||||
return workspace:GetAttribute("ServerLoaded") == true
|
||||
end
|
||||
|
||||
--[[
|
||||
<strong><code>!YIELDS!</code></strong>
|
||||
Yields until the client has loaded all their modules.
|
||||
Returns true if loaded or returns false if player left.
|
||||
]]
|
||||
local function waitForLoadedClient(player: Player): boolean
|
||||
if not player:GetAttribute("_ModulesLoaded") then
|
||||
return waitForEither(player:GetAttributeChangedSignal("_ModulesLoaded"), player:GetPropertyChangedSignal("Parent"))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Modify the default settings determined by the attributes on the module loader.
|
||||
The given <code>settings</code> are reconciled with the current settings.
|
||||
]]
|
||||
local function changeSettings(settings: LoaderSettings)
|
||||
SETTINGS = reconcile(settings, SETTINGS)
|
||||
end
|
||||
|
||||
--[[
|
||||
Errors if the server is not loaded yet.
|
||||
]]
|
||||
local function getServerLoadedTimestamp()
|
||||
assert(isServerLoaded(), "server is not loaded yet!")
|
||||
return workspace:GetAttribute("ServerLoadedTimestamp")
|
||||
end
|
||||
|
||||
if not isClient then
|
||||
loadedEvent.OnServerEvent:Connect(function(player)
|
||||
player:SetAttribute("_ModulesLoaded", true)
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
Start = start,
|
||||
StartCustom = startCustom,
|
||||
ChangeSettings = changeSettings,
|
||||
IsServerLoaded = isServerLoaded,
|
||||
IsClientLoaded = isClientLoaded,
|
||||
WaitForLoadedClient = waitForLoadedClient,
|
||||
GetServerLoadedTimestamp = getServerLoadedTimestamp
|
||||
}
|
||||
10
src/shared/shared/ModuleLoader/init.meta.json
Normal file
10
src/shared/shared/ModuleLoader/init.meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"attributes": {
|
||||
"ClientWaitForServer": true,
|
||||
"FolderSearchDepth": 3.0,
|
||||
"LoaderTag": "LOAD_MODULE",
|
||||
"UseCollectionService": true,
|
||||
"VerboseLoading": false,
|
||||
"YieldThreshold": 10.0
|
||||
}
|
||||
}
|
||||
15
src/shared/shared/SharedUtils/CollisionUtils.luau
Normal file
15
src/shared/shared/SharedUtils/CollisionUtils.luau
Normal file
@ -0,0 +1,15 @@
|
||||
local CollisionUtils = {}
|
||||
|
||||
local pool = {}
|
||||
|
||||
export type CollisionObject = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function CollisionUtils.CreateObject()
|
||||
|
||||
end
|
||||
|
||||
return CollisionUtils
|
||||
202
src/shared/shared/SharedUtils/FerrUtils.luau
Normal file
202
src/shared/shared/SharedUtils/FerrUtils.luau
Normal file
@ -0,0 +1,202 @@
|
||||
local FerrUtils = {}
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
function FerrUtils.LenDict(dict) : number
|
||||
local count = 0
|
||||
|
||||
for _,key in pairs(dict or {}) do
|
||||
count += 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
|
||||
local paddingXZ = 68
|
||||
local paddingY = 18.301
|
||||
|
||||
function FerrUtils.ConvertPosition(pos,modifierY)
|
||||
local worldX = pos.X * paddingXZ
|
||||
local worldZ = pos.Y * paddingXZ
|
||||
local modifier = modifierY or 0
|
||||
return Vector3.new(worldX, paddingY + modifier, worldZ)
|
||||
end
|
||||
function FerrUtils.calculateDirection(pos, direction)-- CameraIndex: 0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X)
|
||||
if direction == 0 then return pos + Vector2.new(0, -1) end -- North
|
||||
if direction == 1 then return pos + Vector2.new(1, 0) end -- East
|
||||
if direction == 2 then return pos + Vector2.new(0, 1) end -- South
|
||||
if direction == 3 then return pos + Vector2.new(-1, 0) end -- West
|
||||
return pos
|
||||
end
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
local isClient = RunService:IsClient()
|
||||
|
||||
|
||||
|
||||
function FerrUtils.DeConvertPosition(pos : Vector3)
|
||||
local x = pos.X
|
||||
local y = pos.Z
|
||||
|
||||
if x == 0 then
|
||||
x = 0.1
|
||||
end
|
||||
if y == 0 then
|
||||
y = 1
|
||||
end
|
||||
return Vector2.new(math.round(x / paddingXZ),math.round(y / paddingXZ))
|
||||
end
|
||||
|
||||
local verbose = false
|
||||
function log(message)
|
||||
if verbose then
|
||||
print(message)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
local function stepDir(a, b)
|
||||
if a < b then return 1 end
|
||||
if a > b then return -1 end
|
||||
return 0
|
||||
end
|
||||
function FerrUtils.CreatePath(start: Vector2, finish: Vector2)
|
||||
local waypoints = {}
|
||||
|
||||
local xStep = stepDir(start.X, finish.X)
|
||||
local yStep = stepDir(start.Y, finish.Y)
|
||||
|
||||
local x, y = start.X, start.Y
|
||||
|
||||
-- Move along X first
|
||||
while x ~= finish.X do
|
||||
x += xStep
|
||||
table.insert(waypoints, FerrUtils.ConvertPosition(Vector2.new(x, y), 5))
|
||||
end
|
||||
|
||||
-- Then move along Y
|
||||
while y ~= finish.Y do
|
||||
y += yStep
|
||||
table.insert(waypoints, FerrUtils.ConvertPosition(Vector2.new(x, y), 5))
|
||||
end
|
||||
|
||||
return waypoints
|
||||
end
|
||||
function FerrUtils.GetNearestPlayer(pos : Vector3)
|
||||
local nearestPlayer = nil
|
||||
local nearestDistance = 1000000
|
||||
|
||||
for i,v in pairs(Players:GetPlayers()) do
|
||||
local char = v.Character
|
||||
if not char then
|
||||
continue
|
||||
end
|
||||
local root : Part = char:FindFirstChild("HumanoidRootPart")
|
||||
if not root then
|
||||
continue
|
||||
end
|
||||
|
||||
local distance = (root.Position - pos).Magnitude
|
||||
|
||||
if distance < nearestDistance then
|
||||
nearestDistance = distance
|
||||
nearestPlayer = v
|
||||
end
|
||||
end
|
||||
return nearestPlayer,nearestDistance
|
||||
end
|
||||
function FerrUtils.GetClosest(values,x)
|
||||
|
||||
local closest = values[1]
|
||||
local minDist = math.abs(x - closest)
|
||||
|
||||
for i = 2, #values do
|
||||
local d = math.abs(x - values[i])
|
||||
if d < minDist then
|
||||
minDist = d
|
||||
closest = values[i]
|
||||
end
|
||||
end
|
||||
|
||||
return closest
|
||||
|
||||
end
|
||||
function FerrUtils.SetCollisionGroup(Descendants,group)
|
||||
for i,v in pairs(Descendants) do
|
||||
if v.ClassName == "Part" or v.ClassName == "MeshPart" then
|
||||
v.CollisionGroup = group
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FerrUtils.GetParent(instance)
|
||||
if not instance then
|
||||
return nil
|
||||
end
|
||||
|
||||
local current = instance
|
||||
while current do
|
||||
if current:GetAttribute("PARENT") then
|
||||
return current
|
||||
end
|
||||
current = current.Parent
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
function FerrUtils.EffectDuration(object,property,value,duration)
|
||||
object[property] += value
|
||||
task.wait(duration)
|
||||
object[property] -= value
|
||||
end
|
||||
function FerrUtils.AddMax(ogValue,sumValue,max)
|
||||
local returnVal
|
||||
if ogValue + sumValue > max then
|
||||
returnVal = max
|
||||
else
|
||||
returnVal = ogValue + sumValue
|
||||
end
|
||||
return returnVal
|
||||
end
|
||||
function FerrUtils.SubMax(ogValue,subValue,min)
|
||||
local returnVal
|
||||
if ogValue - subValue < min then
|
||||
returnVal = min
|
||||
else
|
||||
returnVal = ogValue - subValue
|
||||
end
|
||||
return returnVal
|
||||
end
|
||||
|
||||
|
||||
function FerrUtils.TakeDamage(humanoid,damage)
|
||||
humanoid:TakeDamage(damage)
|
||||
return humanoid.Health <= 0
|
||||
end
|
||||
|
||||
function FerrUtils.UnMarkModel(Model,name)
|
||||
Model:SetAttribute("PARENT",nil)
|
||||
Model:SetAttribute(name,nil)
|
||||
|
||||
for i,v : Instance in pairs(Model:GetDescendants()) do
|
||||
if v.ClassName == "Part" or v.ClassName == "MeshPart" then
|
||||
v:SetAttribute(name,nil)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
function FerrUtils.MarkModel(Model,name,value)
|
||||
Model:SetAttribute("PARENT",true)
|
||||
Model:SetAttribute(name,value)
|
||||
|
||||
for i,v : Instance in pairs(Model:GetDescendants()) do
|
||||
if v.ClassName == "Part" or v.ClassName == "MeshPart" or v.ClassName == "UnionOperation" then
|
||||
v:SetAttribute(name,value)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return FerrUtils
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user