CCCPaste

ae83660a42812

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