Help me with my FPS Framework

i have a problem, that my sway doesn’t work at all. Also, I have a main problem: I can’t shoot after reseting, I tried fixing it and it doesn’t work at all.
FPSClient:

--// FPSClient - Robust Viewmodel, Sway, and Firing System

local Players           = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService  = game:GetService("UserInputService")
local RunService        = game:GetService("RunService")

local FPSFramework      = require(ReplicatedStorage:WaitForChild("FPSFramework"))
local crosshairModule   = require(ReplicatedStorage:WaitForChild("DynamicCrosshair"))
local ViewmodelSway     = require(ReplicatedStorage:WaitForChild("ViewmodelSway"))
local CounterStrafe     = require(ReplicatedStorage:WaitForChild("CounterStrafe"))

local player    = Players.LocalPlayer
local mouse     = player:GetMouse()

-- UI
local UI        = script.Parent:FindFirstChild("crosshair")
local crosshair = crosshairModule.New(UI, 20, 20, 30, 80)

local canFire = true -- Simple debounce, can be expanded for fire rate

-- Ammo UI
local ammoGui, ammoLabel, reserveLabel = nil, nil, nil
do
    local gui = player.PlayerGui:FindFirstChild("Ammo") or script.Parent:FindFirstChild("Ammo")
    if gui then
        ammoGui = gui
        local frame = gui:FindFirstChildWhichIsA("Frame")
        if frame then
            local labels = {}
            for i, child in frame:GetChildren() do
                if child:IsA("TextLabel") then
                    table.insert(labels, child)
                end
            end
            ammoLabel = labels[1]
            reserveLabel = labels[2]
        end
    end
end

local function updateAmmoUI()
    if not ammoLabel or not reserveLabel then return end
    local clip, reserve, clipSize = FPSFramework:GetAmmo()
    ammoLabel.Text = tostring(clip) .. " / " .. tostring(clipSize)
    reserveLabel.Text = tostring(reserve)
end

FPSFramework.OnAmmoChanged = function(clip, reserve, clipSize)
    updateAmmoUI()
end

-- Counter-strafe logic (optional, as in original)
local counterStrafe = CounterStrafe.new(function(direction)
    if typeof(ResetRecoilAccuracy) == "function" then
        ResetRecoilAccuracy()
    elseif self and self.RecoilAccuracy then
        self.RecoilAccuracy = 1
    end
end)
script.AncestryChanged:Connect(function()
    counterStrafe:Destroy()
end)

-- Crosshair logic
local function initCrosshair()
    crosshair:Enable()
    crosshair.Spreading.MinSpread = 10
    crosshair.Spreading.MaxSpread = 10
    updateAmmoUI()
end
local function disableCrosshair()
    crosshair:Disable()
end

local function startAiming()
    crosshair.Spreading.MinSpread = 5
    crosshair.Spreading.MaxSpread = 5
    crosshair:SmoothSet(20, 0.1)
end
local function stopAiming()
    crosshair:SmoothSet(40, 0.1)
    crosshair.Spreading.MinSpread = 10
    crosshair.Spreading.MaxSpread = 10
end

local function updateCrosshair(dt)
    crosshair:Update(dt)
end

-- Gun/tool helpers
local function isGunTool(tool)
    if not tool or not tool:IsA("Tool") then return false end
    local viewmodelsFolder = ReplicatedStorage:FindFirstChild("Game") and ReplicatedStorage.Game:FindFirstChild("Viewmodels")
    if not viewmodelsFolder then return false end
    local vm = viewmodelsFolder:FindFirstChild("v_" .. tool.Name)
    return vm ~= nil and vm:IsA("Model")
end
local function isHoldingGun()
    local character = player.Character
    if not character then return false end
    local tool = character:FindFirstChildOfClass("Tool")
    return isGunTool(tool)
end

-- Viewmodel and connection management
local currentViewmodel = nil
local connections = {}

local function disconnectAll()
    for i, conn in connections do
        if conn then
            conn:Disconnect()
        end
    end
    connections = {}
end

local function destroyCurrentViewmodel()
    -- Always remove any FPSViewmodel from camera, even if currentViewmodel is nil
    local camera = workspace.CurrentCamera
    for i, child in camera:GetChildren() do
        if child:IsA("Model") and child.Name == "FPSViewmodel" then
            child:Destroy()
        end
    end
    -- Also destroy our tracked viewmodel if it exists
    if currentViewmodel and currentViewmodel.Parent then
        currentViewmodel:Destroy()
        currentViewmodel = nil
    end
    -- Double safety: call FPSFramework's RemoveViewmodel
    if FPSFramework and typeof(FPSFramework.RemoveViewmodel) == "function" then
        FPSFramework:RemoveViewmodel()
    end
    currentViewmodel = nil
end

-- Sway
local swayConn = nil
local function enableViewmodelSway()
    if swayConn then swayConn:Disconnect() swayConn = nil end
    swayConn = RunService.RenderStepped:Connect(function(dt)
        local character = player.Character
        if not character then return end
        local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
        if not humanoidRootPart then return end
        local tool = character:FindFirstChildOfClass("Tool")
        if not isGunTool(tool) then return end
        local camera = workspace.CurrentCamera
        local viewmodel = nil
        for i, child in camera:GetChildren() do
            if child:IsA("Model") and child.Name == "FPSViewmodel" then
                viewmodel = child
                break
            end
        end
        if not viewmodel then return end
        local offset = FPSFramework.CurrentOffset or CFrame.new(0, -0.7, -1.2)
        local velocity = humanoidRootPart.Velocity
        local swayCFrame = ViewmodelSway.GetSmoothedSwayCFrame(velocity, dt)
        viewmodel:PivotTo(camera.CFrame * offset * swayCFrame)
    end)
end
local function disableViewmodelSway()
    if swayConn then swayConn:Disconnect() swayConn = nil end
end

