local U=require(script.Parent:WaitForChild("\066\097\115\101\067\104\097\114\097\099\116\101\114\067\111\110\116\114\111\108\108\101\114"))local F=setmetatable({},U)F.__index=F function F.new(i)local A=setmetatable(U.new(),F)A.CONTROL_ACTION_PRIORITY=i A.navigationRequestedConn=nil A.heartbeatConn=nil A.currentDestination=nil A.currentPath=nil A.currentPoints=nil A.currentPointIdx=0 A.expectedTimeToNextPoint=0 A.timeReachedLastPoint=tick()A.moving=false A.isJumpBound=false A.moveLatch=false A.userCFrameEnabledConn=nil return A end function F.SetLaserPointerMode(F,U)pcall(function()StarterGui:SetCore("\086\082\076\097\115\101\114\080\111\105\110\116\101\114\077\111\100\101",U)end)end function F.GetLocalHumanoid(U)local F=LocalPlayer.Character if not F then return end for U,F in pairs(F:GetChildren())do if F:IsA("\072\117\109\097\110\111\105\100")then return F end end return nil end function F.HasBothHandControllers(U)return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand)and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)end function F.HasAnyHandControllers(U)return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand)or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)end function F.IsMobileVR(U)return UserInputService.TouchEnabled end function F.HasGamepad(U)return UserInputService.GamepadEnabled end function F.ShouldUseNavigationLaser(U)if U:IsMobileVR()then return true else if U:HasBothHandControllers()then return false end if not U:HasAnyHandControllers()then return not U:HasGamepad()end return true end end function F.StartFollowingPath(F,U)currentPath=U currentPoints=currentPath:GetPointCoordinates()currentPointIdx=1 moving=true timeReachedLastPoint=tick()local i=F:GetLocalHumanoid()if i and(i.Torso and#currentPoints>=1)then local U=(currentPoints[1]-i.Torso.Position).magnitude expectedTimeToNextPoint=U/i.WalkSpeed end movementUpdateEvent:Fire("\116\097\114\103\101\116\080\111\105\110\116",F.currentDestination)end function F.GoToPoint(F,U)currentPath=true currentPoints={U}currentPointIdx=1 moving=true local i=F:GetLocalHumanoid()local A=(i.Torso.Position-U).magnitude local y=A/i.WalkSpeed timeReachedLastPoint=tick()expectedTimeToNextPoint=y movementUpdateEvent:Fire("\116\097\114\103\101\116\080\111\105\110\116",U)end function F.StopFollowingPath(U)currentPath=nil currentPoints=nil currentPointIdx=0 moving=false U.moveVector=ZERO_VECTOR3 end function F.TryComputePath(i,U,F)local A=0 local y=nil while not y and A<5 do y=PathfindingService:ComputeSmoothPathAsync(U,F,MAX_PATHING_DISTANCE)A=A+1 if y.Status==Enum.PathStatus.ClosestNoPath or y.Status==Enum.PathStatus.ClosestOutOfRange then y=nil break end if y and y.Status==Enum.PathStatus.FailStartNotEmpty then U=U+(F-U).unit y=nil end if y and y.Status==Enum.PathStatus.FailFinishNotEmpty then F=F+Vector3.new(0,1,0)y=nil end end return y end function F.OnNavigationRequest(i,U,F)local A=U.p local y=i.currentDestination if not IsFiniteVector3(A)then return end i.currentDestination=A local d=i:GetLocalHumanoid()if not d or not d.Torso then return end local r=d.Torso.Position local S=(i.currentDestination-r).magnitude if S<NO_PATH_THRESHOLD then i:GoToPoint(i.currentDestination)return end if not y or(i.currentDestination-y).magnitude>RECALCULATE_PATH_THRESHOLD then local U=i:TryComputePath(r,i.currentDestination)if U then i:StartFollowingPath(U)if PathDisplay then PathDisplay.setCurrentPoints(i.currentPoints)PathDisplay.renderPath()end else i:StopFollowingPath()if PathDisplay then PathDisplay.clearRenderedPath()end end else if moving then i.currentPoints[#currentPoints]=i.currentDestination else i:GoToPoint(i.currentDestination)end end end function F.OnJumpAction(A,U,F,i)if F==Enum.UserInputState.Begin then A.isJumping=true end if FFlagPlayerScriptsBindAtPriority then return Enum.ContextActionResult.Sink end end function F.BindJumpAction(F,U)if U then if not F.isJumpBound then F.isJumpBound=true if FFlagPlayerScriptsBindAtPriority then ContextActionService:BindActionAtPriority("\086\082\074\117\109\112\065\099\116\105\111\110",function()return F:OnJumpAction()end,false,F.CONTROL_ACTION_PRIORITY,Enum.KeyCode.ButtonA)else ContextActionService:BindAction("\086\082\074\117\109\112\065\099\116\105\111\110",function()F:OnJumpAction()end,false,Enum.KeyCode.ButtonA)end end else if F.isJumpBound then F.isJumpBound=false ContextActionService:UnbindAction("\086\082\074\117\109\112\065\099\116\105\111\110")end end end function F.ControlCharacterGamepad(A,U,F,i)if i.KeyCode~=Enum.KeyCode.Thumbstick1 then return end if F==Enum.UserInputState.Cancel then A.moveVector=ZERO_VECTOR3 return end if F~=Enum.UserInputState.End then A:StopFollowingPath()if PathDisplay then PathDisplay.clearRenderedPath()end if A:ShouldUseNavigationLaser()then A:BindJumpAction(true)A:SetLaserPointerMode("\072\105\100\100\101\110")end if i.Position.magnitude>THUMBSTICK_DEADZONE then A.moveVector=Vector3.new(i.Position.X,0,-i.Position.Y)if A.moveVector.magnitude>0 then A.moveVector=A.moveVector.unit*math.min(1,i.Position.magnitude)end A.moveLatch=true end else A.moveVector=ZERO_VECTOR3 if A:ShouldUseNavigationLaser()then A:BindJumpAction(false)A:SetLaserPointerMode("\078\097\118\105\103\097\116\105\111\110")end if A.moveLatch then A.moveLatch=false movementUpdateEvent:Fire("\111\102\102\116\114\097\099\107")end end if FFlagPlayerScriptsBindAtPriority then return Enum.ContextActionResult.Sink end end function F.OnHeartbeat(F,U)local i=F.moveVector local A=F:GetLocalHumanoid()if not A or not A.Torso then return end if F.moving and F.currentPoints then local y=A.Torso.Position local d=currentPoints[1]local r=(d-y)*XZ_VECTOR3 local S=r.magnitude local z=r/S if S<POINT_REACHED_THRESHOLD then local U=0 local i=currentPoints[1]for F,y in pairs(currentPoints)do if F~=1 then local F=(y-i).magnitude i=y U=U+F/A.WalkSpeed end end table.remove(currentPoints,1)currentPointIdx=currentPointIdx+1 if#currentPoints==0 then F:StopFollowingPath()if PathDisplay then PathDisplay.clearRenderedPath()end return else if PathDisplay then PathDisplay.setCurrentPoints(currentPoints)PathDisplay.renderPath()end local U=currentPoints[1]local F=(U-y).magnitude expectedTimeToNextPoint=F/A.WalkSpeed timeReachedLastPoint=tick()end else local d={game.Players.LocalPlayer.Character;workspace.CurrentCamera}local r=Ray.new(y-Vector3.new(0,1,0),z*3)local S,c,C=workspace:FindPartOnRayWithIgnoreList(r,d)if S then local U=Vector3.new(0,100,0)local F=Ray.new((c+z*.5)+U,-U)local i,r,S=workspace:FindPartOnRayWithIgnoreList(F,d)local C=r.Y-y.Y if C<6 and C>-2 then A.Jump=true end end local h=tick()-timeReachedLastPoint if h>expectedTimeToNextPoint+OFFTRACK_TIME_THRESHOLD then F:StopFollowingPath()if PathDisplay then PathDisplay.clearRenderedPath()end movementUpdateEvent:Fire("\111\102\102\116\114\097\099\107")end i=F.moveVector:Lerp(z,U*10)end end if IsFiniteVector3(i)then F.moveVector=i end end function F.OnUserCFrameEnabled(U)if U:ShouldUseNavigationLaser()then U:BindJumpAction(false)U:SetLaserPointerMode("\078\097\118\105\103\097\116\105\111\110")else U:BindJumpAction(true)U:SetLaserPointerMode("\072\105\100\100\101\110")end end function F.Enable(F,U)F.moveVector=ZERO_VECTOR3 F.isJumping=false if U then F.navigationRequestedConn=VRService.NavigationRequested:Connect(function(U,i)F:OnNavigationRequest(U,i)end)F.heartbeatConn=RunService.Heartbeat:Connect(function(U)F:OnHeartbeat(U)end)if FFlagPlayerScriptsBindAtPriority then ContextActionService:BindAction("\077\111\118\101\084\104\117\109\098\115\116\105\099\107",function(U,i,A)return F:ControlCharacterGamepad(U,i,A)end,false,F.CONTROL_ACTION_PRIORITY,Enum.KeyCode.Thumbstick1)else ContextActionService:BindAction("\077\111\118\101\084\104\117\109\098\115\116\105\099\107",function(U,i,A)F:ControlCharacterGamepad(U,i,A)end,false,Enum.KeyCode.Thumbstick1)end ContextActionService:BindActivate(Enum.UserInputType.Gamepad1,Enum.KeyCode.ButtonR2)F.userCFrameEnabledConn=VRService.UserCFrameEnabled:Connect(function()F:OnUserCFrameEnabled()end)F:OnUserCFrameEnabled()VRService:SetTouchpadMode(Enum.VRTouchpad.Left,Enum.VRTouchpadMode.VirtualThumbstick)VRService:SetTouchpadMode(Enum.VRTouchpad.Right,Enum.VRTouchpadMode.ABXY)F.enabled=true else F:StopFollowingPath()ContextActionService:UnbindAction("\077\111\118\101\084\104\117\109\098\115\116\105\099\107")ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1,Enum.KeyCode.ButtonR2)F:BindJumpAction(false)F:SetLaserPointerMode("\068\105\115\097\098\108\101\100")if F.navigationRequestedConn then F.navigationRequestedConn:Disconnect()F.navigationRequestedConn=nil end if F.heartbeatConn then F.heartbeatConn:Disconnect()F.heartbeatConn=nil end if F.userCFrameEnabledConn then F.userCFrameEnabledConn:Disconnect()F.userCFrameEnabledConn=nil end F.enabled=false end end return F
--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local VRNavigation = setmetatable({}, BaseCharacterController)
VRNavigation.__index = VRNavigation

function VRNavigation.new(CONTROL_ACTION_PRIORITY)
	local self = setmetatable(BaseCharacterController.new(), VRNavigation)
	
	self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
	
	self.navigationRequestedConn = nil
	self.heartbeatConn = nil
	
	self.currentDestination = nil
	self.currentPath = nil
	self.currentPoints = nil
	self.currentPointIdx = 0
	
	self.expectedTimeToNextPoint = 0
	self.timeReachedLastPoint = tick()
	self.moving = false
	
	self.isJumpBound = false
	self.moveLatch = false	

	self.userCFrameEnabledConn = nil
	
	return self
end

function VRNavigation:SetLaserPointerMode(mode)
	pcall(function()
		StarterGui:SetCore("VRLaserPointerMode", mode)
	end)
end

function VRNavigation:GetLocalHumanoid()
	local character = LocalPlayer.Character
	if not character then
		return
	end
	
	for _, child in pairs(character:GetChildren()) do
		if child:IsA("Humanoid") then
			return child
		end
	end
	return nil
end

function VRNavigation:HasBothHandControllers()
	return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end

function VRNavigation:HasAnyHandControllers()
	return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end

function VRNavigation:IsMobileVR()
	return UserInputService.TouchEnabled
end

function VRNavigation:HasGamepad()
	return UserInputService.GamepadEnabled
end

function VRNavigation:ShouldUseNavigationLaser()
	--Places where we use the navigation laser:
	-- mobile VR with any number of hands tracked
	-- desktop VR with only one hand tracked
	-- desktop VR with no hands and no gamepad (i.e. with Oculus remote?)
	--using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick.
	--in the future, we should query thumbstick presence with a features API
	if self:IsMobileVR() then
		return true		
	else
		if self:HasBothHandControllers() then
			return false
		end
		if not self:HasAnyHandControllers() then
			return not self:HasGamepad()
		end
		return true
	end
end



function VRNavigation:StartFollowingPath(newPath)
	currentPath = newPath
	currentPoints = currentPath:GetPointCoordinates()
	currentPointIdx = 1
	moving = true
	
	timeReachedLastPoint = tick()
	
	local humanoid = self:GetLocalHumanoid()
	if humanoid and humanoid.Torso and #currentPoints >= 1 then
		local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude
		expectedTimeToNextPoint = dist / humanoid.WalkSpeed
	end
	
	movementUpdateEvent:Fire("targetPoint", self.currentDestination)	
end

function VRNavigation:GoToPoint(point)
	currentPath = true
	currentPoints = { point }
	currentPointIdx = 1
	moving = true

	local humanoid = self:GetLocalHumanoid()
	local distance = (humanoid.Torso.Position - point).magnitude	
	local estimatedTimeRemaining = distance / humanoid.WalkSpeed 
	
	timeReachedLastPoint = tick()
	expectedTimeToNextPoint = estimatedTimeRemaining
	
	movementUpdateEvent:Fire("targetPoint", point)
end

function VRNavigation:StopFollowingPath()
	currentPath = nil
	currentPoints = nil
	currentPointIdx = 0
	moving = false
	self.moveVector = ZERO_VECTOR3
end

function VRNavigation:TryComputePath(startPos, destination)
	local numAttempts = 0
	local newPath = nil
	
	while not newPath and numAttempts < 5 do
		newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE)
		numAttempts = numAttempts + 1
		
		if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then
			newPath = nil
			break
		end
		
		if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then
			startPos = startPos + (destination - startPos).unit
			newPath = nil
		end
		
		if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then
			destination = destination + Vector3.new(0, 1, 0)
			newPath = nil
		end
	end
	
	return newPath
