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