-- Tool equip/unequip logic
local function onToolEquipped(tool)
    if not isGunTool(tool) then return end
    destroyCurrentViewmodel()
    currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
    FPSFramework:EnableViewmodelFollow(player)
    FPSFramework:HideMouse()
    initCrosshair()
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
    enableViewmodelSway()
    FPSFramework:BindFireInput()
    -- Enable the tool in case it was disabled on death
    if tool and tool:IsA("Tool") then
        tool.Enabled = true
    end
end

local function onToolUnequipped(tool)
    FPSFramework:DisableViewmodelFollow()
    FPSFramework:ShowMouse()
    disableCrosshair()
    FPSFramework:UpdateWalkSpeed()
    disableViewmodelSway()
    destroyCurrentViewmodel()
    updateAmmoUI()
end

-- Tool event connections
local function connectToolEvents(character)
    disconnectAll()
    for i, tool in character:GetChildren() do
        if tool:IsA("Tool") then
            table.insert(connections, tool.Equipped:Connect(function() onToolEquipped(tool) end))
            table.insert(connections, tool.Unequipped:Connect(function() onToolUnequipped(tool) end))
        end
    end
    table.insert(connections, character.ChildAdded:Connect(function(child)
        if child:IsA("Tool") then
            table.insert(connections, child.Equipped:Connect(function() onToolEquipped(child) end))
            table.insert(connections, child.Unequipped:Connect(function() onToolUnequipped(child) end))
        end
    end))
end

-- Helper to robustly disable a tool (prevents firing, disables input, etc)
local function DisableTool(tool)
    if tool and tool:IsA("Tool") then
        tool.Enabled = false
    end
end

-- Cleanup on death/reset/unequip
local function cleanupCharacter()
    -- Delegate all tool/ammo cleanup to FPSFramework for proper handling
    if FPSFramework and typeof(FPSFramework.CleanupCharacterToolsAndAmmo) == "function" then
        FPSFramework:CleanupCharacterToolsAndAmmo(player)
    end
    disableViewmodelSway()
    disconnectAll()
    destroyCurrentViewmodel()
    updateAmmoUI()
end

-- Character lifecycle
player.CharacterAdded:Connect(function(character)
    task.wait(0.1)
    FPSFramework:SetupMovement(player)
    FPSFramework:SetCrosshair(crosshair)
    FPSFramework:BindFireInput()
    connectToolEvents(character)
    -- If player already has a gun tool equipped on spawn
    for i, tool in character:GetChildren() do
        if tool:IsA("Tool") and isGunTool(tool) then
            destroyCurrentViewmodel()
            currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
            FPSFramework:EnableViewmodelFollow(player)
            enableViewmodelSway()
            FPSFramework:BindFireInput()
            -- Enable the tool in case it was disabled on death
            tool.Enabled = true
        end
    end
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
    -- Clean up on death
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid.Died:Connect(function()
            cleanupCharacter()
        end)
    end
end)

if player.Character then
    FPSFramework:SetupMovement(player)
    FPSFramework:SetCrosshair(crosshair)
    FPSFramework:BindFireInput()
    connectToolEvents(player.Character)
    for i, tool in player.Character:GetChildren() do
        if tool:IsA("Tool") and isGunTool(tool) then
            destroyCurrentViewmodel()
            currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
            FPSFramework:EnableViewmodelFollow(player)
            enableViewmodelSway()
            FPSFramework:BindFireInput()
            -- Enable the tool in case it was disabled on death
            tool.Enabled = true
        end
    end
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
end

-- Listen for reload input (redundant if handled in FPSFramework, but ensures UI updates)
UserInputService.InputBegan:Connect(function(input, processed)
    if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == Enum.KeyCode.R and not processed then
        if isHoldingGun() then
            FPSFramework:Reload()
        end
    end
end)

-- Crosshair update loop
RunService.RenderStepped:Connect(function(dt)
    updateCrosshair(dt)
end)

FPSFramework (module):

--// FPSFramework - Robust Viewmodel, Sway, and Firing System (Reworked)

local FPSFramework = {}
FPSFramework.__index = FPSFramework

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local CounterStrafe = require(ReplicatedStorage:FindFirstChild("CounterStrafe"))
local ViewmodelSway = require(ReplicatedStorage:FindFirstChild("ViewmodelSway"))

-- Settings
FPSFramework.Sensitivity = 0.2
FPSFramework.DefaultWalkSpeed = 16
FPSFramework.JumpPower = 30

-- Viewmodel handling
FPSFramework.ViewmodelOffset = CFrame.new(0, -0.7, -1.2)
FPSFramework.CurrentViewmodel = nil
FPSFramework.CurrentOffset = FPSFramework.ViewmodelOffset
FPSFramework.CurrentGunName = nil

-- Crosshair reference (set by client)
FPSFramework.Crosshair = nil

-- Weapon stats
FPSFramework.WeaponCrosshairStats = {
	Glock19 = {
		MinSpread = 10, MaxSpread = 10, AimMinSpread = 5, AimMaxSpread = 5,
		FireBumpMin = 12, FireBumpMax = 12, FireBumpDuration = 0.08, ReturnDuration = 0.12,
		RecoilUp = 1.7, RecoilSide = 0.7, AccuracyResetTime = 0.5, MaxRecoilMultiplier = 4,
		WalkSpeed = 15, ClipSize = 20, ReserveAmmo = 120, ReloadTime = 1.2,
	},
	AK47 = {
		MinSpread = 18, MaxSpread = 22, AimMinSpread = 10, AimMaxSpread = 12,
		FireBumpMin = 20, FireBumpMax = 28, FireBumpDuration = 0.10, ReturnDuration = 0.16,
		RecoilUp = 3.2, RecoilSide = 1.5, AccuracyResetTime = 0.5, MaxRecoilMultiplier = 6,
		WalkSpeed = 13, ClipSize = 30, ReserveAmmo = 90, ReloadTime = 1.8,
	},
	Default = {
		MinSpread = 10, MaxSpread = 10, AimMinSpread = 5, AimMaxSpread = 5,
		FireBumpMin = 14, FireBumpMax = 14, FireBumpDuration = 0.08, ReturnDuration = 0.12,
		RecoilUp = 1.8, RecoilSide = 0.8, AccuracyResetTime = 0.5, MaxRecoilMultiplier = 4,
		WalkSpeed = 16, ClipSize = 10, ReserveAmmo = 30, ReloadTime = 1.5,
	}
}