end

function VRNavigation:OnNavigationRequest(destinationCFrame, inputUserCFrame )
	local destinationPosition = destinationCFrame.p
	local lastDestination = self.currentDestination

	if not IsFiniteVector3(destinationPosition) then
		return
	end
	
	self.currentDestination = destinationPosition
		
	local humanoid = self:GetLocalHumanoid()
	if not humanoid or not humanoid.Torso then
		return
	end
		
	local currentPosition = humanoid.Torso.Position
	local distanceToDestination = (self.currentDestination - currentPosition).magnitude
		
	if distanceToDestination < NO_PATH_THRESHOLD then
		self:GoToPoint(self.currentDestination)
		return
	end		
		
	if not lastDestination or (self.currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then
		local newPath = self:TryComputePath(currentPosition, self.currentDestination)	
		if newPath then
			self:StartFollowingPath(newPath)
			if PathDisplay then
				PathDisplay.setCurrentPoints(self.currentPoints)
				PathDisplay.renderPath()
			end
		else
			self:StopFollowingPath()
			if PathDisplay then
				PathDisplay.clearRenderedPath()
			end
		end
	else
		if moving then
			self.currentPoints[#currentPoints] = self.currentDestination
		else
			self:GoToPoint(self.currentDestination)
		end
	end
end

function VRNavigation:OnJumpAction(actionName, inputState, inputObj)
	if inputState == Enum.UserInputState.Begin then
		self.isJumping = true
	end
	if FFlagPlayerScriptsBindAtPriority then
		return Enum.ContextActionResult.Sink
	end
end
function VRNavigation:BindJumpAction(active)
	if active then
		if not self.isJumpBound then
			self.isJumpBound = true
			if FFlagPlayerScriptsBindAtPriority then
				ContextActionService:BindActionAtPriority("VRJumpAction", (function() return self:OnJumpAction() end), false,
				 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
			else
				ContextActionService:BindAction("VRJumpAction", (function() self:OnJumpAction() end), false, Enum.KeyCode.ButtonA)
			end
		end
	else
		if self.isJumpBound then
			self.isJumpBound = false
			ContextActionService:UnbindAction("VRJumpAction")
		end
	end
end

function VRNavigation:ControlCharacterGamepad(actionName, inputState, inputObject)
	if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
	
	if inputState == Enum.UserInputState.Cancel then
		self.moveVector =  ZERO_VECTOR3
		return
	end
	
	if inputState ~= Enum.UserInputState.End then
		self:StopFollowingPath()		
		if PathDisplay then
			PathDisplay.clearRenderedPath()
		end
		
		if self:ShouldUseNavigationLaser() then
			self:BindJumpAction(true)
			self:SetLaserPointerMode("Hidden")
		end
		
		if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then
			self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
			if self.moveVector.magnitude > 0 then
				self.moveVector = self.moveVector.unit * math.min(1, inputObject.Position.magnitude)
			end

			self.moveLatch = true
		end
	else
		self.moveVector =  ZERO_VECTOR3
		
		if self:ShouldUseNavigationLaser() then
			self:BindJumpAction(false)
			self:SetLaserPointerMode("Navigation")
		end
		
		if self.moveLatch then
			self.moveLatch = false
			movementUpdateEvent:Fire("offtrack")
		end
	end
	if FFlagPlayerScriptsBindAtPriority then
		return Enum.ContextActionResult.Sink
	end
end

function VRNavigation:OnHeartbeat(dt)
	local newMoveVector = self.moveVector
	local humanoid = self:GetLocalHumanoid()
	if not humanoid or not humanoid.Torso then
		return
	end

	if self.moving and self.currentPoints then
		local currentPosition = humanoid.Torso.Position
		local goalPosition = currentPoints[1]
		local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3
		local moveDist = vectorToGoal.magnitude
		local moveDir = vectorToGoal / moveDist
			
		if moveDist < POINT_REACHED_THRESHOLD then
			local estimatedTimeRemaining = 0
			local prevPoint = currentPoints[1]
			for i, point in pairs(currentPoints) do
				if i ~= 1 then
					local dist = (point - prevPoint).magnitude
					prevPoint = point
					estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed)
				end
			end					
			
			table.remove(currentPoints, 1)
			currentPointIdx = currentPointIdx + 1

			if #currentPoints == 0 then
				self:StopFollowingPath()
				if PathDisplay then
					PathDisplay.clearRenderedPath()
				end
				return
			else
				if PathDisplay then
					PathDisplay.setCurrentPoints(currentPoints)
					PathDisplay.renderPath()
				end
				
				local newGoal = currentPoints[1]
				local distanceToGoal = (newGoal - currentPosition).magnitude
				expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed
				timeReachedLastPoint = tick()
			end
		else
			local ignoreTable = { 
				game.Players.LocalPlayer.Character, 
				workspace.CurrentCamera 
			}
			local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3)
			local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable)		
				
			if obstructPart then
				local heightOffset = Vector3.new(0, 100, 0)
				local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset)
				local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable)		
				
				local heightDifference = jumpCheckPoint.Y - currentPosition.Y
				if heightDifference < 6 and heightDifference > -2 then
					humanoid.Jump = true
				end
			end
			
			local timeSinceLastPoint = tick() - timeReachedLastPoint
			if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then
				self:StopFollowingPath()
				if PathDisplay then
					PathDisplay.clearRenderedPath()
				end
				
				movementUpdateEvent:Fire("offtrack")
			end
				
			newMoveVector = self.moveVector:Lerp(moveDir, dt * 10)
		end
	end
	
	if IsFiniteVector3(newMoveVector) then
		self.moveVector = newMoveVector
	end
