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