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