local RecoilModule = {}
RecoilModule.__index = RecoilModule
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local function lerp(a, b, t)
return a + (b - a) * t
end
local Spring = {}
Spring.__index = Spring
function Spring.new(stiffness, damping, mass)
local self = setmetatable({}, Spring)
self.stiffness = stiffness or 100
self.damping = damping or 10
self.mass = mass or 1
self.position = vector.zero
self.velocity = vector.zero
self.target = vector.zero
return self
end
function Spring:Update(dt)
local force = (self.target - self.position) * self.stiffness
force = force - self.velocity * self.damping
local acceleration = force / self.mass
self.velocity = self.velocity + acceleration * dt
self.position = self.position + self.velocity * dt
return self.position
end
function Spring:Impulse(force)
self.velocity = self.velocity + force / self.mass
end
function Spring:Reset()
self.position = vector.zero
self.velocity = vector.zero
self.target = vector.zero
end
function RecoilModule.new(camera)
local self = setmetatable({}, RecoilModule)
self.camera = camera or workspace.CurrentCamera
self.player = Players.LocalPlayer
self.spring = Spring.new(150, 15, 1)
self.visualSpring = Spring.new(200, 20, 1)
self.shakeSpring = Spring.new(250, 25, 0.8)
self.currentRecoil = vector.zero
self.targetRecoil = vector.zero
self.visualRecoil = vector.zero
self.shakeRecoil = vector.zero
self.patterns = {}
self.currentPatternIndex = 1
self.shotCount = 0
self.recoverySpeed = 8
self.maxRecoilX = 15
self.maxRecoilY = 25
self.connection = nil
self.isActive = false
self.config = {
multiplier = 1;
recoveryDelay = 0.1;
patternReset = .5;
smoothing = .85;
visualIntensity = 1.2;
adsMultiplier = .6;
shakeIntensity = 1.5;
shakeFrequency = 35;
shakeDecay = .88;
horizontalShake = 1.8;
verticalShake = 1.4;
rotationalShake = .8;
}
self.lastShotTime = 0
self.isAiming = false
self.shakeTime = 0
self.currentShakeIntensity = 0
return self
end
function RecoilModule:SetConfig(config)
for key, value in config do
if self.config[key] ~= nil then
self.config[key] = value
end
end
if config.spring then
self.spring.stiffness = config.spring.stiffness or self.spring.stiffness
self.spring.damping = config.spring.damping or self.spring.damping
self.spring.mass = config.spring.mass or self.spring.mass
end
if config.visualSpring then
self.visualSpring.stiffness = config.visualSpring.stiffness or self.visualSpring.stiffness
self.visualSpring.damping = config.visualSpring.damping or self.visualSpring.damping
self.visualSpring.mass = config.visualSpring.mass or self.visualSpring.mass
end
if config.shakeSpring then
self.shakeSpring.stiffness = config.shakeSpring.stiffness or self.shakeSpring.stiffness
self.shakeSpring.damping = config.shakeSpring.damping or self.shakeSpring.damping
self.shakeSpring.mass = config.shakeSpring.mass or self.shakeSpring.mass
end
self.recoverySpeed = config.recoverySpeed or self.recoverySpeed
self.maxRecoilX = config.maxRecoilX or self.maxRecoilX
self.maxRecoilY = config.maxRecoilY or self.maxRecoilY
end
function RecoilModule:LoadPreset(preset)
if preset.pattern then
self.patterns = preset.pattern
end
if preset.config then
self:SetConfig(preset.config)
end
end
function RecoilModule:SetAiming(isAiming)
self.isAiming = isAiming
end
function RecoilModule:AddRecoil(recoilData)
local currentTime = tick()
if currentTime - self.lastShotTime > self.config.patternReset then
self.shotCount = 0
self.currentPatternIndex = 1
end
self.lastShotTime = currentTime
self.shotCount += 1
local recoilAmount = recoilData or {x = 0; y = 1; z = 0}
if self.patterns and #self.patterns > 0 then
local pattern = self.patterns[math.min(self.currentPatternIndex, #self.patterns)]
recoilAmount = {
x = pattern.x or recoilAmount.x;
y = pattern.y or recoilAmount.y;
z = pattern.z or recoilAmount.z;
}
self.currentPatternIndex = math.min(self.currentPatternIndex + 1, #self.patterns)
end
local multiplier = self.config.multiplier
if self.isAiming then
multiplier *= self.config.adsMultiplier
end
local kick = vector.create(
recoilAmount.x * multiplier,
recoilAmount.y * multiplier,
recoilAmount.z * multiplier
)
self.spring:Impulse(kick)
local visualKick = kick * self.config.visualIntensity
self.visualSpring:Impulse(visualKick)
local shakeKick = vector.create(
(math.random() - .5) * self.config.horizontalShake * self.config.shakeIntensity,
(math.random() - .5) * self.config.verticalShake * self.config.shakeIntensity,
(math.random() - .5) * self.config.rotationalShake * self.config.shakeIntensity
)
self.shakeSpring:Impulse(shakeKick)
self.currentShakeIntensity = self.config.shakeIntensity
self.shakeTime = currentTime
if not self.isActive then
self:Start()
end
end
function RecoilModule:AddCameraShake(intensity, duration, frequency)
intensity = intensity or .5
duration = duration or .2
frequency = frequency or 20
local startTime = tick()
local shakeConnection
shakeConnection = RunService.RenderStepped:Connect(function()
local elapsed = tick() - startTime
if elapsed > duration then
shakeConnection:Disconnect()
return
end
local progress = elapsed / duration
local fadeOut = 1 - progress
local shakeX = math.sin(elapsed * frequency) * intensity * fadeOut
local shakeY = math.cos(elapsed * frequency * 1.3) * intensity * fadeOut
local shakeZ = math.sin(elapsed * frequency * .7) * intensity * .5 * fadeOut
self.visualSpring:Impulse(vector.create(shakeX, shakeY, shakeZ) * .1)
end)
end
function RecoilModule:Update(dt)
local recoilSpring = self.spring:Update(dt)
local visualSpring = self.visualSpring:Update(dt)
local shakeSpring = self.shakeSpring:Update(dt)
self.currentRecoil = lerp(self.currentRecoil, recoilSpring, 1 - self.config.smoothing)
self.visualRecoil = visualSpring
local currentTime = tick()
local shakeElapsed = currentTime - self.shakeTime
if self.currentShakeIntensity > .01 then
self.currentShakeIntensity = self.currentShakeIntensity * self.config.shakeDecay
local freq = self.config.shakeFrequency
local shakeNoise = vector.create(
math.sin(shakeElapsed * freq) * self.currentShakeIntensity * .15,
math.cos(shakeElapsed * freq * 1.3) * self.currentShakeIntensity * .12,
math.sin(shakeElapsed * freq * .7) * self.currentShakeIntensity * .08
)
self.shakeRecoil = shakeSpring + shakeNoise
else
self.shakeRecoil = shakeSpring
end
if currentTime - self.lastShotTime > self.config.recoveryDelay then
self.spring.target = vector.zero
self.visualSpring.target = vector.zero
self.shakeSpring.target = vector.zero
if self.spring.position.Magnitude < .01 and self.spring.velocity.Magnitude < .01 then
self.shotCount = 0
self.currentPatternIndex = 1
end
end
local totalRecoil = self.currentRecoil + self.visualRecoil + self.shakeRecoil
totalRecoil = vector.create(
math.clamp(totalRecoil.X, -self.maxRecoilX, self.maxRecoilX),
math.clamp(totalRecoil.Y, -self.maxRecoilY, self.maxRecoilY),
totalRecoil.Z
)
local currentCFrame = self.camera.CFrame
local recoilCFrame = CFrame.Angles(
math.rad(-totalRecoil.Y),
math.rad(totalRecoil.X),
math.rad(totalRecoil.Z)
)
self.camera.CFrame = currentCFrame * recoilCFrame
end
function RecoilModule:Start()
if self.connection then
return
end
self.isActive = true
self.connection = RunService.RenderStepped:Connect(function(dt)
self:Update(dt)
end)
end
function RecoilModule:Stop()
if self.connection then
self.connection:Disconnect()
self.connection = nil
self.isActive = false
end
end
function RecoilModule:Reset()
self.spring:Reset()
self.visualSpring:Reset()
self.shakeSpring:Reset()
self.currentRecoil = vector.zero
self.targetRecoil = vector.zero
self.visualRecoil = vector.zero
self.shakeRecoil = vector.zero
self.shotCount = 0
self.currentPatternIndex = 1
self.lastShotTime = 0
self.currentShakeIntensity = 0
self.shakeTime = 0
end
function RecoilModule:Destroy()
self:Stop()
self.spring = nil
self.visualSpring = nil
self.shakeSpring = nil
self.camera = nil
end