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