--!nonstrict
--[[
	TransparencyController - Manages transparency of player character at close camera-to-subject distances
	2018 Camera Update - AllYourBlox
--]]
local MAX_TWEEN_RATE = 2.8 -- per second

local Util = require(script.Parent:WaitForChild("CameraUtils"))

--[[ The Module ]]--
local TransparencyController = {}
TransparencyController.__index = TransparencyController

function TransparencyController.new()
	local self = setmetatable({}, TransparencyController)

	self.transparencyDirty = false
	self.enabled = false
	self.lastTransparency = nil

	self.descendantAddedConn, self.descendantRemovingConn = nil, nil
	self.toolDescendantAddedConns = {}
	self.toolDescendantRemovingConns = {}
	self.cachedParts = {}

	return self
end

function TransparencyController:HasToolAncestor(object: Instance)
	if object.Parent == nil then return false end
	assert(object.Parent, "")
	return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
end

function TransparencyController:IsValidPartToModify(part: BasePart)
	if part:IsA('BasePart') or part:IsA('Decal') then
		return not self:HasToolAncestor(part)
	end
	return false
end

function TransparencyController:CachePartsRecursive(object)
	if object then
		if self:IsValidPartToModify(object) then
			self.cachedParts[object] = true
			self.transparencyDirty = true
		end
		for _, child in pairs(object:GetChildren()) do
			self:CachePartsRecursive(child)
		end
	end
end

function TransparencyController:TeardownTransparency()
	for child, _ in pairs(self.cachedParts) do
		child.LocalTransparencyModifier = 0
	end
	self.cachedParts = {}
	self.transparencyDirty = true
	self.lastTransparency = nil

	if self.descendantAddedConn then
		self.descendantAddedConn:disconnect()
		self.descendantAddedConn = nil
	end
	if self.descendantRemovingConn then
		self.descendantRemovingConn:disconnect()
		self.descendantRemovingConn = nil
	end
	for object, conn in pairs(self.toolDescendantAddedConns) do
		conn:Disconnect()
		self.toolDescendantAddedConns[object] = nil
	end
	for object, conn in pairs(self.toolDescendantRemovingConns) do
		conn:Disconnect()
		self.toolDescendantRemovingConns[object] = nil
	end
end

function TransparencyController:SetupTransparency(character)
	self:TeardownTransparency()

	if self.descendantAddedConn then self.descendantAddedConn:disconnect() end
	self.descendantAddedConn = character.DescendantAdded:Connect(function(object)
		-- This is a part we want to invisify
		if self:IsValidPartToModify(object) then
			self.cachedParts[object] = true
			self.transparencyDirty = true
		-- There is now a tool under the character
		elseif object:IsA('Tool') then
			if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end
			self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild)
				self.cachedParts[toolChild] = nil
				if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
					-- Reset the transparency
					toolChild.LocalTransparencyModifier = 0
				end
			end)
			if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end
			self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild)
				wait() -- wait for new parent
				if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
					if self:IsValidPartToModify(formerToolChild) then
						self.cachedParts[formerToolChild] = true
						self.transparencyDirty = true
					end
				end
			end)
		end
	end)
	if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end
	self.descendantRemovingConn = character.DescendantRemoving:connect(function(object)
		if self.cachedParts[object] then
			self.cachedParts[object] = nil
			-- Reset the transparency
			object.LocalTransparencyModifier = 0
		end
	end)
	self:CachePartsRecursive(character)
end

function TransparencyController:Enable(enable: boolean)
	if self.enabled ~= enable then
		self.enabled = enable
	end
end

function TransparencyController:SetSubject(subject)
	local character = nil
	if subject and subject:IsA("Humanoid") then
		character = subject.Parent
	end
	if subject and subject:IsA("VehicleSeat") and subject.Occupant then
		character = subject.Occupant.Parent
	end
	if character then
		self:SetupTransparency(character)
	else
		self:TeardownTransparency()
	end
end

function TransparencyController:Update(dt)
	local currentCamera = workspace.CurrentCamera

	if currentCamera and self.enabled then
		-- calculate goal transparency based on distance
		local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
		local transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 -- (7 - distance) / 5
		if transparency < 0.5 then -- too far, don't control transparency
			transparency = 0
		end

		-- tween transparency if the goal is not fully transparent and the subject was not fully transparent last frame
		if self.lastTransparency and transparency < 1 and self.lastTransparency < 0.95 then
			local deltaTransparency = transparency - self.lastTransparency
			local maxDelta = MAX_TWEEN_RATE * dt
			deltaTransparency = math.clamp(deltaTransparency, -maxDelta, maxDelta)
			transparency = self.lastTransparency + deltaTransparency
		else
			self.transparencyDirty = true
		end

		transparency = math.clamp(Util.Round(transparency, 2), 0, 1)

		-- update transparencies
		if self.transparencyDirty or self.lastTransparency ~= transparency then
			for child, _ in pairs(self.cachedParts) do
				child.LocalTransparencyModifier = transparency
			end
			self.transparencyDirty = false
			self.lastTransparency = transparency
		end
	end
end

return TransparencyController


--!nonstrict
--[[
	VRCamera - Roblox VR camera control module
	2021 Roblox VR
--]]

--[[ Services ]]--
local PlayersService = game:GetService("Players")
local VRService = game:GetService("VRService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")

-- Local private variables and constants
local CAMERA_BLACKOUT_TIME = 0.1
local FP_ZOOM = 0.5

-- requires
local CameraInput = require(script.Parent:WaitForChild("CameraInput"))
local Util = require(script.Parent:WaitForChild("CameraUtils"))

local FFlagUserVRRotationUpdate do
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserVRRotationUpdate")
	end)
	FFlagUserVRRotationUpdate = success and result
end

--[[ The Module ]]--
local VRBaseCamera = require(script.Parent:WaitForChild("VRBaseCamera"))
local VRCamera = setmetatable({}, VRBaseCamera)
VRCamera.__index = VRCamera

function VRCamera.new()
	local self = setmetatable(VRBaseCamera.new(), VRCamera)

	self.lastUpdate = tick()
	self:Reset()

	return self
end

function VRCamera:Reset()
	self.needsReset = true
	self.needsBlackout = true
	self.motionDetTime = 0.0
	self.blackOutTimer = 0
	self.lastCameraResetPosition = nil
	if not FFlagUserVRRotationUpdate then
		self.stepRotateTimeout = 0.0
		self.cameraOffsetRotation = 0
		self.cameraOffsetRotationDiscrete = 0
	else
		VRBaseCamera.Reset(self)
	end
end

function VRCamera:Update(timeDelta)
	local camera = workspace.CurrentCamera
	local newCameraCFrame = camera.CFrame
	local newCameraFocus = camera.Focus

	local player = PlayersService.LocalPlayer
	local humanoid = self:GetHumanoid()
	local cameraSubject = camera.CameraSubject

	if self.lastUpdate == nil or timeDelta > 1 then
		self.lastCameraTransform = nil
	end

	-- update fullscreen effects
	self:UpdateFadeFromBlack(timeDelta)
	self:UpdateEdgeBlur(player, timeDelta)

	local lastSubjPos = self.lastSubjectPosition
	local subjectPosition: Vector3 = self:GetSubjectPosition()
	-- transition from another camera or from spawn
	if self.needsBlackout then
		self:StartFadeFromBlack()

		local dt = math.clamp(timeDelta, 0.0001, 0.1)
		self.blackOutTimer += dt
		if self.blackOutTimer > CAMERA_BLACKOUT_TIME and game:IsLoaded() then
			self.needsBlackout = false
			self.needsReset = true
		end
	end

	if subjectPosition and player and camera then
		newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)

		if self:IsInFirstPerson() then
			-- update camera CFrame
			newCameraCFrame, newCameraFocus = self:UpdateFirstPersonTransform(
				timeDelta,newCameraCFrame, newCameraFocus, lastSubjPos, subjectPosition)
		else -- 3rd person
			-- update camera CFrame
			newCameraCFrame, newCameraFocus = self:UpdateThirdPersonTransform(
				timeDelta, newCameraCFrame, newCameraFocus, lastSubjPos, subjectPosition)
		end

		self.lastCameraTransform = newCameraCFrame
		self.lastCameraFocus = newCameraFocus
	end

	self.lastUpdate = tick()
	return newCameraCFrame, newCameraFocus
end

-- returns where the floor should be placed given the camera subject, nil if anything is invalid
function VRCamera:GetAvatarFeetWorldYValue(): number?
	local camera = workspace.CurrentCamera
	local cameraSubject = camera.CameraSubject
	if not cameraSubject then
		return nil
	end

	if cameraSubject:IsA("Humanoid") and cameraSubject.RootPart then
		local rootPart = cameraSubject.RootPart
		return rootPart.Position.Y - rootPart.Size.Y / 2 - cameraSubject.HipHeight
	end

	return nil
end

function VRCamera:UpdateFirstPersonTransform(timeDelta, newCameraCFrame, newCameraFocus, lastSubjPos, subjectPosition)
	-- transition from TP to FP
	if self.needsReset then
		self:StartFadeFromBlack()
		self.needsReset = false
		if not FFlagUserVRRotationUpdate then
			self.stepRotateTimeout = 0.25
			self.VRCameraFocusFrozen = true
			self.cameraOffsetRotation = 0
			self.cameraOffsetRotationDiscrete = 0
		end
	end

	-- blur screen edge during movement
	local player = PlayersService.LocalPlayer
	local subjectDelta = lastSubjPos - subjectPosition
	if subjectDelta.magnitude > 0.01 then
		self:StartVREdgeBlur(player)
	end
	-- straight view, not angled down
	local cameraFocusP = newCameraFocus.p
	local cameraLookVector = self:GetCameraLookVector()
	cameraLookVector = Vector3.new(cameraLookVector.X, 0, cameraLookVector.Z).Unit

	local yawDelta -- inline with FFlagVRRotationUpdate
	if FFlagUserVRRotationUpdate then

		yawDelta = self:getRotation(timeDelta)
	else
		if self.stepRotateTimeout > 0 then
			self.stepRotateTimeout -= timeDelta
		end

		-- step rotate in 1st person
		local rotateInput = CameraInput.getRotation()
		yawDelta = 0
		if UserGameSettings.VRSmoothRotationEnabled then
			yawDelta = rotateInput.X
		else
			if self.stepRotateTimeout <= 0.0 and math.abs(rotateInput.X) > 0.03 then
				yawDelta = 0.5
				if rotateInput.X < 0 then
					yawDelta = -0.5
				end
				self.needsReset = true
			end
		end
	end

	local newLookVector = self:CalculateNewLookVectorFromArg(cameraLookVector, Vector2.new(yawDelta, 0))
	newCameraCFrame = CFrame.new(cameraFocusP - (FP_ZOOM * newLookVector), cameraFocusP)

	return newCameraCFrame, newCameraFocus
end

