if RunService:IsClient()then if LocalPlayer.Character then onCharacterAdded(LocalPlayer.Character)end LocalPlayer.CharacterAdded:Connect(onCharacterAdded)LocalPlayer.CharacterRemoving:Connect(onCharacterRemoving)end local N={}N.__index=N N.SpringService=nil function N.new(v)local Q=setmetatable({},N)Q.weaponsSystem=v Q.fieldOfView=70 Q.minPitch=math.rad(-75)Q.maxPitch=math.rad(75)Q.normalOffset=Vector3.new(2.25,2.25,10.5)Q.zoomedOffsetDistance=8 Q.normalCrosshairScale=1 Q.zoomedCrosshairScale=.75 Q.defaultZoomFactor=1 Q.canZoom=true Q.zoomInputs={Enum.UserInputType.MouseButton2,Enum.KeyCode.ButtonL2}Q.sprintInputs={Enum.KeyCode.LeftShift}Q.mouseRadsPerPixel=Vector2.new(.0020833333333333,.0020833333333333)Q.zoomedMouseRadsPerPixel=Vector2.new(.00083333333333333,.00083333333333333)Q.touchSensitivity=Vector2.new(.01,.01)Q.zoomedTouchSensitivity=Vector2.new(.005,.005)Q.touchDelayTime=.25 Q.recoilDecay=2 Q.rotateCharacterWithCamera=true Q.gamepadSensitivityModifier=Vector2.new(.85,.65)Q.zoomWalkSpeed=8 Q.normalWalkSpeed=16 Q.sprintingWalkSpeed=24 Q.enabled=false Q.yaw=0 Q.pitch=0 Q.currentCFrame=CFrame.new()Q.currentOffset=Q.normalOffset Q.currentRecoil=Vector2.new(0,0)Q.currentMouseRadsPerPixel=Q.mouseRadsPerPixel Q.currentTouchSensitivity=Q.touchSensitivity Q.mouseLocked=true Q.touchPanAccumulator=Vector2.new(0,0)Q.currentTool=nil Q.sprintingInputActivated=false Q.desiredWalkSpeed=Q.normalWalkSpeed Q.sprintEnabled=false Q.slowZoomWalkEnabled=false Q.desiredFieldOfView=Q.fieldOfView Q.zoomedFromInput=false Q.forcedZoomed=false Q.zoomState=false Q.zoomAlpha=0 Q.hasScope=false Q.hideToolWhileZoomed=false Q.currentZoomFactor=Q.defaultZoomFactor Q.zoomedFOV=Q.fieldOfView Q.gamepadPan=Vector2.new(0,0)Q.movementPan=Vector2.new(0,0)Q.lastThumbstickPos=Vector2.new(0,0)Q.lastThumbstickTime=nil Q.currentGamepadSpeed=0 Q.lastGamepadVelocity=Vector2.new(0,0)Q.lastOcclusionDistance=0 Q.lastOcclusionReachedTime=0 Q.defaultTimeUntilZoomOut=0 Q.timeUntilZoomOut=Q.defaultTimeUntilZoomOut Q.timeLastPoppedWayIn=0 Q.isZoomingOut=false Q.tweenOutTime=.2 Q.curOcclusionTween=nil Q.occlusionTweenObject=nil Q.sideCorrectionGoalVector=nil Q.lastSideCorrectionMagnitude=0 Q.lastSideCorrectionReachedTime=0 Q.revertSideCorrectionSpeedMultiplier=2 Q.defaultTimeUntilRevertSideCorrection=.75 Q.timeUntilRevertSideCorrection=Q.defaultTimeUntilRevertSideCorrection Q.isRevertingSideCorrection=false Q.eventConnections={}Q.raycastIgnoreList={}Q.currentCamera=nil Q.currentCharacter=nil Q.currentHumanoid=nil Q.currentRootPart=nil Q.controlModule=nil Q.random=Random.new()return Q end function N.setEnabled(v,N)if v.enabled==N then return end v.enabled=N if v.enabled then RunService:BindToRenderStep(CAMERA_RENDERSTEP_NAME,Enum.RenderPriority.Camera.Value-1,function(N)v:onRenderStep(N)end)ContextActionService:BindAction(ZOOM_ACTION_NAME,function(...)v:onZoomAction(...)end,false,unpack(v.zoomInputs))ContextActionService:BindAction(SPRINT_ACTION_NAME,function(...)v:onSprintAction(...)end,false,unpack(v.sprintInputs))table.insert(v.eventConnections,LocalPlayer.CharacterAdded:Connect(function(N)v:onCurrentCharacterChanged(N)end))table.insert(v.eventConnections,LocalPlayer.CharacterRemoving:Connect(function()v:onCurrentCharacterChanged(nil)end))table.insert(v.eventConnections,(workspace:GetPropertyChangedSignal("\067\117\114\114\101\110\116\067\097\109\101\114\097")):Connect(function()v:onCurrentCameraChanged(workspace.CurrentCamera)end))table.insert(v.eventConnections,UserInputService.InputBegan:Connect(function(N,Q)v:onInputBegan(N,Q)end))table.insert(v.eventConnections,UserInputService.InputChanged:Connect(function(N,Q)v:onInputChanged(N,Q)end))table.insert(v.eventConnections,UserInputService.InputEnded:Connect(function(N,Q)v:onInputEnded(N,Q)end))v:onCurrentCharacterChanged(LocalPlayer.Character)v:onCurrentCameraChanged(workspace.CurrentCamera)local N=v.currentCamera.CFrame.lookVector v.yaw=math.atan2(-N.X,-N.Z)v.pitch=math.asin(N.Y)v.currentCamera.CameraType=Enum.CameraType.Scriptable v:setZoomFactor(v.currentZoomFactor)workspace.CurrentCamera.CameraSubject=v.currentRootPart v.occlusionTweenObject=Instance.new("\078\117\109\098\101\114\086\097\108\117\101")v.occlusionTweenObject.Name="\079\099\099\108\117\115\105\111\110\084\119\101\101\110\079\098\106\101\099\116"v.occlusionTweenObject.Parent=script v.occlusionTweenObject.Changed:Connect(function(N)v.lastOcclusionDistance=N end)v.weaponsSystem.aimRayCallback=function()local N=v.currentCFrame return Ray.new(N.p,N.LookVector*500)end else RunService:UnbindFromRenderStep(CAMERA_RENDERSTEP_NAME)ContextActionService:UnbindAction(ZOOM_ACTION_NAME)ContextActionService:UnbindAction(SPRINT_ACTION_NAME)if v.currentHumanoid then v.currentHumanoid.AutoRotate=true end if v.currentCamera then v.currentCamera.CameraType=Enum.CameraType.Custom end v:updateZoomState()v.yaw=0 v.pitch=0 for N,v in pairs(v.eventConnections)do v:Disconnect()end v.eventConnections={}UserInputService.MouseBehavior=Enum.MouseBehavior.Default UserInputService.MouseIconEnabled=true end end function N.onRenderStep(Q,v)if not Q.enabled or not Q.currentCamera or not Q.currentCharacter or not Q.currentHumanoid or not Q.currentRootPart then return end if Q.mouseLocked and not GuiService:GetEmotesMenuOpen()then UserInputService.MouseBehavior=Enum.MouseBehavior.LockCenter UserInputService.MouseIconEnabled=false else UserInputService.MouseBehavior=Enum.MouseBehavior.Default UserInputService.MouseIconEnabled=true end Q:processGamepadInput(v)if Q.hasScope then N.SpringService:Target(Q,.8,8,{zoomAlpha=Q.zoomState and 1 or 0})N.SpringService:Target(Q.currentCamera,.8,8,{FieldOfView=Q.desiredFieldOfView})else N.SpringService:Target(Q,.8,3,{zoomAlpha=Q.zoomState and 1 or 0})N.SpringService:Target(Q.currentCamera,.8,3,{FieldOfView=Q.desiredFieldOfView})end if Q.sprintEnabled or Q.slowZoomWalkEnabled then Q.desiredWalkSpeed=Q.normalWalkSpeed if Q.sprintEnabled and((Q.sprintingInputActivated or Q:sprintFromTouchInput()or Q:sprintFromGamepadInput())and not Q.zoomState)then Q.desiredWalkSpeed=Q.sprintingWalkSpeed end if Q.slowZoomWalkEnabled and Q.zoomAlpha>.1 then Q.desiredWalkSpeed=Q.zoomWalkSpeed end N.SpringService:Target(Q.currentHumanoid,.95,4,{WalkSpeed=Q.desiredWalkSpeed})end local Z=Q.currentRootPart.CFrame.Position local J=CFrame.new(Z)local A=CFrame.Angles(0,Q.yaw,0)local j=CFrame.Angles(Q.pitch+Q.currentRecoil.Y,0,0)local u=CFrame.new(Q.normalOffset.X,0,0)local O=CFrame.new(0,Q.normalOffset.Y,0)local n=CFrame.new(0,0,Q.normalOffset.Z)local K=Q:getCollisionRadius()local R=A*u local U=J*R local w=U.p-Z local q=Ray.new(Z,w+w.Unit*K)local h,X,e=Q:penetrateCast(q,Q.raycastIgnoreList)local t=tick()local D=Vector3.new()if h then X=X+e*K D=X-U.p if D.Magnitude>=Q.lastSideCorrectionMagnitude then if t>Q.lastSideCorrectionReachedTime+Q.timeUntilRevertSideCorrection and Q.lastSideCorrectionMagnitude~=0 then Q.timeUntilRevertSideCorrection=Q.defaultTimeUntilRevertSideCorrection*2 elseif Q.lastSideCorrectionMagnitude==0 and Q.timeUntilRevertSideCorrection~=Q.defaultTimeUntilRevertSideCorrection then Q.timeUntilRevertSideCorrection=Q.defaultTimeUntilRevertSideCorrection end Q.lastSideCorrectionMagnitude=D.Magnitude Q.lastSideCorrectionReachedTime=t Q.isRevertingSideCorrection=false else Q.isRevertingSideCorrection=true end elseif Q.lastSideCorrectionMagnitude~=0 then Q.isRevertingSideCorrection=true end if Q.isRevertingSideCorrection then if D.Magnitude>Q.lastSideCorrectionMagnitude-1 and D.Magnitude~=0 then Q.lastSideCorrectionReachedTime=t end if t>Q.lastSideCorrectionReachedTime+Q.timeUntilRevertSideCorrection then local N=(v*w.Magnitude)*Q.revertSideCorrectionSpeedMultiplier Q.lastSideCorrectionMagnitude=Q.lastSideCorrectionMagnitude-N if D.Magnitude>=Q.lastSideCorrectionMagnitude then Q.lastSideCorrectionMagnitude=D.Magnitude Q.lastSideCorrectionReachedTime=t Q.isRevertingSideCorrection=false end end end R=R+-w.Unit*Q.lastSideCorrectionMagnitude U=J*R Q.currentCamera.Focus=U local S=((R*j)*O)*n Q.currentCFrame=J*S if Q.zoomAlpha>0 then local N=math.max(Q.zoomedOffsetDistance-Q.lastOcclusionDistance,0)Q.currentCFrame=Q.currentCFrame:lerp(Q.currentCFrame+N*Q.currentCFrame.LookVector.Unit,Q.zoomAlpha)end Q.currentCamera.CFrame=Q.currentCFrame local G=Q.currentCamera:GetLargestCutoffDistance(Q.raycastIgnoreList)if G>1e-05 then G=G+K end if G>=Q.lastOcclusionDistance then if Q.curOcclusionTween~=nil then Q.curOcclusionTween:Cancel()Q.curOcclusionTween=nil end if t>Q.lastOcclusionReachedTime+Q.timeUntilZoomOut and Q.lastOcclusionDistance~=0 then Q.timeUntilZoomOut=Q.defaultTimeUntilZoomOut*2 elseif Q.lastOcclusionDistance==0 and Q.timeUntilZoomOut~=Q.defaultTimeUntilZoomOut then Q.timeUntilZoomOut=Q.defaultTimeUntilZoomOut end if G/Q.normalOffset.Z>.8 and Q.timeLastPoppedWayIn==0 then Q.timeLastPoppedWayIn=t end Q.lastOcclusionDistance=G Q.lastOcclusionReachedTime=t Q.isZoomingOut=false else Q.isZoomingOut=true if G>Q.lastOcclusionDistance-2 and G~=0 then Q.lastOcclusionReachedTime=t end if t<Q.timeLastPoppedWayIn+Q.defaultTimeUntilZoomOut and Q.lastOcclusionDistance/Q.normalOffset.Z>.8 then Q.lastOcclusionDistance=G Q.lastOcclusionReachedTime=t Q.isZoomingOut=false elseif t>=Q.timeLastPoppedWayIn+Q.defaultTimeUntilZoomOut and Q.timeLastPoppedWayIn~=0 then Q.timeLastPoppedWayIn=0 end end if t>=Q.lastOcclusionReachedTime+Q.timeUntilZoomOut and not Q.zoomState then if Q.curOcclusionTween==nil then Q.occlusionTweenObject.Value=Q.lastOcclusionDistance local N=TweenInfo.new(Q.tweenOutTime)local v={}v.Value=Q.lastOcclusionDistance-Q.normalOffset.Z Q.curOcclusionTween=TweenService:Create(Q.occlusionTweenObject,N,v)Q.curOcclusionTween:Play()end end local L=Q.currentCFrame.LookVector.Unit Q.currentCFrame=Q.currentCFrame+L*Q.lastOcclusionDistance Q.currentCamera.CFrame=Q.currentCFrame Q.currentRecoil=Q.currentRecoil-(Q.currentRecoil*Q.recoilDecay)*v if Q:isHumanoidControllable()and Q.rotateCharacterWithCamera then Q.currentHumanoid.AutoRotate=false Q.currentRootPart.CFrame=CFrame.Angles(0,Q.yaw,0)+Q.currentRootPart.Position Q:applyRootJointFix()else Q.currentHumanoid.AutoRotate=true end Q:handlePartTransparencies()Q:handleTouchToolFiring()end

