local function T()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 s()local s=RootCameraCreator()T()s.DefaultZoom=curDistance local b=math_rad(220)local k=math_rad(0)local p=math_rad(250)local C=2 local Y=tick()s.LastUserPanCamera=tick()function s.Update(T)s:ProcessTweens()local n=tick()local c=n-Y local I=T.UserPanningTheCamera==true local Z=workspace.CurrentCamera local i=PlayersService.LocalPlayer local W=T:GetHumanoid()local a=Z and Z.CameraSubject local D=a and a:IsA("\086\101\104\105\099\108\101\083\101\097\116")local X=a and a:IsA("\083\107\097\116\101\098\111\097\114\100\080\108\097\116\102\111\114\109")if Y==nil or n-Y>1 then s:ResetCameraLook()T.LastCameraTransform=nil end if Y then local s=T:UpdateGamepad()if T:ShouldUseVRRotation()then T.RotateInput=T.RotateInput+T:GetVRRotationInput()else local b=math_min(.1,n-Y)if s~=ZERO_VECTOR2 then I=true T.RotateInput=T.RotateInput+s*b end local k=0 if not(D or X)then k=k+(T.TurningLeft and-120 or 0)k=k+(T.TurningRight and 120 or 0)end if k~=0 then T.RotateInput=T.RotateInput+Vector2.new(math_rad(k*b),0)I=true end end end if I then k=0 s.LastUserPanCamera=tick()end local K=n-s.LastUserPanCamera<C local A=T:GetSubjectPosition()if A and(i and Z)then local s=T:GetCameraZoom()if not I and T.LastCameraTransform then local s=T:IsInFirstPerson()if(D or X)and(Y and(W and W.Torso))then if s then if T.LastSubjectCFrame and((D or X)and a:IsA("\066\097\115\101\080\097\114\116"))then local s=-findAngleBetweenXZVectors(T.LastSubjectCFrame.lookVector,a.CFrame.lookVector)if IsFinite(s)then T.RotateInput=T.RotateInput+Vector2.new(s,0)end k=0 end elseif not K then local s=W.Torso.CFrame.lookVector if X then s=a.CFrame.lookVector end k=clamp(0,p,k+b*c)local C=clamp(0,1,k*c)if T:IsInFirstPerson()then C=1 end local Y=findAngleBetweenXZVectors(s,T:GetCameraLook())if IsFinite(Y)and math_abs(Y)>.0001 then T.RotateInput=T.RotateInput+Vector2.new(Y*C,0)end end end end local C=VRService.VREnabled Z.Focus=C and T:GetVRFocus(A,c)or CFrame_new(A)local n=Z.Focus.p if C and not T:IsInFirstPerson()then local b=T:GetCameraHeight()local k=A-Z.CFrame.p local p=k.magnitude if p>s or T.RotateInput.x~=0 then local C=math_min(p,s)k=T:RotateCamera(k.unit*XZ_VECTOR,Vector2.new(T.RotateInput.x,0))*C local Y=n-k local c=Z.CFrame.lookVector if T.RotateInput.x~=0 then c=k end local I=Vector3.new(Y.x+c.x,Y.y,Y.z+c.z)T.RotateInput=ZERO_VECTOR2 Z.CFrame=CFrame_new(Y,I)+Vector3_new(0,b,0)end else curAzimuthRad=curAzimuthRad-T.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(s,minDistance,maxDistance)curElevationRad=clamp(curElevationRad+T.RotateInput.y,minElevationRad,maxElevationRad)local b=curDistance*(CFrame.fromEulerAnglesYXZ(-curElevationRad,curAzimuthRad,0)*UNIT_Z)local k=A+b Z.CFrame=CFrame.new(k,A)T.RotateInput=ZERO_VECTOR2 end T.LastCameraTransform=Z.CFrame T.LastCameraFocus=Z.Focus if(D or X)and a:IsA("\066\097\115\101\080\097\114\116")then T.LastSubjectCFrame=a.CFrame else T.LastSubjectCFrame=nil end end Y=n end function s.SetInitialOrientation(s,T)local b=(T.Torso.CFrame.lookVector-Vector3.new(0,.23,0)).unit local k=findAngleBetweenXZVectors(b,s:GetCameraLook())local p=math.asin((s:GetCameraLook()).y)-math.asin(b.y)if not IsFinite(k)then k=0 end if not IsFinite(p)then p=0 end s.RotateInput=Vector2.new(k,p)end return s end return s

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:GetCameraZoom()
			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: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
	
	return module
end

return CreateOrbitalCamera