function VRCamera:UpdateThirdPersonTransform(timeDelta, newCameraCFrame, newCameraFocus, lastSubjPos, subjectPosition)
	local zoom = self:GetCameraToSubjectDistance()
	if zoom < 0.5 then
		zoom = 0.5
	end

	if lastSubjPos ~= nil and self.lastCameraFocus ~= nil then
		-- compute delta of subject since last update
		local player = PlayersService.LocalPlayer
		local subjectDelta = lastSubjPos - subjectPosition
		local moveVector = require(player:WaitForChild("PlayerScripts").PlayerModule:WaitForChild("ControlModule")):GetMoveVector()

		-- is the subject still moving?
		local isMoving = subjectDelta.magnitude > 0.01 or moveVector.magnitude > 0.01
		if isMoving then
			self.motionDetTime = 0.1
		end

		self.motionDetTime = self.motionDetTime - timeDelta
		if self.motionDetTime > 0 then
			isMoving = true
		end

		if isMoving and not self.needsReset then
			-- if subject moves keep old camera focus
			newCameraFocus = self.lastCameraFocus

			-- if the focus subject stopped, time to reset the camera
			self.VRCameraFocusFrozen = true
		else
			local subjectMoved = self.lastCameraResetPosition == nil or (subjectPosition - self.lastCameraResetPosition).Magnitude > 1

			-- compute offset for 3rd person camera rotation
			if FFlagUserVRRotationUpdate then
				local yawDelta = self:getRotation(timeDelta)
				if math.abs(yawDelta) > 0 then
					local cameraOffset = newCameraFocus:ToObjectSpace(newCameraCFrame)
					local rotatedFocus = newCameraFocus * CFrame.Angles(0, yawDelta, 0)
					newCameraCFrame = rotatedFocus * cameraOffset
				end

				-- recenter the camera on teleport
				if (self.VRCameraFocusFrozen and subjectMoved) or self.needsReset then
					VRService:RecenterUserHeadCFrame()

					self.VRCameraFocusFrozen = false
					self.needsReset = false
					self.lastCameraResetPosition = subjectPosition

					self:ResetZoom()
					self:StartFadeFromBlack()

					-- get player facing direction
					local humanoid = self:GetHumanoid()
					local forwardVector = humanoid.Torso and humanoid.Torso.CFrame.lookVector or Vector3.new(1,0,0)
					-- adjust camera height
					local vecToCameraAtHeight = Vector3.new(forwardVector.X, 0, forwardVector.Z)
					local newCameraPos = newCameraFocus.Position - vecToCameraAtHeight * zoom
					-- compute new cframe at height level to subject
					local lookAtPos = Vector3.new(newCameraFocus.Position.X, newCameraPos.Y, newCameraFocus.Position.Z)

					newCameraCFrame = CFrame.new(newCameraPos, lookAtPos)
				end

			else
				local rotateInput = CameraInput.getRotation()
				local userCameraPan = rotateInput ~= Vector2.new()
				local panUpdate = false
				if userCameraPan then
					if rotateInput.X ~= 0 then
						local tempRotation = self.cameraOffsetRotation + rotateInput.X;
						if(tempRotation < -math.pi) then
							tempRotation = math.pi - (tempRotation + math.pi)
						else
							if (tempRotation > math.pi) then
								tempRotation = -math.pi + (tempRotation - math.pi)
							end
						end
						self.cameraOffsetRotation = math.clamp(tempRotation, -math.pi, math.pi)
						if UserGameSettings.VRSmoothRotationEnabled then
							self.cameraOffsetRotationDiscrete = self.cameraOffsetRotation

							-- get player facing direction
							local humanoid = self:GetHumanoid()
							local forwardVector = humanoid.Torso and humanoid.Torso.CFrame.lookVector or Vector3.new(1,0,0)
							-- adjust camera height
							local vecToCameraAtHeight = Vector3.new(forwardVector.X, 0, forwardVector.Z)
							local newCameraPos = newCameraFocus.Position - vecToCameraAtHeight * zoom
							-- compute new cframe at height level to subject
							local lookAtPos = Vector3.new(newCameraFocus.Position.X, newCameraPos.Y, newCameraFocus.Position.Z)

							local tempCF = CFrame.new(newCameraPos, lookAtPos)
							tempCF = tempCF * CFrame.fromAxisAngle(Vector3.new(0,1,0), self.cameraOffsetRotationDiscrete)
							newCameraPos = lookAtPos - (tempCF.LookVector * (lookAtPos - newCameraPos).Magnitude)

							newCameraCFrame = CFrame.new(newCameraPos, lookAtPos)
						else
							local tempRotDisc = math.floor(self.cameraOffsetRotation * 12 / 12)
							if tempRotDisc ~= self.cameraOffsetRotationDiscrete then
								self.cameraOffsetRotationDiscrete = tempRotDisc
								panUpdate = true
							end
						end
					end
				end

				-- recenter the camera on teleport
				if (self.VRCameraFocusFrozen and subjectMoved) or self.needsReset or panUpdate then
					if not panUpdate then
						self.cameraOffsetRotationDiscrete = 0
						self.cameraOffsetRotation = 0
					end

					VRService:RecenterUserHeadCFrame()

					self.VRCameraFocusFrozen = false
					self.needsReset = false
					self.lastCameraResetPosition = subjectPosition

					self:ResetZoom()
					self:StartFadeFromBlack()

					-- get player facing direction
					local humanoid = self:GetHumanoid()
					local forwardVector = humanoid.Torso and humanoid.Torso.CFrame.lookVector or Vector3.new(1,0,0)
					-- adjust camera height
					local vecToCameraAtHeight = Vector3.new(forwardVector.X, 0, forwardVector.Z)
					local newCameraPos = newCameraFocus.Position - vecToCameraAtHeight * zoom
					-- compute new cframe at height level to subject
					local lookAtPos = Vector3.new(newCameraFocus.Position.X, newCameraPos.Y, newCameraFocus.Position.Z)

					if self.cameraOffsetRotation ~= 0 then
						local tempCF = CFrame.new(newCameraPos, lookAtPos)
						tempCF = tempCF * CFrame.fromAxisAngle(Vector3.new(0,1,0), self.cameraOffsetRotationDiscrete)
						newCameraPos = lookAtPos - (tempCF.LookVector * (lookAtPos - newCameraPos).Magnitude)
					end

					newCameraCFrame = CFrame.new(newCameraPos, lookAtPos)
				end
			end
		end
	end

	return newCameraCFrame, newCameraFocus
end

function VRCamera:EnterFirstPerson()
	self.inFirstPerson = true

	self:UpdateMouseBehavior()
end

function VRCamera:LeaveFirstPerson()
	self.inFirstPerson = false
	self.needsReset = true

	self:UpdateMouseBehavior()

	if self.VRBlur then
		self.VRBlur.Visible = false
	end
end

return VRCamera


--!nonstrict
--[[
	VRVehicleCamera - Roblox VR vehicle camera control module
	2021 Roblox VR
--]]

local EPSILON = 1e-3
local PITCH_LIMIT = math.rad(80)
local YAW_DEFAULT = math.rad(0)
local ZOOM_MINIMUM = 0.5
local ZOOM_SENSITIVITY_CURVATURE = 0.5
local DEFAULT_CAMERA_DIST = 16
local TP_FOLLOW_DIST = 200
local TP_FOLLOW_ANGLE_DOT = 0.56

local VRBaseCamera = require(script.Parent:WaitForChild("VRBaseCamera"))
local CameraInput = require(script.Parent:WaitForChild("CameraInput"))
local CameraUtils = require(script.Parent:WaitForChild("CameraUtils"))
local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
local VehicleCamera = require(script.Parent:WaitForChild("VehicleCamera"))
local VehicleCameraCore =  require(script.Parent.VehicleCamera:FindFirstChild("VehicleCameraCore")) :: any
local VehicleCameraConfig = require(script.Parent.VehicleCamera:FindFirstChild("VehicleCameraConfig")) :: any
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local VRService = game:GetService("VRService")

local localPlayer = Players.LocalPlayer
local Spring = CameraUtils.Spring
local mapClamp = CameraUtils.mapClamp
local sanitizeAngle = CameraUtils.sanitizeAngle

local ZERO_VECTOR3 = Vector3.new(0,0,0)

-- pitch-axis rotational velocity of a part with a given CFrame and total RotVelocity
local function pitchVelocity(rotVel, cf)
	return math.abs(cf.XVector:Dot(rotVel))
end

-- yaw-axis rotational velocity of a part with a given CFrame and total RotVelocity
local function yawVelocity(rotVel, cf)
	return math.abs(cf.YVector:Dot(rotVel))
end

local worldDt = 1/60
local VRVehicleCamera = setmetatable({}, VRBaseCamera)
VRVehicleCamera.__index = VRVehicleCamera

function VRVehicleCamera.new()
	local self = setmetatable(VRBaseCamera.new(), VRVehicleCamera)
	self:Reset()

	-- track physics solver time delta separately from the render loop to correctly synchronize time delta
	RunService.Stepped:Connect(function(_, _worldDt)
		worldDt = _worldDt
	end)

	return self
end

function VRVehicleCamera:Reset()
	self.vehicleCameraCore = VehicleCameraCore.new(self:GetSubjectCFrame())
	self.pitchSpring = Spring.new(0, -math.rad(VehicleCameraConfig.pitchBaseAngle))
	self.yawSpring = Spring.new(0, YAW_DEFAULT)

	local camera = workspace.CurrentCamera
	local cameraSubject = camera and camera.CameraSubject

	assert(camera, "VRVehicleCamera initialization error")
	assert(cameraSubject)
	assert(cameraSubject:IsA("VehicleSeat"))

	local assemblyParts = cameraSubject:GetConnectedParts(true) -- passing true to recursively get all assembly parts
	local assemblyPosition, assemblyRadius = CameraUtils.getLooseBoundingSphere(assemblyParts)

	assemblyRadius = math.max(assemblyRadius, EPSILON)

	self.assemblyRadius = assemblyRadius
	self.assemblyOffset = cameraSubject.CFrame:Inverse()*assemblyPosition -- seat-space offset of the assembly bounding sphere center

	self.lastCameraFocus = nil

	self:_StepInitialZoom()
end

function VRVehicleCamera:_StepInitialZoom()
	self:SetCameraToSubjectDistance(math.max(
		ZoomController.GetZoomRadius(),
		self.assemblyRadius*VehicleCameraConfig.initialZoomRadiusMul
	))
end

function VRVehicleCamera:_GetThirdPersonLocalOffset()
	return self.assemblyOffset + Vector3.new(0, self.assemblyRadius*VehicleCameraConfig.verticalCenterOffset, 0)
end

function VRVehicleCamera:_GetFirstPersonLocalOffset(subjectCFrame: CFrame)
	local character = localPlayer.Character

	if character and character.Parent then
		local head = character:FindFirstChild("Head")

		if head and head:IsA("BasePart") then
			return subjectCFrame:Inverse() * head.Position
		end
	end

	return self:_GetThirdPersonLocalOffset()
end