if RunService:IsClient() then
	if LocalPlayer.Character then
		onCharacterAdded(LocalPlayer.Character)
	end
	LocalPlayer.CharacterAdded:Connect(onCharacterAdded)
	LocalPlayer.CharacterRemoving:Connect(onCharacterRemoving)
end


local ShoulderCamera = {}
ShoulderCamera.__index = ShoulderCamera
ShoulderCamera.SpringService = nil

function ShoulderCamera.new(weaponsSystem)
	local self = setmetatable({}, ShoulderCamera)
	self.weaponsSystem = weaponsSystem

	-- Configuration parameters (constants)
	self.fieldOfView = 70
	self.minPitch = math.rad(-75) -- min degrees camera can angle down
	self.maxPitch = math.rad(75) -- max degrees camera can cangle up
	self.normalOffset = Vector3.new(2.25, 2.25, 10.5) -- this is the camera's offset from the player
	self.zoomedOffsetDistance = 8 -- number of studs to zoom in from default offset when zooming
	self.normalCrosshairScale = 1
	self.zoomedCrosshairScale = 0.75
	self.defaultZoomFactor = 1
	self.canZoom = true
	self.zoomInputs = { Enum.UserInputType.MouseButton2, Enum.KeyCode.ButtonL2 }
	self.sprintInputs = { Enum.KeyCode.LeftShift }
	self.mouseRadsPerPixel = Vector2.new(1 / 480, 1 / 480)
	self.zoomedMouseRadsPerPixel = Vector2.new(1 / 1200, 1 / 1200)
	self.touchSensitivity = Vector2.new(1 / 100, 1 / 100)
	self.zoomedTouchSensitivity = Vector2.new(1 / 200, 1 / 200)
	self.touchDelayTime = 0.25 -- max time for a touch to count as a tap (to shoot the weapon instead of control camera),
	                           -- also the amount of time players have to start a second touch after releasing the first time to trigger automatic fire
	self.recoilDecay = 2 -- higher number means faster recoil decay rate
	self.rotateCharacterWithCamera = true
	self.gamepadSensitivityModifier = Vector2.new(0.85, 0.65)
	-- Walk speeds
	self.zoomWalkSpeed = 8
	self.normalWalkSpeed = 16
	self.sprintingWalkSpeed = 24

	-- Current state
	self.enabled = false
	self.yaw = 0
	self.pitch = 0
	self.currentCFrame = CFrame.new()
	self.currentOffset = self.normalOffset
	self.currentRecoil = Vector2.new(0, 0)
	self.currentMouseRadsPerPixel = self.mouseRadsPerPixel
	self.currentTouchSensitivity = self.touchSensitivity
	self.mouseLocked = true
	self.touchPanAccumulator = Vector2.new(0, 0) -- used for touch devices, represents amount the player has dragged their finger since starting a touch
	self.currentTool = nil
	self.sprintingInputActivated = false
	self.desiredWalkSpeed = self.normalWalkSpeed
	self.sprintEnabled = false -- true means player will move faster while doing sprint inputs
	self.slowZoomWalkEnabled = false -- true means player will move slower while doing zoom inputs
	self.desiredFieldOfView = self.fieldOfView
	-- Zoom variables
	self.zoomedFromInput = false -- true if player has performed input to zoom
	self.forcedZoomed = false -- ignores zoomedFromInput and canZoom
	self.zoomState = false -- true if player is currently zoomed in
	self.zoomAlpha = 0
	self.hasScope = false
	self.hideToolWhileZoomed = false
	self.currentZoomFactor = self.defaultZoomFactor
	self.zoomedFOV = self.fieldOfView
	-- Gamepad variables
	self.gamepadPan = Vector2.new(0, 0) -- essentially the amount the gamepad has moved from resting position
	self.movementPan = Vector2.new(0, 0) -- this is for movement (gamepadPan is for camera)
	self.lastThumbstickPos = Vector2.new(0, 0)
	self.lastThumbstickTime = nil
	self.currentGamepadSpeed = 0
	self.lastGamepadVelocity = Vector2.new(0, 0)

	-- Occlusion
	self.lastOcclusionDistance = 0
	self.lastOcclusionReachedTime = 0 -- marks the last time camera was at the true occlusion distance
	self.defaultTimeUntilZoomOut = 0
	self.timeUntilZoomOut = self.defaultTimeUntilZoomOut -- time after lastOcclusionReachedTime that camera will zoom out
	self.timeLastPoppedWayIn = 0 -- this holds the last time camera popped nearly into first person
	self.isZoomingOut = false
	self.tweenOutTime = 0.2
	self.curOcclusionTween = nil
	self.occlusionTweenObject = nil

	-- Side correction (when player is against a wall)
	self.sideCorrectionGoalVector = nil
	self.lastSideCorrectionMagnitude = 0
	self.lastSideCorrectionReachedTime = 0 -- marks the last time the camera was at the true correction distance
	self.revertSideCorrectionSpeedMultiplier = 2 -- speed at which camera reverts the side correction (towards 0 correction)
	self.defaultTimeUntilRevertSideCorrection = 0.75
	self.timeUntilRevertSideCorrection = self.defaultTimeUntilRevertSideCorrection -- time after lastSideCorrectionReachedTime that camera will revert the correction
	self.isRevertingSideCorrection = false

	-- Datamodel references
	self.eventConnections = {}
	self.raycastIgnoreList = {}
	self.currentCamera = nil
	self.currentCharacter = nil
	self.currentHumanoid = nil
	self.currentRootPart = nil
	self.controlModule = nil -- used to get player's touch input for moving character
	self.random = Random.new()

	return self