local function GetCrosshairStats(weaponName)
	if not weaponName then return FPSFramework.WeaponCrosshairStats.Default end
	local stats = FPSFramework.WeaponCrosshairStats[weaponName]
	if stats then return stats end
	stats = FPSFramework.WeaponCrosshairStats[weaponName:lower()]
	if stats then return stats end
	stats = FPSFramework.WeaponCrosshairStats[weaponName:upper()]
	if stats then return stats end
	return FPSFramework.WeaponCrosshairStats.Default
end

local function GetWeaponWalkSpeed(weaponName)
	local stats = GetCrosshairStats(weaponName)
	return stats.WalkSpeed or FPSFramework.DefaultWalkSpeed
end

local function isGunTool(tool)
	if not tool or not tool:IsA("Tool") then return false end
	local viewmodelsFolder = ReplicatedStorage:FindFirstChild("Game") and ReplicatedStorage.Game:FindFirstChild("Viewmodels")
	if not viewmodelsFolder then return false end
	local vm = viewmodelsFolder:FindFirstChild("v_" .. tool.Name)
	return vm ~= nil and vm:IsA("Model")
end

local function isHoldingGun()
	local player = Players.LocalPlayer
	local character = player.Character
	if not character then return false end
	local tool = character:FindFirstChildOfClass("Tool")
	return isGunTool(tool)
end

-- Ammo state per weapon (client-side)
FPSFramework._ammoState = {}

local function getAmmoState(weaponName)
	if not weaponName then weaponName = "Default" end
	if not FPSFramework._ammoState[weaponName] then
		local stats = GetCrosshairStats(weaponName)
		FPSFramework._ammoState[weaponName] = {
			clip = stats.ClipSize or 10,
			reserve = stats.ReserveAmmo or 30,
			reloading = false,
		}
	end
	return FPSFramework._ammoState[weaponName]
end

function FPSFramework:GetAmmo()
	local weaponName = FPSFramework.CurrentGunName or "Default"
	local ammo = getAmmoState(weaponName)
	local stats = GetCrosshairStats(weaponName)
	return ammo.clip, ammo.reserve, stats.ClipSize or 10
end

function FPSFramework:CanReload()
	local weaponName = FPSFramework.CurrentGunName or "Default"
	local ammo = getAmmoState(weaponName)
	local stats = GetCrosshairStats(weaponName)
	return (ammo.clip < (stats.ClipSize or 10)) and (ammo.reserve > 0) and not ammo.reloading
end

function FPSFramework:Reload()
	local weaponName = FPSFramework.CurrentGunName or "Default"
	local ammo = getAmmoState(weaponName)
	local stats = GetCrosshairStats(weaponName)
	if not FPSFramework:CanReload() then return end
	ammo.reloading = true
	task.spawn(function()
		task.wait(stats.ReloadTime or 1.5)
		local needed = (stats.ClipSize or 10) - ammo.clip
		local toLoad = math.min(needed, ammo.reserve)
		ammo.clip = ammo.clip + toLoad
		ammo.reserve = ammo.reserve - toLoad
		ammo.reloading = false
		if FPSFramework.OnAmmoChanged then
			FPSFramework.OnAmmoChanged(ammo.clip, ammo.reserve, stats.ClipSize or 10)
		end
	end)
end

function FPSFramework:IsReloading()
	local weaponName = FPSFramework.CurrentGunName or "Default"
	local ammo = getAmmoState(weaponName)
	return ammo.reloading
end

function FPSFramework:ResetAmmo(weaponName)
	-- Resets ammo for the given weapon to full magazine and full reserve.
	weaponName = weaponName or FPSFramework.CurrentGunName or "Default"
	local stats = GetCrosshairStats(weaponName)
	FPSFramework._ammoState[weaponName] = {
		clip = stats.ClipSize or 10,
		reserve = stats.ReserveAmmo or 30,
		reloading = false,
	}
	if FPSFramework.OnAmmoChanged then
		local ammo = FPSFramework._ammoState[weaponName]
		FPSFramework.OnAmmoChanged(ammo.clip, ammo.reserve, stats.ClipSize or 10)
	end
end

-- NEW: Robust cleanup for tools and ammo on death/reset
function FPSFramework:CleanupCharacterToolsAndAmmo(player)
	-- player: Player instance (should be LocalPlayer on client)
	if not player then return end
	local character = player.Character
	local equippedTool = nil
	local equippedToolName = nil
	if character then
		local tool = character:FindFirstChildOfClass("Tool")
		if tool and isGunTool(tool) then
			equippedTool = tool
			equippedToolName = tool.Name
		end
	end
	-- Reset ammo for the equipped weapon (if any)
	if equippedToolName then
		self:ResetAmmo(equippedToolName)
	end

	-- Robustly move any gun tool from character to Backpack for a short period after death
	local startTime = tick()
	local movedTool = false
	while tick() - startTime < 0.5 do
		local char = player.Character
		if char then
			for i, tool in char:GetChildren() do
				if tool:IsA("Tool") and isGunTool(tool) then
					tool.Enabled = false
					tool.Parent = player.Backpack
					movedTool = true
				end
			end
		end
		if movedTool then break end
		task.wait(0.05)
	end

	-- Also try Humanoid:UnequipTools() for redundancy
	if character then
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if humanoid then
			humanoid:UnequipTools()
		end
	end
end

function FPSFramework:SetCrosshair(crosshair)
	FPSFramework.Crosshair = crosshair