function VRVehicleCamera:Update()
	local camera = workspace.CurrentCamera
	local cameraSubject = camera and camera.CameraSubject
	local vehicleCameraCore = self.vehicleCameraCore

	assert(camera)
	assert(cameraSubject)
	assert(cameraSubject:IsA("VehicleSeat"))

	-- consume the physics solver time delta to account for mismatched physics/render cycles
	local dt = worldDt
	worldDt = 0

	-- get subject info
	local subjectCFrame: CFrame = self:GetSubjectCFrame()
	local subjectVel: Vector3 = self:GetSubjectVelocity()
	local subjectRotVel = self:GetSubjectRotVelocity()

	-- measure the local-to-world-space forward velocity of the vehicle
	local vDotZ = math.abs(subjectVel:Dot(subjectCFrame.ZVector))
	local yawVel = yawVelocity(subjectRotVel, subjectCFrame)
	local pitchVel = pitchVelocity(subjectRotVel, subjectCFrame)

	-- step camera components forward
	local zoom = self:StepZoom()

	-- mix third and first person offsets in local space
	local firstPerson = mapClamp(zoom, ZOOM_MINIMUM, self.assemblyRadius, 1, 0)

	local tpOffset = self:_GetThirdPersonLocalOffset()
	local fpOffset = self:_GetFirstPersonLocalOffset(subjectCFrame)
	local localOffset = tpOffset:Lerp(fpOffset, firstPerson)

	-- step core forward
	vehicleCameraCore:setTransform(subjectCFrame)
	local processedRotation = vehicleCameraCore:step(dt, pitchVel, yawVel, firstPerson)

	-- end product of this function
	local focus = nil
	local cf = nil

	-- update fade from black
	self:UpdateFadeFromBlack(dt)

	if not self:IsInFirstPerson() then
		-- third person comfort camera
		focus =  CFrame.new(subjectCFrame*localOffset)*processedRotation
		cf = focus*CFrame.new(0, 0, zoom)

		if not self.lastCameraFocus then
			self.lastCameraFocus = focus
			self.needsReset = true
		end

		local curCameraDir = focus.Position - camera.CFrame.Position
		local curCameraDist = curCameraDir.magnitude
		curCameraDir = curCameraDir.Unit
		local cameraDot = curCameraDir:Dot(camera.CFrame.LookVector)
		if cameraDot > TP_FOLLOW_ANGLE_DOT and curCameraDist < TP_FOLLOW_DIST and not self.needsReset then -- vehicle in view
			-- keep old focus
			focus = self.lastCameraFocus

			-- new cf result
			local cameraFocusP = focus.p
			local cameraLookVector = self:GetCameraLookVector()
			cameraLookVector = Vector3.new(cameraLookVector.X, 0, cameraLookVector.Z).Unit
			local newLookVector = self:CalculateNewLookVectorFromArg(cameraLookVector, Vector2.new(0, 0))
			cf = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
		else
			-- new focus / teleport
			self.currentSubjectDistance = DEFAULT_CAMERA_DIST
			self.lastCameraFocus = self:GetVRFocus(subjectCFrame.Position, dt)
			self.needsReset = false
			self:StartFadeFromBlack()
			self:ResetZoom()
		end

		self:UpdateEdgeBlur(localPlayer, dt)
	else
		-- first person in vehicle : lock orientation for stable camera
		local dir = Vector3.new(processedRotation.LookVector.X, 0, processedRotation.LookVector.Z).Unit
		local planarRotation = CFrame.new(processedRotation.Position, dir)

		-- this removes the pitch to reduce motion sickness
		focus =  CFrame.new(subjectCFrame * localOffset) * planarRotation
		cf = focus * CFrame.new(0, 0, zoom)

		self:StartVREdgeBlur(localPlayer)
	end

	return cf, focus
end

function VRVehicleCamera:EnterFirstPerson()
	self.inFirstPerson = true
	self:UpdateMouseBehavior()
end

function VRVehicleCamera:LeaveFirstPerson()
	self.inFirstPerson = false
	self:UpdateMouseBehavior()
end

return VRVehicleCamera


--!nonstrict
--------------------------------------------------------------------------------
-- Popper.lua
-- Prevents your camera from clipping through walls.
--------------------------------------------------------------------------------

local Players = game:GetService("Players")

local camera = game.Workspace.CurrentCamera

local min = math.min
local tan = math.tan
local rad = math.rad
local inf = math.huge
local ray = Ray.new

local function getTotalTransparency(part)
	return 1 - (1 - part.Transparency)*(1 - part.LocalTransparencyModifier)
end

local function eraseFromEnd(t, toSize)
	for i = #t, toSize + 1, -1 do
		t[i] = nil
	end
end

local nearPlaneZ, projX, projY do
	local function updateProjection()
		local fov = rad(camera.FieldOfView)
		local view = camera.ViewportSize
		local ar = view.X/view.Y

		projY = 2*tan(fov/2)
		projX = ar*projY
	end

	camera:GetPropertyChangedSignal("FieldOfView"):Connect(updateProjection)
	camera:GetPropertyChangedSignal("ViewportSize"):Connect(updateProjection)

	updateProjection()

	nearPlaneZ = camera.NearPlaneZ
	camera:GetPropertyChangedSignal("NearPlaneZ"):Connect(function()
		nearPlaneZ = camera.NearPlaneZ
	end)
end

local blacklist = {} do
	local charMap = {}

	local function refreshIgnoreList()
		local n = 1
		blacklist = {}
		for _, character in pairs(charMap) do
			blacklist[n] = character
			n = n + 1
		end
	end

	local function playerAdded(player)
		local function characterAdded(character)
			charMap[player] = character
			refreshIgnoreList()
		end
		local function characterRemoving()
			charMap[player] = nil
			refreshIgnoreList()
		end

		player.CharacterAdded:Connect(characterAdded)
		player.CharacterRemoving:Connect(characterRemoving)
		if player.Character then
			characterAdded(player.Character)
		end
	end

	local function playerRemoving(player)
		charMap[player] = nil
		refreshIgnoreList()
	end

	Players.PlayerAdded:Connect(playerAdded)
	Players.PlayerRemoving:Connect(playerRemoving)

	for _, player in ipairs(Players:GetPlayers()) do
		playerAdded(player)
	end
	refreshIgnoreList()
end

--------------------------------------------------------------------------------------------
-- Popper uses the level geometry find an upper bound on subject-to-camera distance.
--
-- Hard limits are applied immediately and unconditionally. They are generally caused
-- when level geometry intersects with the near plane (with exceptions, see below).
--
-- Soft limits are only applied under certain conditions.
-- They are caused when level geometry occludes the subject without actually intersecting
-- with the near plane at the target distance.
--
-- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits.
-- We usually don"t want the latter to happen.
--
-- A soft limit will be promoted to a hard limit if an obstruction
-- lies between the current and target camera positions.
--------------------------------------------------------------------------------------------

local subjectRoot
local subjectPart

camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
	local subject = camera.CameraSubject
	if subject:IsA("Humanoid") then
		subjectPart = subject.RootPart
	elseif subject:IsA("BasePart") then
		subjectPart = subject
	else
		subjectPart = nil
	end
end)

local function canOcclude(part)
	-- Occluders must be:
	-- 1. Opaque
	-- 2. Interactable
	-- 3. Not in the same assembly as the subject

	return
		getTotalTransparency(part) < 0.25 and
		part.CanCollide and
		subjectRoot ~= (part:GetRootPart() or part) and
		not part:IsA("TrussPart")
end

-- Offsets for the volume visibility test
local SCAN_SAMPLE_OFFSETS = {
	Vector2.new( 0.4, 0.0),
	Vector2.new(-0.4, 0.0),
	Vector2.new( 0.0,-0.4),
	Vector2.new( 0.0, 0.4),
	Vector2.new( 0.0, 0.2),
}

-- Maximum number of rays that can be cast
local QUERY_POINT_CAST_LIMIT = 64

--------------------------------------------------------------------------------
-- Piercing raycasts

local function getCollisionPoint(origin, dir)
	local originalSize = #blacklist
	repeat
		local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
			ray(origin, dir), blacklist, false, true
		)

		if hitPart then
			if hitPart.CanCollide then
				eraseFromEnd(blacklist, originalSize)
				return hitPoint, true
			end
			blacklist[#blacklist + 1] = hitPart
		end
	until not hitPart

	eraseFromEnd(blacklist, originalSize)
	return origin + dir, false
end

--------------------------------------------------------------------------------

local function queryPoint(origin, unitDir, dist, lastPos)
	debug.profilebegin("queryPoint")

	local originalSize = #blacklist

	dist = dist + nearPlaneZ
	local target = origin + unitDir*dist

	local softLimit = inf
	local hardLimit = inf
	local movingOrigin = origin

	local numPierced = 0

	repeat
		local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
		numPierced += 1

		if entryPart then
			-- forces the current iteration into a hard limit to cap the number of raycasts
			local earlyAbort = numPierced >= QUERY_POINT_CAST_LIMIT

			if canOcclude(entryPart) or earlyAbort then
				local wl = {entryPart}
				local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)

				local lim = (entryPos - origin).Magnitude

				if exitPart and not earlyAbort then
					local promote = false
					if lastPos then
						promote =
							workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
							workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
					end

					if promote then
						-- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
						hardLimit = lim
					elseif dist < softLimit then
						-- Trivial soft limit
						softLimit = lim
					end
				else
					-- Trivial hard limit
					hardLimit = lim
				end
			end

			blacklist[#blacklist + 1] = entryPart
			movingOrigin = entryPos - unitDir*1e-3
		end
	until hardLimit < inf or not entryPart

	eraseFromEnd(blacklist, originalSize)

	debug.profileend()
	return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
end

local function queryViewport(focus, dist)
	debug.profilebegin("queryViewport")

	local fP =  focus.p
	local fX =  focus.rightVector
	local fY =  focus.upVector
	local fZ = -focus.lookVector

	local viewport = camera.ViewportSize

	local hardBoxLimit = inf
	local softBoxLimit = inf

	-- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
	for viewX = 0, 1 do
		local worldX = fX*((viewX - 0.5)*projX)

		for viewY = 0, 1 do
			local worldY = fY*((viewY - 0.5)*projY)

			local origin = fP + nearPlaneZ*(worldX + worldY)
			local lastPos = camera:ViewportPointToRay(
				viewport.x*viewX,
				viewport.y*viewY
			).Origin

			local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)

			if hardPointLimit < hardBoxLimit then
				hardBoxLimit = hardPointLimit
			end
			if softPointLimit < softBoxLimit then
				softBoxLimit = softPointLimit
			end
		end
	end
	debug.profileend()

	return softBoxLimit, hardBoxLimit
end

local function testPromotion(focus, dist, focusExtrapolation)
	debug.profilebegin("testPromotion")

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	do
		-- Dead reckoning the camera rotation and focus
		debug.profilebegin("extrapolate")

		local SAMPLE_DT = 0.0625
		local SAMPLE_MAX_T = 1.25

		local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
		-- Metric that decides how many samples to take
		local combinedSpeed = focusExtrapolation.posVelocity.magnitude

		for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
			local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt

			if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
				return false
			end
		end

		debug.profileend()
	end

	do
		-- Test screen-space offsets from the focus for the presence of soft limits
		debug.profilebegin("testOffsets")

		for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
			local scaledOffset = offset
			local pos = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
			if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
				return false
			end
		end

		debug.profileend()
	end

	debug.profileend()
	return true
end

local function Popper(focus, targetDist, focusExtrapolation)
	debug.profilebegin("popper")

	subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart

	local dist = targetDist
	local soft, hard = queryViewport(focus, targetDist)
	if hard < dist then
		dist = hard
	end
	if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
		dist = soft
	end

	subjectRoot = nil

	debug.profileend()
	return dist
end

return Popper


--!nonstrict
--[[
	-- Original By Kip Turner, Copyright Roblox 2014
	-- Updated by Garnold to utilize the new PathfindingService API, 2017
	-- 2018 PlayerScripts Update - AllYourBlox
--]]

--[[ Flags ]]
local FFlagUserExcludeNonCollidableForPathfindingSuccess, FFlagUserExcludeNonCollidableForPathfindingResult =
    pcall(function() return UserSettings():IsUserFeatureEnabled("UserExcludeNonCollidableForPathfinding") end)
local FFlagUserExcludeNonCollidableForPathfinding = FFlagUserExcludeNonCollidableForPathfindingSuccess and FFlagUserExcludeNonCollidableForPathfindingResult

