project init

This commit is contained in:
ZareMate 2026-05-02 23:53:49 +02:00
commit 3ff5337437
130 changed files with 12578 additions and 0 deletions

12
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
std = "roblox"

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

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ServerOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ServerOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ServerOnly": true
}
}

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

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View File

@ -0,0 +1,7 @@
-- dont change indexes!!!!!!
local DataStoreNames = {
Cash = "MoneyStore",
Wins = "WinStore"
}
return DataStoreNames

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

View File

@ -0,0 +1,12 @@
return {
Money = 50,
Wins = 0,
UnlockedSkins = {},
UnlockedTanks = {},
UnlockedAccessories = {},
Loadouts = {},
CurrentLoadout = "Default",
CurrentTank = "Tank"
}

View File

@ -0,0 +1,6 @@
local ValueNames = {
Cash = "Money",
Wins = "Wins"
}
return ValueNames

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{
"properties": {
"SourceAssetId": 109379033046155.0
}
}

View File

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

View 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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View 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

View File

@ -0,0 +1,5 @@
{
"attributes": {
"StudioEnoughPlayersOverride": false
}
}

View 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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

View 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

View 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

View File

@ -0,0 +1,7 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
}
}

View 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

View 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

View 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

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

View File

@ -0,0 +1,3 @@
{
"className": "IntValue"
}

View 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

View File

@ -0,0 +1,11 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true,
"ClientWaitForServer": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,12 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true,
"LoaderPriority": 99.0,
"StudioLoading": false
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View 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

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View File

@ -0,0 +1,3 @@
local module = {}
return module

View 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

View File

@ -0,0 +1,10 @@
{
"properties": {
"Tags": [
"LOAD_MODULE"
]
},
"attributes": {
"ClientOnly": true
}
}

View 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

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

View File

@ -0,0 +1,11 @@
local GameState = {
LOBBY = 1,
GRACE = 2,
AIMING = 3,
RESOLVING = 4,
RESULTS = 5,
}
return GameState

View File

@ -0,0 +1,13 @@
local DataTypes = require("../DataTypes")
local MapsData : {DataTypes.VoteOptionData} = {
LayoutMap1 = {
Name = "LayoutMap1",
DisplayName = "Test Map",
DisplayImage = "rbxassetid://13979677676"
},
}
return MapsData

View 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

View 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

View 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

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

Binary file not shown.

Binary file not shown.

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

View 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

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

View File

@ -0,0 +1,10 @@
{
"attributes": {
"ClientWaitForServer": true,
"FolderSearchDepth": 3.0,
"LoaderTag": "LOAD_MODULE",
"UseCollectionService": true,
"VerboseLoading": false,
"YieldThreshold": 10.0
}
}

View File

@ -0,0 +1,15 @@
local CollisionUtils = {}
local pool = {}
export type CollisionObject = {
}
function CollisionUtils.CreateObject()
end
return CollisionUtils

View 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