(if this is too big of a request to post here, let me know and I'll remove this)

I'm trying to develop an npc that follows players around. Goals of this npc:

-searches for target based on closest player & line-of-sight

-checks for obstructions whenever it is moving

-if gets stuck trying to chase the player, will pathfind to the last position the player was seen at

-if it does not find a target again, it patrols (random movement) until it finds a target again

Currently, this script is super buggy:

  1. the patrol() function sometimes doesn't make the NPC move at ALL
  2. target acquisition is bad
  3. pathfinding doesn't always activate
  4. target isn't always removed after a certain amount of time not being able to find target

If anyone can help me out with this, here is the code: (I've explained what everything does, but let me know if I can help clarify further)

link to NPC model: https://www.roblox.com/NPC-v1-item?id=459931346

--master variables
----------------------------------------------------
----------------------------------------------------
bot = script.Parent
target = nil
maxDist = 300
targDist = maxDist

--variables for determining what counts as "stuck"
stuckDist = 10 
stuckTime = 2

-- how long it can try to find a target until it sets target to nil
maxTargetTime = 5
targetTime = 0

-- last vector3 the target was seen at
lastSeenPos = nil

--the amount a time the NPC has been moving for (for checking if stuck after a certain time)
moveTime = 0

--variables for checking current vs old position 
curPos = nil
lastPos = nil
lastPosReset = false -- moveTime is set to 0 if lastPosReset is false (happens every 2 seconds)

-- self explainatory
pathfindingService = game:GetService("PathfindingService")
pathing = false

-- bool that SHOULD be set to true whenever the NPC is moving
moving = false

-- patrol variables, including debounce, the position to WalkTo, and current time spent patrolling (for checking if stuck)
patrolDeb = false
patrolPos = nil
patrolTime = 0

--movement functions
----------------------------------------------------
----------------------------------------------------
function moveToTarget(patrolPos)
	if target then
		local torso = target:findFirstChild("Torso")
		if torso then
			print("torso found, moving to it")
			print("torso pos = " .. tostring(torso.Position))
			bot.Humanoid:MoveTo(torso.Position)
		end

	
	elseif lastSeenPos then
		bot.Humanoid:MoveTo(lastSeenPos)
		
	elseif patrolPos then

		bot.Humanoid:MoveTo(patrolPos)
		patrolPos = nil
		
	elseif not target then	
		
	end
	
	
end
----------------------------------------------------
----------------------------------------------------



--AI/pathfind functions
----------------------------------------------------
----------------------------------------------------

-- patrol randomly
patrolDirs = {math.pi,math.pi/2,math.pi/4,math.pi*2}
function patrol()
	if not patrolDeb then	
		patrolDeb = true
		local dir = math.random(1,#patrolDirs)
		local dirChosen = patrolDirs[dir]
		print("new Dir = " .. dirChosen)		
		
		local newCF = bot.Torso.CFrame * CFrame.Angles(0,math.rad(math.random(30,170)),0)
		local direction = newCF.lookVector * (math.random(10,30)*10)
		print("direction = " .. tostring(direction))
		patrolPos = direction
		
		patrolTime = 0
		moveToTarget(patrolPos)
	else
		if ((bot.Torso.Position - patrolPos).magnitude) < 5 then
			patrolDeb = false
		elseif patrolTime > stuckTime then
			print("stuck on patrol")
			patrolDeb = false			
		end
	end
	patrolTime = patrolTime + .1
end

-- check to makesure it can see the player using raycasting
function checkSight(char)
	local sight = false
		local direction = (char.Torso.Position - bot.Head.Position).unit; -- Unit makes it length 1.
		direction = direction * maxDist; -- Allow raycasting up to maxDist studs away
		
		local partHit, positionHit = workspace:FindPartOnRay( Ray.new(bot.Head.Position, direction) , bot );
		-- We ignore the local player's character for the raycast.
		-- (This would require this script to be in a local script)
		
		-- The laser should stretch from "from" to "positionHit".
		if partHit.Parent == char then
			sight = true
			lastSeenPos = char.Torso.Position
		end
	return sight
end

-- find targets
function findTarget()
	local potTarget = nil
	targDist = maxDist
	local pList = game.Players:GetPlayers()
	for i,v in pairs(pList) do
		local char = v["Character"]
		if char then
			print("char found")
			local hum = char:findFirstChild("Humanoid")
			if hum and (hum.Health ~= 0) then -- valid humanoid
				local torso = char:findFirstChild("Torso")
					if torso then
						local distance = (torso.Position - bot.Torso.Position).magnitude
							if (distance < maxDist) and (distance < targDist) and checkSight(char) then
								print("target passed checks, setting to PotTarget")
								targDist = distance
								potTarget = char
							end
					end
			--	target = char
			--	print("target = " .. target.Name )
			end
		end
	end
	if potTarget then
		target = potTarget	
	else 
		target = nil
	end

	--print("target = " ..target.Name)
end


-- check for obstructions using a raycasting
function checkObstruction()
	-- assume "from" is some Vector3, in your example the Torso's Position
	-- assume "target" is some other Vector3, in your example
	-- the mouse's hover position (mouse.Hit.p)
	if target then
		checkTarg = target
	else
		checkTarg = nil
	end
		local newCF = bot.Torso.CFrame * CFrame.new(0,-.5,0)
		local direction = newCF.lookVector -- Unit makes it length 1.
		direction = direction * 3; -- Allow raycasting up to 1000 studs away
		
		local partHit, positionHit = workspace:FindPartOnRay( Ray.new(bot.Torso.Position, direction) , checkTarg );
		-- We ignore the local player's character for the raycast.
		-- (This would require this script to be in a local script)
		
		-- The laser should stretch from "from" to "positionHit".
		if partHit then
			bot.Humanoid.Jump = true
		else
			bot.Humanoid.Jump = false
		end
		

end

-- a loop that checks for obstacles whenever "moving" is true
checkObsLoop = coroutine.wrap(function()
	while true do
		wait(.1)
		if moving then
			print("moving")
			checkObstruction()
		else
			print("not moving")
		end
		
	end
end)

-- function to check if the NPC got stuck (hasn't moved out of a certain radius in x number of seconds)
function checkStuck(cur,last)
	local stuck = false
	local curDist = (last - cur).magnitude
	if curDist < stuckDist then
		stuck = true		
	else
		
	end
	return stuck
end

-- pathfinding function (also has a check to make sure it doesn't get stuck)
function pathfind(lastPos)
	local start = bot.Torso.Position
	local path = pathfindingService:ComputeRawPathAsync(start, lastPos, 500)
	local points = path:GetPointCoordinates()	
	pathing = true
	
	if #points < 1 then return end
	moving = true
			
	for _, point in ipairs(points) do
		print("moving to: ", point)
		bot.Humanoid:MoveTo(point)
		moveTime = 0
		lastPosReset = false
		-- Don't try moving to the next point until we are close enough to the current point in the path
		repeat
			wait(.1)
			
			
			local distance = (point - bot.Torso.Position).magnitude
			curPos = bot.Torso.Position
			
			if not lastPosReset then
				moveTime = 0
				lastPos = curPos
				lastPosReset = true
			else
				if moveTime > stuckTime then -- check to see if got stuck during the pathing
					if checkStuck(curPos,lastPos) then
						target = nil
						lastSeenPos = nil
						break
					end
				end		
			end
			moveTime = moveTime + .1
		until distance < 3
	end
	pathing = false
	moving = false
	print("path done")
end


checkObsLoop() -- activates this thread


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

-- master true loop
----------------------------------------------------
----------------------------------------------------
while true do
	wait(.1)	
	
	-- this chunk of the loop is for Finding/Moving towards the target, or moving towards the last position it saw them @
	if target then
		print("yes target")
		local distance = (target.Torso.Position - bot.Torso.Position).magnitude
		if distance > 3 then
			print("dist is larger")
			if checkSight(target) then
				print("target in sight")
				moving = true
				moveToTarget()

			else
				target = nil
				moving = true
				moveToTarget(lastSeenPos)	
		

			end
		end
		
		if targetTime > maxTargetTime then
			target = nil
			targetTime = 0
			moving = false
		end
	
	elseif not target then
		patrol()
		findTarget()

	end
	
	
	-- this chunk of the loop is for determining if the NPC has gotten stuck during its movement
	curPos = bot.Torso.Position
	
	if not lastPosReset then
		moveTime = 0
		lastPos = curPos
		lastPosReset = true
	else
		if moveTime > stuckTime then
			if checkStuck(curPos,lastPos)then
				if lastSeenPos then
					if not pathing then
						if target then
							pathfind(lastSeenPos)
							lastPosReset = false
							moveTime = 0	
						else						
							lastPosReset = false
							moveTime = 0
							patrol()				
						end
	
					else
						lastPosReset = false
						moveTime = 0
						patrol()		
					end
			
				else
					lastPosReset = false
					moveTime = 0
					patrol()
				end
			else
				lastPosReset = false
				moveTime = 0
			end
		end		
	end
	targetTime = targetTime + .1
	moveTime = moveTime + .1
end



----------------------------------------------------
--end of script