local FFlagUserClickToMoveSupportAgentCanClimbSuccess, FFlagUserClickToMoveSupportAgentCanClimbResult =
    pcall(function() return UserSettings():IsUserFeatureEnabled("UserClickToMoveSupportAgentCanClimb2") end)
local FFlagUserClickToMoveSupportAgentCanClimb = FFlagUserClickToMoveSupportAgentCanClimbSuccess and FFlagUserClickToMoveSupportAgentCanClimbResult

--[[ Roblox Services ]]--
local UserInputService = game:GetService("UserInputService")
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local DebrisService = game:GetService('Debris')
local StarterGui = game:GetService("StarterGui")
local Workspace = game:GetService("Workspace")
local CollectionService = game:GetService("CollectionService")
local GuiService = game:GetService("GuiService")

--[[ Configuration ]]
local ShowPath = true
local PlayFailureAnimation = true
local UseDirectPath = false
local UseDirectPathForVehicle = true
local AgentSizeIncreaseFactor = 1.0
local UnreachableWaypointTimeout = 8

--[[ Constants ]]--
local movementKeys = {
	[Enum.KeyCode.W] = true;
	[Enum.KeyCode.A] = true;
	[Enum.KeyCode.S] = true;
	[Enum.KeyCode.D] = true;
	[Enum.KeyCode.Up] = true;
	[Enum.KeyCode.Down] = true;
}

local Player = Players.LocalPlayer

local ClickToMoveDisplay = require(script.Parent:WaitForChild("ClickToMoveDisplay"))

local ZERO_VECTOR3 = Vector3.new(0,0,0)
local ALMOST_ZERO = 0.000001

--------------------------UTIL LIBRARY-------------------------------
local Utility = {}
do
	local function FindCharacterAncestor(part)
		if part then
			local humanoid = part:FindFirstChildOfClass("Humanoid")
			if humanoid then
				return part, humanoid
			else
				return FindCharacterAncestor(part.Parent)
			end
		end
	end
	Utility.FindCharacterAncestor = FindCharacterAncestor

	local function Raycast(ray, ignoreNonCollidable: boolean, ignoreList: {Model})
		ignoreList = ignoreList or {}
		local hitPart, hitPos, hitNorm, hitMat = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
		if hitPart then
			if ignoreNonCollidable and hitPart.CanCollide == false then
				-- We always include character parts so a user can click on another character
				-- to walk to them.
				local _, humanoid = FindCharacterAncestor(hitPart)
				if humanoid == nil then
					table.insert(ignoreList, hitPart)
					return Raycast(ray, ignoreNonCollidable, ignoreList)
				end
			end
			return hitPart, hitPos, hitNorm, hitMat
		end
		return nil, nil
	end
	Utility.Raycast = Raycast
end

local humanoidCache = {}
local function findPlayerHumanoid(player: Player)
	local character = player and player.Character
	if character then
		local resultHumanoid = humanoidCache[player]
		if resultHumanoid and resultHumanoid.Parent == character then
			return resultHumanoid
		else
			humanoidCache[player] = nil -- Bust Old Cache
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if humanoid then
				humanoidCache[player] = humanoid
			end
			return humanoid
		end
	end
end

--------------------------CHARACTER CONTROL-------------------------------
local CurrentIgnoreList: {Model}
local CurrentIgnoreTag = nil

local TaggedInstanceAddedConnection: RBXScriptConnection? = nil
local TaggedInstanceRemovedConnection: RBXScriptConnection? = nil

local function GetCharacter(): Model
	return Player and Player.Character
end