end

function ShoulderCamera:setEnabled(enabled)
	if self.enabled == enabled then
		return
	end
	self.enabled = enabled

	if self.enabled then
		RunService:BindToRenderStep(CAMERA_RENDERSTEP_NAME, Enum.RenderPriority.Camera.Value - 1, function(dt) self:onRenderStep(dt) end)
		ContextActionService:BindAction(ZOOM_ACTION_NAME, function(...) self:onZoomAction(...) end, false, unpack(self.zoomInputs))
		ContextActionService:BindAction(SPRINT_ACTION_NAME, function(...) self:onSprintAction(...) end, false, unpack(self.sprintInputs))

		table.insert(self.eventConnections, LocalPlayer.CharacterAdded:Connect(function(character) self:onCurrentCharacterChanged(character) end))
		table.insert(self.eventConnections, LocalPlayer.CharacterRemoving:Connect(function() self:onCurrentCharacterChanged(nil) end))
		table.insert(self.eventConnections, workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() self:onCurrentCameraChanged(workspace.CurrentCamera) end))
		table.insert(self.eventConnections, UserInputService.InputBegan:Connect(function(inputObj, wasProcessed) self:onInputBegan(inputObj, wasProcessed) end))
		table.insert(self.eventConnections, UserInputService.InputChanged:Connect(function(inputObj, wasProcessed) self:onInputChanged(inputObj, wasProcessed) end))
		table.insert(self.eventConnections, UserInputService.InputEnded:Connect(function(inputObj, wasProcessed) self:onInputEnded(inputObj, wasProcessed) end))

		self:onCurrentCharacterChanged(LocalPlayer.Character)
		self:onCurrentCameraChanged(workspace.CurrentCamera)

		-- Make transition to shouldercamera smooth by facing in same direction as previous camera
		local cameraLook = self.currentCamera.CFrame.lookVector
		self.yaw = math.atan2(-cameraLook.X, -cameraLook.Z)
		self.pitch = math.asin(cameraLook.Y)

		self.currentCamera.CameraType = Enum.CameraType.Scriptable

		self:setZoomFactor(self.currentZoomFactor) -- this ensures that zoomedFOV reflecs currentZoomFactor

		workspace.CurrentCamera.CameraSubject = self.currentRootPart

		self.occlusionTweenObject = Instance.new("NumberValue")
		self.occlusionTweenObject.Name = "OcclusionTweenObject"
		self.occlusionTweenObject.Parent = script
		self.occlusionTweenObject.Changed:Connect(function(value)
			self.lastOcclusionDistance = value
		end)

		-- Sets up weapon system to use camera for raycast direction instead of gun look vector
		self.weaponsSystem.aimRayCallback = function()
			local cameraCFrame = self.currentCFrame
			return Ray.new(cameraCFrame.p, cameraCFrame.LookVector * 500)
		end
	else
		RunService:UnbindFromRenderStep(CAMERA_RENDERSTEP_NAME)
		ContextActionService:UnbindAction(ZOOM_ACTION_NAME)
		ContextActionService:UnbindAction(SPRINT_ACTION_NAME)

		if self.currentHumanoid then
			self.currentHumanoid.AutoRotate = true
		end

		if self.currentCamera then
			self.currentCamera.CameraType = Enum.CameraType.Custom
		end

		self:updateZoomState()

		self.yaw = 0
		self.pitch = 0

		for _, conn in pairs(self.eventConnections) do
			conn:Disconnect()
		end
		self.eventConnections = {}

		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
		UserInputService.MouseIconEnabled = true
	end