end

function FPSFramework:HideMouse()
	UserInputService.MouseIconEnabled = false
end

function FPSFramework:ShowMouse()
	UserInputService.MouseIconEnabled = true
end

function FPSFramework:ShowMuzzleFlash()
	if not FPSFramework.CurrentViewmodel then return end
	local base = FPSFramework.CurrentViewmodel:FindFirstChild("Base")
	if not base then return end
	local effectPart = nil
	for _, child in base:GetChildren() do
		if child.Name == "EffectPart" and child:IsA("Part") then
			effectPart = child
			break
		end
	end
	if not effectPart then return end
	for _, v in effectPart:GetChildren() do
		if v:IsA("ParticleEmitter") then
			local nameLower = v.Name:lower()
			if nameLower == "flash" then
				v:Emit(100)
			elseif nameLower:find("core") then
				v:Emit(500)
			else
				v.Enabled = true
				task.delay(0.15, function()
					v.Enabled = false
				end)
			end
		end
	end
end

function FPSFramework:HideMuzzleFlash()
	if isHoldingGun() and FPSFramework.CurrentViewmodel and FPSFramework.CurrentViewmodel:FindFirstChild("Base") then
		local base = FPSFramework.CurrentViewmodel:FindFirstChild("Base")
		local effectPart = nil
		for _, child in base:GetChildren() do
			if child.Name == "EffectPart" and child:IsA("Part") then
				effectPart = child
				break
			end
		end
		if effectPart then
			for i, v in effectPart:GetChildren() do
				if v:IsA("ParticleEmitter") then
					v.Enabled = false
				end
			end
		end
	end
end

-- Robust viewmodel cleanup
function FPSFramework:RemoveViewmodel()
	if FPSFramework.CurrentViewmodel then
		FPSFramework.CurrentViewmodel:Destroy()
		FPSFramework.CurrentViewmodel = nil
		FPSFramework.CurrentOffset = FPSFramework.ViewmodelOffset
		FPSFramework.CurrentGunName = nil
	end
	-- Fallback: remove any FPSViewmodel from camera
	local camera = workspace.CurrentCamera
	for i, child in camera:GetChildren() do
		if child:IsA("Model") and child.Name == "FPSViewmodel" then
			child:Destroy()
		end
	end
end

function FPSFramework:SpawnViewmodel(player, gunName)
	self:RemoveViewmodel()
	local viewmodelsFolder = ReplicatedStorage:FindFirstChild("Game") and ReplicatedStorage.Game:FindFirstChild("Viewmodels")
	if not viewmodelsFolder then return end
	local viewmodelTemplate = viewmodelsFolder:FindFirstChild("v_" .. gunName)
	if not viewmodelTemplate or not viewmodelTemplate:IsA("Model") then return end
	local viewmodel = viewmodelTemplate:Clone()
	viewmodel.Name = "FPSViewmodel"
	viewmodel.Parent = workspace.CurrentCamera
	FPSFramework.CurrentViewmodel = viewmodel
	FPSFramework.CurrentGunName = gunName
	for i, part in viewmodel:GetDescendants() do
		if part:IsA("BasePart") then
			part.Anchored = true
			part.CanCollide = false
			if part.SetNetworkOwner then
				pcall(function() part:SetNetworkOwner(Players.LocalPlayer) end)
			end
		end
	end
	FPSFramework.CurrentOffset = FPSFramework.ViewmodelOffset
	local offsetModule = viewmodel:FindFirstChild("offset")
	if offsetModule and offsetModule:IsA("ModuleScript") then
		local success, offset = pcall(function() return require(offsetModule) end)
		if success and typeof(offset) == "CFrame" then
			FPSFramework.CurrentOffset = offset
		end
	end
	if FPSFramework.Crosshair then
		local stats = GetCrosshairStats(gunName)
		FPSFramework.Crosshair.Spreading.MinSpread = stats.MinSpread
		FPSFramework.Crosshair.Spreading.MaxSpread = stats.MaxSpread
	end
	if ViewmodelSway and typeof(ViewmodelSway.Reset) == "function" then
		ViewmodelSway.Reset()
	end
	getAmmoState(gunName)
	if FPSFramework.OnAmmoChanged then
		local ammo = getAmmoState(gunName)
		local stats = GetCrosshairStats(gunName)
		FPSFramework.OnAmmoChanged(ammo.clip, ammo.reserve, stats.ClipSize or 10)
	end
end

-- Viewmodel follow logic
function FPSFramework:EnableViewmodelFollow(player)
	if FPSFramework._viewmodelConn then
		FPSFramework._viewmodelConn:Disconnect()
	end
	FPSFramework._viewmodelConn = RunService.RenderStepped:Connect(function()
		local camera = workspace.CurrentCamera
		if FPSFramework.CurrentViewmodel and FPSFramework.CurrentViewmodel.Parent ~= camera then
			FPSFramework.CurrentViewmodel.Parent = camera
		end
		if FPSFramework.CurrentViewmodel then
			FPSFramework.CurrentViewmodel:PivotTo(camera.CFrame * FPSFramework.CurrentOffset)
		end
	end)
end

function FPSFramework:DisableViewmodelFollow()
	if FPSFramework._viewmodelConn then
		FPSFramework._viewmodelConn:Disconnect()
		FPSFramework._viewmodelConn = nil
	end
	self:RemoveViewmodel()
end

function FPSFramework:SetupMovement(player)
	local character = player.Character or player.CharacterAdded:Wait()
	local humanoid = character:WaitForChild("Humanoid")
	local tool = character:FindFirstChildOfClass("Tool")
	if tool and isGunTool(tool) then
		humanoid.WalkSpeed = GetWeaponWalkSpeed(tool.Name)
	else
		humanoid.WalkSpeed = FPSFramework.DefaultWalkSpeed
	end
	humanoid.JumpPower = FPSFramework.JumpPower
end