local function UpdateIgnoreTag(newIgnoreTag)
	if newIgnoreTag == CurrentIgnoreTag then
		return
	end
	if TaggedInstanceAddedConnection then
		TaggedInstanceAddedConnection:Disconnect()
		TaggedInstanceAddedConnection = nil
	end
	if TaggedInstanceRemovedConnection then
		TaggedInstanceRemovedConnection:Disconnect()
		TaggedInstanceRemovedConnection = nil
	end
	CurrentIgnoreTag = newIgnoreTag
	CurrentIgnoreList = {GetCharacter()}
	if CurrentIgnoreTag ~= nil then
		local ignoreParts = CollectionService:GetTagged(CurrentIgnoreTag)
		for _, ignorePart in ipairs(ignoreParts) do
			table.insert(CurrentIgnoreList, ignorePart)
		end
		TaggedInstanceAddedConnection = CollectionService:GetInstanceAddedSignal(
			CurrentIgnoreTag):Connect(function(ignorePart)
			table.insert(CurrentIgnoreList, ignorePart)
		end)
		TaggedInstanceRemovedConnection = CollectionService:GetInstanceRemovedSignal(
			CurrentIgnoreTag):Connect(function(ignorePart)
			for i = 1, #CurrentIgnoreList do
				if CurrentIgnoreList[i] == ignorePart then
					CurrentIgnoreList[i] = CurrentIgnoreList[#CurrentIgnoreList]
					table.remove(CurrentIgnoreList)
					break
				end
			end
		end)
	end
end

local function getIgnoreList(): {Model}
	if CurrentIgnoreList then
		return CurrentIgnoreList
	end
	CurrentIgnoreList = {}
	assert(CurrentIgnoreList, "")
	table.insert(CurrentIgnoreList, GetCharacter())
	return CurrentIgnoreList
end

local function minV(a: Vector3, b: Vector3)
	return Vector3.new(math.min(a.X, b.X), math.min(a.Y, b.Y), math.min(a.Z, b.Z))
end
local function maxV(a, b)
	return Vector3.new(math.max(a.X, b.X), math.max(a.Y, b.Y), math.max(a.Z, b.Z))
end
local function getCollidableExtentsSize(character: Model?)
	if character == nil or character.PrimaryPart == nil then return end
	assert(character, "")
	assert(character.PrimaryPart, "")
	local toLocalCFrame = character.PrimaryPart.CFrame:Inverse()
	local min = Vector3.new(math.huge, math.huge, math.huge)
	local max = Vector3.new(-math.huge, -math.huge, -math.huge)
	for _,descendant in pairs(character:GetDescendants()) do
		if descendant:IsA('BasePart') and descendant.CanCollide then
			local localCFrame = toLocalCFrame * descendant.CFrame
			local size = Vector3.new(descendant.Size.X / 2, descendant.Size.Y / 2, descendant.Size.Z / 2)
			local vertices = {
				Vector3.new( size.X,  size.Y,  size.Z),
				Vector3.new( size.X,  size.Y, -size.Z),
				Vector3.new( size.X, -size.Y,  size.Z),
				Vector3.new( size.X, -size.Y, -size.Z),
				Vector3.new(-size.X,  size.Y,  size.Z),
				Vector3.new(-size.X,  size.Y, -size.Z),
				Vector3.new(-size.X, -size.Y,  size.Z),
				Vector3.new(-size.X, -size.Y, -size.Z)
			}
			for _,vertex in ipairs(vertices) do
				local v = localCFrame * vertex
				min = minV(min, v)
				max = maxV(max, v)
			end
		end
	end
	local r = max - min
	if r.X < 0 or r.Y < 0 or r.Z < 0 then return nil end
	return r
end

-----------------------------------PATHER--------------------------------------

local function Pather(endPoint, surfaceNormal, overrideUseDirectPath: boolean?)
	local this = {}

	local directPathForHumanoid
	local directPathForVehicle
	if overrideUseDirectPath ~= nil then
		directPathForHumanoid = overrideUseDirectPath
		directPathForVehicle = overrideUseDirectPath
	else
		directPathForHumanoid = UseDirectPath
		directPathForVehicle = UseDirectPathForVehicle
	end

	this.Cancelled = false
	this.Started = false

	this.Finished = Instance.new("BindableEvent")
	this.PathFailed = Instance.new("BindableEvent")

	this.PathComputing = false
	this.PathComputed = false

	this.OriginalTargetPoint = endPoint
	this.TargetPoint = endPoint
	this.TargetSurfaceNormal = surfaceNormal

	this.DiedConn = nil
	this.SeatedConn = nil
	this.BlockedConn = nil
	this.TeleportedConn = nil

	this.CurrentPoint = 0

	this.HumanoidOffsetFromPath = ZERO_VECTOR3

	this.CurrentWaypointPosition = nil
	this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
	this.CurrentWaypointPlaneDistance = 0
	this.CurrentWaypointNeedsJump = false;

	this.CurrentHumanoidPosition = ZERO_VECTOR3
	this.CurrentHumanoidVelocity = 0 :: Vector3 | number

	this.NextActionMoveDirection = ZERO_VECTOR3
	this.NextActionJump = false

	this.Timeout = 0

	this.Humanoid = findPlayerHumanoid(Player)
	this.OriginPoint = nil
	this.AgentCanFollowPath = false
	this.DirectPath = false
	this.DirectPathRiseFirst = false

	this.stopTraverseFunc = nil :: (() -> ())?
	this.setPointFunc = nil :: ((number) -> ())?
	this.pointList = nil :: {PathWaypoint}?

	local rootPart: BasePart = this.Humanoid and this.Humanoid.RootPart
	if rootPart then
		-- Setup origin
		this.OriginPoint = rootPart.CFrame.Position

		-- Setup agent
		local agentRadius = 2
		local agentHeight = 5
		local agentCanJump = true

		local seat = this.Humanoid.SeatPart
		if seat and seat:IsA("VehicleSeat") then
			-- Humanoid is seated on a vehicle
			local vehicle = seat:FindFirstAncestorOfClass("Model")
			if vehicle then
				-- Make sure the PrimaryPart is set to the vehicle seat while we compute the extends.
				local tempPrimaryPart = vehicle.PrimaryPart
				vehicle.PrimaryPart = seat

				-- For now, only direct path
				if directPathForVehicle then
					local extents: Vector3 = vehicle:GetExtentsSize()
					agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
					agentHeight = AgentSizeIncreaseFactor * extents.Y
					agentCanJump = false
					this.AgentCanFollowPath = true
					this.DirectPath = directPathForVehicle
				end

				-- Reset PrimaryPart
				vehicle.PrimaryPart = tempPrimaryPart
			end
		else
			local extents: Vector3?
			if FFlagUserExcludeNonCollidableForPathfinding then
				local character: Model? = GetCharacter()
				if character ~= nil then
					extents = getCollidableExtentsSize(character)
				end
			end
			if extents == nil then
				extents = GetCharacter():GetExtentsSize()
			end
			assert(extents, "")
			agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
			agentHeight = AgentSizeIncreaseFactor * extents.Y
			agentCanJump = (this.Humanoid.JumpPower > 0)
			this.AgentCanFollowPath = true
			this.DirectPath = directPathForHumanoid :: boolean
			this.DirectPathRiseFirst = this.Humanoid.Sit
		end

		-- Build path object
		if FFlagUserClickToMoveSupportAgentCanClimb then
			this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump, AgentCanClimb = true})
		else
			this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump})
		end
	end

	function this:Cleanup()
		if this.stopTraverseFunc then
			this.stopTraverseFunc()
			this.stopTraverseFunc = nil
		end

		if this.BlockedConn then
			this.BlockedConn:Disconnect()
			this.BlockedConn = nil
		end

		if this.DiedConn then
			this.DiedConn:Disconnect()
			this.DiedConn = nil
		end

		if this.SeatedConn then
			this.SeatedConn:Disconnect()
			this.SeatedConn = nil
		end

		if this.TeleportedConn then
			this.TeleportedConn:Disconnect()
			this.TeleportedConn = nil
		end

		this.Started = false
	end

	function this:Cancel()
		this.Cancelled = true
		this:Cleanup()
	end

	function this:IsActive()
		return this.AgentCanFollowPath and this.Started and not this.Cancelled
	end

	function this:OnPathInterrupted()
		-- Stop moving
		this.Cancelled = true
		this:OnPointReached(false)
	end

	function this:ComputePath()
		if this.OriginPoint then
			if this.PathComputed or this.PathComputing then return end
			this.PathComputing = true
			if this.AgentCanFollowPath then
				if this.DirectPath then
					this.pointList = {
						PathWaypoint.new(this.OriginPoint, Enum.PathWaypointAction.Walk),
						PathWaypoint.new(this.TargetPoint, this.DirectPathRiseFirst and Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk)
					}
					this.PathComputed = true
				else
					this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
					this.pointList = this.pathResult:GetWaypoints()
					this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
					this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success
				end
			end
			this.PathComputing = false
		end
	end

	function this:IsValidPath()
		this:ComputePath()
		return this.PathComputed and this.AgentCanFollowPath
	end

	this.Recomputing = false
	function this:OnPathBlocked(blockedWaypointIdx)
		local pathBlocked = blockedWaypointIdx >= this.CurrentPoint
		if not pathBlocked or this.Recomputing then
			return
		end

		this.Recomputing = true

		if this.stopTraverseFunc then
			this.stopTraverseFunc()
			this.stopTraverseFunc = nil
		end

		this.OriginPoint = this.Humanoid.RootPart.CFrame.p

		this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
		this.pointList = this.pathResult:GetWaypoints()
		if #this.pointList > 0 then
			this.HumanoidOffsetFromPath = this.pointList[1].Position - this.OriginPoint
		end
		this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success

		if ShowPath then
			this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList)
		end
		if this.PathComputed then
			this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
			this:OnPointReached(true) -- Move to first point
		else
			this.PathFailed:Fire()
			this:Cleanup()
		end

		this.Recomputing = false
	end

	function this:OnRenderStepped(dt: number)
		if this.Started and not this.Cancelled then
			-- Check for Timeout (if a waypoint is not reached within the delay, we fail)
			this.Timeout = this.Timeout + dt
			if this.Timeout > UnreachableWaypointTimeout then
				this:OnPointReached(false)
				return
			end

			-- Get Humanoid position and velocity
			this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
			this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity

			-- Check if it has reached some waypoints
			while this.Started and this:IsCurrentWaypointReached() do
				this:OnPointReached(true)
			end

			-- If still started, update actions
			if this.Started then
				-- Move action
				this.NextActionMoveDirection = this.CurrentWaypointPosition - this.CurrentHumanoidPosition
				if this.NextActionMoveDirection.Magnitude > ALMOST_ZERO then
					this.NextActionMoveDirection = this.NextActionMoveDirection.Unit
				else
					this.NextActionMoveDirection = ZERO_VECTOR3
				end
				-- Jump action
				if this.CurrentWaypointNeedsJump then
					this.NextActionJump = true
					this.CurrentWaypointNeedsJump = false	-- Request jump only once
				else
					this.NextActionJump = false
				end
			end
		end
	end

	function this:IsCurrentWaypointReached()
		local reached = false

		-- Check we do have a plane, if not, we consider the waypoint reached
		if this.CurrentWaypointPlaneNormal ~= ZERO_VECTOR3 then
			-- Compute distance of Humanoid from destination plane
			local dist = this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidPosition) - this.CurrentWaypointPlaneDistance
			-- Compute the component of the Humanoid velocity that is towards the plane
			local velocity = -this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidVelocity)
			-- Compute the threshold from the destination plane based on Humanoid velocity
			local threshold = math.max(1.0, 0.0625 * velocity)
			-- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it
			reached = dist < threshold
		else
			reached = true
		end

		if reached then
			this.CurrentWaypointPosition = nil
			this.CurrentWaypointPlaneNormal	= ZERO_VECTOR3
			this.CurrentWaypointPlaneDistance = 0
		end

		return reached
	end

	function this:OnPointReached(reached)

		if reached and not this.Cancelled then
			-- First, destroyed the current displayed waypoint
			if this.setPointFunc then
				this.setPointFunc(this.CurrentPoint)
			end

			local nextWaypointIdx = this.CurrentPoint + 1

			if nextWaypointIdx > #this.pointList then
				-- End of path reached
				if this.stopTraverseFunc then
					this.stopTraverseFunc()
				end
				this.Finished:Fire()
				this:Cleanup()
			else
				local currentWaypoint = this.pointList[this.CurrentPoint]
				local nextWaypoint = this.pointList[nextWaypointIdx]

				-- If airborne, only allow to keep moving
				-- if nextWaypoint.Action ~= Jump, or path mantains a direction
				-- Otherwise, wait until the humanoid gets to the ground
				local currentState = this.Humanoid:GetState()
				local isInAir = currentState == Enum.HumanoidStateType.FallingDown
					or currentState == Enum.HumanoidStateType.Freefall
					or currentState == Enum.HumanoidStateType.Jumping

				if isInAir then
					local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump
					if not shouldWaitForGround and this.CurrentPoint > 1 then
						local prevWaypoint = this.pointList[this.CurrentPoint - 1]

						local prevDir = currentWaypoint.Position - prevWaypoint.Position
						local currDir = nextWaypoint.Position - currentWaypoint.Position

						local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit
						local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit

						local THRESHOLD_COS = 0.996 -- ~cos(5 degrees)
						shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS
					end

					if shouldWaitForGround then
						this.Humanoid.FreeFalling:Wait()

						-- Give time to the humanoid's state to change
						-- Otherwise, the jump flag in Humanoid
						-- will be reset by the state change
						wait(0.1)
					end
				end

				-- Move to the next point
				this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx)
			end
		else
			this.PathFailed:Fire()
			this:Cleanup()
		end
	end

	function this:MoveToNextWayPoint(currentWaypoint: PathWaypoint, nextWaypoint: PathWaypoint, nextWaypointIdx: number)
		-- Build next destination plane
		-- (plane normal is perpendicular to the y plane and is from next waypoint towards current one (provided the two waypoints are not at the same location))
		-- (plane location is at next waypoint)
		this.CurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position

		-- plane normal isn't perpendicular to the y plane when climbing up
		if not FFlagUserClickToMoveSupportAgentCanClimb or (nextWaypoint.Label ~= "Climb") then
			this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z)
		end
		if this.CurrentWaypointPlaneNormal.Magnitude > ALMOST_ZERO then
			this.CurrentWaypointPlaneNormal	= this.CurrentWaypointPlaneNormal.Unit
			this.CurrentWaypointPlaneDistance = this.CurrentWaypointPlaneNormal:Dot(nextWaypoint.Position)
		else
			-- Next waypoint is the same as current waypoint so no plane
			this.CurrentWaypointPlaneNormal	= ZERO_VECTOR3
			this.CurrentWaypointPlaneDistance = 0
		end

		-- Should we jump
		this.CurrentWaypointNeedsJump = nextWaypoint.Action == Enum.PathWaypointAction.Jump;

		-- Remember next waypoint position
		this.CurrentWaypointPosition = nextWaypoint.Position

		-- Move to next point
		this.CurrentPoint = nextWaypointIdx

		-- Finally reset Timeout
		this.Timeout = 0
	end

	function this:Start(overrideShowPath)
		if not this.AgentCanFollowPath then
			this.PathFailed:Fire()
			return
		end

		if this.Started then return end
		this.Started = true

		ClickToMoveDisplay.CancelFailureAnimation()

		if ShowPath then
			if overrideShowPath == nil or overrideShowPath then
				this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList, this.OriginalTargetPoint)
			end
		end

		if #this.pointList > 0 then
			-- Determine the humanoid offset from the path's first point
			-- Offset of the first waypoint from the path's origin point
			this.HumanoidOffsetFromPath = Vector3.new(0, this.pointList[1].Position.Y - this.OriginPoint.Y, 0)

			-- As well as its current position and velocity
			this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
			this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity

			-- Connect to events
			this.SeatedConn = this.Humanoid.Seated:Connect(function(isSeated, seat) this:OnPathInterrupted() end)
			this.DiedConn = this.Humanoid.Died:Connect(function() this:OnPathInterrupted() end)
			this.TeleportedConn = this.Humanoid.RootPart:GetPropertyChangedSignal("CFrame"):Connect(function() this:OnPathInterrupted() end)

			-- Actually start
			this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
			this:OnPointReached(true) -- Move to first point
		else
			this.PathFailed:Fire()
			if this.stopTraverseFunc then
				this.stopTraverseFunc()
			end
		end
	end

	--We always raycast to the ground in the case that the user clicked a wall.
	local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
	local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50)
	local newHitPart, newHitPos = Workspace:FindPartOnRayWithIgnoreList(ray, getIgnoreList())
	if newHitPart then
		this.TargetPoint = newHitPos
	end
	this:ComputePath()

	return this
end

-------------------------------------------------------------------------

local function CheckAlive()
	local humanoid = findPlayerHumanoid(Player)
	return humanoid ~= nil and humanoid.Health > 0
end

local function GetEquippedTool(character: Model?)
	if character ~= nil then
		for _, child in pairs(character:GetChildren()) do
			if child:IsA('Tool') then
				return child
			end
		end
	end
end

local ExistingPather = nil
local ExistingIndicator = nil
local PathCompleteListener = nil
local PathFailedListener = nil

local function CleanupPath()
	if ExistingPather then
		ExistingPather:Cancel()
		ExistingPather = nil
	end
	if PathCompleteListener then
		PathCompleteListener:Disconnect()
		PathCompleteListener = nil
	end
	if PathFailedListener then
		PathFailedListener:Disconnect()
		PathFailedListener = nil
	end
	if ExistingIndicator then
		ExistingIndicator:Destroy()
	end
end

local function HandleMoveTo(thisPather, hitPt, hitChar, character, overrideShowPath)
	if ExistingPather then
		CleanupPath()
	end
	ExistingPather = thisPather
	thisPather:Start(overrideShowPath)

	PathCompleteListener = thisPather.Finished.Event:Connect(function()
		CleanupPath()
		if hitChar then
			local currentWeapon = GetEquippedTool(character)
			if currentWeapon then
				currentWeapon:Activate()
			end
		end
	end)
	PathFailedListener = thisPather.PathFailed.Event:Connect(function()
		CleanupPath()
		if overrideShowPath == nil or overrideShowPath then
			local shouldPlayFailureAnim = PlayFailureAnimation and not (ExistingPather and ExistingPather:IsActive())
			if shouldPlayFailureAnim then
				ClickToMoveDisplay.PlayFailureAnimation()
			end
			ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
		end
	end)
end

local function ShowPathFailedFeedback(hitPt)
	if ExistingPather and ExistingPather:IsActive() then
		ExistingPather:Cancel()
	end
	if PlayFailureAnimation then
		ClickToMoveDisplay.PlayFailureAnimation()
	end
	ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
end