end


function VRNavigation:OnUserCFrameEnabled()
	if self:ShouldUseNavigationLaser() then
		self:BindJumpAction(false)
		self:SetLaserPointerMode("Navigation")
	else
		self:BindJumpAction(true)
		self:SetLaserPointerMode("Hidden")
	end
end

function VRNavigation:Enable(enable)
	
	self.moveVector = ZERO_VECTOR3
	self.isJumping = false
	
	if enable then
		self.navigationRequestedConn = VRService.NavigationRequested:Connect(function(destinationCFrame, inputUserCFrame) self:OnNavigationRequest(destinationCFrame, inputUserCFrame) end)
		self.heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:OnHeartbeat(dt) end)
		
		if FFlagPlayerScriptsBindAtPriority then 
			ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) return self:ControlCharacterGamepad(actionName, inputState, inputObject) end),
				false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
		else
			ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) self:ControlCharacterGamepad(actionName, inputState, inputObject) end), false, Enum.KeyCode.Thumbstick1)
		end
		ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)	
		
		self.userCFrameEnabledConn = VRService.UserCFrameEnabled:Connect(function() self:OnUserCFrameEnabled() end)
		self:OnUserCFrameEnabled()

		VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick)
		VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY)

		self.enabled = true
	else
		-- Disable
		self:StopFollowingPath()
		
		ContextActionService:UnbindAction("MoveThumbstick")
		ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
		
		self:BindJumpAction(false)
		self:SetLaserPointerMode("Disabled")
		
		if self.navigationRequestedConn then
			self.navigationRequestedConn:Disconnect()
			self.navigationRequestedConn = nil
		end
		if self.heartbeatConn then
			self.heartbeatConn:Disconnect()
			self.heartbeatConn = nil
		end
		if self.userCFrameEnabledConn then
			self.userCFrameEnabledConn:Disconnect()
			self.userCFrameEnabledConn = nil
		end
		self.enabled = false
	end
end

return VRNavigation
