1102 lines
32 KiB
Lua
1102 lines
32 KiB
Lua
--[[
|
|
-- Original By Kip Turner, Copyright Roblox 2014
|
|
-- Updated by Garnold to utilize the new PathfindingService API, 2017
|
|
-- 2018 PlayerScripts Update - AllYourBlox
|
|
--]]
|
|
|
|
--[[ 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, ignoreList)
|
|
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)
|
|
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 CurrentIgnoreTag = nil
|
|
|
|
local TaggedInstanceAddedConnection = nil
|
|
local TaggedInstanceRemovedConnection = nil
|
|
|
|
local function GetCharacter()
|
|
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()
|
|
if CurrentIgnoreList then
|
|
return CurrentIgnoreList
|
|
end
|
|
CurrentIgnoreList = {}
|
|
table.insert(CurrentIgnoreList, GetCharacter())
|
|
return CurrentIgnoreList
|
|
end
|
|
|
|
-----------------------------------PATHER--------------------------------------
|
|
|
|
local function Pather(endPoint, surfaceNormal, overrideUseDirectPath)
|
|
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
|
|
|
|
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
|
|
|
|
local rootPart = this.Humanoid and this.Humanoid.RootPart
|
|
if rootPart then
|
|
-- Setup origin
|
|
this.OriginPoint = rootPart.CFrame.p
|
|
|
|
-- 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 = 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 = GetCharacter():GetExtentsSize()
|
|
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
|
|
this.DirectPathRiseFirst = this.Humanoid.Sit
|
|
end
|
|
|
|
-- Build path object
|
|
this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump})
|
|
end
|
|
|
|
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
|
|
|
|
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)
|
|
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, nextWaypoint, nextWaypointIdx)
|
|
-- 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
|
|
this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z)
|
|
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)
|
|
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, goToPoint, wasTouchTap)
|
|
-- 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 = 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, enableWASD, 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)
|
|
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
|