function OnTap(tapPositions: {Vector3}, goToPoint: Vector3?, wasTouchTap: boolean?)
	-- Good to remember if this is the latest tap event
	local camera = Workspace.CurrentCamera
	local character = Player.Character

	if not CheckAlive() then return end

	-- This is a path tap position
	if #tapPositions == 1 or goToPoint then
		if camera then
			local unitRay = camera:ScreenPointToRay(tapPositions[1].X, tapPositions[1].Y)
			local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)

			local myHumanoid = findPlayerHumanoid(Player)
			local hitPart, hitPt, hitNormal = Utility.Raycast(ray, true, getIgnoreList())

			local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart)
			if wasTouchTap and hitHumanoid and StarterGui:GetCore("AvatarContextMenuEnabled") then
				local clickedPlayer = Players:GetPlayerFromCharacter(hitHumanoid.Parent)
				if clickedPlayer then
					CleanupPath()
					return
				end
			end
			if goToPoint then
				hitPt = goToPoint
				hitChar = nil
			end
			if hitPt and character then
				-- Clean up current path
				CleanupPath()
				local thisPather = Pather(hitPt, hitNormal)
				if thisPather:IsValidPath() then
					HandleMoveTo(thisPather, hitPt, hitChar, character)
				else
					-- Clean up
					thisPather:Cleanup()
					-- Feedback here for when we don't have a good path
					ShowPathFailedFeedback(hitPt)
				end
			end
		end
	elseif #tapPositions >= 2 then
		if camera then
			-- Do shoot
			local currentWeapon = GetEquippedTool(character)
			if currentWeapon then
				currentWeapon:Activate()
			end
		end
	end
end

local function DisconnectEvent(event)
	if event then
		event:Disconnect()
	end
end

--[[ The ClickToMove Controller Class ]]--
local KeyboardController = require(script.Parent:WaitForChild("Keyboard"))
local ClickToMove = setmetatable({}, KeyboardController)
ClickToMove.__index = ClickToMove

function ClickToMove.new(CONTROL_ACTION_PRIORITY)
	local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove)

	self.fingerTouches = {}
	self.numUnsunkTouches = 0
	-- PC simulation
	self.mouse1Down = tick()
	self.mouse1DownPos = Vector2.new()
	self.mouse2DownTime = tick()
	self.mouse2DownPos = Vector2.new()
	self.mouse2UpTime = tick()

	self.keyboardMoveVector = ZERO_VECTOR3

	self.tapConn = nil
	self.inputBeganConn = nil
	self.inputChangedConn = nil
	self.inputEndedConn = nil
	self.humanoidDiedConn = nil
	self.characterChildAddedConn = nil
	self.onCharacterAddedConn = nil
	self.characterChildRemovedConn = nil
	self.renderSteppedConn = nil
	self.menuOpenedConnection = nil

	self.running = false

	self.wasdEnabled = false

	return self
end

function ClickToMove:DisconnectEvents()
	DisconnectEvent(self.tapConn)
	DisconnectEvent(self.inputBeganConn)
	DisconnectEvent(self.inputChangedConn)
	DisconnectEvent(self.inputEndedConn)
	DisconnectEvent(self.humanoidDiedConn)
	DisconnectEvent(self.characterChildAddedConn)
	DisconnectEvent(self.onCharacterAddedConn)
	DisconnectEvent(self.renderSteppedConn)
	DisconnectEvent(self.characterChildRemovedConn)
	DisconnectEvent(self.menuOpenedConnection)
end

function ClickToMove:OnTouchBegan(input, processed)
	if self.fingerTouches[input] == nil and not processed then
		self.numUnsunkTouches = self.numUnsunkTouches + 1
	end
	self.fingerTouches[input] = processed
end

function ClickToMove:OnTouchChanged(input, processed)
	if self.fingerTouches[input] == nil then
		self.fingerTouches[input] = processed
		if not processed then
			self.numUnsunkTouches = self.numUnsunkTouches + 1
		end
	end
end

function ClickToMove:OnTouchEnded(input, processed)
	if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
		self.numUnsunkTouches = self.numUnsunkTouches - 1
	end
	self.fingerTouches[input] = nil
end

function ClickToMove:OnCharacterAdded(character)
	self:DisconnectEvents()

	self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
		if input.UserInputType == Enum.UserInputType.Touch then
			self:OnTouchBegan(input, processed)
		end

		-- Cancel path when you use the keyboard controls if wasd is enabled.
		if self.wasdEnabled and processed == false and input.UserInputType == Enum.UserInputType.Keyboard
			and movementKeys[input.KeyCode] then
			CleanupPath()
			ClickToMoveDisplay.CancelFailureAnimation()
		end
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			self.mouse1DownTime = tick()
			self.mouse1DownPos = input.Position
		end
		if input.UserInputType == Enum.UserInputType.MouseButton2 then
			self.mouse2DownTime = tick()
			self.mouse2DownPos = input.Position
		end
	end)

	self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
		if input.UserInputType == Enum.UserInputType.Touch then
			self:OnTouchChanged(input, processed)
		end
	end)

	self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
		if input.UserInputType == Enum.UserInputType.Touch then
			self:OnTouchEnded(input, processed)
		end

		if input.UserInputType == Enum.UserInputType.MouseButton2 then
			self.mouse2UpTime = tick()
			local currPos: Vector3 = input.Position
			-- We allow click to move during path following or if there is no keyboard movement
			local allowed = ExistingPather or self.keyboardMoveVector.Magnitude <= 0
			if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and allowed then
				local positions = {currPos}
				OnTap(positions)
			end
		end
	end)

	self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed)
		if not processed then
			OnTap(touchPositions, nil, true)
		end
	end)

	self.menuOpenedConnection = GuiService.MenuOpened:Connect(function()
		CleanupPath()
	end)

	local function OnCharacterChildAdded(child)
		if UserInputService.TouchEnabled then
			if child:IsA('Tool') then
				child.ManualActivationOnly = true
			end
		end
		if child:IsA('Humanoid') then
			DisconnectEvent(self.humanoidDiedConn)
			self.humanoidDiedConn = child.Died:Connect(function()
				if ExistingIndicator then
					DebrisService:AddItem(ExistingIndicator.Model, 1)
				end
			end)
		end
	end

	self.characterChildAddedConn = character.ChildAdded:Connect(function(child)
		OnCharacterChildAdded(child)
	end)
	self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child)
		if UserInputService.TouchEnabled then
			if child:IsA('Tool') then
				child.ManualActivationOnly = false
			end
		end
	end)
	for _, child in pairs(character:GetChildren()) do
		OnCharacterChildAdded(child)
	end
end

function ClickToMove:Start()
	self:Enable(true)
end

function ClickToMove:Stop()
	self:Enable(false)
end

function ClickToMove:CleanupPath()
	CleanupPath()
end

function ClickToMove:Enable(enable: boolean, enableWASD: boolean, touchJumpController)
	if enable then
		if not self.running then
			if Player.Character then -- retro-listen
				self:OnCharacterAdded(Player.Character)
			end
			self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char)
				self:OnCharacterAdded(char)
			end)
			self.running = true
		end
		self.touchJumpController = touchJumpController
		if self.touchJumpController then
			self.touchJumpController:Enable(self.jumpEnabled)
		end
	else
		if self.running then
			self:DisconnectEvents()
			CleanupPath()
			-- Restore tool activation on shutdown
			if UserInputService.TouchEnabled then
				local character = Player.Character
				if character then
					for _, child in pairs(character:GetChildren()) do
						if child:IsA('Tool') then
							child.ManualActivationOnly = false
						end
					end
				end
			end
			self.running = false
		end
		if self.touchJumpController and not self.jumpEnabled then
			self.touchJumpController:Enable(true)
		end
		self.touchJumpController = nil
	end

	-- Extension for initializing Keyboard input as this class now derives from Keyboard
	if UserInputService.KeyboardEnabled and enable ~= self.enabled then

		self.forwardValue  = 0
		self.backwardValue = 0
		self.leftValue = 0
		self.rightValue = 0

		self.moveVector = ZERO_VECTOR3

		if enable then
			self:BindContextActions()
			self:ConnectFocusEventListeners()
		else
			self:UnbindContextActions()
			self:DisconnectFocusEventListeners()
		end
	end

	self.wasdEnabled = enable and enableWASD or false
	self.enabled = enable
end

function ClickToMove:OnRenderStepped(dt)
	-- Reset jump
	self.isJumping = false

	-- Handle Pather
	if ExistingPather then
		-- Let the Pather update
		ExistingPather:OnRenderStepped(dt)

		-- If we still have a Pather, set the resulting actions
		if ExistingPather then
			-- Setup move (NOT relative to camera)
			self.moveVector = ExistingPather.NextActionMoveDirection
			self.moveVectorIsCameraRelative = false

			-- Setup jump (but do NOT prevent the base Keayboard class from requesting jumps as well)
			if ExistingPather.NextActionJump then
				self.isJumping = true
			end
		else
			self.moveVector = self.keyboardMoveVector
			self.moveVectorIsCameraRelative = true
		end
	else
		self.moveVector = self.keyboardMoveVector
		self.moveVectorIsCameraRelative = true
	end

	-- Handle Keyboard's jump
	if self.jumpRequested then
		self.isJumping = true
	end
end

-- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled and let OnRenderStepped handle the movement
function ClickToMove:UpdateMovement(inputState)
	if inputState == Enum.UserInputState.Cancel then
		self.keyboardMoveVector = ZERO_VECTOR3
	elseif self.wasdEnabled then
		self.keyboardMoveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
	end
end

-- Overrides Keyboard:UpdateJump() because jump is handled in OnRenderStepped
function ClickToMove:UpdateJump()
	-- Nothing to do (handled in OnRenderStepped)
end

--Public developer facing functions
function ClickToMove:SetShowPath(value)
	ShowPath = value
end

function ClickToMove:GetShowPath()
	return ShowPath
end

function ClickToMove:SetWaypointTexture(texture)
	ClickToMoveDisplay.SetWaypointTexture(texture)
end

function ClickToMove:GetWaypointTexture()
	return ClickToMoveDisplay.GetWaypointTexture()
end

function ClickToMove:SetWaypointRadius(radius)
	ClickToMoveDisplay.SetWaypointRadius(radius)
end

function ClickToMove:GetWaypointRadius()
	return ClickToMoveDisplay.GetWaypointRadius()
end

function ClickToMove:SetEndWaypointTexture(texture)
	ClickToMoveDisplay.SetEndWaypointTexture(texture)
end

function ClickToMove:GetEndWaypointTexture()
	return ClickToMoveDisplay.GetEndWaypointTexture()
end

function ClickToMove:SetWaypointsAlwaysOnTop(alwaysOnTop)
	ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
end

function ClickToMove:GetWaypointsAlwaysOnTop()
	return ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
end

function ClickToMove:SetFailureAnimationEnabled(enabled)
	PlayFailureAnimation = enabled
end

function ClickToMove:GetFailureAnimationEnabled()
	return PlayFailureAnimation
end

function ClickToMove:SetIgnoredPartsTag(tag)
	UpdateIgnoreTag(tag)
end

function ClickToMove:GetIgnoredPartsTag()
	return CurrentIgnoreTag
end

function ClickToMove:SetUseDirectPath(directPath)
	UseDirectPath = directPath
end

function ClickToMove:GetUseDirectPath()
	return UseDirectPath
end

function ClickToMove:SetAgentSizeIncreaseFactor(increaseFactorPercent: number)
	AgentSizeIncreaseFactor = 1.0 + (increaseFactorPercent / 100.0)
end

function ClickToMove:GetAgentSizeIncreaseFactor()
	return (AgentSizeIncreaseFactor - 1.0) * 100.0
end

function ClickToMove:SetUnreachableWaypointTimeout(timeoutInSec)
	UnreachableWaypointTimeout = timeoutInSec
end

function ClickToMove:GetUnreachableWaypointTimeout()
	return UnreachableWaypointTimeout