function FPSFramework:UpdateWalkSpeed()
	local player = Players.LocalPlayer
	local character = player.Character
	if not character then return end
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	if not humanoid then return end
	local tool = character:FindFirstChildOfClass("Tool")
	if tool and isGunTool(tool) then
		humanoid.WalkSpeed = GetWeaponWalkSpeed(tool.Name)
	else
		humanoid.WalkSpeed = FPSFramework.DefaultWalkSpeed
	end
end

function FPSFramework:AnimateCrosshairFire()
	if FPSFramework.Crosshair then
		local weaponName = FPSFramework.CurrentGunName
		local stats = GetCrosshairStats(weaponName)
		local originalMin = FPSFramework.Crosshair.Spreading.MinSpread
		local originalMax = FPSFramework.Crosshair.Spreading.MaxSpread
		local fireMin = stats.FireBumpMin or (originalMin + 10)
		local fireMax = stats.FireBumpMax or (originalMax + 10)
		local fireDuration = stats.FireBumpDuration or 0.08
		local returnDuration = stats.ReturnDuration or 0.12
		FPSFramework.Crosshair:SmoothSet(fireMax, fireDuration)
		FPSFramework.Crosshair.Spreading.MinSpread = fireMin
		FPSFramework.Crosshair.Spreading.MaxSpread = fireMax
		task.delay(fireDuration, function()
			FPSFramework.Crosshair:SmoothSet(originalMax, returnDuration)
			FPSFramework.Crosshair.Spreading.MinSpread = originalMin
			FPSFramework.Crosshair.Spreading.MaxSpread = originalMax
		end)
	end
end

-- Recoil/accuracy state
FPSFramework._recoilState = {
	consecutiveShots = 0,
	lastShotTime = 0,
}

local function rotateDirection(direction, upAngle, sideAngle)
	local cf = CFrame.new(Vector3.new(), direction)
	cf = cf * CFrame.Angles(-upAngle, sideAngle, 0)
	return cf.LookVector
end

local function resetRecoilAccuracy()
	FPSFramework._recoilState.consecutiveShots = 0
	FPSFramework._recoilState.lastShotTime = tick()
	if FPSFramework.Crosshair then
		local weaponName = FPSFramework.CurrentGunName
		local stats = GetCrosshairStats(weaponName)
		FPSFramework.Crosshair.Spreading.MinSpread = stats.MinSpread
		FPSFramework.Crosshair.Spreading.MaxSpread = stats.MaxSpread
		FPSFramework.Crosshair:SmoothSet(stats.MinSpread, 0.05)
	end
end

if not FPSFramework._counterStrafe then
	FPSFramework._counterStrafe = CounterStrafe.new(function()
		resetRecoilAccuracy()
	end)
end

-- Sway management
FPSFramework._swayConn = nil
function FPSFramework:EnableSway()
	self:DisableSway()
	FPSFramework._swayConn = RunService.RenderStepped:Connect(function(dt)
		local player = Players.LocalPlayer
		local character = player.Character
		if not character then return end
		local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
		if not humanoidRootPart then return end
		local tool = character:FindFirstChildOfClass("Tool")
		if not isGunTool(tool) then return end
		local camera = workspace.CurrentCamera
		local viewmodel = FPSFramework.CurrentViewmodel
		if not viewmodel then return end
		local offset = FPSFramework.CurrentOffset or CFrame.new(0, -0.7, -1.2)
		local velocity = humanoidRootPart.Velocity
		local swayCFrame = ViewmodelSway.GetSmoothedSwayCFrame(velocity, dt)
		viewmodel:PivotTo(camera.CFrame * offset * swayCFrame)
	end)
end

function FPSFramework:DisableSway()
	if FPSFramework._swayConn then
		FPSFramework._swayConn:Disconnect()
		FPSFramework._swayConn = nil
	end
end

-- Fire input management
FPSFramework._fireInputConn = nil
function FPSFramework:BindFireInput()
	if FPSFramework._fireInputConn then return end
	FPSFramework._fireInputConn = UserInputService.InputBegan:Connect(function(input, processed)
		if input.UserInputType == Enum.UserInputType.MouseButton1 and not processed then
			if isHoldingGun() then
				FPSFramework:FireWeapon()
			end
		end
		if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == Enum.KeyCode.R and not processed then
			if isHoldingGun() then
				FPSFramework:Reload()
			end
		end
	end)
end

function FPSFramework:UnbindFireInput()
	if FPSFramework._fireInputConn then
		FPSFramework._fireInputConn:Disconnect()
		FPSFramework._fireInputConn = nil
	end
end

-- Shooting
function FPSFramework:FireWeapon()
	if not isHoldingGun() then return end
	local weaponName = FPSFramework.CurrentGunName or "Default"
	local ammo = getAmmoState(weaponName)
	local stats = GetCrosshairStats(weaponName)
	if ammo.reloading then return end
	if ammo.clip <= 0 then
		return
	end
	local mouse = Players.LocalPlayer:GetMouse()
	local origin = workspace.CurrentCamera.CFrame.Position
	local direction = (mouse.Hit.Position - origin).Unit
	local character = Players.LocalPlayer.Character
	local tool = character and character:FindFirstChildOfClass("Tool")
	local up = stats.RecoilUp or 1.8
	local side = stats.RecoilSide or 0.8
	local accuracyResetTime = stats.AccuracyResetTime or 0.5
	local maxRecoilMultiplier = stats.MaxRecoilMultiplier or 4
	local now = tick()
	if now - FPSFramework._recoilState.lastShotTime > accuracyResetTime then
		FPSFramework._recoilState.consecutiveShots = 0
	end
	FPSFramework._recoilState.lastShotTime = now
	local consecutive = FPSFramework._recoilState.consecutiveShots
	local newDirection = direction
	if consecutive == 0 then
		FPSFramework._recoilState.consecutiveShots = 1
	else
		local spreadMultiplier = math.min(1 + (consecutive * 0.18), maxRecoilMultiplier)
		local horizontal = (math.random() - 0.5) * 2 * side * spreadMultiplier
		local vertical = math.random() * up * spreadMultiplier
		local horizontalRad = math.rad(horizontal)
		local verticalRad = math.rad(vertical)
		newDirection = rotateDirection(direction, verticalRad, horizontalRad)
		FPSFramework._recoilState.consecutiveShots = consecutive + 1
	end
	ReplicatedStorage.FPS_Fire:FireServer(origin, newDirection, weaponName)
	FPSFramework:ShowMuzzleFlash()
	FPSFramework:AnimateCrosshairFire()
	ammo.clip = ammo.clip - 1
	if FPSFramework.OnAmmoChanged then
		FPSFramework.OnAmmoChanged(ammo.clip, ammo.reserve, stats.ClipSize or 10)
	end
