I want to manage multiple different types of ability cooldowns inside a single cooldown module. Some abilities have:
- Multiple charges (e.g. 3 uses before cooldown),
- A duration before cooldown starts (like a temporary buff),
- Or a standard single-use cooldown.
I’m trying to build a clean and efficient system that supports all of these variations without making the module bloated or inconsistent.
My current cooldown module works for basic charge-based cooldowns, but I’m running into trouble trying to expand it to support more complex cooldown behavior, such as duration-based cooldowns or abilities with mixed logic.
Here’s the current cooldown module I’m working with:
--Services
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--Assets
local PlayerGUIRemote = ReplicatedStorage.Assets.Remotes.GUI
--Script
local Cooldown = {}
Cooldown.__index = Cooldown
function Cooldown.new(parent,cooldownTime,maxCharges)
local self = setmetatable({}, Cooldown)
self.CooldownTime = cooldownTime
self.Parent = parent
self._isOnCooldown = false
self._currentTime = nil
self.StartTime = nil
self.DeltaTime = nil
self.TimeRemaining = nil
self.Debounce = false
self.MaxCharges = maxCharges or 1
self.Charge = maxCharges
return self
end
function Cooldown:Start()
self.Charge -=1
PlayerGUIRemote:FireClient(self.Parent.Player,"Charge",self.Charge)
if self._isOnCooldown == false then
self._isOnCooldown = true
task.spawn(function()
while self.Charge < self.MaxCharges do
self.StartTime = os.clock()
repeat
task.wait()
self.DeltaTime = os.clock() - self.StartTime
self.TimeRemaining = math.max(0,(self.CooldownTime - self.DeltaTime))
PlayerGUIRemote:FireClient(self.Parent.Player,"Time",self.TimeRemaining)
until self.TimeRemaining <= 0
self:Refresh()
PlayerGUIRemote:FireClient(self.Parent.Player,"Charge",self.Charge)
end
self._isOnCooldown = false
end)
end
end
function Cooldown:Reduce(cooldownType,number)
if self._isOnCooldown == false then print("Notoncooldown") return end
if cooldownType == "Flat" then
self.StartTime = self.StartTime - number
print("CooldownReducedBy"..number)
elseif cooldownType == "Percent" then
self.StartTime = self.StartTime - (self.TimeRemaining * (number/100))
print("CooldownReducedBy "..number.."%")
end
end
function Cooldown:Refresh()
self.Charge += 1
print("ChargeRefreshed")
self._currentTime = nil
self.StartTime = nil
self.DeltaTime = nil
self.TimeRemaining = nil
end
return Cooldown
Here is how I currently Use it in a server script:
--Services
local Replicated = game:GetService('ReplicatedStorage')
local UIS = game:GetService('UserInputService')
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
--Modules
local MockP = require(Replicated.Modules.Shared.MockPart)
local Raycast = require(Replicated.Modules.Shared.Raycast)
local Bezier = require(Replicated.Modules.Shared.Bezier)
local AbilityManager = require(Replicated.Modules.Combat.AbilityManager)
local PlayerManager = require(Replicated.Modules.Combat.PlayerManager)
--Remotes
local CharacterTwoRemote = Replicated.Characters.CharacterTwo.Remotes.CharacterTwoRemote
local FXRemote = Replicated.Assets.Remotes.FX
--Body
CharacterTwoRemote.OnServerEvent:Connect(function(player,abiltyType,...)
--PlayerInitialize
if PlayerManager:GetPlayer(player) == nil then
PlayerManager:InitializePlayer(player,"CharacterTwo")
end
--Body
local Player = PlayerManager:GetPlayer(player)
local PlayerAbilities = Player.Abilities
if abiltyType == "AA" then
FXRemote:FireAllClients("CharacterTwo","AA",player,...)
elseif abiltyType == "Shift" then
if PlayerAbilities.Shift.Cooldown.Debounce == true or PlayerAbilities.Shift.Cooldown.Charge <= 0 then return end
PlayerAbilities.Shift.Cooldown.Debounce = true
PlayerAbilities.Shift.Cooldown:Start()
FXRemote:FireAllClients("CharacterTwo","Shift",player,...)
task.wait(0.3)
PlayerAbilities.Shift.Cooldown.Debounce = false
elseif abiltyType == "Skill" then
FXRemote:FireAllClients("CharacterTwo","Skill",player,...)
elseif abiltyType == "Ultimate" then
FXRemote:FireAllClients("CharacterTwo","Ultimate",player,...)
end
end)
And here is my ability module:
--Services
local Replicated = game:GetService("ReplicatedStorage")
--Modules
local Cooldown = require(Replicated.Modules.Combat.CooldownManager)
--Assets
local Characters = Replicated.Characters
--Module
local Ability = {}
function Ability.new(player,abilityType,cooldownTime,maxCharges,cooldownType)
local self = setmetatable({}, Ability)
self.AbilityType = abilityType
self.Cooldown = Cooldown.new(self,cooldownTime,maxCharges,cooldownType)
self.Player = player
self.Duration = 10
self.isActive = false
return self
end
function Ability.InitalizePlayer(player,character)
local CharacterAbilities = Characters[character].Abilities
local self = setmetatable({}, Ability)
self.AA = Ability.new(player,"AA",CharacterAbilities.AA.Stats.Cooldown.Value,CharacterAbilities.AA.Stats.MaxCharges.Value)
self.Shift = Ability.new(player,"Shift",CharacterAbilities.Shift.Stats.Cooldown.Value,CharacterAbilities.Shift.Stats.MaxCharges.Value,"Duration")
self.Skill = Ability.new(player,"Skill",CharacterAbilities.Skill.Stats.Cooldown.Value,CharacterAbilities.Skill.Stats.MaxCharges.Value,"Charge")
self.Ultimate = Ability.new(player,"Ultimate",CharacterAbilities.Ultimate.Stats.Cooldown.Value,CharacterAbilities.Ultimate.Stats.MaxCharges.Value)
return self
end
return Ability
I was also wondering — would it be a good design decision to store things like CooldownTime, DurationTime, InternalDebounce, and MaxCharges inside a folder in ReplicatedStorage instead of hardcoding those values into scripts?
Would this be a more scalable or modular approach for managing different character abilities and their configurations? Or would it just lead to unnecessary replication/network clutter? I’m thinking this could also allow easier character customization without changing scripts directly.
If anyone has any general tips on:
- Better naming conventions for variables like
_isOnCooldown,DeltaTime, etc. - Lua OOP structuring and keeping the module lean/organized
- Syntax or readability improvements
That would be very helpful as well!