end

function ClickToMove:SetUserJumpEnabled(jumpEnabled)
	self.jumpEnabled = jumpEnabled
	if self.touchJumpController then
		self.touchJumpController:Enable(jumpEnabled)
	end
end

function ClickToMove:GetUserJumpEnabled()
	return self.jumpEnabled
end

function ClickToMove:MoveTo(position, showPath, useDirectPath)
	local character = Player.Character
	if character == nil then
		return false
	end
	local thisPather = Pather(position, Vector3.new(0, 1, 0), useDirectPath)
	if thisPather and thisPather:IsValidPath() then
		HandleMoveTo(thisPather, position, nil, character, showPath)
		return true
	end
	return false
end

return ClickToMove


--!nonstrict
local ClickToMoveDisplay = {}

local FAILURE_ANIMATION_ID = "rbxassetid://2874840706"

local TrailDotIcon = "rbxasset://textures/ui/traildot.png"
local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png"

local WaypointsAlwaysOnTop = false

local WAYPOINT_INCLUDE_FACTOR = 2
local LAST_DOT_DISTANCE = 3

local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25)

local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5)
local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1)

local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5)
local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5)
local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5)

local FAILURE_TWEEN_LENGTH = 0.125
local FAILURE_TWEEN_COUNT = 4

local TWEEN_WAYPOINT_THRESHOLD = 5

local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay"

local TrailDotSize = Vector2.new(1.5, 1.5)

local TRAIL_DOT_MIN_SCALE = 1
local TRAIL_DOT_MIN_DISTANCE = 10
local TRAIL_DOT_MAX_SCALE = 2.5
local TRAIL_DOT_MAX_DISTANCE = 100

local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local LocalPlayer = PlayersService.LocalPlayer

local function CreateWaypointTemplates()
	local TrailDotTemplate = Instance.new("Part")
	TrailDotTemplate.Size = Vector3.new(1, 1, 1)
	TrailDotTemplate.Anchored = true
	TrailDotTemplate.CanCollide = false
	TrailDotTemplate.Name = "TrailDot"
	TrailDotTemplate.Transparency = 1
	local TrailDotImage = Instance.new("ImageHandleAdornment")
	TrailDotImage.Name = "TrailDotImage"
	TrailDotImage.Size = TrailDotSize
	TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
	TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop
	TrailDotImage.Image = TrailDotIcon
	TrailDotImage.Adornee = TrailDotTemplate
	TrailDotImage.Parent = TrailDotTemplate

	local EndWaypointTemplate = Instance.new("Part")
	EndWaypointTemplate.Size = Vector3.new(2, 2, 2)
	EndWaypointTemplate.Anchored = true
	EndWaypointTemplate.CanCollide = false
	EndWaypointTemplate.Name = "EndWaypoint"
	EndWaypointTemplate.Transparency = 1
	local EndWaypointImage = Instance.new("ImageHandleAdornment")
	EndWaypointImage.Name = "TrailDotImage"
	EndWaypointImage.Size = TrailDotSize
	EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
	EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
	EndWaypointImage.Image = TrailDotIcon
	EndWaypointImage.Adornee = EndWaypointTemplate
	EndWaypointImage.Parent = EndWaypointTemplate
	local EndWaypointBillboard = Instance.new("BillboardGui")
	EndWaypointBillboard.Name = "EndWaypointBillboard"
	EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
	EndWaypointBillboard.LightInfluence = 0
	EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN
	EndWaypointBillboard.AlwaysOnTop = true
	EndWaypointBillboard.Adornee = EndWaypointTemplate
	EndWaypointBillboard.Parent = EndWaypointTemplate
	local EndWaypointImageLabel = Instance.new("ImageLabel")
	EndWaypointImageLabel.Image = EndWaypointIcon
	EndWaypointImageLabel.BackgroundTransparency = 1
	EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0)
	EndWaypointImageLabel.Parent = EndWaypointBillboard

	local FailureWaypointTemplate = Instance.new("Part")
	FailureWaypointTemplate.Size = Vector3.new(2, 2, 2)
	FailureWaypointTemplate.Anchored = true
	FailureWaypointTemplate.CanCollide = false
	FailureWaypointTemplate.Name = "FailureWaypoint"
	FailureWaypointTemplate.Transparency = 1
	local FailureWaypointImage = Instance.new("ImageHandleAdornment")
	FailureWaypointImage.Name = "TrailDotImage"
	FailureWaypointImage.Size = TrailDotSize
	FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
	FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
	FailureWaypointImage.Image = TrailDotIcon
	FailureWaypointImage.Adornee = FailureWaypointTemplate
	FailureWaypointImage.Parent = FailureWaypointTemplate
	local FailureWaypointBillboard = Instance.new("BillboardGui")
	FailureWaypointBillboard.Name = "FailureWaypointBillboard"
	FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
	FailureWaypointBillboard.LightInfluence = 0
	FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER
	FailureWaypointBillboard.AlwaysOnTop = true
	FailureWaypointBillboard.Adornee = FailureWaypointTemplate
	FailureWaypointBillboard.Parent = FailureWaypointTemplate
	local FailureWaypointFrame = Instance.new("Frame")
	FailureWaypointFrame.BackgroundTransparency = 1
	FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0)
	FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0)
	FailureWaypointFrame.Parent = FailureWaypointBillboard
	local FailureWaypointImageLabel = Instance.new("ImageLabel")
	FailureWaypointImageLabel.Image = EndWaypointIcon
	FailureWaypointImageLabel.BackgroundTransparency = 1
	FailureWaypointImageLabel.Position = UDim2.new(
		0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset
	)
	FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE
	FailureWaypointImageLabel.Parent = FailureWaypointFrame

	return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate
end

local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()

local function getTrailDotParent()
	local camera = Workspace.CurrentCamera
	local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME)
	if not trailParent then
		trailParent = Instance.new("Model")
		trailParent.Name = TRAIL_DOT_PARENT_NAME
		trailParent.Parent = camera
	end
	return trailParent
end

local function placePathWaypoint(waypointModel, position: Vector3)
	local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
	local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
		ray,
		{ Workspace.CurrentCamera, LocalPlayer.Character }
	)
	if hitPart then
		waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
		waypointModel.Parent = getTrailDotParent()
	end
end

local TrailDot = {}
TrailDot.__index = TrailDot

function TrailDot:Destroy()
	self.DisplayModel:Destroy()
end

function TrailDot:NewDisplayModel(position)
	local newDisplayModel: Part = TrailDotTemplate:Clone()
	placePathWaypoint(newDisplayModel, position)
	return newDisplayModel
end

function TrailDot.new(position, closestWaypoint)
	local self = setmetatable({}, TrailDot)

	self.DisplayModel = self:NewDisplayModel(position)
	self.ClosestWayPoint = closestWaypoint

	return self
end

local EndWaypoint = {}
EndWaypoint.__index = EndWaypoint

function EndWaypoint:Destroy()
	self.Destroyed = true
	self.Tween:Cancel()
	self.DisplayModel:Destroy()
end

function EndWaypoint:NewDisplayModel(position)
	local newDisplayModel: Part = EndWaypointTemplate:Clone()
	placePathWaypoint(newDisplayModel, position)
	return newDisplayModel
end

function EndWaypoint:CreateTween()
	local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true)
	local tween = TweenService:Create(
		self.DisplayModel.EndWaypointBillboard,
		tweenInfo,
		{ SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX }
	)
	tween:Play()
	return tween
end

function EndWaypoint:TweenInFrom(originalPosition: Vector3)
	local currentPositon: Vector3 = self.DisplayModel.Position
	local studsOffset = originalPosition - currentPositon
	self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0)
	local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
	local tween = TweenService:Create(
		self.DisplayModel.EndWaypointBillboard,
		tweenInfo,
		{ StudsOffset = Vector3.new(0, 0, 0) }
	)
	tween:Play()
	return tween
end

function EndWaypoint.new(position: Vector3, closestWaypoint: number?, originalPosition: Vector3?)
	local self = setmetatable({}, EndWaypoint)

	self.DisplayModel = self:NewDisplayModel(position)
	self.Destroyed = false
	if originalPosition and (originalPosition - position).Magnitude > TWEEN_WAYPOINT_THRESHOLD then
		self.Tween = self:TweenInFrom(originalPosition)
		coroutine.wrap(function()
			self.Tween.Completed:Wait()
			if not self.Destroyed then
				self.Tween = self:CreateTween()
			end
		end)()
	else
		self.Tween = self:CreateTween()
	end
	self.ClosestWayPoint = closestWaypoint

	return self
end

local FailureWaypoint = {}
FailureWaypoint.__index = FailureWaypoint

function FailureWaypoint:Hide()
	self.DisplayModel.Parent = nil
end

function FailureWaypoint:Destroy()
	self.DisplayModel:Destroy()
end

function FailureWaypoint:NewDisplayModel(position)
	local newDisplayModel: Part = FailureWaypointTemplate:Clone()
	placePathWaypoint(newDisplayModel, position)
	local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
	local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
		ray, { Workspace.CurrentCamera, LocalPlayer.Character }
	)
	if hitPart then
		newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
		newDisplayModel.Parent = getTrailDotParent()
	end
	return newDisplayModel
end

function FailureWaypoint:RunFailureTween()
	wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening
	-- Tween out from center
	local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
	local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
		{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT })
	tweenLeft:Play()

	local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
		{ Rotation = 10 })
	tweenLeftRoation:Play()

	tweenLeft.Completed:wait()

	-- Tween back and forth
	tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
		FAILURE_TWEEN_COUNT - 1, true)
	local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
		{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT})
	tweenSideToSide:Play()

	-- Tween flash dark and roate left and right
	tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
		FAILURE_TWEEN_COUNT - 1, true)
	local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo,
		{ ImageColor3 = Color3.new(0.75, 0.75, 0.75)})
	tweenFlash:Play()

	local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
		{ Rotation = -10 })
	tweenRotate:Play()

	tweenSideToSide.Completed:wait()

	-- Tween back to center
	tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
	local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
		{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER })
	tweenCenter:Play()

	local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
		{ Rotation = 0 })
	tweenRoation:Play()

	tweenCenter.Completed:wait()

	wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing
end

function FailureWaypoint.new(position)
	local self = setmetatable({}, FailureWaypoint)

	self.DisplayModel = self:NewDisplayModel(position)

	return self
end

local failureAnimation = Instance.new("Animation")
failureAnimation.AnimationId = FAILURE_ANIMATION_ID

local lastHumanoid = nil
local lastFailureAnimationTrack: AnimationTrack? = nil

local function getFailureAnimationTrack(myHumanoid)
	if myHumanoid == lastHumanoid then
		return lastFailureAnimationTrack
	end
	lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation)
	assert(lastFailureAnimationTrack, "")
	lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action
	lastFailureAnimationTrack.Looped = false
	return lastFailureAnimationTrack
end

local function findPlayerHumanoid()
	local character = LocalPlayer.Character
	if character then
		return character:FindFirstChildOfClass("Humanoid")
	end
end

local function createTrailDots(wayPoints: {PathWaypoint}, originalEndWaypoint: Vector3)
	local newTrailDots = {}
	local count = 1
	for i = 1, #wayPoints - 1 do
		local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).Magnitude < LAST_DOT_DISTANCE
		local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd
		if includeWaypoint then
			local trailDot = TrailDot.new(wayPoints[i].Position, i)
			newTrailDots[count] = trailDot
			count = count + 1
		end
	end

	local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint)
	table.insert(newTrailDots, newEndWaypoint)

	local reversedTrailDots = {}
	count = 1
	for i = #newTrailDots, 1, -1 do
		reversedTrailDots[count] = newTrailDots[i]
		count = count + 1
	end
	return reversedTrailDots