end

-- Robust cleanup for all connections and state
function FPSFramework:CleanupAll()
	self:DisableSway()
	self:UnbindFireInput()
	self:DisableViewmodelFollow()
	self:RemoveViewmodel()
end

return FPSFramework

Video:

Help

1 Like

Looking at the code, there is no call to the FPSFramework:UnbindFireInput() function when the player dies or respawns. So the condition if FPSFramework._fireInputConn then return end
prevents new events from being created. I think this is the cause of the problem.

1 Like

I’ll try this and then say if it works, thank you

I tried using this simple code to fix the issue:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local FPSFramework = require(ReplicatedStorage:WaitForChild("FPSFramework"))

local player = Players.LocalPlayer

-- Helper to (re)bind input after respawn
local function onCharacterAdded()
	FPSFramework:BindFireInput()
end

-- Helper to cleanup input and state on death/reset
local function onCharacterRemoving()
	FPSFramework:CleanupAll()
end

-- Connect events
player.CharacterAdded:Connect(onCharacterAdded)
player.CharacterRemoving:Connect(onCharacterRemoving)

-- Initial bind if character already exists
if player.Character then
	onCharacterAdded()
end

Seems to have the same issue.

Then, maybe humanoid.Died Event trigger issue? This connection is only considered when player.CharacterAdded Event triggered; doesn’t if player.Character already exists.

Now it works, but I still can’t fire shots after reseting (reminding: also sway isnt working)
Updated script:

--// FPSClient - Robust Viewmodel, Sway, and Firing System

local Players           = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService  = game:GetService("UserInputService")
local RunService        = game:GetService("RunService")

local FPSFramework      = require(ReplicatedStorage:WaitForChild("FPSFramework"))
local crosshairModule   = require(ReplicatedStorage:WaitForChild("DynamicCrosshair"))
local ViewmodelSway     = require(ReplicatedStorage:WaitForChild("ViewmodelSway"))
local CounterStrafe     = require(ReplicatedStorage:WaitForChild("CounterStrafe"))

local player    = Players.LocalPlayer
local mouse     = player:GetMouse()

-- UI
local UI        = script.Parent:FindFirstChild("crosshair")
local crosshair = crosshairModule.New(UI, 20, 20, 30, 80)

local canFire = true -- Simple debounce, can be expanded for fire rate

-- Ammo UI
local ammoGui, ammoLabel, reserveLabel = nil, nil, nil
do
    local gui = player.PlayerGui:FindFirstChild("Ammo") or script.Parent:FindFirstChild("Ammo")
    if gui then
        ammoGui = gui
        local frame = gui:FindFirstChildWhichIsA("Frame")
        if frame then
            local labels = {}
            for i, child in frame:GetChildren() do
                if child:IsA("TextLabel") then
                    table.insert(labels, child)
                end
            end
            ammoLabel = labels[1]
            reserveLabel = labels[2]
        end
    end
end

local function updateAmmoUI()
    if not ammoLabel or not reserveLabel then return end
    local clip, reserve, clipSize = FPSFramework:GetAmmo()
    ammoLabel.Text = tostring(clip) .. " / " .. tostring(clipSize)
    reserveLabel.Text = tostring(reserve)
end

FPSFramework.OnAmmoChanged = function(clip, reserve, clipSize)
    updateAmmoUI()
end

-- Counter-strafe logic (optional, as in original)
local counterStrafe = CounterStrafe.new(function(direction)
    if typeof(ResetRecoilAccuracy) == "function" then
        ResetRecoilAccuracy()
    elseif self and self.RecoilAccuracy then
        self.RecoilAccuracy = 1
    end
end)
script.AncestryChanged:Connect(function()
    counterStrafe:Destroy()
end)

-- Crosshair logic
local function initCrosshair()
    crosshair:Enable()
    crosshair.Spreading.MinSpread = 10
    crosshair.Spreading.MaxSpread = 10
    updateAmmoUI()
end
local function disableCrosshair()
    crosshair:Disable()
end

local function startAiming()
    crosshair.Spreading.MinSpread = 5
    crosshair.Spreading.MaxSpread = 5
    crosshair:SmoothSet(20, 0.1)
end
local function stopAiming()
    crosshair:SmoothSet(40, 0.1)
    crosshair.Spreading.MinSpread = 10
    crosshair.Spreading.MaxSpread = 10
end

local function updateCrosshair(dt)
    crosshair:Update(dt)
end

-- Gun/tool helpers
local function isGunTool(tool)
    if not tool or not tool:IsA("Tool") then return false end
    local viewmodelsFolder = ReplicatedStorage:FindFirstChild("Game") and ReplicatedStorage.Game:FindFirstChild("Viewmodels")
    if not viewmodelsFolder then return false end
    local vm = viewmodelsFolder:FindFirstChild("v_" .. tool.Name)
    return vm ~= nil and vm:IsA("Model")
end
local function isHoldingGun()
    local character = player.Character
    if not character then return false end
    local tool = character:FindFirstChildOfClass("Tool")
    return isGunTool(tool)
end