end

function ShoulderCamera:onRenderStep(dt)
	if not self.enabled or
	   not self.currentCamera or
	   not self.currentCharacter or
	   not self.currentHumanoid or
	   not self.currentRootPart
	then
		return
	end

	-- Hide mouse and lock to center if applicable
	if self.mouseLocked and not GuiService:GetEmotesMenuOpen() then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
		UserInputService.MouseIconEnabled = false
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
		UserInputService.MouseIconEnabled = true
	end

	-- Handle gamepad input
	self:processGamepadInput(dt)

	-- Smoothly zoom to desired values
	if self.hasScope then
		ShoulderCamera.SpringService:Target(self, 0.8, 8, { zoomAlpha = self.zoomState and 1 or 0 })
		ShoulderCamera.SpringService:Target(self.currentCamera, 0.8, 8, { FieldOfView = self.desiredFieldOfView })
	else
		ShoulderCamera.SpringService:Target(self, 0.8, 3, { zoomAlpha = self.zoomState and 1 or 0 })
		ShoulderCamera.SpringService:Target(self.currentCamera, 0.8, 3, { FieldOfView = self.desiredFieldOfView })
	end

	-- Handle walk speed changes
	if self.sprintEnabled or self.slowZoomWalkEnabled then
		self.desiredWalkSpeed = self.normalWalkSpeed
		if self.sprintEnabled and (self.sprintingInputActivated or self:sprintFromTouchInput() or self:sprintFromGamepadInput()) and not self.zoomState then
			self.desiredWalkSpeed = self.sprintingWalkSpeed
		end
		if self.slowZoomWalkEnabled and self.zoomAlpha > 0.1 then
			self.desiredWalkSpeed = self.zoomWalkSpeed
		end

		ShoulderCamera.SpringService:Target(self.currentHumanoid, 0.95, 4, { WalkSpeed = self.desiredWalkSpeed })
	end

	-- Initialize variables used for side correction, occlusion, and calculating camera focus/rotation
	local rootPartPos = self.currentRootPart.CFrame.Position
	local rootPartUnrotatedCFrame = CFrame.new(rootPartPos)
	local yawRotation = CFrame.Angles(0, self.yaw, 0)
	local pitchRotation = CFrame.Angles(self.pitch + self.currentRecoil.Y, 0, 0)
	local xOffset = CFrame.new(self.normalOffset.X, 0, 0)
	local yOffset = CFrame.new(0, self.normalOffset.Y, 0)
	local zOffset = CFrame.new(0, 0, self.normalOffset.Z)
	local collisionRadius = self:getCollisionRadius()
	local cameraYawRotationAndXOffset =
		yawRotation * 		-- First rotate around the Y axis (look left/right)
		xOffset 			-- Then perform the desired offset (so camera is centered to side of player instead of directly on player)
	local cameraFocus = rootPartUnrotatedCFrame * cameraYawRotationAndXOffset

	-- Handle/Calculate side correction when player is adjacent to a wall (so camera doesn't go in the wall)
	local vecToFocus = cameraFocus.p - rootPartPos
	local rayToFocus = Ray.new(rootPartPos, vecToFocus + (vecToFocus.Unit * collisionRadius))
	local hitPart, hitPoint, hitNormal = self:penetrateCast(rayToFocus, self.raycastIgnoreList)
	local currentTime = tick()
	local sideCorrectionGoalVector = Vector3.new() -- if nothing is adjacent to player, goal vector is (0, 0, 0)
	if hitPart then
		hitPoint = hitPoint + (hitNormal * collisionRadius)
		sideCorrectionGoalVector = hitPoint - cameraFocus.p
		if sideCorrectionGoalVector.Magnitude >= self.lastSideCorrectionMagnitude then -- make it easy for camera to pop closer to player (move left)
			if currentTime > self.lastSideCorrectionReachedTime + self.timeUntilRevertSideCorrection and self.lastSideCorrectionMagnitude ~= 0 then
				self.timeUntilRevertSideCorrection = self.defaultTimeUntilRevertSideCorrection * 2 -- double time until revert if popping in repeatedly
			elseif self.lastSideCorrectionMagnitude == 0 and self.timeUntilRevertSideCorrection ~= self.defaultTimeUntilRevertSideCorrection then
				self.timeUntilRevertSideCorrection = self.defaultTimeUntilRevertSideCorrection
			end
			self.lastSideCorrectionMagnitude = sideCorrectionGoalVector.Magnitude
			self.lastSideCorrectionReachedTime = currentTime
			self.isRevertingSideCorrection = false
		else
			self.isRevertingSideCorrection = true
		end
	elseif self.lastSideCorrectionMagnitude ~= 0 then
		self.isRevertingSideCorrection = true
	end
	if self.isRevertingSideCorrection then -- make it hard/slow for camera to revert side correction (move right)
		if sideCorrectionGoalVector.Magnitude > self.lastSideCorrectionMagnitude - 1 and sideCorrectionGoalVector.Magnitude ~= 0 then
			self.lastSideCorrectionReachedTime = currentTime -- reset timer if occlusion significantly increased since last frame
		end
		if currentTime > self.lastSideCorrectionReachedTime + self.timeUntilRevertSideCorrection then
			local sideCorrectionChangeAmount = dt * (vecToFocus.Magnitude) * self.revertSideCorrectionSpeedMultiplier
			self.lastSideCorrectionMagnitude = self.lastSideCorrectionMagnitude - sideCorrectionChangeAmount
			if sideCorrectionGoalVector.Magnitude >= self.lastSideCorrectionMagnitude then
				self.lastSideCorrectionMagnitude = sideCorrectionGoalVector.Magnitude
				self.lastSideCorrectionReachedTime = currentTime
				self.isRevertingSideCorrection = false
			end
		end
	end

	-- Update cameraFocus to reflect side correction
	cameraYawRotationAndXOffset = cameraYawRotationAndXOffset + (-vecToFocus.Unit * self.lastSideCorrectionMagnitude)
	cameraFocus = rootPartUnrotatedCFrame * cameraYawRotationAndXOffset
	self.currentCamera.Focus = cameraFocus

	-- Calculate and apply CFrame for camera
	local cameraCFrameInSubjectSpace =
		cameraYawRotationAndXOffset *
		pitchRotation * 	-- rotate around the X axis (look up/down)
		yOffset *			-- move camera up/vertically
		zOffset				-- move camera back
	self.currentCFrame = rootPartUnrotatedCFrame * cameraCFrameInSubjectSpace

	-- Move camera forward if zoomed in
	if self.zoomAlpha > 0 then
		local trueZoomedOffset = math.max(self.zoomedOffsetDistance - self.lastOcclusionDistance, 0) -- don't zoom too far in if already occluded
		self.currentCFrame = self.currentCFrame:lerp(self.currentCFrame + trueZoomedOffset * self.currentCFrame.LookVector.Unit, self.zoomAlpha)
	end

	self.currentCamera.CFrame = self.currentCFrame

	-- Handle occlusion
	local occlusionDistance = self.currentCamera:GetLargestCutoffDistance(self.raycastIgnoreList)
	if occlusionDistance > 1e-5 then
		occlusionDistance = occlusionDistance + collisionRadius
	end
	if occlusionDistance >= self.lastOcclusionDistance then -- make it easy for the camera to pop in towards the player
		if self.curOcclusionTween ~= nil then
			self.curOcclusionTween:Cancel()
			self.curOcclusionTween = nil
		end
		if currentTime > self.lastOcclusionReachedTime + self.timeUntilZoomOut and self.lastOcclusionDistance ~= 0 then
			self.timeUntilZoomOut = self.defaultTimeUntilZoomOut * 2 -- double time until zoom out if popping in repeatedly
		elseif self.lastOcclusionDistance == 0  and self.timeUntilZoomOut ~= self.defaultTimeUntilZoomOut then
			self.timeUntilZoomOut = self.defaultTimeUntilZoomOut
		end

		if occlusionDistance / self.normalOffset.Z > 0.8 and self.timeLastPoppedWayIn == 0 then
			self.timeLastPoppedWayIn = currentTime
		end

		self.lastOcclusionDistance = occlusionDistance
		self.lastOcclusionReachedTime = currentTime
		self.isZoomingOut = false
	else -- make it hard/slow for camera to zoom out
		self.isZoomingOut = true
		if occlusionDistance > self.lastOcclusionDistance - 2 and occlusionDistance ~= 0 then -- reset timer if occlusion significantly increased since last frame
			self.lastOcclusionReachedTime = currentTime
		end

		-- If occlusion pops camera in to almost first person for a short time, pop out instantly
		if currentTime < self.timeLastPoppedWayIn + self.defaultTimeUntilZoomOut and self.lastOcclusionDistance / self.normalOffset.Z > 0.8 then
			self.lastOcclusionDistance = occlusionDistance
			self.lastOcclusionReachedTime = currentTime
			self.isZoomingOut = false
		elseif currentTime >= self.timeLastPoppedWayIn + self.defaultTimeUntilZoomOut and self.timeLastPoppedWayIn ~= 0 then
			self.timeLastPoppedWayIn = 0
		end
	end

	-- Update occlusion amount if timeout time has passed
	if currentTime >= self.lastOcclusionReachedTime + self.timeUntilZoomOut and not self.zoomState then
		if self.curOcclusionTween == nil then
			self.occlusionTweenObject.Value = self.lastOcclusionDistance
			local tweenInfo = TweenInfo.new(self.tweenOutTime)
			local goal = {}
			goal.Value = self.lastOcclusionDistance - self.normalOffset.Z
			self.curOcclusionTween = TweenService:Create(self.occlusionTweenObject, tweenInfo, goal)
			self.curOcclusionTween:Play()
		end
	end

	-- Apply occlusion to camera CFrame
	local currentOffsetDir = self.currentCFrame.LookVector.Unit
	self.currentCFrame = self.currentCFrame + (currentOffsetDir * self.lastOcclusionDistance)
	self.currentCamera.CFrame = self.currentCFrame

	-- Apply recoil decay
	self.currentRecoil = self.currentRecoil - (self.currentRecoil * self.recoilDecay * dt)

	if self:isHumanoidControllable() and self.rotateCharacterWithCamera then
		self.currentHumanoid.AutoRotate = false
		self.currentRootPart.CFrame = CFrame.Angles(0, self.yaw, 0) + self.currentRootPart.Position -- rotate character to be upright and facing the same direction as camera
		self:applyRootJointFix()
	else
		self.currentHumanoid.AutoRotate = true
	end

	self:handlePartTransparencies()
	self:handleTouchToolFiring()
end
