1188 lines
35 KiB
Lua
1188 lines
35 KiB
Lua
-- Written By Kip Turner, Copyright Roblox 2014
|
|
-- Updated by Garnold to utilize the new PathfindingService API, 2017
|
|
|
|
local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end)
|
|
local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult
|
|
|
|
local FFlagUserNavigationClickToMoveNoDirectPathSuccess, FFlagUserNavigationClickToMoveNoDirectPathResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveNoDirectPath") end)
|
|
local FFlagUserNavigationClickToMoveNoDirectPath = FFlagUserNavigationClickToMoveNoDirectPathSuccess and FFlagUserNavigationClickToMoveNoDirectPathResult
|
|
|
|
local DEBUG_NAME = "ClickToMoveController"
|
|
|
|
local UIS = game:GetService("UserInputService")
|
|
local PathfindingService = game:GetService("PathfindingService")
|
|
local PlayerService = game:GetService("Players")
|
|
local RunService = game:GetService("RunService")
|
|
local DebrisService = game:GetService('Debris')
|
|
local ReplicatedStorage = game:GetService('ReplicatedStorage')
|
|
local TweenService = game:GetService("TweenService")
|
|
|
|
local Player = PlayerService.LocalPlayer
|
|
local PlayerScripts = Player.PlayerScripts
|
|
|
|
local CameraScript = script:FindFirstAncestor("CameraScript")
|
|
local InvisicamModule = nil
|
|
if CameraScript then
|
|
InvisicamModule = require(CameraScript:WaitForChild("Invisicam"))
|
|
end
|
|
|
|
local MasterControlModule = script.Parent
|
|
local MasterControl = require(MasterControlModule)
|
|
local TouchJump = nil
|
|
if MasterControl then
|
|
local TouchJumpModule = MasterControlModule:FindFirstChild("TouchJump")
|
|
if TouchJumpModule then
|
|
TouchJump = require(TouchJumpModule)
|
|
end
|
|
end
|
|
|
|
local SHOW_PATH = true
|
|
|
|
local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList
|
|
|
|
local math_min = math.min
|
|
local math_max = math.max
|
|
local math_pi = math.pi
|
|
local math_atan2 = math.atan2
|
|
|
|
local Vector3_new = Vector3.new
|
|
local Vector2_new = Vector2.new
|
|
local CFrame_new = CFrame.new
|
|
|
|
local CurrentSeatPart = nil
|
|
local DrivingTo = nil
|
|
|
|
local XZ_VECTOR3 = Vector3_new(1, 0, 1)
|
|
local ZERO_VECTOR3 = Vector3_new(0, 0, 0)
|
|
local ZERO_VECTOR2 = Vector2_new(0, 0)
|
|
|
|
local BindableEvent_OnFailStateChanged = nil
|
|
if UIS.TouchEnabled then
|
|
BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged()
|
|
end
|
|
|
|
--------------------------UTIL LIBRARY-------------------------------
|
|
local Utility = {}
|
|
do
|
|
local function ViewSizeX()
|
|
local camera = workspace.CurrentCamera
|
|
local x = camera and camera.ViewportSize.X or 0
|
|
local y = camera and camera.ViewportSize.Y or 0
|
|
if x == 0 then
|
|
return 1024
|
|
else
|
|
if x > y then
|
|
return x
|
|
else
|
|
return y
|
|
end
|
|
end
|
|
end
|
|
Utility.ViewSizeX = ViewSizeX
|
|
|
|
local function ViewSizeY()
|
|
local camera = workspace.CurrentCamera
|
|
local x = camera and camera.ViewportSize.X or 0
|
|
local y = camera and camera.ViewportSize.Y or 0
|
|
if y == 0 then
|
|
return 768
|
|
else
|
|
if x > y then
|
|
return y
|
|
else
|
|
return x
|
|
end
|
|
end
|
|
end
|
|
Utility.ViewSizeY = ViewSizeY
|
|
|
|
local function FindCharacterAncestor(part)
|
|
if part then
|
|
local humanoid = part:FindFirstChild("Humanoid")
|
|
if humanoid then
|
|
return part, humanoid
|
|
else
|
|
return FindCharacterAncestor(part.Parent)
|
|
end
|
|
end
|
|
end
|
|
Utility.FindCharacterAncestor = FindCharacterAncestor
|
|
|
|
local function Raycast(ray, ignoreNonCollidable, ignoreList)
|
|
local ignoreList = ignoreList or {}
|
|
local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList)
|
|
if hitPart then
|
|
if ignoreNonCollidable and hitPart.CanCollide == false then
|
|
table.insert(ignoreList, hitPart)
|
|
return Raycast(ray, ignoreNonCollidable, ignoreList)
|
|
end
|
|
return hitPart, hitPos, hitNorm, hitMat
|
|
end
|
|
return nil, nil
|
|
end
|
|
Utility.Raycast = Raycast
|
|
|
|
local function AveragePoints(positions)
|
|
local avgPos = ZERO_VECTOR2
|
|
if #positions > 0 then
|
|
for i = 1, #positions do
|
|
avgPos = avgPos + positions[i]
|
|
end
|
|
avgPos = avgPos / #positions
|
|
end
|
|
return avgPos
|
|
end
|
|
Utility.AveragePoints = AveragePoints
|
|
end
|
|
|
|
local humanoidCache = {}
|
|
local function findPlayerHumanoid(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
|
|
|
|
local function GetCharacter()
|
|
return Player and Player.Character
|
|
end
|
|
|
|
local function GetTorso()
|
|
local humanoid = findPlayerHumanoid(Player)
|
|
return humanoid and humanoid.RootPart
|
|
end
|
|
|
|
local function getIgnoreList()
|
|
if CurrentIgnoreList then
|
|
return CurrentIgnoreList
|
|
end
|
|
CurrentIgnoreList = {}
|
|
table.insert(CurrentIgnoreList, GetCharacter())
|
|
return CurrentIgnoreList
|
|
end
|
|
|
|
-----------------------------------------------------------------------------
|
|
|
|
-----------------------------------PATHER--------------------------------------
|
|
|
|
local popupAdornee
|
|
local function getPopupAdorneePart()
|
|
--Handle the case of the adornee part getting deleted (camera changed, maybe)
|
|
if popupAdornee and not popupAdornee.Parent then
|
|
popupAdornee = nil
|
|
end
|
|
|
|
--If the adornee doesn't exist yet, create it
|
|
if not popupAdornee then
|
|
popupAdornee = Instance.new("Part")
|
|
popupAdornee.Name = "ClickToMovePopupAdornee"
|
|
popupAdornee.Transparency = 1
|
|
popupAdornee.CanCollide = false
|
|
popupAdornee.Anchored = true
|
|
popupAdornee.Size = Vector3.new(2, 2, 2)
|
|
popupAdornee.CFrame = CFrame.new()
|
|
|
|
popupAdornee.Parent = workspace.CurrentCamera
|
|
end
|
|
|
|
return popupAdornee
|
|
end
|
|
|
|
local activePopups = {}
|
|
local function createNewPopup(popupType)
|
|
local newModel = Instance.new("ImageHandleAdornment")
|
|
|
|
newModel.AlwaysOnTop = false
|
|
newModel.Transparency = 1
|
|
newModel.Size = ZERO_VECTOR2
|
|
newModel.SizeRelativeOffset = ZERO_VECTOR3
|
|
newModel.Image = "rbxasset://textures/ui/move.png"
|
|
newModel.ZIndex = 20
|
|
|
|
local radius = 0
|
|
if popupType == "DestinationPopup" then
|
|
newModel.Color3 = Color3.fromRGB(0, 175, 255)
|
|
radius = 1.25
|
|
elseif popupType == "DirectWalkPopup" then
|
|
newModel.Color3 = Color3.fromRGB(0, 175, 255)
|
|
radius = 1.25
|
|
elseif popupType == "FailurePopup" then
|
|
newModel.Color3 = Color3.fromRGB(255, 100, 100)
|
|
radius = 1.25
|
|
elseif popupType == "PatherPopup" then
|
|
newModel.Color3 = Color3.fromRGB(255, 255, 255)
|
|
radius = 1
|
|
newModel.ZIndex = 10
|
|
end
|
|
newModel.Size = Vector2.new(5, 0.1) * radius
|
|
|
|
local dataStructure = {}
|
|
dataStructure.Model = newModel
|
|
|
|
activePopups[#activePopups + 1] = newModel
|
|
|
|
function dataStructure:TweenIn()
|
|
local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out)
|
|
local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius })
|
|
tween1:Play()
|
|
TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play()
|
|
return tween1
|
|
end
|
|
|
|
function dataStructure:TweenOut()
|
|
local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
|
|
local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 })
|
|
tween1:Play()
|
|
|
|
coroutine.wrap(function()
|
|
tween1.Completed:Wait()
|
|
|
|
for i = 1, #activePopups do
|
|
if activePopups[i] == newModel then
|
|
table.remove(activePopups, i)
|
|
break
|
|
end
|
|
end
|
|
end)()
|
|
return tween1
|
|
end
|
|
|
|
function dataStructure:Place(position, dest)
|
|
-- place the model at position
|
|
if not self.Model.Parent then
|
|
local popupAdorneePart = getPopupAdorneePart()
|
|
self.Model.Parent = popupAdorneePart
|
|
self.Model.Adornee = popupAdorneePart
|
|
|
|
--Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find
|
|
--the actual ground 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, Player.Character })
|
|
|
|
self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0)
|
|
end
|
|
end
|
|
|
|
return dataStructure
|
|
end
|
|
|
|
local function createPopupPath(points, numCircles)
|
|
-- creates a path with the provided points, using the path and number of circles provided
|
|
local popups = {}
|
|
local stopTraversing = false
|
|
|
|
local function killPopup(i)
|
|
-- kill all popups before and at i
|
|
for iter, v in pairs(popups) do
|
|
if iter <= i then
|
|
local tween = v:TweenOut()
|
|
spawn(function()
|
|
tween.Completed:Wait()
|
|
v.Model:Destroy()
|
|
end)
|
|
popups[iter] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function stopFunction()
|
|
stopTraversing = true
|
|
killPopup(#points)
|
|
end
|
|
|
|
spawn(function()
|
|
for i = 1, #points do
|
|
if stopTraversing then
|
|
break
|
|
end
|
|
|
|
local includeWaypoint = i % numCircles == 0
|
|
and i < #points
|
|
and (points[#points].Position - points[i].Position).magnitude > 4
|
|
if includeWaypoint then
|
|
local popup = createNewPopup("PatherPopup")
|
|
popups[i] = popup
|
|
local nextPopup = points[i+1]
|
|
popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position)
|
|
local tween = popup:TweenIn()
|
|
wait(0.2)
|
|
end
|
|
end
|
|
end)
|
|
|
|
return stopFunction, killPopup
|
|
end
|
|
|
|
local function Pather(character, endPoint, surfaceNormal)
|
|
local this = {}
|
|
|
|
this.Cancelled = false
|
|
this.Started = false
|
|
|
|
this.Finished = Instance.new("BindableEvent")
|
|
this.PathFailed = Instance.new("BindableEvent")
|
|
|
|
this.PathComputing = false
|
|
this.PathComputed = false
|
|
|
|
this.TargetPoint = endPoint
|
|
this.TargetSurfaceNormal = surfaceNormal
|
|
|
|
this.DiedConn = nil
|
|
this.SeatedConn = nil
|
|
this.MoveToConn = nil
|
|
this.BlockedConn = nil
|
|
this.CurrentPoint = 0
|
|
|
|
function this:Cleanup()
|
|
if this.stopTraverseFunc then
|
|
this.stopTraverseFunc()
|
|
this.stopTraverseFunc = nil
|
|
end
|
|
|
|
if this.MoveToConn then
|
|
this.MoveToConn:Disconnect()
|
|
this.MoveToConn = 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
|
|
|
|
this.humanoid = nil
|
|
end
|
|
|
|
function this:Cancel()
|
|
this.Cancelled = true
|
|
this:Cleanup()
|
|
end
|
|
|
|
function this:OnPathInterrupted()
|
|
-- Stop moving
|
|
this.Cancelled = true
|
|
this:OnPointReached(false)
|
|
end
|
|
|
|
function this:ComputePath()
|
|
local humanoid = findPlayerHumanoid(Player)
|
|
local torso = humanoid and humanoid.Torso
|
|
local success = false
|
|
if torso then
|
|
if this.PathComputed or this.PathComputing then return end
|
|
this.PathComputing = true
|
|
success = pcall(function()
|
|
this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint)
|
|
end)
|
|
this.pointList = this.pathResult and this.pathResult:GetWaypoints()
|
|
if this.pathResult then
|
|
this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
|
|
end
|
|
this.PathComputing = false
|
|
this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function this:IsValidPath()
|
|
if not this.pathResult then
|
|
this:ComputePath()
|
|
end
|
|
return this.pathResult.Status == Enum.PathStatus.Success
|
|
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.pathResult:ComputeAsync(this.humanoid.Torso.CFrame.p, this.TargetPoint)
|
|
this.pointList = this.pathResult:GetWaypoints()
|
|
this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
|
|
|
|
if SHOW_PATH then
|
|
this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4, true)
|
|
end
|
|
if this.PathComputed then
|
|
this.humanoid = findPlayerHumanoid(Player)
|
|
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:OnPointReached(reached)
|
|
|
|
if reached and not this.Cancelled then
|
|
|
|
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
|
|
|
|
if FFlagUserNavigationClickToMoveSkipPassedWaypoints then
|
|
-- First, check if we already passed the next point
|
|
local nextWaypointAlreadyReached
|
|
-- 1) Build plane (normal is from next waypoint towards current one (provided the two waypoints are not at the same location); location is at next waypoint)
|
|
local planeNormal = currentWaypoint.Position - nextWaypoint.Position
|
|
if planeNormal.Magnitude > 0.000001 then
|
|
planeNormal = planeNormal.Unit
|
|
local planeDistance = planeNormal:Dot(nextWaypoint.Position)
|
|
-- 2) Find current Humanoid position
|
|
local humanoidPosition = this.humanoid.RootPart.Position - Vector3.new(0, 0.5 * this.humanoid.RootPart.Size.y + this.humanoid.HipHeight, 0)
|
|
-- 3) Compute distance from plane
|
|
local dist = planeNormal:Dot(humanoidPosition) - planeDistance
|
|
-- 4) If we are less then a stud in front of the plane or if we are behing the plane, we consider we reached it
|
|
nextWaypointAlreadyReached = dist < 1.0
|
|
else
|
|
-- Next waypoint is the same as current waypoint so we reached it as well
|
|
nextWaypointAlreadyReached = true
|
|
end
|
|
|
|
-- Prepare for next point
|
|
if this.setPointFunc then
|
|
this.setPointFunc(nextWaypointIdx)
|
|
end
|
|
this.CurrentPoint = nextWaypointIdx
|
|
|
|
-- Either callback here right away if next waypoint is already passed
|
|
-- Otherwise, ask the Humanoid to MoveTo
|
|
if nextWaypointAlreadyReached then
|
|
this:OnPointReached(true)
|
|
else
|
|
if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
|
|
this.humanoid.Jump = true
|
|
end
|
|
this.humanoid:MoveTo(nextWaypoint.Position)
|
|
end
|
|
else
|
|
if this.setPointFunc then
|
|
this.setPointFunc(nextWaypointIdx)
|
|
end
|
|
|
|
if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
|
|
this.humanoid.Jump = true
|
|
end
|
|
this.humanoid:MoveTo(nextWaypoint.Position)
|
|
|
|
this.CurrentPoint = nextWaypointIdx
|
|
end
|
|
end
|
|
else
|
|
this.PathFailed:Fire()
|
|
this:Cleanup()
|
|
end
|
|
end
|
|
|
|
function this:Start()
|
|
if CurrentSeatPart then
|
|
return
|
|
end
|
|
|
|
this.humanoid = findPlayerHumanoid(Player)
|
|
if not this.humanoid then
|
|
this.PathFailed:Fire()
|
|
return
|
|
end
|
|
|
|
if this.Started then return end
|
|
this.Started = true
|
|
|
|
if SHOW_PATH then
|
|
-- choose whichever one Mike likes best
|
|
this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4)
|
|
end
|
|
|
|
if #this.pointList > 0 then
|
|
this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end)
|
|
this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end)
|
|
this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end)
|
|
|
|
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
|
|
|
|
this:ComputePath()
|
|
if not this.PathComputed then
|
|
-- set the end point towards the camera and raycasted towards the ground in case we hit a wall
|
|
local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
|
|
local ray = Ray.new(offsetPoint, Vector3_new(0,-1,0)*50)
|
|
local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList())
|
|
if newHitPart then
|
|
this.TargetPoint = newHitPos
|
|
end
|
|
-- try again
|
|
this:ComputePath()
|
|
end
|
|
|
|
return this
|
|
end
|
|
|
|
-------------------------------------------------------------------------
|
|
|
|
local function IsInBottomLeft(pt)
|
|
local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250)
|
|
local joystickWidth = joystickHeight
|
|
return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight
|
|
end
|
|
|
|
local function IsInBottomRight(pt)
|
|
local joystickHeight = math_min(Utility.ViewSizeY() * 0.33, 250)
|
|
local joystickWidth = joystickHeight
|
|
return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight
|
|
end
|
|
|
|
local function CheckAlive(character)
|
|
local humanoid = findPlayerHumanoid(Player)
|
|
return humanoid ~= nil and humanoid.Health > 0
|
|
end
|
|
|
|
local function GetEquippedTool(character)
|
|
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()
|
|
DrivingTo = nil
|
|
if ExistingPather then
|
|
ExistingPather:Cancel()
|
|
end
|
|
if PathCompleteListener then
|
|
PathCompleteListener:Disconnect()
|
|
PathCompleteListener = nil
|
|
end
|
|
if PathFailedListener then
|
|
PathFailedListener:Disconnect()
|
|
PathFailedListener = nil
|
|
end
|
|
if ExistingIndicator then
|
|
local obj = ExistingIndicator
|
|
local tween = obj:TweenOut()
|
|
local tweenCompleteEvent = nil
|
|
tweenCompleteEvent = tween.Completed:connect(function()
|
|
tweenCompleteEvent:Disconnect()
|
|
obj.Model:Destroy()
|
|
end)
|
|
ExistingIndicator = nil
|
|
end
|
|
end
|
|
|
|
local function getExtentsSize(Parts)
|
|
local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge
|
|
local minX,minY,minZ = math.huge,math.huge,math.huge
|
|
for i = 1, #Parts do
|
|
maxX,maxY,maxZ = math_max(maxX, Parts[i].Position.X), math_max(maxY, Parts[i].Position.Y), math_max(maxZ, Parts[i].Position.Z)
|
|
minX,minY,minZ = math_min(minX, Parts[i].Position.X), math_min(minY, Parts[i].Position.Y), math_min(minZ, Parts[i].Position.Z)
|
|
end
|
|
return Region3.new(Vector3_new(minX, minY, minZ), Vector3_new(maxX, maxY, maxZ))
|
|
end
|
|
|
|
local function inExtents(Extents, Position)
|
|
if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then
|
|
return false
|
|
end
|
|
if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then
|
|
return false
|
|
end
|
|
--ignoring Y for now
|
|
return true
|
|
end
|
|
|
|
local function showQuickPopupAsync(position, popupType)
|
|
local popup = createNewPopup(popupType)
|
|
popup:Place(position, Vector3_new(0,position.y,0))
|
|
local tweenIn = popup:TweenIn()
|
|
tweenIn.Completed:Wait()
|
|
local tweenOut = popup:TweenOut()
|
|
tweenOut.Completed:Wait()
|
|
popup.Model:Destroy()
|
|
popup = nil
|
|
end
|
|
|
|
local FailCount = 0
|
|
local function OnTap(tapPositions, goToPoint)
|
|
-- Good to remember if this is the latest tap event
|
|
local camera = workspace.CurrentCamera
|
|
local character = Player.Character
|
|
|
|
if not CheckAlive(character) 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)
|
|
|
|
-- inivisicam stuff
|
|
local initIgnore = getIgnoreList()
|
|
local invisicamParts = InvisicamModule and InvisicamModule:GetObscuredParts() or {}
|
|
local ignoreTab = {}
|
|
|
|
-- add to the ignore list
|
|
for i, v in pairs(invisicamParts) do
|
|
ignoreTab[#ignoreTab+1] = i
|
|
end
|
|
for i = 1, #initIgnore do
|
|
ignoreTab[#ignoreTab+1] = initIgnore[i]
|
|
end
|
|
--
|
|
local myHumanoid = findPlayerHumanoid(Player) -- To remove when cleaning up FFlagUserNavigationClickToMoveNoDirectPath
|
|
local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab)
|
|
|
|
local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart)
|
|
local torso = GetTorso() -- To remove when cleaning up FFlagUserNavigationClickToMoveNoDirectPath
|
|
local startPos = torso.CFrame.p -- To remove when cleaning up FFlagUserNavigationClickToMoveNoDirectPath
|
|
if goToPoint then
|
|
hitPt = goToPoint
|
|
hitChar = nil
|
|
end
|
|
if not FFlagUserNavigationClickToMoveNoDirectPath and hitChar and hitHumanoid and hitHumanoid.RootPart and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then
|
|
-- Do shoot
|
|
local currentWeapon = GetEquippedTool(character)
|
|
if currentWeapon then
|
|
currentWeapon:Activate()
|
|
LastFired = tick()
|
|
end
|
|
elseif hitPt and character and not CurrentSeatPart then
|
|
local thisPather = Pather(character, hitPt, hitNormal)
|
|
if thisPather:IsValidPath() then
|
|
FailCount = 0
|
|
|
|
thisPather:Start()
|
|
if BindableEvent_OnFailStateChanged then
|
|
BindableEvent_OnFailStateChanged:Fire(false)
|
|
end
|
|
CleanupPath()
|
|
|
|
local destinationPopup = createNewPopup("DestinationPopup")
|
|
destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0))
|
|
local failurePopup = createNewPopup("FailurePopup")
|
|
local currentTween = destinationPopup:TweenIn()
|
|
|
|
|
|
ExistingPather = thisPather
|
|
ExistingIndicator = destinationPopup
|
|
|
|
PathCompleteListener = thisPather.Finished.Event:Connect(function()
|
|
if destinationPopup then
|
|
if ExistingIndicator == destinationPopup then
|
|
ExistingIndicator = nil
|
|
end
|
|
local tween = destinationPopup:TweenOut()
|
|
local tweenCompleteEvent = nil
|
|
tweenCompleteEvent = tween.Completed:Connect(function()
|
|
tweenCompleteEvent:Disconnect()
|
|
destinationPopup.Model:Destroy()
|
|
destinationPopup = nil
|
|
end)
|
|
end
|
|
if FFlagUserNavigationClickToMoveNoDirectPath then
|
|
if hitChar then
|
|
local currentWeapon = GetEquippedTool(character)
|
|
if currentWeapon then
|
|
currentWeapon:Activate()
|
|
LastFired = tick()
|
|
end
|
|
end
|
|
else
|
|
if hitChar then
|
|
local humanoid = findPlayerHumanoid(Player)
|
|
local currentWeapon = GetEquippedTool(character)
|
|
if currentWeapon then
|
|
currentWeapon:Activate()
|
|
LastFired = tick()
|
|
end
|
|
if humanoid then
|
|
humanoid:MoveTo(hitPt)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
PathFailedListener = thisPather.PathFailed.Event:Connect(function()
|
|
CleanupPath()
|
|
if failurePopup then
|
|
failurePopup:Place(hitPt, Vector3_new(0,hitPt.y,0))
|
|
local failTweenIn = failurePopup:TweenIn()
|
|
failTweenIn.Completed:Wait()
|
|
local failTweenOut = failurePopup:TweenOut()
|
|
failTweenOut.Completed:Wait()
|
|
failurePopup.Model:Destroy()
|
|
failurePopup = nil
|
|
end
|
|
end)
|
|
else
|
|
if not FFlagUserNavigationClickToMoveNoDirectPath and hitPt then
|
|
-- Feedback here for when we don't have a good path
|
|
local foundDirectPath = false
|
|
if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then
|
|
-- move directly here
|
|
if myHumanoid then
|
|
if myHumanoid.Sit then
|
|
myHumanoid.Jump = true
|
|
end
|
|
myHumanoid:MoveTo(hitPt)
|
|
foundDirectPath = true
|
|
end
|
|
end
|
|
|
|
coroutine.wrap(showQuickPopupAsync)(hitPt, foundDirectPath and "DirectWalkPopup" or "FailurePopup")
|
|
end
|
|
end
|
|
elseif hitPt and character and CurrentSeatPart then
|
|
local destinationPopup = createNewPopup("DestinationPopup")
|
|
ExistingIndicator = destinationPopup
|
|
destinationPopup:Place(hitPt, Vector3_new(0,hitPt.y,0))
|
|
destinationPopup:TweenIn()
|
|
|
|
DrivingTo = hitPt
|
|
local ConnectedParts = CurrentSeatPart:GetConnectedParts(true)
|
|
|
|
while wait() do
|
|
if CurrentSeatPart and ExistingIndicator == destinationPopup then
|
|
local ExtentsSize = getExtentsSize(ConnectedParts)
|
|
if inExtents(ExtentsSize, hitPt) then
|
|
local popup = destinationPopup
|
|
spawn(function()
|
|
local tweenOut = popup:TweenOut()
|
|
tweenOut.Completed:Wait()
|
|
popup.Model:Destroy()
|
|
end)
|
|
destinationPopup = nil
|
|
DrivingTo = nil
|
|
break
|
|
end
|
|
else
|
|
if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then
|
|
DrivingTo = nil
|
|
OnTap(tapPositions, hitPt)
|
|
end
|
|
local popup = destinationPopup
|
|
spawn(function()
|
|
local tweenOut = popup:TweenOut()
|
|
tweenOut.Completed:Wait()
|
|
popup.Model:Destroy()
|
|
end)
|
|
destinationPopup = nil
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif #tapPositions >= 2 then
|
|
if camera then
|
|
-- Do shoot
|
|
local avgPoint = Utility.AveragePoints(tapPositions)
|
|
local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y)
|
|
local currentWeapon = GetEquippedTool(character)
|
|
if currentWeapon then
|
|
currentWeapon:Activate()
|
|
LastFired = tick()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function CreateClickToMoveModule()
|
|
local this = {}
|
|
|
|
local LastStateChange = 0
|
|
local LastState = Enum.HumanoidStateType.Running
|
|
local FingerTouches = {}
|
|
local NumUnsunkTouches = 0
|
|
-- PC simulation
|
|
local mouse1Down = tick()
|
|
local mouse1DownPos = Vector2_new()
|
|
local mouse2Down = tick()
|
|
local mouse2DownPos = Vector2_new()
|
|
local mouse2Up = tick()
|
|
|
|
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 TapConn = nil
|
|
local InputBeganConn = nil
|
|
local InputChangedConn = nil
|
|
local InputEndedConn = nil
|
|
local HumanoidDiedConn = nil
|
|
local CharacterChildAddedConn = nil
|
|
local OnCharacterAddedConn = nil
|
|
local CharacterChildRemovedConn = nil
|
|
local RenderSteppedConn = nil
|
|
local HumanoidSeatedConn = nil
|
|
|
|
local function disconnectEvent(event)
|
|
if event then
|
|
event:Disconnect()
|
|
end
|
|
end
|
|
|
|
local function DisconnectEvents()
|
|
disconnectEvent(TapConn)
|
|
disconnectEvent(InputBeganConn)
|
|
disconnectEvent(InputChangedConn)
|
|
disconnectEvent(InputEndedConn)
|
|
disconnectEvent(HumanoidDiedConn)
|
|
disconnectEvent(CharacterChildAddedConn)
|
|
disconnectEvent(OnCharacterAddedConn)
|
|
disconnectEvent(RenderSteppedConn)
|
|
disconnectEvent(CharacterChildRemovedConn)
|
|
pcall(function() RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate") end)
|
|
disconnectEvent(HumanoidSeatedConn)
|
|
end
|
|
|
|
|
|
|
|
local function IsFinite(num)
|
|
return num == num and num ~= 1/0 and num ~= -1/0
|
|
end
|
|
|
|
local function findAngleBetweenXZVectors(vec2, vec1)
|
|
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
|
|
end
|
|
|
|
local function OnTouchBegan(input, processed)
|
|
if FingerTouches[input] == nil and not processed then
|
|
NumUnsunkTouches = NumUnsunkTouches + 1
|
|
end
|
|
FingerTouches[input] = processed
|
|
end
|
|
|
|
local function OnTouchChanged(input, processed)
|
|
if FingerTouches[input] == nil then
|
|
FingerTouches[input] = processed
|
|
if not processed then
|
|
NumUnsunkTouches = NumUnsunkTouches + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local function OnTouchEnded(input, processed)
|
|
if FingerTouches[input] ~= nil and FingerTouches[input] == false then
|
|
NumUnsunkTouches = NumUnsunkTouches - 1
|
|
end
|
|
FingerTouches[input] = nil
|
|
end
|
|
|
|
|
|
local function OnCharacterAdded(character)
|
|
DisconnectEvents()
|
|
|
|
InputBeganConn = UIS.InputBegan:Connect(function(input, processed)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
OnTouchBegan(input, processed)
|
|
|
|
-- Give back controls when they tap both sticks
|
|
local wasInBottomLeft = IsInBottomLeft(input.Position)
|
|
local wasInBottomRight = IsInBottomRight(input.Position)
|
|
if wasInBottomRight or wasInBottomLeft then
|
|
for otherInput, _ in pairs(FingerTouches) do
|
|
if otherInput ~= input then
|
|
local otherInputInLeft = IsInBottomLeft(otherInput.Position)
|
|
local otherInputInRight = IsInBottomRight(otherInput.Position)
|
|
if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then
|
|
if BindableEvent_OnFailStateChanged then
|
|
BindableEvent_OnFailStateChanged:Fire(true)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cancel path when you use the keyboard controls.
|
|
if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then
|
|
CleanupPath()
|
|
end
|
|
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
|
mouse1Down = tick()
|
|
mouse1DownPos = input.Position
|
|
end
|
|
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
|
mouse2Down = tick()
|
|
mouse2DownPos = input.Position
|
|
end
|
|
end)
|
|
|
|
InputChangedConn = UIS.InputChanged:Connect(function(input, processed)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
OnTouchChanged(input, processed)
|
|
end
|
|
end)
|
|
|
|
InputEndedConn = UIS.InputEnded:Connect(function(input, processed)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
OnTouchEnded(input, processed)
|
|
end
|
|
|
|
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
|
mouse2Up = tick()
|
|
local currPos = input.Position
|
|
if mouse2Up - mouse2Down < 0.25 and (currPos - mouse2DownPos).magnitude < 5 then
|
|
local positions = {currPos}
|
|
OnTap(positions)
|
|
end
|
|
end
|
|
end)
|
|
|
|
TapConn = UIS.TouchTap:Connect(function(touchPositions, processed)
|
|
if not processed then
|
|
OnTap(touchPositions)
|
|
end
|
|
end)
|
|
|
|
local function computeThrottle(dist)
|
|
if dist > .2 then
|
|
return 0.5+(dist^2)/2
|
|
else
|
|
return 0
|
|
end
|
|
end
|
|
|
|
local lastSteer = 0
|
|
|
|
--kP = how much the steering corrects for the current error in driving angle
|
|
--kD = how much the steering corrects for how quickly the error in driving angle is changing
|
|
local kP = 1
|
|
local kD = 0.5
|
|
local function getThrottleAndSteer(object, point)
|
|
local throttle, steer = 0, 0
|
|
local oCF = object.CFrame
|
|
|
|
local relativePosition = oCF:pointToObjectSpace(point)
|
|
local relativeZDirection = -relativePosition.z
|
|
local relativeDistance = relativePosition.magnitude
|
|
|
|
-- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1.
|
|
-- this allows shorter distance travel to have more fine-tuned control.
|
|
throttle = computeThrottle(math_min(1,relativeDistance/50))*math.sign(relativeZDirection)
|
|
|
|
local steerAngle = -math_atan2(-relativePosition.x, -relativePosition.z)
|
|
steer = steerAngle/(math_pi/4)
|
|
|
|
local steerDelta = steer - lastSteer
|
|
lastSteer = steer
|
|
local pdSteer = kP * steer + kD * steer
|
|
return throttle, pdSteer
|
|
end
|
|
|
|
local function Update()
|
|
if CurrentSeatPart then
|
|
if DrivingTo then
|
|
local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo)
|
|
CurrentSeatPart.ThrottleFloat = throttle
|
|
CurrentSeatPart.SteerFloat = steer
|
|
else
|
|
CurrentSeatPart.ThrottleFloat = 0
|
|
CurrentSeatPart.SteerFloat = 0
|
|
end
|
|
end
|
|
|
|
local cameraPos = workspace.CurrentCamera.CFrame.p
|
|
for i = 1, #activePopups do
|
|
local popup = activePopups[i]
|
|
popup.CFrame = CFrame.new(popup.CFrame.p, cameraPos)
|
|
end
|
|
end
|
|
|
|
RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update)
|
|
|
|
local function onSeated(child, active, currentSeatPart)
|
|
if active then
|
|
if TouchJump and UIS.TouchEnabled then
|
|
TouchJump:Enable()
|
|
end
|
|
if currentSeatPart and currentSeatPart.ClassName == "VehicleSeat" then
|
|
CurrentSeatPart = currentSeatPart
|
|
end
|
|
else
|
|
CurrentSeatPart = nil
|
|
if TouchJump and UIS.TouchEnabled then
|
|
TouchJump:Disable()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function OnCharacterChildAdded(child)
|
|
if UIS.TouchEnabled then
|
|
if child:IsA('Tool') then
|
|
child.ManualActivationOnly = true
|
|
end
|
|
end
|
|
if child:IsA('Humanoid') then
|
|
disconnectEvent(HumanoidDiedConn)
|
|
HumanoidDiedConn = child.Died:Connect(function()
|
|
if ExistingIndicator then
|
|
DebrisService:AddItem(ExistingIndicator.Model, 1)
|
|
end
|
|
end)
|
|
HumanoidSeatedConn = child.Seated:Connect(function(active, seat) onSeated(child, active, seat) end)
|
|
if child.SeatPart then
|
|
onSeated(child, true, child.SeatPart)
|
|
end
|
|
end
|
|
end
|
|
|
|
CharacterChildAddedConn = character.ChildAdded:Connect(function(child)
|
|
OnCharacterChildAdded(child)
|
|
end)
|
|
CharacterChildRemovedConn = character.ChildRemoved:Connect(function(child)
|
|
if UIS.TouchEnabled then
|
|
if child:IsA('Tool') then
|
|
child.ManualActivationOnly = false
|
|
end
|
|
end
|
|
end)
|
|
for _, child in pairs(character:GetChildren()) do
|
|
OnCharacterChildAdded(child)
|
|
end
|
|
end
|
|
|
|
local Running = false
|
|
|
|
function this:Disable()
|
|
if Running then
|
|
DisconnectEvents()
|
|
CleanupPath()
|
|
-- Restore tool activation on shutdown
|
|
if UIS.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
|
|
DrivingTo = nil
|
|
Running = false
|
|
end
|
|
end
|
|
function this:Stop()
|
|
this:Disable()
|
|
end
|
|
|
|
function this:Enable()
|
|
if not Running then
|
|
if Player.Character then -- retro-listen
|
|
OnCharacterAdded(Player.Character)
|
|
end
|
|
OnCharacterAddedConn = Player.CharacterAdded:Connect(OnCharacterAdded)
|
|
Running = true
|
|
end
|
|
end
|
|
function this:Start()
|
|
this:Enable()
|
|
end
|
|
|
|
function this:GetName()
|
|
return DEBUG_NAME
|
|
end
|
|
|
|
return this
|
|
end
|
|
|
|
return CreateClickToMoveModule() |