-- Viewmodel and connection management
local currentViewmodel = nil
local connections = {}

local function disconnectAll()
    for i, conn in connections do
        if conn then
            conn:Disconnect()
        end
    end
    connections = {}
end

local function destroyCurrentViewmodel()
    -- Always remove any FPSViewmodel from camera, even if currentViewmodel is nil
    local camera = workspace.CurrentCamera
    for i, child in camera:GetChildren() do
        if child:IsA("Model") and child.Name == "FPSViewmodel" then
            child:Destroy()
        end
    end
    -- Also destroy our tracked viewmodel if it exists
    if currentViewmodel and currentViewmodel.Parent then
        currentViewmodel:Destroy()
        currentViewmodel = nil
    end
    -- Double safety: call FPSFramework's RemoveViewmodel
    if FPSFramework and typeof(FPSFramework.RemoveViewmodel) == "function" then
        FPSFramework:RemoveViewmodel()
    end
    currentViewmodel = nil
end

-- Sway
local swayConn = nil
local function enableViewmodelSway()
    if swayConn then swayConn:Disconnect() swayConn = nil end
    swayConn = RunService.RenderStepped:Connect(function(dt)
        local character = player.Character
        if not character then return end
        local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
        if not humanoidRootPart then return end
        local tool = character:FindFirstChildOfClass("Tool")
        if not isGunTool(tool) then return end
        local camera = workspace.CurrentCamera
        local viewmodel = nil
        for i, child in camera:GetChildren() do
            if child:IsA("Model") and child.Name == "FPSViewmodel" then
                viewmodel = child
                break
            end
        end
        if not viewmodel then return end
        local offset = FPSFramework.CurrentOffset or CFrame.new(0, -0.7, -1.2)
        local velocity = humanoidRootPart.Velocity
        local swayCFrame = ViewmodelSway.GetSmoothedSwayCFrame(velocity, dt)
        viewmodel:PivotTo(camera.CFrame * offset * swayCFrame)
    end)
end
local function disableViewmodelSway()
    if swayConn then swayConn:Disconnect() swayConn = nil end
end

-- Tool equip/unequip logic
local function onToolEquipped(tool)
    if not isGunTool(tool) then return end
    destroyCurrentViewmodel()
    currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
    FPSFramework:EnableViewmodelFollow(player)
    FPSFramework:HideMouse()
    initCrosshair()
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
    enableViewmodelSway()
    FPSFramework:BindFireInput()
    -- Enable the tool in case it was disabled on death
    if tool and tool:IsA("Tool") then
        tool.Enabled = true
    end
end

local function onToolUnequipped(tool)
    FPSFramework:DisableViewmodelFollow()
    FPSFramework:ShowMouse()
    disableCrosshair()
    FPSFramework:UpdateWalkSpeed()
    disableViewmodelSway()
    destroyCurrentViewmodel()
    updateAmmoUI()
end

-- Tool event connections
local function connectToolEvents(character)
    disconnectAll()
    for i, tool in character:GetChildren() do
        if tool:IsA("Tool") then
            table.insert(connections, tool.Equipped:Connect(function() onToolEquipped(tool) end))
            table.insert(connections, tool.Unequipped:Connect(function() onToolUnequipped(tool) end))
        end
    end
    table.insert(connections, character.ChildAdded:Connect(function(child)
        if child:IsA("Tool") then
            table.insert(connections, child.Equipped:Connect(function() onToolEquipped(child) end))
            table.insert(connections, child.Unequipped:Connect(function() onToolUnequipped(child) end))
        end
    end))
end

-- Helper to robustly disable a tool (prevents firing, disables input, etc)
local function DisableTool(tool)
    if tool and tool:IsA("Tool") then
        tool.Enabled = false
    end
end

-- Cleanup on death/reset/unequip
local function cleanupCharacter()
    -- Delegate all tool/ammo cleanup to FPSFramework for proper handling
    if FPSFramework and typeof(FPSFramework.CleanupCharacterToolsAndAmmo) == "function" then
        FPSFramework:CleanupCharacterToolsAndAmmo(player)
    end
    disableViewmodelSway()
    disconnectAll()
    destroyCurrentViewmodel()
    updateAmmoUI()
end

-- Character lifecycle
player.CharacterAdded:Connect(function(character)
    task.wait(0.1)
    FPSFramework:SetupMovement(player)
    FPSFramework:SetCrosshair(crosshair)
    FPSFramework:BindFireInput()
    connectToolEvents(character)
    -- If player already has a gun tool equipped on spawn
    for i, tool in character:GetChildren() do
        if tool:IsA("Tool") and isGunTool(tool) then
            destroyCurrentViewmodel()
            currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
            FPSFramework:EnableViewmodelFollow(player)
            enableViewmodelSway()
            FPSFramework:BindFireInput()
            -- Enable the tool in case it was disabled on death
            tool.Enabled = true
        end
    end
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
    -- Clean up on death
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid.Died:Connect(function()
            cleanupCharacter()
        end)
    end
end)

if player.Character then
    FPSFramework:SetupMovement(player)
    FPSFramework:SetCrosshair(crosshair)
    FPSFramework:BindFireInput()
    connectToolEvents(player.Character)
    for i, tool in player.Character:GetChildren() do
        if tool:IsA("Tool") and isGunTool(tool) then
            destroyCurrentViewmodel()
            currentViewmodel = FPSFramework:SpawnViewmodel(player, tool.Name)
            FPSFramework:EnableViewmodelFollow(player)
            enableViewmodelSway()
            FPSFramework:BindFireInput()
            -- Enable the tool in case it was disabled on death
            tool.Enabled = true
        end
    end
    FPSFramework:UpdateWalkSpeed()
    updateAmmoUI()
    -- Ensure Humanoid.Died is connected if character already exists
    local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid.Died:Connect(function()
            cleanupCharacter()
        end)
    end
end

