Hello, I’ve created a melee system but I’m having a couple of problems with it. I’d really appreciate some help.
Issues:
The cooldown doesn’t work properly after an attack. For example, when I click, the animation plays and then waits for the cooldown. However, during that cooldown period, I can still spam left-click and it keeps damaging the target as if there’s no cooldown at all. This makes the opponent’s health drop way too fast.
When I hit an NPC, sometimes it instantly dies. Occasionally, it applies the correct damage value that I set in my config, but after a few seconds, the NPC suddenly drops dead as if it took fatal damage.
I’ll include both my server script and module script below.
MODULE SCRIPT
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local RagdollModule = require(script.Parent:WaitForChild("RigsModule"))
local MeleeSystem = {}
local PlayerCooldown = {}
local HumanoidCooldown = {}
local function isCrit(chance)
return math.random() < (chance or 0)
end
local function tagHumanoid(h, p)
local tag = Instance.new("ObjectValue")
tag.Name = "creator"
tag.Value = p
tag.Parent = h
Debris:AddItem(tag, 2)
end
function MeleeSystem.StartAttack(player, tool)
local config = require(tool:FindFirstChild("Config"))
local now = tick()
if PlayerCooldown[player] and now - PlayerCooldown[player] < config.HitCooldown then
return
end
PlayerCooldown[player] = now
local char = player.Character
local hum = char and char:FindFirstChild("Humanoid")
local animator = hum and hum:FindFirstChild("Animator")
local anims = tool:FindFirstChild("Animations")
if animator and anims then
local swing = anims:FindFirstChild("swing2") or anims:FindFirstChild("Swing")
if swing then
local track = animator:LoadAnimation(swing)
track:Play()
end
end
local sfx = tool:FindFirstChild("sfx")
if sfx and sfx:FindFirstChild("swing") then
sfx.swing:Play()
end
local handle = tool:FindFirstChild("Handle")
if not handle then return end
local alreadyHit = {}
local conn
conn = handle.Touched:Connect(function(part)
local targetChar = part:FindFirstAncestorOfClass("Model")
if not targetChar or targetChar == char then return end
local targetHum = targetChar:FindFirstChildOfClass("Humanoid")
if not targetHum or targetHum.Health <= 0 or alreadyHit[targetHum] then return end
local last = HumanoidCooldown[targetHum]
if last and now - last < config.HitCooldown then return end
HumanoidCooldown[targetHum] = now
alreadyHit[targetHum] = true
local damage = config.Damage or 25
local crit = isCrit(config.CritChance)
if crit then damage *= (config.CritMultiplier or 2) end
targetHum:TakeDamage(damage)
tagHumanoid(targetHum, player)
if sfx and sfx:FindFirstChild("hitmaker") then
local sound = sfx.hitmaker:Clone()
sound.Parent = targetChar:FindFirstChild("HumanoidRootPart") or handle
sound:Play()
Debris:AddItem(sound, 2)
end
if crit and config.RagdollOnCrit then
local motors = RagdollModule.CreateJoints(targetChar)
RagdollModule.Ragdoll(targetChar)
task.delay(3, function()
if targetHum and targetHum.Health > 0 then
RagdollModule.DestroyJoints(targetChar)
RagdollModule.SetMotorsEnabled(motors, true)
RagdollModule.UnRagdoll(targetChar)
end
end)
end
end)
task.delay(config.SwingCooldown or 0.5, function()
if conn then conn:Disconnect() end
end)
end
function MeleeSystem.OnToolEquipped(player, tool)
local char = player.Character
local humanoid = char and char:FindFirstChild("Humanoid")
local animator = humanoid and humanoid:FindFirstChild("Animator")
local anims = tool:FindFirstChild("Animations")
if not (animator and anims) then return end
local equip = anims:FindFirstChild("equip")
local idle = anims:FindFirstChild("Idle")
if equip then
local equipTrack = animator:LoadAnimation(equip)
equipTrack:Play()
-- equip bitince idle başlat
if idle then
equipTrack.Stopped:Connect(function()
local idleTrack = animator:LoadAnimation(idle)
idleTrack.Looped = true
idleTrack:Play()
end)
end
elseif idle then
-- Eğer equip yoksa direk idle başlat
local idleTrack = animator:LoadAnimation(idle)
idleTrack.Looped = true
idleTrack:Play()
end
local sfx = tool:FindFirstChild("sfx")
if sfx and sfx:FindFirstChild("equip") then
sfx.equip:Play()
end
end
function MeleeSystem.OnToolUnequipped(player)
local char = player.Character
local humanoid = char and char:FindFirstChild("Humanoid")
local animator = humanoid and humanoid:FindFirstChild("Animator")
if animator then
for _, track in ipairs(animator:GetPlayingAnimationTracks()) do
local name = track.Animation.Name:lower()
if name == "equip" or name == "swing" or name == "swing2" or name == "idle" then
track:Stop()
end
end
end
end
return MeleeSystem
SERVER SCRIPT
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local MeleeSystem = require(ReplicatedStorage:WaitForChild("MeleeSystem"))
local MeleeRemote = ReplicatedStorage:WaitForChild("MeleeRemote")
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(char)
char.ChildAdded:Connect(function(tool)
if tool:IsA("Tool") then
tool.Equipped:Connect(function()
MeleeSystem.OnToolEquipped(player, tool)
end)
tool.Unequipped:Connect(function()
if MeleeSystem.OnToolUnequipped then
MeleeSystem.OnToolUnequipped(player)
end
end)
end
end)
end)
end)
MeleeRemote.OnServerEvent:Connect(function(player, action)
if action == "StartAttack" then
local char = player.Character
local tool = char and char:FindFirstChildOfClass("Tool")
if tool then
MeleeSystem.StartAttack(player, tool)
end
end
end)
Cooldown bug (stale time): You capture now = tick() once at the start of StartAttack and reuse it inside the Touched callback. That means HumanoidCooldown compares against a frozen timestamp; the per-target cooldown won’t expire correctly until the next StartAttack call.
Possible nil index on animation stop: In OnToolUnequipped, you do track.Animation.Name:lower(). track.Animation can be nil (depends on how/where it was loaded), causing a runtime error.
I recommend doing the debounce/cooldown on server to avoid exploits It may not be to important now but its a good habit
require(Config) fragility: You require(tool:FindFirstChild("Config")) without verifying it’s a ModuleScript. If it’s missing or the wrong class, it errors.
Event lifecycle leak/race: The handle.Touched connection only disconnects after SwingCooldown. If the tool is unequipped/destroyed early, the connection can fire on stale state. Also not cleared on unequip.
Name mismatches / typos: You look for anims:FindFirstChild("Idle") (capital I) but later stop tracks by comparing lowercase "idle". If the actual asset is named idle (lowercase), equip won’t find it. Similarly, SFX child named "hitmaker" may be a typo (“hitmarker”?).
Module name mismatch risk:local RagdollModule = require(script.Parent:WaitForChild("RigsModule"))—variable says Ragdoll, path says RigsModule. Easy to point at the wrong module.
Ragdoll flow inconsistency: Calling DestroyJoints and then SetMotorsEnabled(motors, true)/UnRagdoll suggests an out-of-order or conflicting API usage (depends on your module). Likely to break reattachment.
Self-hits & multi-hits edge cases:handle.Touched is noisy; you filter with alreadyHit[targetHum], but without collision groups you can still get odd triggers (e.g., accessories). Not a crash, but causes flaky hits.
Minor:Players is required but unused; just a warning, not a crash.
#1#2#3 are the most important the others are just extras I can edit it if you want also
If you fix #1 and #2 first, you’ll likely eliminate the “red” runtime errors. The rest are stability/consistency issues.
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local RagdollModule = require(script.Parent:WaitForChild("RigsModule"))
local MeleeSystem = {}
-- Use stable keys to avoid leaks when players leave
local PlayerCooldown = {} -- keyed by player.UserId
local HumanoidCooldown = {} -- keyed by Humanoid instance
local function isCrit(chance)
return math.random() < (chance or 0)
end
local function tagHumanoid(h, p)
local tag = Instance.new("ObjectValue")
tag.Name = "creator"
tag.Value = p
tag.Parent = h
Debris:AddItem(tag, 2)
end
function MeleeSystem.StartAttack(player, tool)
local config = require(tool:FindFirstChild("Config"))
-- Treat these as distinct knobs:
-- - AttackCooldown: how often the player can start a new swing
-- - HitCooldown: per-target i-frames to avoid multi-hits from one swing
local AttackCooldown = config.SwingCooldown or config.HitCooldown or 0.5
local HitCooldown = config.HitCooldown or 0.25
-- Player swing cooldown (use userId as key)
local now = time()
local pkey = player.UserId
if PlayerCooldown[pkey] and (now - PlayerCooldown[pkey]) < AttackCooldown then
return
end
PlayerCooldown[pkey] = now
local char = player.Character
local hum = char and char:FindFirstChildOfClass("Humanoid")
local animator = hum and hum:FindFirstChildOfClass("Animator")
local anims = tool:FindFirstChild("Animations")
if animator and anims then
local swing = anims:FindFirstChild("swing2") or anims:FindFirstChild("Swing")
if swing then
local track = animator:LoadAnimation(swing)
track:Play()
end
end
local sfx = tool:FindFirstChild("sfx")
if sfx and sfx:FindFirstChild("swing") then
sfx.swing:Play()
end
local handle = tool:FindFirstChild("Handle")
if not handle then return end
local alreadyHit = {}
local conn
conn = handle.Touched:Connect(function(part)
local targetChar = part:FindFirstAncestorOfClass("Model")
if not targetChar or targetChar == char then return end
local targetHum = targetChar:FindFirstChildOfClass("Humanoid")
if not targetHum or targetHum.Health <= 0 or alreadyHit[targetHum] then return end
-- Per-target cooldown uses the CURRENT time, not the stale 'now'
local t = time()
local last = HumanoidCooldown[targetHum]
if last and (t - last) < HitCooldown then return end
HumanoidCooldown[targetHum] = t
alreadyHit[targetHum] = true
local damage = config.Damage or 25
local crit = isCrit(config.CritChance)
if crit then damage *= (config.CritMultiplier or 2) end
targetHum:TakeDamage(damage)
tagHumanoid(targetHum, player)
if sfx and sfx:FindFirstChild("hitmaker") then
local sound = sfx.hitmaker:Clone()
sound.Parent = targetChar:FindFirstChild("HumanoidRootPart") or handle
sound:Play()
Debris:AddItem(sound, 2)
end
if crit and config.RagdollOnCrit then
local motors = RagdollModule.CreateJoints(targetChar)
RagdollModule.Ragdoll(targetChar)
task.delay(3, function()
if targetHum and targetHum.Health > 0 then
RagdollModule.DestroyJoints(targetChar)
RagdollModule.SetMotorsEnabled(motors, true)
RagdollModule.UnRagdoll(targetChar)
end
end)
end
end)
-- Disconnect the hitbox window after the swing cooldown window
task.delay(AttackCooldown, function()
if conn then conn:Disconnect() end
end)
end
function MeleeSystem.OnToolEquipped(player, tool)
local char = player.Character
local humanoid = char and char:FindFirstChildOfClass("Humanoid")
local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
local anims = tool:FindFirstChild("Animations")
if not (animator and anims) then return end
local equip = anims:FindFirstChild("equip")
local idle = anims:FindFirstChild("Idle")
if equip then
local equipTrack = animator:LoadAnimation(equip)
equipTrack:Play()
if idle then
equipTrack.Stopped:Connect(function()
local idleTrack = animator:LoadAnimation(idle)
idleTrack.Looped = true
idleTrack:Play()
end)
end
elseif idle then
local idleTrack = animator:LoadAnimation(idle)
idleTrack.Looped = true
idleTrack:Play()
end
local sfx = tool:FindFirstChild("sfx")
if sfx and sfx:FindFirstChild("equip") then
sfx.equip:Play()
end
end
function MeleeSystem.OnToolUnequipped(player)
local char = player.Character
local humanoid = char and char:FindFirstChildOfClass("Humanoid")
local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
if animator then
for _, track in ipairs(animator:GetPlayingAnimationTracks()) do
local anim = track.Animation
local name = (anim and anim.Name or ""):lower()
if name == "equip" or name == "swing" or name == "swing2" or name == "idle" then
track:Stop()
end
end
end
end
return MeleeSystem