local function n()LoadOrCreateNumberValueParameter("\073\110\105\116\105\097\108\069\108\101\118\097\116\105\111\110","\078\117\109\098\101\114\086\097\108\117\101",nil)LoadOrCreateNumberValueParameter("\073\110\105\116\105\097\108\068\105\115\116\097\110\099\101","\078\117\109\098\101\114\086\097\108\117\101",nil)LoadOrCreateNumberValueParameter("\082\101\102\101\114\101\110\099\101\065\122\105\109\117\116\104","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckAzimuthValues)LoadOrCreateNumberValueParameter("\067\087\065\122\105\109\117\116\104\084\114\097\118\101\108","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckAzimuthValues)LoadOrCreateNumberValueParameter("\067\067\087\065\122\105\109\117\116\104\084\114\097\118\101\108","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckAzimuthValues)LoadOrCreateNumberValueParameter("\077\105\110\069\108\101\118\097\116\105\111\110","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckElevationValues)LoadOrCreateNumberValueParameter("\077\097\120\069\108\101\118\097\116\105\111\110","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckElevationValues)LoadOrCreateNumberValueParameter("\077\105\110\068\105\115\116\097\110\099\101","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckDistanceValues)LoadOrCreateNumberValueParameter("\077\097\120\068\105\115\116\097\110\099\101","\078\117\109\098\101\114\086\097\108\117\101",SetAndBoundsCheckDistanceValues)LoadOrCreateNumberValueParameter("\085\115\101\065\122\105\109\117\116\104\076\105\109\105\116\115","\066\111\111\108\086\097\108\117\101",SetAndBoundsCheckAzimuthValues)curAzimuthRad=math.rad(externalProperties.ReferenceAzimuth)curElevationRad=math.rad(externalProperties.InitialElevation)curDistance=externalProperties.InitialDistance SetAndBoundsCheckAzimuthValues()SetAndBoundsCheckElevationValues()SetAndBoundsCheckDistanceValues()end local function F()local F=RootCameraCreator()n()F.DefaultZoom=curDistance local C=math_rad(220)local a=math_rad(0)local E=math_rad(250)local l=2 local O=tick()F.LastUserPanCamera=tick()function F.Update(n)F:ProcessTweens()local i=tick()local f=i-O local A=n.UserPanningTheCamera==true local g=workspace.CurrentCamera local p=PlayersService.LocalPlayer local G=n:GetHumanoid()local T=g and g.CameraSubject local M=T and T:IsA("\086\101\104\105\099\108\101\083\101\097\116")local K=T and T:IsA("\083\107\097\116\101\098\111\097\114\100\080\108\097\116\102\111\114\109")if O==nil or i-O>1 then F:ResetCameraLook()n.LastCameraTransform=nil end if O then local F=n:UpdateGamepad()if n:ShouldUseVRRotation()then n.RotateInput=n.RotateInput+n:GetVRRotationInput()else local C=math_min(.1,i-O)if F~=ZERO_VECTOR2 then A=true n.RotateInput=n.RotateInput+F*C end local a=0 if not(M or K)then a=a+(n.TurningLeft and-120 or 0)a=a+(n.TurningRight and 120 or 0)end if a~=0 then n.RotateInput=n.RotateInput+Vector2.new(math_rad(a*C),0)A=true end end end if A then a=0 F.LastUserPanCamera=tick()end local I=i-F.LastUserPanCamera<l local y=n:GetSubjectPosition()if y and(p and g)then local F=n:ZoomCamera(curDistance*currentZoomSpeed)if not A and n.LastCameraTransform then local F=n:IsInFirstPerson()if(M or K)and(O and(G and G.Torso))then if F then if n.LastSubjectCFrame and((M or K)and T:IsA("\066\097\115\101\080\097\114\116"))then local F=-findAngleBetweenXZVectors(n.LastSubjectCFrame.lookVector,T.CFrame.lookVector)if IsFinite(F)then n.RotateInput=n.RotateInput+Vector2.new(F,0)end a=0 end elseif not I then local F=G.Torso.CFrame.lookVector if K then F=T.CFrame.lookVector end a=clamp(0,E,a+C*f)local l=clamp(0,1,a*f)if n:IsInFirstPerson()then l=1 end local O=findAngleBetweenXZVectors(F,n:GetCameraLook())if IsFinite(O)and math_abs(O)>.0001 then n.RotateInput=n.RotateInput+Vector2.new(O*l,0)end end end end local l=VRService.VREnabled g.Focus=l and n:GetVRFocus(y,f)or CFrame_new(y)local i=g.Focus.p if l and not n:IsInFirstPerson()then local C=n:GetCameraHeight()local a=y-g.CFrame.p local E=a.magnitude if E>F or n.RotateInput.x~=0 then local l=math_min(E,F)a=n:RotateCamera(a.unit*XZ_VECTOR,Vector2.new(n.RotateInput.x,0))*l local O=i-a local f=g.CFrame.lookVector if n.RotateInput.x~=0 then f=a end local A=Vector3.new(O.x+f.x,O.y,O.z+f.z)n.RotateInput=ZERO_VECTOR2 g.CFrame=CFrame_new(O,A)+Vector3_new(0,C,0)end else curAzimuthRad=curAzimuthRad-n.RotateInput.x if useAzimuthLimits then curAzimuthRad=clamp(curAzimuthRad,minAzimuthAbsoluteRad,maxAzimuthAbsoluteRad)else curAzimuthRad=curAzimuthRad~=0 and math.sign(curAzimuthRad)*(math.abs(curAzimuthRad)%TAU)or 0 end curDistance=clamp(F,minDistance,maxDistance)curElevationRad=clamp(curElevationRad+n.RotateInput.y,minElevationRad,maxElevationRad)local C=curDistance*(CFrame.fromEulerAnglesYXZ(-curElevationRad,curAzimuthRad,0)*UNIT_Z)local a=y+C g.CFrame=CFrame.new(a,y)n.RotateInput=ZERO_VECTOR2 end n.LastCameraTransform=g.CFrame n.LastCameraFocus=g.Focus if(M or K)and T:IsA("\066\097\115\101\080\097\114\116")then n.LastSubjectCFrame=T.CFrame else n.LastSubjectCFrame=nil end end O=i end function F.GetCameraZoom(n)return curDistance end function F.ZoomCamera(F,n)local C=PlayersService.LocalPlayer if C then curDistance=clamp(n,minDistance,maxDistance)end local a=F:GetCameraZoom()<2 F:UpdateMouseBehavior()return F:GetCameraZoom()end function F.ZoomCameraBy(F,n)local C=curDistance C=F:rk4Integrator(curDistance,n,1)F:ZoomCamera(C)return F:GetCameraZoom()end function F.ZoomCameraFixedBy(F,n)return F:ZoomCamera(F:GetCameraZoom()+n)end function F.SetInitialOrientation(F,n)local C=(n.Torso.CFrame.lookVector-Vector3.new(0,.23,0)).unit local a=findAngleBetweenXZVectors(C,F:GetCameraLook())local E=math.asin((F:GetCameraLook()).y)-math.asin(C.y)if not IsFinite(a)then a=0 end if not IsFinite(E)then E=0 end F.RotateInput=Vector2.new(a,E)end function F.getGamepadPan(n,C,a)if a.UserInputType==F.activeGamepad and a.KeyCode==Enum.KeyCode.Thumbstick2 then if r3ButtonDown or l3ButtonDown then if a.Position.Y>THUMBSTICK_DEADZONE then currentZoomSpeed=.96 elseif a.Position.Y<-THUMBSTICK_DEADZONE then currentZoomSpeed=1.04 else currentZoomSpeed=1.0 end else if C==Enum.UserInputState.Cancel then F.GamepadPanningCamera=ZERO_VECTOR2 return end local n=Vector2.new(a.Position.X,-a.Position.Y)if n.magnitude>THUMBSTICK_DEADZONE then F.GamepadPanningCamera=Vector2.new(a.Position.X,-a.Position.Y)else F.GamepadPanningCamera=ZERO_VECTOR2 end end end end function F.doGamepadZoom(n,C,a)if a.UserInputType==F.activeGamepad and(a.KeyCode==Enum.KeyCode.ButtonR3 or a.KeyCode==Enum.KeyCode.ButtonL3)then if C==Enum.UserInputState.Begin then r3ButtonDown=a.KeyCode==Enum.KeyCode.ButtonR3 l3ButtonDown=a.KeyCode==Enum.KeyCode.ButtonL3 elseif C==Enum.UserInputState.End then if a.KeyCode==Enum.KeyCode.ButtonR3 then r3ButtonDown=false elseif a.KeyCode==Enum.KeyCode.ButtonL3 then l3ButtonDown=false end if not r3ButtonDown and not l3ButtonDown then currentZoomSpeed=1.0 end end end end function F.BindGamepadInputActions(n)local C=game:GetService("\067\111\110\116\101\120\116\065\099\116\105\111\110\083\101\114\118\105\099\101")C:BindAction("\079\114\098\105\116\097\108\067\097\109\071\097\109\101\112\097\100\080\097\110",F.getGamepadPan,false,Enum.KeyCode.Thumbstick2)C:BindAction("\079\114\098\105\116\097\108\067\097\109\071\097\109\101\112\097\100\090\111\111\109",F.doGamepadZoom,false,Enum.KeyCode.ButtonR3)C:BindAction("\079\114\098\105\116\097\108\067\097\109\071\097\109\101\112\097\100\090\111\111\109\065\108\116",F.doGamepadZoom,false,Enum.KeyCode.ButtonL3)end return F end return F