-- Listen for reload input (redundant if handled in FPSFramework, but ensures UI updates)
UserInputService.InputBegan:Connect(function(input, processed)
    if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == Enum.KeyCode.R and not processed then
        if isHoldingGun() then
            FPSFramework:Reload()
        end
    end
end)

-- Crosshair update loop
RunService.RenderStepped:Connect(function(dt)
    updateCrosshair(dt)
end)

Here’s sway module btw:

-- ViewmodelSway Module (Smoothed & Extra Subtle)
-- Provides a function to compute a smoothed sway CFrame based on velocity
-- NOTE: Call ViewmodelSway.Reset() whenever you re-equip or recreate the viewmodel to avoid smoothing glitches.

local ViewmodelSway = {}

-- Sway settings (further reduced for extra subtle effect)
local SWAY_TRANSLATE_FACTOR = 0.005 -- studs per (stud/s) of velocity (was 0.007)
local SWAY_ROTATE_FACTOR = 0.003 -- radians per (stud/s) of velocity (was 0.004)
local SWAY_MAX_TRANSLATE = 0.06 -- max translation in studs (was 0.08)
local SWAY_MAX_ROTATE = math.rad(1.8) -- max rotation in radians (was 2.5 deg)

local SMOOTH_SPEED = 10 -- higher = snappier, lower = smoother

-- Internal state for smoothing
local lastTranslate = Vector3.new()
local lastRotX = 0
local lastRotY = 0

-- Resets the smoothing state (call this when you re-equip or recreate the viewmodel)
function ViewmodelSway.Reset()
    lastTranslate = Vector3.new()
    lastRotX = 0
    lastRotY = 0
end

-- Returns a CFrame offset for the viewmodel based on velocity (Vector3), smoothed over time
function ViewmodelSway.GetSmoothedSwayCFrame(velocity: Vector3, dt)
    -- Only use X and Z (horizontal movement)
    local moveXZ = Vector3.new(velocity.X, 0, velocity.Z)
    local speed = moveXZ.Magnitude
    local targetTranslate = Vector3.new()
    local targetRotX = 0
    local targetRotY = 0

    if speed >= 0.1 then
        local dir = moveXZ.Unit
        targetTranslate = dir * -math.min(speed * SWAY_TRANSLATE_FACTOR, SWAY_MAX_TRANSLATE)
        targetRotY = math.clamp(dir.X * speed * SWAY_ROTATE_FACTOR, -SWAY_MAX_ROTATE, SWAY_MAX_ROTATE)
        targetRotX = math.clamp(-dir.Z * speed * SWAY_ROTATE_FACTOR, -SWAY_MAX_ROTATE, SWAY_MAX_ROTATE)
    end

    -- Smoothly interpolate translation and rotation
    local alpha = 1 - math.exp(-SMOOTH_SPEED * (dt or 1/60))
    lastTranslate = lastTranslate:Lerp(targetTranslate, alpha)
    lastRotX = lastRotX + (targetRotX - lastRotX) * alpha
    lastRotY = lastRotY + (targetRotY - lastRotY) * alpha

    local swayCFrame = CFrame.new(lastTranslate) * CFrame.Angles(lastRotX, lastRotY, 0)
    return swayCFrame
end

-- For backward compatibility (non-smoothed, but now uses reduced factors)
function ViewmodelSway.GetSwayCFrame(velocity: Vector3)
    local moveXZ = Vector3.new(velocity.X, 0, velocity.Z)
    local speed = moveXZ.Magnitude
    if speed < 0.1 then
        return CFrame.new()
    end

    local dir = moveXZ.Unit
    local translate = dir * -math.min(speed * SWAY_TRANSLATE_FACTOR, SWAY_MAX_TRANSLATE)
    local rotY = math.clamp(dir.X * speed * SWAY_ROTATE_FACTOR, -SWAY_MAX_ROTATE, SWAY_MAX_ROTATE)
    local rotX = math.clamp(-dir.Z * speed * SWAY_ROTATE_FACTOR, -SWAY_MAX_ROTATE, SWAY_MAX_ROTATE)
    local swayCFrame = CFrame.new(translate) * CFrame.Angles(rotX, rotY, 0)
    return swayCFrame
end

return ViewmodelSway

Could you please provide a video again?

This is what i added to fps client, so you don’t have to find for it:

-- Ensure Humanoid.Died is connected if character already exists
    local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid.Died:Connect(function()
            cleanupCharacter()
        end)
    end

Have you tried putting the code that outputs to the gun fire event and checking that it outputs when you click the screen to fire the gun after respawning?

no, tried it just now, script:

-- Debug script: Listen for FPS_Fire RemoteEvent firing from this client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local FPS_Fire = ReplicatedStorage:FindFirstChild("FPS_Fire")
if FPS_Fire then
    FPS_Fire.OnClientEvent:Connect(function(...)
        print("[DEBUG] FPS_Fire RemoteEvent received on client with args:", ...)
    end)
end

-- Optionally, monkey-patch FPSFramework to print when firing
local FPSFramework = require(ReplicatedStorage:WaitForChild("FPSFramework"))
local oldFire = FPSFramework.Fire
FPSFramework.Fire = function(self, ...)
    print("[DEBUG] FPSFramework.Fire called with args:", ...)
    if oldFire then
        return oldFire(self, ...)
    end
end

seems to not even print anything after I reset and try to fire

It means there is a problem with the event connections.
if FPSFramework._fireInputConn then return end
I think there is a problem with this codes. Could you please put the code that prints just above and below this code and let me know the result?

before reset:


after:
image
(btw, checking 5 was checking 2 before reset)

Then, trying just deleting this line:

if FPSFramework._fireInputConn then return end

and just putting:

self:UnbindFireInput()

This would be more efficient since whenever you call the FPSFramework:BindFireInput() function, it would automatically unbind and delete the previous event, and if this works, it would probably be a good idea to remove some of the code that calls this function multiple times.

1 Like

Thank you! Anything for the sway though?

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