end

local function getTrailDotScale(distanceToCamera: number, defaultSize: Vector2)
	local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE
	local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength
	local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint
	return defaultSize * scale
end

local createPathCount = 0
-- originalEndWaypoint is optional, causes the waypoint to tween from that position.
function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint)
	createPathCount = createPathCount + 1
	local trailDots = createTrailDots(wayPoints, originalEndWaypoint)

	local function removePathBeforePoint(wayPointNumber)
		-- kill all trailDots before and at wayPointNumber
		for i = #trailDots, 1, -1 do
			local trailDot = trailDots[i]
			if trailDot.ClosestWayPoint <= wayPointNumber then
				trailDot:Destroy()
				trailDots[i] = nil
			else
				break
			end
		end
	end

	local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount
	local function resizeTrailDots()
		if #trailDots == 0 then
			RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName)
			return
		end
		local cameraPos = Workspace.CurrentCamera.CFrame.p
		for i = 1, #trailDots do
			local trailDotImage: ImageHandleAdornment = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage")
			if trailDotImage then
				local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).Magnitude
				trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize)
			end
		end
	end
	RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots)

	local function removePath()
		removePathBeforePoint(#wayPoints)
	end

	return removePath, removePathBeforePoint
end

local lastFailureWaypoint = nil
function ClickToMoveDisplay.DisplayFailureWaypoint(position)
	if lastFailureWaypoint then
		lastFailureWaypoint:Hide()
	end
	local failureWaypoint = FailureWaypoint.new(position)
	lastFailureWaypoint = failureWaypoint
	coroutine.wrap(function()
		failureWaypoint:RunFailureTween()
		failureWaypoint:Destroy()
		failureWaypoint = nil
	end)()
end

function ClickToMoveDisplay.CreateEndWaypoint(position)
	return EndWaypoint.new(position)
end

function ClickToMoveDisplay.PlayFailureAnimation()
	local myHumanoid = findPlayerHumanoid()
	if myHumanoid then
		local animationTrack = getFailureAnimationTrack(myHumanoid)
		animationTrack:Play()
	end
end

function ClickToMoveDisplay.CancelFailureAnimation()
	if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then
		lastFailureAnimationTrack:Stop()
	end
end

function ClickToMoveDisplay.SetWaypointTexture(texture)
	TrailDotIcon = texture
	TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end

function ClickToMoveDisplay.GetWaypointTexture()
	return TrailDotIcon
end

function ClickToMoveDisplay.SetWaypointRadius(radius)
	TrailDotSize = Vector2.new(radius, radius)
	TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end

function ClickToMoveDisplay.GetWaypointRadius()
	return TrailDotSize.X
end

function ClickToMoveDisplay.SetEndWaypointTexture(texture)
	EndWaypointIcon = texture
	TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end

function ClickToMoveDisplay.GetEndWaypointTexture()
	return EndWaypointIcon
end

function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
	WaypointsAlwaysOnTop = alwaysOnTop
	TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end

function ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
	return WaypointsAlwaysOnTop
end

return ClickToMoveDisplay


--!nonstrict

local PathDisplay = {}
PathDisplay.spacing = 8
PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
PathDisplay.imageSize = Vector2.new(2, 2)

local currentPoints = {}
local renderedPoints = {}

local pointModel = Instance.new("Model")
pointModel.Name = "PathDisplayPoints"

local adorneePart = Instance.new("Part")
adorneePart.Anchored = true
adorneePart.CanCollide = false
adorneePart.Transparency = 1
adorneePart.Name = "PathDisplayAdornee"
adorneePart.CFrame = CFrame.new(0, 0, 0)
adorneePart.Parent = pointModel

local pointPool = {}
local poolTop = 30
for i = 1, poolTop do
	local point = Instance.new("ImageHandleAdornment")
	point.Archivable = false
	point.Adornee = adorneePart
	point.Image = PathDisplay.image
	point.Size = PathDisplay.imageSize
	pointPool[i] = point
end

local function retrieveFromPool(): ImageHandleAdornment?
	local point = pointPool[1]
	if not point then
		return nil
	end

	pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil
	poolTop = poolTop - 1
	return point
end

local function returnToPool(point: ImageHandleAdornment)
	poolTop = poolTop + 1
	pointPool[poolTop] = point
end

local function renderPoint(point: Vector3, isLast): ImageHandleAdornment?
	if poolTop == 0 then
		return nil
	end

	local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0))
	local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { (game.Players.LocalPlayer :: Player).Character :: Model, workspace.CurrentCamera :: Camera })
	if not hitPart then
		return nil
	end

	local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal)

	local point = retrieveFromPool()
	point.CFrame = pointCFrame
	point.Parent = pointModel
	return point
end

function PathDisplay.setCurrentPoints(points)
	if typeof(points) == 'table' then
		currentPoints = points
	else
		currentPoints = {}
	end
end

function PathDisplay.clearRenderedPath()
	for _, oldPoint in ipairs(renderedPoints) do
		oldPoint.Parent = nil
		returnToPool(oldPoint)
	end
	renderedPoints = {}
	pointModel.Parent = nil
end

function PathDisplay.renderPath()
	PathDisplay.clearRenderedPath()
	if not currentPoints or #currentPoints == 0 then
		return
	end

	local currentIdx = #currentPoints
	local lastPos = currentPoints[currentIdx]
	local distanceBudget = 0

	renderedPoints[1] = renderPoint(lastPos, true)
	if not renderedPoints[1] then
		return
	end

	while true do
		local currentPoint = currentPoints[currentIdx]
		local nextPoint = currentPoints[currentIdx - 1]

		if currentIdx < 2 then
			break
		else

			local toNextPoint = nextPoint - currentPoint
			local distToNextPoint = toNextPoint.magnitude

			if distanceBudget > distToNextPoint then
				distanceBudget = distanceBudget - distToNextPoint
				currentIdx = currentIdx - 1
			else
				local dirToNextPoint = toNextPoint.unit
				local pointPos = currentPoint + (dirToNextPoint * distanceBudget)
				local point = renderPoint(pointPos, false)

				if point then
					renderedPoints[#renderedPoints + 1] = point
				end

				distanceBudget = distanceBudget + PathDisplay.spacing
			end
		end
	end

	pointModel.Parent = workspace.CurrentCamera
end

return PathDisplay


--!nonstrict
--[[
	// FileName: TouchJump
	// Version 1.0
	// Written by: jmargh
	// Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad
--]]

local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")

--[[ Constants ]]--
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"

--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchJump = setmetatable({}, BaseCharacterController)
TouchJump.__index = TouchJump

function TouchJump.new()
	local self = setmetatable(BaseCharacterController.new() :: any, TouchJump)

	self.parentUIFrame = nil
	self.jumpButton = nil
	self.characterAddedConn = nil
	self.humanoidStateEnabledChangedConn = nil
	self.humanoidJumpPowerConn = nil
	self.humanoidParentConn = nil
	self.externallyEnabled = false
	self.jumpPower = 0
	self.jumpStateEnabled = true
	self.isJumping = false
	self.humanoid = nil -- saved reference because property change connections are made using it

	return self
end

function TouchJump:EnableButton(enable)
	if enable then
		if not self.jumpButton then
			self:Create()
		end
		local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
		if humanoid and self.externallyEnabled then
			if self.externallyEnabled then
				if humanoid.JumpPower > 0 then
					self.jumpButton.Visible = true
				end
			end
		end
	else
		self.jumpButton.Visible = false
		self.isJumping = false
		self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
	end
end

function TouchJump:UpdateEnabled()
	if self.jumpPower > 0 and self.jumpStateEnabled then
		self:EnableButton(true)
	else
		self:EnableButton(false)
	end
end

function TouchJump:HumanoidChanged(prop)
	local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		if prop == "JumpPower" then
			self.jumpPower =  humanoid.JumpPower
			self:UpdateEnabled()
		elseif prop == "Parent" then
			if not humanoid.Parent then
				self.humanoidChangeConn:Disconnect()
			end
		end
	end
end

function TouchJump:HumanoidStateEnabledChanged(state, isEnabled)
	if state == Enum.HumanoidStateType.Jumping then
		self.jumpStateEnabled = isEnabled
		self:UpdateEnabled()
	end
end

function TouchJump:CharacterAdded(char)
	if self.humanoidChangeConn then
		self.humanoidChangeConn:Disconnect()
		self.humanoidChangeConn = nil
	end

	self.humanoid = char:FindFirstChildOfClass("Humanoid")
	while not self.humanoid do
		char.ChildAdded:wait()
		self.humanoid = char:FindFirstChildOfClass("Humanoid")
	end

	self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function()
		self.jumpPower =  self.humanoid.JumpPower
		self:UpdateEnabled()
	end)

	self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function()
		if not self.humanoid.Parent then
			self.humanoidJumpPowerConn:Disconnect()
			self.humanoidJumpPowerConn = nil
			self.humanoidParentConn:Disconnect()
			self.humanoidParentConn = nil
		end
	end)

	self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled)
		self:HumanoidStateEnabledChanged(state, enabled)
	end)

	self.jumpPower = self.humanoid.JumpPower
	self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping)
	self:UpdateEnabled()
end

function TouchJump:SetupCharacterAddedFunction()
	self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char)
		self:CharacterAdded(char)
	end)
	if Players.LocalPlayer.Character then
		self:CharacterAdded(Players.LocalPlayer.Character)
	end
end

function TouchJump:Enable(enable, parentFrame)
	if parentFrame then
		self.parentUIFrame = parentFrame
	end
	self.externallyEnabled = enable
	self:EnableButton(enable)
end

function TouchJump:Create()
	if not self.parentUIFrame then
		return
	end

	if self.jumpButton then
		self.jumpButton:Destroy()
		self.jumpButton = nil
	end

	local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y)
	local isSmallScreen = minAxis <= 500
	local jumpButtonSize = isSmallScreen and 70 or 120

	self.jumpButton = Instance.new("ImageButton")
	self.jumpButton.Name = "JumpButton"
	self.jumpButton.Visible = false
	self.jumpButton.BackgroundTransparency = 1
	self.jumpButton.Image = TOUCH_CONTROL_SHEET
	self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
	self.jumpButton.ImageRectSize = Vector2.new(144, 144)
	self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize)

    self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or
        UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75)

	local touchObject: InputObject? = nil
	self.jumpButton.InputBegan:connect(function(inputObject)
		--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
		--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
		if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
			or inputObject.UserInputState ~= Enum.UserInputState.Begin then
			return
		end

		touchObject = inputObject
		self.jumpButton.ImageRectOffset = Vector2.new(146, 146)
		self.isJumping = true
	end)

	local OnInputEnded = function()
		touchObject = nil
		self.isJumping = false
		self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
	end

	self.jumpButton.InputEnded:connect(function(inputObject: InputObject)
		if inputObject == touchObject then
			OnInputEnded()
		end
	end)

	GuiService.MenuOpened:connect(function()
		if touchObject then
			OnInputEnded()
		end
	end)

	if not self.characterAddedConn then
		self:SetupCharacterAddedFunction()
	end

	self.jumpButton.Parent = self.parentUIFrame
end

return TouchJump