How to make secure cooldown for local script?

Okay! I believe I’m in the home stretch of making this client-sided hitbox system! I have the local ability script which houses the hitbox and gets sent to a server script, the server script then sends the validation values (magnitude and angle) to a module script to verify it and then if it’s verified the victim VFX plays to those players on all clients on a VFX handler local script.

This is all well, but there’s one last point I’m struggling with, the player animation begins on the ability script and the client-sided hitbox is bound to a flag in the animation track. My issue is, I don’t know how to make a cooldown for the CLIENT that’s SAFE so the animation isn’t being spammed (Even though the actual hitbox isn’t doing anything because of server debounce and validation checks)


I may be overthinking this, or I may be right there at the end and just need a bit more of a push, I’ve been getting a lot of help with this recently and I appreciate it! This would be the final hurdle and it feels like it may be either something small or I’ll need to completely rearrange my scripts.

Here are the scripts in order and what each one does:
Ability (Local Script) Initializes the ability and the client-sided hitbox. Player animation plays here because everyone can see it.

local function hitboxEffects() --Function that creates hitbox and fires server for hit validation
	
	local hrp = pChar:FindFirstChild("HumanoidRootPart")

	local hitbox = Instance.new("Part")
	local pos = hrp.Position
	local hitCharacters = {}
	local parameters = OverlapParams.new()
	hitbox.Shape = Enum.PartType.Ball
	hitbox.Size = Vector3.new(10, 10, 10)
	hitbox.CanCollide = false
	hitbox.CanTouch = false
	hitbox.CanQuery = false
	hitbox.Anchored = true
	hitbox.Transparency = 0.5

	local cf = hrp.CFrame
	local size = hitbox.Size

	parameters.FilterDescendantsInstances = {pChar}

	local hitboxPart = workspace:GetPartBoundsInBox(cf, size, parameters)

	local vTable = {}

	for _, hitPart in pairs(hitboxPart) do
		local vHum = hitPart.Parent:FindFirstChild("Humanoid")
		if vHum and not table.find(vTable, hitPart.Parent) then
			table.insert(vTable, hitPart.Parent)
		end
	end

	hitbox.Position = pos
	hitbox.Parent = workspace

	abilityRemote:FireServer(vTable)
	game.Debris:AddItem(hitbox, 0.1)
	
end

--Hitbox is created on the client for WYSIWYG flow. We will need checks on the server side for exploit purposes
UIS.InputBegan:Connect(function(input, gameProcessed)
	
	if gameProcessed then
		return
	end
	
	if input.KeyCode == Enum.KeyCode.Q then -- POST QUESTION: How do I make a safe cooldown for this?
			
		gravity:Play()
							
	end
end)

gravity:GetMarkerReachedSignal("GravEffect"):Connect(hitboxEffects) --Displays the hitbox and effects whenever the animation reaches this point

Ability Validation (Server Script) Sends information to the validation module

local validationModule = require(SS:WaitForChild("HitValidation"))

abilityRemote.OnServerEvent:Connect(function(player, vTable)
	
	validationModule.HitValidation(player, vTable, abilityVFX, 3, 6, 75) --Validation is sent to a module script 
	
end)

Hit Validation (Module Script) Checks if the hit was actually valid

local module = {}

local RS = game:GetService("ReplicatedStorage")

local abilityEvents = RS:WaitForChild("Ability")
local abilityRemote = abilityEvents:WaitForChild("AbilityRemote")

local debounce = {}

function module.HitValidation(player, vTable, abilityVFX, c, m, a)
	
	local cooldown = c

	local hitChars = {}

	local pChar = player.Character
	local hrp = pChar:FindFirstChild("HumanoidRootPart")
	local pos = hrp.Position
	local pDirection = hrp.CFrame.LookVector

	if table.find(debounce, player.Name) ~= nil then --Debounce cooldown so exploiters cannot spam the server with requests
		print("Under debounce!")
	else
		print("Ability casted!")
		table.insert(debounce, player.Name)

		for _, vChar in pairs (vTable) do --Checks if hit was actually valid

			local vrp = vChar:FindFirstChild("HumanoidRootPart")
			local vPos = vrp.Position

			local magCheck = (vPos - pos)
			local distance = magCheck.Magnitude --Gets distance between the players
			local normalizedDist = magCheck.Unit --Normalizes that distance number

			local dot = pDirection:Dot(normalizedDist)  --Normalizes look vector
			local radAngle = math.acos(dot) --Finds the angle between the vrp and player look direction
			local degAngle = math.deg(radAngle) --Converts above into an angle

			if distance <= m and degAngle <= a and vChar and not table.find(hitChars, vChar) then --Validation check
				print("Hit")
				table.insert(hitChars, vChar) --Adds player to table for VFX purposes
				vChar.Humanoid:TakeDamage(10)
			end	
		end

		abilityVFX:FireAllClients(pChar, hitChars) --VFX

		task.wait(cooldown)
		debounce[table.find(debounce, player.Name)] = nil

	end
	
end

return module

VFX Handler (Local Script) Plays VFX for all validated players that were in the hitbox

local RS = game:GetService("ReplicatedStorage")

local abilityEvents = RS:WaitForChild("Ability")
local abilityVFX = abilityEvents:WaitForChild("AbilityVFX")

local abilityAnims = RS:WaitForChild("AbilityAnims")
local victimAnim = abilityAnims:WaitForChild("Victim")

abilityVFX.OnClientEvent:Connect(function(pChar, hitChars)
	
	if #hitChars == 0 then --If nobody was hit this will fire and then stop the script
		print("Nobody was hit!")
		return
	end

	if #hitChars >= 1 then --If there was a hit then run this
			
		for _, victim in pairs (hitChars) do
			
			local animate = victim.Humanoid:LoadAnimation(victimAnim)
			animate:Play()
			
		end
		print(hitChars)
	end
end)

I guess this functions as a sort of code review as well, so go ahead and give me all the pointers and advice you got! But the main goal is making a way so that the initial animation cooldown isn’t spammable.

Hey there! You’re really close, and you’re definitely right about needing a secure client-side cooldown to prevent spamming animations. Even though your server checks handle exploit prevention well, it’s good practice to have client-side checks too, mainly for smooth user experience.

Recommended approach:

Implementing a simple boolean or timestamp check on the client-side, with minimal risk since the server already handles true validation.

Here’s a quick example:

local cooldownTime = 3 -- seconds
local canUseAbility = true

UIS.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then
		return
	end

	if input.KeyCode == Enum.KeyCode.Q and canUseAbility then
		canUseAbility = false
		gravity:Play()

		task.delay(cooldownTime, function()
			canUseAbility = true
		end)
	end
end)

Why this works securely:
Even if someone bypasses the client cooldown, your server-side debounce ensures exploiters can’t spam your system. The client-side cooldown is simply for visual smoothness and user feedback.

Extra recommendation:
Optionally, consider adding UI feedback (like an icon or countdown) to clearly show players when their ability is ready again.

Your current validation setup looks great—this final step should make your system feel polished!

Hope this helps, you’re super close! Let me know if there’s anything else you wanna clarify or improve. :slight_smile:

Ah okay, that’s sort of what I was thinking and the original idea I had going forward. So is it kind of just unavoidable that an exploiter would be able to spam the animation if the cooldown is on the client? It wouldn’t really do anything else other than just look annoying?

Yeah I mean that’s why the server has to have checks

1 Like

That is the more important goal for sure. Just curious if more could have been done!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.