Local Players = game:GetService("Players")
export type ActionId = string
export type CooldownInfo = {
duration: number?,
maxCharges: number?,
rechargeInterval: number?,
}
type ActionState = {
nextReadyTime: number,
charges: number?,
lastConsumeTime: number?,
}
type PerPlayerState = { [ActionId]: ActionState }
local CooldownService = {}
local actionConfig: { [ActionId]: CooldownInfo } = {}
local stateByUser: { [number]: PerPlayerState } = {}
local function getTime(): number
local ok, now = pcall(function()
return workspace:GetServerTimeNow()
end)
if ok and typeof(now) == "number" then
return now
end
return time()
end
local function getPlayerState(userId: number): PerPlayerState
local perPlayer = stateByUser[userId]
if not perPlayer then
perPlayer = {}
stateByUser[userId] = perPlayer
end
return perPlayer
end
local function validateConfig(info: CooldownInfo)
local duration = info.duration
local maxCharges = info.maxCharges
local rechargeInterval = info.rechargeInterval
if duration ~= nil then
assert(typeof(duration) == "number" and duration >= 0, "CooldownService.Use: duration must be a non-negative number if provided")
end
if maxCharges ~= nil then
assert(typeof(maxCharges) == "number" and maxCharges >= 1, "CooldownService.Use: maxCharges must be >= 1 when provided")
assert(rechargeInterval ~= nil, "CooldownService.Use: rechargeInterval must be provided when maxCharges is used")
end
if rechargeInterval ~= nil then
assert(typeof(rechargeInterval) == "number" and rechargeInterval > 0, "CooldownService.Use: rechargeInterval must be > 0 when provided")
end
assert((duration and duration > 0) or maxCharges, "CooldownService.Use: must define duration or maxCharges")
end
local function updateCharges(userId: number, actionId: ActionId, now: number)
local cfg = actionConfig[actionId]
if not cfg then
return
end
if not cfg.maxCharges or not cfg.rechargeInterval then
return
end
local perPlayer = getPlayerState(userId)
local state = perPlayer[actionId]
if not state then
return
end
local maxCharges = cfg.maxCharges
local rechargeInterval = cfg.rechargeInterval
local charges = state.charges or maxCharges
local lastConsumeTime = state.lastConsumeTime or now
if charges >= maxCharges then
state.charges = maxCharges
state.lastConsumeTime = now
return
end
local elapsed = now - lastConsumeTime
if elapsed <= 0 then
return
end
local regenerated = math.floor(elapsed / rechargeInterval)
if regenerated <= 0 then
return
end
charges += regenerated
if charges >= maxCharges then
charges = maxCharges
state.lastConsumeTime = now
else
local leftover = elapsed % rechargeInterval
state.lastConsumeTime = now - leftover
end
state.charges = charges
end
local function clearPlayer(userId: number)
local perPlayer = stateByUser[userId]
if perPlayer then
table.clear(perPlayer)
end
stateByUser[userId] = nil
end
function CooldownService.Use(player: Player, actionId: ActionId, info: CooldownInfo?): (boolean, number)
assert(typeof(player) == "Instance" and player:IsA("Player"), "CooldownService.Use: player must be a Player instance")
assert(typeof(actionId) == "string" and actionId ~= "", "CooldownService.Use: actionId must be non-empty string")
local cfg = actionConfig[actionId]
if not cfg then
assert(info ~= nil, `CooldownService.Use: unknown actionId "{actionId}" and no config provided`)
validateConfig(info :: CooldownInfo)
local duration = info.duration or 0
local maxCharges = info.maxCharges
local rechargeInterval = info.rechargeInterval
cfg = {
duration = duration > 0 and duration or nil,
maxCharges = maxCharges,
rechargeInterval = rechargeInterval,
}
actionConfig[actionId] = cfg
end
local now = getTime()
local userId = player.UserId
local perPlayer = getPlayerState(userId)
local state = perPlayer[actionId]
local allowed = false
local remaining = 0
if cfg.maxCharges and cfg.rechargeInterval then
if not state then
state = {
nextReadyTime = now,
charges = cfg.maxCharges,
lastConsumeTime = now,
}
perPlayer[actionId] = state
end
updateCharges(userId, actionId, now)
local charges = state.charges or cfg.maxCharges
if charges > 0 then
charges -= 1
state.charges = charges
state.lastConsumeTime = now
if charges <= 0 then
state.nextReadyTime = now + cfg.rechargeInterval
else
state.nextReadyTime = now
end
allowed = true
remaining = 0
else
local lastConsumeTime = state.lastConsumeTime or now
remaining = (lastConsumeTime + cfg.rechargeInterval) - now
if remaining < 0 then
remaining = 0
end
allowed = false
end
else
local duration = cfg.duration or 0
if not state then
if duration > 0 then
perPlayer[actionId] = {
nextReadyTime = now + duration,
charges = nil,
lastConsumeTime = nil,
}
end
allowed = true
remaining = 0
else
if now >= state.nextReadyTime then
state.nextReadyTime = now + duration
allowed = true
remaining = 0
else
remaining = state.nextReadyTime - now
if remaining < 0 then
remaining = 0
end
allowed = false
end
end
end
return allowed, remaining
end
function CooldownService.GetRemaining(player: Player, actionId: ActionId): number
assert(typeof(player) == "Instance" and player:IsA("Player"), "CooldownService.GetRemaining: player must be a Player instance")
assert(typeof(actionId) == "string" and actionId ~= "", "CooldownService.GetRemaining: actionId must be non-empty string")
local cfg = actionConfig[actionId]
if not cfg then
return 0
end
local userId = player.UserId
local perPlayer = stateByUser[userId]
if not perPlayer then
return 0
end
local state = perPlayer[actionId]
if not state then
return 0
end
local now = getTime()
if cfg.maxCharges and cfg.rechargeInterval then
updateCharges(userId, actionId, now)
local charges = state.charges or cfg.maxCharges
if charges > 0 then
return 0
end
local lastConsumeTime = state.lastConsumeTime or now
local remaining = (lastConsumeTime + cfg.rechargeInterval) - now
if remaining < 0 then
remaining = 0
end
return remaining
end
if now >= state.nextReadyTime then
return 0
end
return state.nextReadyTime - now
end
Players.PlayerRemoving:Connect(function(player: Player)
clearPlayer(player.UserId)
end)
function CooldownService.DebugClearAll()
for userId in stateByUser do
clearPlayer(userId)
end
end
return CooldownService