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
