local p=require((game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")):WaitForChild("\078\101\118\101\114\109\111\114\101"))local N=game:GetService("\085\115\101\114\073\110\112\117\116\083\101\114\118\105\099\101")local q=game:GetService("\067\111\110\116\101\120\116\065\099\116\105\111\110\083\101\114\118\105\099\101")local k=game:GetService("\082\117\110\083\101\114\118\105\099\101")local a=p("\077\097\105\100")local Y=p("\071\097\109\101\112\097\100\082\111\116\097\116\101\077\111\100\101\108")local function E(p,N,q)local k=N<0 and-1 or 1 local function a(p,N)local q=k*math.max(1,p/3.3+.5)return q end local Y=p local E=N local J=a(Y,E)local F=Y+E*(q/2)local s=E+J*(q/2)local w=a(F,s)local i=Y+s*(q/2)local G=E+w*(q/2)local m=a(i,G)local x=Y+G*q local l=E+m*q local B=a(x,l)local S=p+(((E+2*s)+2*G)+l)*(q/6)local O=N+(((J+2*w)+2*m)+B)*(q/6)return S,O end local J={}J.__index=J J.ClassName="\067\097\109\101\114\097\067\111\110\116\114\111\108\115"J.MOUSE_SENSITIVITY=Vector2.new(math.pi*4,math.pi*1.9)J._dragBeginTypes={Enum.UserInputType.MouseButton2,Enum.UserInputType.Touch}function J.new(p,N)local q=setmetatable({},J)q._enabled=false q._key=tostring(q).."\067\097\109\101\114\097\067\111\110\116\114\111\108\115"q._gamepadRotateModel=Y.new()if p then q:SetZoomedCamera(p)end if N then q:SetRotatedCamera(N)end return q end function J.GetKey(p)return p._key end function J.IsEnabled(p)return p._enabled end function J.Enable(p)if p._enabled then return end assert(not p._maid)p._enabled=true p._maid=a.new()p._maid:GiveTask(p._gamepadRotateModel.IsRotating.Changed:Connect(function()if p._gamepadRotateModel.IsRotating.Value then p:_handleGamepadRotateStart()else p:_handleGamepadRotateStop()end end))q:BindAction(p._key,function(N,q,k)if k.UserInputType==Enum.UserInputType.MouseWheel then p:_handleMouseWheel(k)end end,false,Enum.UserInputType.MouseWheel)q:BindAction(p._key.."\068\114\097\103",function(N,q,k)if q==Enum.UserInputState.Begin then p:BeginDrag(k)end end,false,unpack(p._dragBeginTypes))q:BindAction(p._key.."\082\111\116\097\116\101",function(N,q,k)p:_handleThumbstickInput(k)end,false,Enum.KeyCode.Thumbstick2)p._maid:GiveTask(N.TouchPinch:Connect(function(N,q,k,a)p:_handleTouchPinch(q,k,a)end))p._maid:GiveTask(function()q:UnbindAction(p._key)q:UnbindAction(p._key.."\068\114\097\103")q:UnbindAction(p._key.."\082\111\116\097\116\101")end)end function J.Disable(p)if not p._enabled then return end p._enabled=false p._maid:DoCleaning()p._maid=nil p._lastMousePosition=nil end function J.BeginDrag(q,p)if not q._rotatedCamera then q._maid._dragMaid=nil return end local k=a.new()q._lastMousePosition=p.Position local Y=p.UserInputType.Name:find("\077\111\117\115\101")if Y then N.MouseBehavior=Enum.MouseBehavior.LockCurrentPosition end k:GiveTask(N.InputEnded:Connect(function(N,k)if N==p then q:_endDrag()end end))k:GiveTask(N.InputChanged:Connect(function(N)if Y and N.UserInputType==Enum.UserInputType.MouseMovement or N==p then q:_handleMouseMovement(N)end end))if q._rotatedCamera.ClassName=="\083\109\111\111\116\104\082\111\116\097\116\101\100\067\097\109\101\114\097"then q._rotVelocityTracker=q:_getVelocityTracker(.05,Vector2.new())end q._maid._dragMaid=k end function J.SetZoomedCamera(N,p)N._zoomedCamera=p or error()N._startZoomScale=N._zoomedCamera.Zoom return N end function J.SetRotatedCamera(N,p)N._rotatedCamera=p or error()return N end function J._handleMouseWheel(N,p)if N._zoomedCamera then local q=math.clamp(-p.Position.Z,-1,1)*1.4 local k=E(N._zoomedCamera.TargetZoom,q,1)N._zoomedCamera.TargetZoom=k end if N._rotatedCamera then if N._rotatedCamera.ClassName=="\080\117\115\104\067\097\109\101\114\097"then N._rotatedCamera:StopRotateBack()end end end function J._handleTouchPinch(k,p,N,q)if k._zoomedCamera then if q==Enum.UserInputState.Begin then k._startZoomScale=k._zoomedCamera.Zoom k._zoomedCamera.Zoom=(k._startZoomScale*1)/p elseif q==Enum.UserInputState.End then k._zoomedCamera.Zoom=(k._startZoomScale*1)/p k._zoomedCamera.TargetZoom=k._zoomedCamera.Zoom+-N/5 elseif q==Enum.UserInputState.Change then if k._startZoomScale then k._zoomedCamera.TargetZoom=(k._startZoomScale*1)/p k._zoomedCamera.Zoom=k._zoomedCamera.TargetZoom else warn("\091\067\097\109\101\114\097\067\111\110\116\114\111\108\115\046\095\104\097\110\100\108\101\084\111\117\099\104\080\105\110\099\104\093 - \078\111 \115\101\108\102\046\095\115\116\097\114\116\090\111\111\109\083\099\097\108\101")end end end end function J._mouseTranslationToAngle(N,p)local q=p.x/1920 local k=p.y/1200 return Vector2.new(q,k)end function J._getVelocityTracker(q,p,N)p=p or 1 local k=tick()local a=N return{Update=function(N,q)local Y=tick()-k k=tick()a=a/2^(Y/p)+(q/(.0001+Y))*p end;GetVelocity=function(p)p:Update(N*0)return a end}end function J._handleMouseMovement(q,p,N)if q._lastMousePosition then if q._rotatedCamera then local k if N then k=(-p.Delta+q._lastMousePosition)-p.Position else k=-p.Delta end local a=q:_mouseTranslationToAngle(k)*q.MOUSE_SENSITIVITY q._rotatedCamera:RotateXY(a)if q._rotVelocityTracker then q._rotVelocityTracker:Update(a)end end q._lastMousePosition=p.Position end end function J._handleThumbstickInput(N,p)N._gamepadRotateModel:HandleThumbstickInput(p)end function J._applyRotVelocityTracker(N,p)if N._rotatedCamera then local q=N._rotatedCamera.AngleXZ local k=(p:GetVelocity()).X local a=q+k local Y=N._rotatedCamera.TargetAngleXZ if math.abs(a-q)>math.abs(Y-q)then N._rotatedCamera.TargetAngleXZ=a end N._rotatedCamera:SnapIntoBounds()end end function J._endDrag(p)if p._rotVelocityTracker then p:_applyRotVelocityTracker(p._rotVelocityTracker)p._rotVelocityTracker=nil end p._lastMousePosition=nil N.MouseBehavior=Enum.MouseBehavior.Default p._maid._dragMaid=nil end function J._handleGamepadRotateStop(p)if p._rotVelocityTracker then p:_applyRotVelocityTracker(p._rotVelocityTracker)p._rotVelocityTracker=nil end p._maid._dragMaid=nil end function J._handleGamepadRotateStart(p)if not p._rotatedCamera then p._maid._dragMaid=nil return end local N=a.new()if p._rotatedCamera.ClassName=="\083\109\111\111\116\104\082\111\116\097\116\101\100\067\097\109\101\114\097"then p._rotVelocityTracker=p:_getVelocityTracker(.05,Vector2.new())end N:GiveTask(k.Heartbeat:Connect(function()local N=p._gamepadRotateModel:GetThumbstickDeltaAngle()if p._rotatedCamera then p._rotatedCamera:RotateXY(N)end if p._rotVelocityTracker then p._rotVelocityTracker:Update(N)end end))p._maid._dragMaid=N end function J.Destroy(p)p:Disable()setmetatable(p,nil)end return J
--- Interface between user input and camera controls
-- @classmod CameraControls

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")

local Maid = require("Maid")
local GamepadRotateModel = require("GamepadRotateModel")

--- Stolen directly from ROBLOX's core scripts.
-- Looks like a simple integrator.
-- Called (zoom, zoomScale, 1) returns zoom
local function rk4Integrator(position, velocity, t)
	local direction = velocity < 0 and -1 or 1
	local function acceleration(p, v)
		local accel = direction * math.max(1, (p / 3.3) + 0.5)
		return accel
	end

	local p1 = position
	local v1 = velocity
	local a1 = acceleration(p1, v1)
	local p2 = p1 + v1 * (t / 2)
	local v2 = v1 + a1 * (t / 2)
	local a2 = acceleration(p2, v2)
	local p3 = p1 + v2 * (t / 2)
	local v3 = v1 + a2 * (t / 2)
	local a3 = acceleration(p3, v3)
	local p4 = p1 + v3 * t
	local v4 = v1 + a3 * t
	local a4 = acceleration(p4, v4)

	local positionResult = position + (v1 + 2 * v2 + 2 * v3 + v4) * (t / 6)
	local velocityResult = velocity + (a1 + 2 * a2 + 2 * a3 + a4) * (t / 6)
	return positionResult, velocityResult
end

local CameraControls = {}
CameraControls.__index = CameraControls
CameraControls.ClassName = "CameraControls"
CameraControls.MOUSE_SENSITIVITY = Vector2.new(math.pi*4, math.pi*1.9)
CameraControls._dragBeginTypes = { Enum.UserInputType.MouseButton2, Enum.UserInputType.Touch }

function CameraControls.new(zoomCamera, rotatedCamera)
	local self = setmetatable({}, CameraControls)

	self._enabled = false
	self._key = tostring(self) .. "CameraControls"
	self._gamepadRotateModel = GamepadRotateModel.new()

	if zoomCamera then
		self:SetZoomedCamera(zoomCamera)
	end
	if rotatedCamera then
		self:SetRotatedCamera(rotatedCamera)
	end

	return self
end

function CameraControls:GetKey()
	return self._key
end

function CameraControls:IsEnabled()
	return self._enabled
end

function CameraControls:Enable()
	if self._enabled then
		return
	end

	assert(not self._maid)
	self._enabled = true

	self._maid = Maid.new()

	self._maid:GiveTask(self._gamepadRotateModel.IsRotating.Changed:Connect(function()
		if self._gamepadRotateModel.IsRotating.Value then
			self:_handleGamepadRotateStart()
		else
			self:_handleGamepadRotateStop()
		end
	end))

	ContextActionService:BindAction(self._key, function(actionName, userInputState, inputObject)
		if inputObject.UserInputType == Enum.UserInputType.MouseWheel then
			self:_handleMouseWheel(inputObject)
		end
	end, false, Enum.UserInputType.MouseWheel)

	ContextActionService:BindAction(self._key .. "Drag", function(actionName, userInputState, inputObject)
		if userInputState == Enum.UserInputState.Begin then
			self:BeginDrag(inputObject)
		end
	end, false, unpack(self._dragBeginTypes))

	ContextActionService:BindAction(self._key .. "Rotate", function(actionName, userInputState, inputObject)
		self:_handleThumbstickInput(inputObject)
	end, false, Enum.KeyCode.Thumbstick2)

	self._maid:GiveTask(UserInputService.TouchPinch:Connect(function(touchPositions, scale, velocity, userInputState)
		self:_handleTouchPinch(scale, velocity, userInputState)
	end))

	self._maid:GiveTask(function()
		ContextActionService:UnbindAction(self._key)
		ContextActionService:UnbindAction(self._key .. "Drag")
		ContextActionService:UnbindAction(self._key .. "Rotate")
	end)
end

function CameraControls:Disable()
	if not self._enabled then
		return
	end

	self._enabled = false

	self._maid:DoCleaning()
	self._maid = nil

	self._lastMousePosition = nil
end

function CameraControls:BeginDrag(beginInputObject)
	if not self._rotatedCamera then
		self._maid._dragMaid = nil
		return
	end

	local maid = Maid.new()

	self._lastMousePosition = beginInputObject.Position
	local isMouse = beginInputObject.UserInputType.Name:find("Mouse")
	if isMouse then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
	end

	maid:GiveTask(UserInputService.InputEnded:Connect(function(inputObject, GameProcessed)
		if inputObject == beginInputObject then
			self:_endDrag()
		end
	end))

	maid:GiveTask(UserInputService.InputChanged:Connect(function(inputObject)
		if isMouse and inputObject.UserInputType == Enum.UserInputType.MouseMovement
			or inputObject == beginInputObject then

			self:_handleMouseMovement(inputObject)
		end
	end))

	if self._rotatedCamera.ClassName == "SmoothRotatedCamera" then
		self._rotVelocityTracker = self:_getVelocityTracker(0.05, Vector2.new())
	end

	self._maid._dragMaid = maid
end

function CameraControls:SetZoomedCamera(zoomedCamera)
	self._zoomedCamera = zoomedCamera or error()
	self._startZoomScale = self._zoomedCamera.Zoom

	return self
end

function CameraControls:SetRotatedCamera(rotatedCamera)
	self._rotatedCamera = rotatedCamera or error()
	return self
end

--- This code was the same algorithm used by ROBLOX. It makes it so you can zoom easier at further distances.
function CameraControls:_handleMouseWheel(inputObject)
	if self._zoomedCamera then
		local delta = math.clamp(-inputObject.Position.Z, -1, 1)*1.4
		local zoom = rk4Integrator(self._zoomedCamera.TargetZoom, delta, 1)

		self._zoomedCamera.TargetZoom = zoom
	end

	if self._rotatedCamera then
		if self._rotatedCamera.ClassName == "PushCamera" then
			self._rotatedCamera:StopRotateBack()
		end
	end
end

function CameraControls:_handleTouchPinch(scale, velocity, userInputState)
	if self._zoomedCamera then
		if userInputState == Enum.UserInputState.Begin then
			self._startZoomScale = self._zoomedCamera.Zoom
			self._zoomedCamera.Zoom = self._startZoomScale*1/scale
		elseif userInputState == Enum.UserInputState.End then
			self._zoomedCamera.Zoom = self._startZoomScale*1/scale
			self._zoomedCamera.TargetZoom = self._zoomedCamera.Zoom + -velocity/5
		elseif userInputState == Enum.UserInputState.Change then
			if self._startZoomScale then
				self._zoomedCamera.TargetZoom = self._startZoomScale*1/scale
				self._zoomedCamera.Zoom = self._zoomedCamera.TargetZoom
			else
				warn("[CameraControls._handleTouchPinch] - No self._startZoomScale")
			end
		end
	end
end

--- This is also a ROBLOX algorithm. Not sure why screen resolution is locked like it is.
function CameraControls:_mouseTranslationToAngle(translationVector)
	local xTheta = (translationVector.x / 1920)
	local yTheta = (translationVector.y / 1200)
	return Vector2.new(xTheta, yTheta)
end

function CameraControls:_getVelocityTracker(strength, startVelocity)
	strength = strength or 1

	local lastUpdate = tick()
	local velocity = startVelocity

	return {
		Update = function(this, delta)
			local elapsed = tick() - lastUpdate
			lastUpdate = tick()
			velocity = velocity / (2^(elapsed/strength)) + (delta / (0.0001 + elapsed)) * strength
		end;
		GetVelocity = function(this)
			this:Update(startVelocity*0)
			return velocity
		end;
	}
end

function CameraControls:_handleMouseMovement(inputObject, isMouse)
	if self._lastMousePosition then
		if self._rotatedCamera then
			-- This calculation may seem weird, but either .Position updates (if it's locked), or .Delta updates (if it's not).
			local delta
			if isMouse then
				delta = -inputObject.Delta + self._lastMousePosition - inputObject.Position
			else
				delta = -inputObject.Delta
			end

			local deltaAngle = self:_mouseTranslationToAngle(delta) * self.MOUSE_SENSITIVITY
			self._rotatedCamera:RotateXY(deltaAngle)

			if self._rotVelocityTracker then
				self._rotVelocityTracker:Update(deltaAngle)
			end
		end

		self._lastMousePosition = inputObject.Position
	end
end

function CameraControls:_handleThumbstickInput(inputObject)
	self._gamepadRotateModel:HandleThumbstickInput(inputObject)
end

function CameraControls:_applyRotVelocityTracker(rotVelocityTracker)
	if self._rotatedCamera then
		local position = self._rotatedCamera.AngleXZ
		local velocity = rotVelocityTracker:GetVelocity().X
		local newVelocityTarget = position + velocity
		local target = self._rotatedCamera.TargetAngleXZ

		if math.abs(newVelocityTarget - position) > math.abs(target - position) then
			self._rotatedCamera.TargetAngleXZ = newVelocityTarget
		end

		self._rotatedCamera:SnapIntoBounds()
	end
end

function CameraControls:_endDrag()
	if self._rotVelocityTracker then
		self:_applyRotVelocityTracker(self._rotVelocityTracker)
		self._rotVelocityTracker = nil
	end

	self._lastMousePosition = nil
	UserInputService.MouseBehavior = Enum.MouseBehavior.Default

	self._maid._dragMaid = nil
end



function CameraControls:_handleGamepadRotateStop()
	if self._rotVelocityTracker then
		self:_applyRotVelocityTracker(self._rotVelocityTracker)
		self._rotVelocityTracker = nil
	end

	self._maid._dragMaid = nil
end

function CameraControls:_handleGamepadRotateStart()
	if not self._rotatedCamera then
		self._maid._dragMaid = nil
		return
	end

	local maid = Maid.new()

	if self._rotatedCamera.ClassName == "SmoothRotatedCamera" then
		self._rotVelocityTracker = self:_getVelocityTracker(0.05, Vector2.new())
	end

	maid:GiveTask(RunService.Heartbeat:Connect(function()
		local DeltaAngle = self._gamepadRotateModel:GetThumbstickDeltaAngle()

		if self._rotatedCamera then
			self._rotatedCamera:RotateXY(DeltaAngle)
		end

		if self._rotVelocityTracker then
			self._rotVelocityTracker:Update(DeltaAngle)
		end
	end))

	self._maid._dragMaid = maid
end

function CameraControls:Destroy()
	self:Disable()
	setmetatable(self, nil)
end

return CameraControls