Does ability cooldown need to be done on the client and server?

I’m working on abilities and it seems like cooldowns should be handled by the server to prevent exploiters from spamming the ability, however, it seems like I also need to handle cooldown on the client to prevent particle and animation spam which is just for cosmetic.

What’s the best way to do this though? Currently the ability cooldowns are stored on the server so my options are either put them in replicated storage or use remote events/functions or perhaps there’s a completely different approach to do this?

This is my current code.

-- Client
function Input:InputBegan(inputObject, gameProcessed)
    if UI:GetCurrentMenu() == "StartScreen" then PressAnyKeyToStart(inputObject) return end
    
    if self.Player.Class ~= nil then        
        if inputObject.UserInputType == Keybinds.PrimaryAttack then
            local requestAttackTime = ReplicatedStorage.Remotes.RemoteFunction
            local attackTime = requestAttackTime:InvokeServer()
            if playersAttacking[game.Players.LocalPlayer.Name] and os.clock() - playersAttacking[game.Players.LocalPlayer.Name] < attackTime then
                warn("We're already attacking")
                return
            end
            playersAttacking[game.Players.LocalPlayer.Name] = os.clock()
            Action:FireServer("PrimaryAttack",nil)
            self.Player.Animation:PlayAnimation("Hit1")
            return
        end        
    end
end
-- Server
local function HandleAction(player,actionName,keycode)
    -- Cooldown Check
    local name = player.Name..actionName
    local cooldown, attackTime = classModule[actionName](player.Character,true)
    if playerCooldowns[name] and os.clock() - playerCooldowns[name] < cooldown then
        warn("On cooldown")
        return
    end
    
    -- Checks if they're doing another attack already
    if playersAttacking[player.Name] and os.clock() - playersAttacking[player.Name] < attackTime then
        -- warn("Player is already doing another action")
        return
    end
    
    playerCooldowns[name] = os.clock()
    playersAttacking[player.Name] = os.clock()
    classModule[actionName](player.Character) -- Do ability
end

RequestAttackTime.OnServerInvoke = function(player)
    return 0.7
end

Always server. Maybe use a bool value, or give them a cooldown attribute so the client knows if they’re on cooldown.

My solution is always having synchronized value of the last time the ability was used. Server sets the variable (so also server checks if the player can use ability) and sends it to the client. With boolean, client is unable to tell, when the ability will be available again, so timestamp is for me personally better solution.
I recommend using DateTime:now().UnixTimestampMillis / 1000 for timestamp in seconds with milliseconds precision. Time value like tick() is very unstable since the values can differ for both machines.

1 Like

What if I make collision checks on the client? Would I make like a castId for an attack, replicate it to server, sanity check there and if the client hits something, it will only damage if the server has a copy of the skill with the same id and validated?

All the client should ever do is send inputs to the server. This prevents discrepencies between the server and client, and the client playing effects when it shouldn’t. More importantly, this makes it almost impossible for exploiters to spam attacks.

For the cooldown, simply send via RemoteEvent and recieve it with a LocalScript.