local function LoadNumberValueParameters()
	-- These initial values do not require change listeners since they are read only once
	LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
	LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
	
	-- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
	LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues)
	LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues)
	LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
	LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
	LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues)
	
	-- Internal values set (in radians, from degrees), plus sanitization
	curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"])
	curElevationRad = math.rad(externalProperties["InitialElevation"])
	curDistance = externalProperties["InitialDistance"]
	
	SetAndBoundsCheckAzimuthValues()
	SetAndBoundsCheckElevationValues()
	SetAndBoundsCheckDistanceValues()
end

local function CreateOrbitalCamera()
	local module = RootCameraCreator()
	
	LoadNumberValueParameters()	
	
	module.DefaultZoom = curDistance
	
	local tweenAcceleration = math_rad(220)
	local tweenSpeed = math_rad(0)
	local tweenMaxSpeed = math_rad(250)
	local timeBeforeAutoRotate = 2
	
	local lastUpdate = tick()
	module.LastUserPanCamera = tick()
	
	function module:Update()
		module:ProcessTweens()
		local now = tick()
		local timeDelta = (now - lastUpdate)
		local userPanningTheCamera = (self.UserPanningTheCamera == true)
		local camera = 	workspace.CurrentCamera
		local player = PlayersService.LocalPlayer
		local humanoid = self:GetHumanoid()
		local cameraSubject = camera and camera.CameraSubject
		local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
		local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
		
		if lastUpdate == nil or now - lastUpdate > 1 then
			module:ResetCameraLook()
			self.LastCameraTransform = nil
		end	
		
		if lastUpdate then
			local gamepadRotation = self:UpdateGamepad()
			
			if self:ShouldUseVRRotation() then				
				self.RotateInput = self.RotateInput + self:GetVRRotationInput()
			else
				-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
				local delta = math_min(0.1, now - lastUpdate)
				
				if gamepadRotation ~= ZERO_VECTOR2 then
					userPanningTheCamera = true
					self.RotateInput = self.RotateInput + (gamepadRotation * delta)
				end

				local angle = 0
				if not (isInVehicle or isOnASkateboard) then
					angle = angle + (self.TurningLeft and -120 or 0)
					angle = angle + (self.TurningRight and 120 or 0)
				end
				
				if angle ~= 0 then
					self.RotateInput = self.RotateInput +  Vector2.new(math_rad(angle * delta), 0)
					userPanningTheCamera = true
				end
			end
		end

		-- Reset tween speed if user is panning
		if userPanningTheCamera then
			tweenSpeed = 0
			module.LastUserPanCamera = tick()
		end
		
		local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
		local subjectPosition = self:GetSubjectPosition()
		
		if subjectPosition and player and camera then
		
			
			local zoom = self:ZoomCamera(curDistance * currentZoomSpeed)
			
			
			if not userPanningTheCamera and self.LastCameraTransform then
				local isInFirstPerson = self:IsInFirstPerson()
				if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
					if isInFirstPerson then
						if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
							local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
							if IsFinite(y) then
								self.RotateInput = self.RotateInput + Vector2.new(y, 0)
							end
							tweenSpeed = 0
						end
					elseif not userRecentlyPannedCamera then
						local forwardVector = humanoid.Torso.CFrame.lookVector
						if isOnASkateboard then
							forwardVector = cameraSubject.CFrame.lookVector
						end
						
						tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)

						local percent = clamp(0, 1, tweenSpeed * timeDelta)
						if self:IsInFirstPerson() then
							percent = 1
						end
						
						local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
						if IsFinite(y) and math_abs(y) > 0.0001 then
							self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
						end
					end
				end
			end
			
			local VREnabled = VRService.VREnabled
			camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)

			local cameraFocusP = camera.Focus.p
			if VREnabled and not self:IsInFirstPerson() then
				local cameraHeight = self:GetCameraHeight()
				local vecToSubject = (subjectPosition - camera.CFrame.p)
				local distToSubject = vecToSubject.magnitude

				-- Only move the camera if it exceeded a maximum distance to the subject in VR
				if distToSubject > zoom or self.RotateInput.x ~= 0 then
					local desiredDist = math_min(distToSubject, zoom)
					vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist
					local newPos = cameraFocusP - vecToSubject
					local desiredLookDir = camera.CFrame.lookVector
					if self.RotateInput.x ~= 0 then
						desiredLookDir = vecToSubject
					end
					local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
					self.RotateInput = ZERO_VECTOR2
					
					camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
				end
			else
				-- self.RotateInput is a Vector2 of mouse movement deltas since last update
				curAzimuthRad = curAzimuthRad - self.RotateInput.x
				
				if useAzimuthLimits then
					curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad)
				else
					curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0
				end

				curDistance = clamp(zoom, minDistance, maxDistance)
				
				curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad)
				
				local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z ) 
				local camPos = subjectPosition + cameraPosVector
				
				camera.CFrame = CFrame.new(camPos, subjectPosition)
			
				self.RotateInput = ZERO_VECTOR2
			end

			self.LastCameraTransform = camera.CFrame
			self.LastCameraFocus = camera.Focus
			if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
				self.LastSubjectCFrame = cameraSubject.CFrame
			else
				self.LastSubjectCFrame = nil
			end
		end
		
		lastUpdate = now
	end
	
	function module:GetCameraZoom()
		return curDistance
	end
	
	function module:ZoomCamera(desiredZoom)
		local player = PlayersService.LocalPlayer
		if player then
			curDistance = clamp(desiredZoom, minDistance, maxDistance)
		end

		local isFirstPerson = self:GetCameraZoom() < 2

		--ShiftLockController:SetIsInFirstPerson(isFirstPerson)
		-- set mouse behavior
		self:UpdateMouseBehavior()
		return self:GetCameraZoom()
	end
	
	function module:ZoomCameraBy(zoomScale)
		local newDist = curDistance
		-- Can break into more steps to get more accurate integration
        newDist = self:rk4Integrator(curDistance, zoomScale, 1)
		self:ZoomCamera(newDist)
		return self:GetCameraZoom()
	end

	function module:ZoomCameraFixedBy(zoomIncrement)
		return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement)
	end
	
	function module:SetInitialOrientation(humanoid)
		local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
		local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook())
		local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y)
		if not IsFinite(horizontalShift) then
			horizontalShift = 0
		end
		if not IsFinite(vertShift) then
			vertShift = 0
		end
		self.RotateInput = Vector2.new(horizontalShift, vertShift)
	end

	function module.getGamepadPan(name, state, input)
		if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
			if r3ButtonDown or l3ButtonDown then
			-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
				if (input.Position.Y > THUMBSTICK_DEADZONE) then
					currentZoomSpeed = 0.96
				elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
					currentZoomSpeed = 1.04
				else 
					currentZoomSpeed = 1.00
				end
			else
				if state == Enum.UserInputState.Cancel then
					module.GamepadPanningCamera = ZERO_VECTOR2
					return
				end		
				
				local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
				if inputVector.magnitude > THUMBSTICK_DEADZONE then
					module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
				else
					module.GamepadPanningCamera = ZERO_VECTOR2
				end
			end
		end
	end

	function module.doGamepadZoom(name, state, input)
		if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
			if (state == Enum.UserInputState.Begin) then
				r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
				l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
			elseif (state == Enum.UserInputState.End) then
				if (input.KeyCode == Enum.KeyCode.ButtonR3) then
					r3ButtonDown = false
				elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
					l3ButtonDown = false
				end
				if (not r3ButtonDown) and (not l3ButtonDown) then
					currentZoomSpeed = 1.00
				end
			end
		end
	end
	
	function module:BindGamepadInputActions()
		local ContextActionService = game:GetService('ContextActionService')
		ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2)
		ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3)
		ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3)
	end
	
	return module
end

return CreateOrbitalCamera
