Error in AI script

I have the following code:

task.wait(5)

local shootModule  = require(game.ServerScriptService.NPCShoot)
local rayCastModule = require(game.ReplicatedStorage:WaitForChild("LineOfSight"))

local PathfindingService = game:GetService("PathfindingService")



local AI = script.Parent
local RP = script.Parent.HumanoidRootPart
local Hum = script.Parent.Humanoid
local OverheadUI = AI.Head.HeadUI

local Gun = script.Parent:FindFirstChildOfClass("Tool")

local Waypoints = game.Workspace:WaitForChild("Waypoints"):GetChildren()

RP:SetNetworkOwner(nil)

local attackAnim = Hum.Animator:LoadAnimation(script.Attack)
local walkAnim = Hum.Animator:LoadAnimation(script.Walk)
local runAnim = Hum.Animator:LoadAnimation(script.Run)

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {Hum}

local Damage = 25
local AttackRange = 15

local shootTrack = AI.Humanoid.Animator:LoadAnimation(game.ReplicatedStorage.Animations.NPCPistolShoot)

walkAnim.Looped = true
runAnim.Looped = true
walkAnim:Play()

local LastSeenPos
local patrolPath

local RayParams = RaycastParams.new()
RayParams.FilterType = Enum.RaycastFilterType.Exclude

local alreadyDetected = false

local chaseCooldown = false

local pathParams = {
	AgentHeight = 1,
	AgentRadius = 1,
	AgentCanJump = false,
	Costs = {}

}

for i, v in pairs(game.Workspace:WaitForChild("AvoidPathing"):GetChildren()) do
	pathParams.Costs[v] = math.huge
end

local function getPath(destination)

	if typeof(destination) ~= "Vector3" then 
		warn("Destination variable was not a vector3")
	end


	if destination then
		local path = PathfindingService:CreatePath(pathParams)

		path:ComputeAsync(RP.Position , destination)

		return path	
	else
		warn("Pathfinding fail")
		return "Fail"
	end

end

function lineOfSight(target)
	if rayCastModule.lineOfSight(AI,target.Parent) then
		return true
	else
		return false
	end
end


function getTarget()

	local closestTarget = nil
	local distanceFromClosestTarget = 1000000000

	for i, player in pairs(game.Players:GetChildren()) do
		local distance = (player.Character.HumanoidRootPart.Position - RP.Position).Magnitude

		if distance < distanceFromClosestTarget and player.Character.Humanoid.Health > 0 then
			if lineOfSight(player.Character.HumanoidRootPart) then
				if player.Character.Humanoid.Health > 0 then
					distanceFromClosestTarget = distance
					closestTarget = player
				end
			end
		end
	end

	if closestTarget ~= nil then

		alreadyDetected = true
		OverheadUI.TextLabel.Text = "!"
		OverheadUI.TextLabel.TextColor3 = Color3.new(1, 0, 0.0156863)
		OverheadUI.TextLabel.Visible = true

		return(closestTarget)
	else
		--task.wait(5)
		--	patrol()
	end


end

local db = false

local runAnimPlayingStatus = false
local walkAnimPlayingStatus = false


function chaseTarget(target)
	local path

	path = getPath(target.Character.HumanoidRootPart.Position)

	if path ~= "Fail" then

		local Waypoints = path:GetWaypoints()

		if Waypoints[2] then

			Hum:MoveTo(Waypoints[2].Position)
			task.spawn(shoot)

			if lineOfSight(target.Character.HumanoidRootPart) then
				patrol()
			else
				moveToLastSeen(target.Character.HumanoidRootPart.Position)
			end


		else
			OverheadUI.TextLabel.Text = Color3.new(255,255,255)
			OverheadUI.TextLabel.Visible = false
			patrol()
			return
		end


	end

end

function moveToLastSeen(location)
	local path = getPath(location)

	if path ~= "Fail" then
		for i, waypoint in pairs(path:GetWaypoints()) do

			local humTarget = getTarget()

			if humTarget then
				patrol()
				break
			else
				if walkAnimPlayingStatus == false then
					walkAnim:Play()
					walkAnimPlayingStatus = true
				end
				runAnimPlayingStatus = false
				runAnim:Stop()

				Hum:MoveTo(waypoint.Position)
				Hum.MoveToFinished:Wait()

				OverheadUI.TextLabel.Text = "?"
				OverheadUI.TextLabel.TextColor3 = Color3.new(1, 0, 0.0156863)
				OverheadUI.TextLabel.Visible = true

				if i == #path:GetWaypoints() then
					patrol()
					break
				end
			end
		end
	else
		patrol()
	end

end


function moveTo(target)
	local humTarget = getTarget()

	if humTarget then
		if patrolPath then 
			patrolPath:Destroy()
		end
		chaseTarget(humTarget)
		return
	else

		patrolPath = getPath(target)

		if patrolPath.Status == Enum.PathStatus.Success then
			local Waypoints = patrolPath:GetWaypoints()
			for i, waypoint in pairs(Waypoints) do

				if i == #patrolPath:GetWaypoints() then
					patrol()
					break
				end

				local humTarget = getTarget()

				if humTarget then
					chaseTarget(humTarget)
					break

				else
					OverheadUI.TextLabel.TextColor3 = Color3.new(255,255,255)
					OverheadUI.TextLabel.Visible = false
					Hum:MoveTo(waypoint.Position)
					Hum.MoveToFinished:Wait()
				end
			end
		end
	end
end

local patrolDB = false


function patrol()
	local ChosenWapoint = math.random(1, #Waypoints)
	moveTo(game.Workspace.Waypoints:FindFirstChild(ChosenWapoint).Position)
end

script.Parent.Humanoid.Died:Connect(function()
	script:Destroy()
	AI.Head.HeadUI:Destroy()
	Hum.HealthDisplayDistance = 0
	OverheadUI.TextLabel.Visible = false
end)


local lastPos = RP.Position

function stuckDetect()
	while task.wait(1) do
		if (RP.Position - lastPos).Magnitude < 1 and Gun:GetAttribute("Reloading") == false then
			warn("AI got stuck falling back")
			if patrolPath then
				patrolPath:Destroy()
				patrol()
			end
		else
			lastPos = RP.Position
		end
	end
end

function shoot()
	local target = getTarget()

	if target and (target.Character.HumanoidRootPart.Position - RP.Position).Magnitude <= 20 and db == false then
		db = true
		shootTrack:Play()
		shootModule.Shoot(target,Gun,AI)

		task.wait(Gun:GetAttribute("FireRate"))
		db = false
	end
end

task.spawn(stuckDetect)


patrol()

it has an issue in that when you get out of sight of the AI chase humanoid it will print

Script 'Workspace.GunAi.GunAi', Line 239 - function patrol  -  Studio - GunAi:239
  21:34:40.577  Script 'Workspace.GunAi.GunAi', Line 140 - function chaseTarget  -  Studio - GunAi:140
  21:34:40.577  Script 'Workspace.GunAi.GunAi', Line 204 - function moveTo  -  Studio - GunAi:204
  21:34:40.577  Script 'Workspace.GunAi.GunAi', Line 239 - function patrol  -  Studio - GunAi:239
  21:34:40.577  Script 'Workspace.GunAi.GunAi', Line 140 - function chaseTarget  -  Studio - GunAi:140
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 204 - function moveTo  -  Studio - GunAi:204
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 239 - function patrol  -  Studio - GunAi:239
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 140 - function chaseTarget  -  Studio - GunAi:140
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 204 - function moveTo  -  Studio - GunAi:204
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 239 - function patrol  -  Studio - GunAi:239
  21:34:40.578  Script 'Workspace.GunAi.GunAi', Line 282  -  Studio - GunAi:282
  21:34:40.578  Stack End  -  Studio

it prints the lines above StackEnd many times and then the ai just freezes completely, now I know that the issue is that the function calls are circular so function a calls function b calls function c calls function a again 100 times a second and then the script crashes.

What I don’t know is how to fix it, cooldown that prevent functions being called more then 10 times a second result in the AI never moving, I tried getting AI to fix it and it did a “State Management System” that resulted in the AI never even beggining to move.

I know what causes the issue but all my attempts to fix it made it worse and so I’m making this post. And I know some people are seeing the modules, and while I don’t believe they are at all the issue here they are anyways

NPCShoot:

local module = {}

local BulletsFolder = game.Workspace:WaitForChild("Bullets")
local FastCast = require(game.ReplicatedStorage.FastCastRedux)
local castBehavior = FastCast.newBehavior()

local castParams = RaycastParams.new()
castParams.FilterType = Enum.RaycastFilterType.Exclude
castParams.IgnoreWater = true

local animationsFolder = game.ReplicatedStorage.Animations

local bulletTemplate = Instance.new("Part")
bulletTemplate.Anchored = true
bulletTemplate.CanCollide = false
bulletTemplate.Size = Vector3.new(0.1, 0.1, 4)
bulletTemplate.Material = Enum.Material.Neon
bulletTemplate.BrickColor = BrickColor.new("Yellow flip/flop")

castBehavior.RaycastParams = castParams
castBehavior.CosmeticBulletContainer = BulletsFolder
castBehavior.CosmeticBulletTemplate = bulletTemplate

local function Hit(cast, result, velocity, bullet, Gun)
    local hit = result.Instance
    local character = hit:FindFirstAncestorWhichIsA("Model")
	--local gun = cast.UserData

	
    game.Debris:AddItem(bullet, 0.25)

    if character:FindFirstChild("ISAI") then return end

    if character and character:FindFirstChild("Humanoid") then
        character.Humanoid:TakeDamage(bullet:GetAttribute("Damage") or 5)
    end

    game.Workspace.Bullets:ClearAllChildren()
end

local function onLengthChanged(cast, lastPoint, direction, length, velocity, bullet)
    if bullet then
        local bulletLength = bullet.Size.Z / 2
        local offset = CFrame.new(0, 0, -(length - bulletLength))
        bullet.CFrame = CFrame.lookAt(lastPoint, lastPoint + direction):ToWorldSpace(offset)
    end
end

local caster = FastCast.new()

caster.LengthChanged:Connect(onLengthChanged)
caster.RayHit:Connect(Hit)

function module.Shoot(target, Gun, AI, reloadTime, stationary)
    castParams.FilterDescendantsInstances = {Gun, BulletsFolder, AI, game.Workspace.AIS}

    if not AI then return end

    local audioPlayer = AI:FindFirstChild("HumanoidRootPart"):FindFirstChild("AudioPlayer")
    if not audioPlayer then return end

    local bulletsLeft = Gun:GetAttribute("BulletsLeft")
    local inMag = Gun:GetAttribute("InMag")

    if Gun:GetAttribute("InMag") > 0 then
        Gun:SetAttribute("InMag", Gun:GetAttribute("InMag") - 1)
        local firePoint = Gun.Barrel.FirePoint
        local origin = firePoint.WorldPosition
        local direction = target.Character.HumanoidRootPart.Position - origin

        audioPlayer.AssetId = Gun.ShootSound.SoundId
			
		Gun.ShootSound:Play()

		bulletTemplate:SetAttribute("Damage", Gun:GetAttribute("Damage")) 
		
		caster:Fire(origin, direction.Unit * 100, 400, castBehavior, Gun)
    else
        local animation = animationsFolder:FindFirstChild(Gun.Name .. "Reload")

      --  audioPlayer.AssetId = game.ReplicatedStorage.Sounds.GunEmpty.SoundId
        --audioPlayer:Play()
        Gun:SetAttribute("Reloading", true)

        AI.Head.HeadUI.TextLabel.Visible = true
        AI.Head.HeadUI.TextLabel.Text = "Reloading..."

        if animation then
            local animationTrack = AI.Humanoid.Animator:LoadAnimation(animation)
            animationTrack:Play()
        end

     --   audioPlayer.AssetId = game.ReplicatedStorage.Sounds.AIReloading.Reload.SoundId
       -- audioPlayer:Play()

        task.wait(reloadTime or 2)

        local maxMagSize = Gun:GetAttribute("MagSize")
        local ammoToFill = maxMagSize - inMag

        if bulletsLeft > 0 then
            Gun:SetAttribute("Reloading", false)

            if bulletsLeft >= ammoToFill then
                Gun:SetAttribute("InMag", maxMagSize)
                Gun:SetAttribute("BulletsLeft", bulletsLeft - ammoToFill)
            else
                Gun:SetAttribute("InMag", bulletsLeft)
                Gun:SetAttribute("BulletsLeft", 0)
            end

            AI.Head.HeadUI.TextLabel.Visible = false
       --     AI.Head.HeadUI.TextLabel.Text = "Reloading..."
         --   module.Shoot(target, Gun, AI)
        end
    end
end

return module

Line of sight:

local RaycastModule = {}

function RaycastModule.lineOfSight(ai, targetChar)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {ai, game.Workspace.Important, game.Workspace["Outside/Inside"],game.Workspace.MainDoor,game.Workspace.MainDoors}
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.IgnoreWater = true

	local targetParts = {
		targetChar:WaitForChild("Head"),
		targetChar:WaitForChild("HumanoidRootPart"),
		targetChar:WaitForChild("LeftHand"),
		targetChar:WaitForChild("RightHand"),
		targetChar:WaitForChild("LeftFoot"),
		targetChar:WaitForChild("RightFoot")
	}

	for _, part in ipairs(targetParts) do
		if part and part:IsDescendantOf(targetChar) then
			local direction = (part.Position - ai.Head.Position).unit
			local ray = workspace:Raycast(ai.Head.Position, direction * 100, raycastParams)
			if ray and ray.Instance then
				if ray.Instance:IsDescendantOf(targetChar) then 
					return true
				else
				end
			end
		end
	end

	return false
end

return RaycastModule

Help is much appreciated

Fixed code via grok AI

task.wait(5)

local shootModule = require(game.ServerScriptService.NPCShoot)
local rayCastModule = require(game.ReplicatedStorage:WaitForChild("LineOfSight"))
local PathfindingService = game:GetService("PathfindingService")

local AI = script.Parent
local RP = script.Parent.HumanoidRootPart
local Hum = script.Parent.Humanoid
local OverheadUI = AI.Head.HeadUI
local Gun = script.Parent:FindFirstChildOfClass("Tool")

local Waypoints = game.Workspace:WaitForChild("Waypoints"):GetChildren()

RP:SetNetworkOwner(nil)

local attackAnim = Hum.Animator:LoadAnimation(script.Attack)
local walkAnim = Hum.Animator:LoadAnimation(script.Walk)
local runAnim = Hum.Animator:LoadAnimation(script.Run)
local shootTrack = AI.Humanoid.Animator:LoadAnimation(game.ReplicatedStorage.Animations.NPCPistolShoot)

walkAnim.Looped = true
runAnim.Looped = true
walkAnim:Play()

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {Hum}

local Damage = 25
local AttackRange = 15

local alreadyDetected = false
local chaseCooldown = false
local db = false
local runAnimPlayingStatus = false
local walkAnimPlayingStatus = false
local lastPos = RP.Position

local pathParams = {
	AgentHeight = 1,
	AgentRadius = 1,
	AgentCanJump = false,
	Costs = {}
}

for i, v in pairs(game.Workspace:WaitForChild("AvoidPathing"):GetChildren()) do
	pathParams.Costs[v] = math.huge
end

local function getPath(destination)
	if typeof(destination) ~= "Vector3" then
		warn("Destination variable was not a vector3")
		return nil
	end

	local path = PathfindingService:CreatePath(pathParams)
	path:ComputeAsync(RP.Position, destination)
	if path.Status == Enum.PathStatus.Success then
		return path
	else
		warn("Pathfinding failed")
		return nil
	end
end

function lineOfSight(target)
	return rayCastModule.lineOfSight(AI, target.Parent)
end

function getTarget()
	local closestTarget = nil
	local distanceFromClosestTarget = math.huge

	for i, player in pairs(game.Players:GetChildren()) do
		if player.Character and player.Character.Humanoid.Health > 0 then
			local distance = (player.Character.HumanoidRootPart.Position - RP.Position).Magnitude
			if distance < distanceFromClosestTarget and lineOfSight(player.Character.HumanoidRootPart) then
				distanceFromClosestTarget = distance
				closestTarget = player
			end
		end
	end

	if closestTarget then
		alreadyDetected = true
		OverheadUI.TextLabel.Text = "!"
		OverheadUI.TextLabel.TextColor3 = Color3.new(1, 0, 0.0156863)
		OverheadUI.TextLabel.Visible = true
		return closestTarget
	else
		return nil
	end
end

function shoot(target)
	if target and (target.Character.HumanoidRootPart.Position - RP.Position).Magnitude <= 20 and not db then
		db = true
		shootTrack:Play()
		shootModule.Shoot(target, Gun, AI)
		task.wait(Gun:GetAttribute("FireRate"))
		db = false
	end
end

function moveToPosition(destination)
	local path = getPath(destination)
	if not path then
		return false
	end

	local waypoints = path:GetWaypoints()
	for i, waypoint in pairs(waypoints) do
		Hum:MoveTo(waypoint.Position)
		Hum.MoveToFinished:Wait()
		if getTarget() then
			return true -- Interrupt if a target is found
		end
	end
	return false
end

function chaseTarget(target)
	local path = getPath(target.Character.HumanoidRootPart.Position)
	if not path then
		return false
	end

	local waypoints = path:GetWaypoints()
	if waypoints[2] then
		Hum:MoveTo(waypoints[2].Position)
		task.spawn(function()
			shoot(target)
		end)
		return lineOfSight(target.Character.HumanoidRootPart)
	end
	return false
end

function moveToLastSeen(location)
	local path = getPath(location)
	if not path then
		return false
	end

	local waypoints = path:GetWaypoints()
	for i, waypoint in pairs(waypoints) do
		if getTarget() then
			return true
		end
		if walkAnimPlayingStatus == false then
			walkAnim:Play()
			walkAnimPlayingStatus = true
		end
		runAnimPlayingStatus = false
		runAnim:Stop()

		OverheadUI.TextLabel.Text = "?"
		OverheadUI.TextLabel.TextColor3 = Color3.new(1, 0, 0.0156863)
		OverheadUI.TextLabel.Visible = true

		Hum:MoveTo(waypoint.Position)
		Hum.MoveToFinished:Wait()
	end

	-- Clear the question mark after reaching the last seen position
	OverheadUI.TextLabel.Text = ""
	OverheadUI.TextLabel.Visible = false
	return false
end

function patrol()
	local chosenWaypoint = math.random(1, #Waypoints)
	return moveToPosition(game.Workspace.Waypoints:FindFirstChild(chosenWaypoint).Position)
end

function stuckDetect()
	while task.wait(1) do
		if (RP.Position - lastPos).Magnitude < 1 and Gun:GetAttribute("Reloading") == false then
			warn("AI got stuck, falling back to patrol")
			return true
		end
		lastPos = RP.Position
	end
end

function updateAI()
	while Hum.Health > 0 do
		local target = getTarget()
		if target then
			if chaseTarget(target) then
				-- Continue chasing if target is in sight
				continue
			else
				-- Move to last seen position if target is out of sight
				if moveToLastSeen(target.Character.HumanoidRootPart.Position) then
					continue -- Target was reacquired during moveToLastSeen
				end
			end
		else
			-- No target, patrol
			if patrol() or task.spawn(stuckDetect) then
				continue -- Patrol was interrupted or AI was stuck
			end
		end
		task.wait(0.2) -- Small delay to prevent tight looping
	end
end

script.Parent.Humanoid.Died:Connect(function()
	script:Destroy()
	AI.Head.HeadUI:Destroy()
	Hum.HealthDisplayDistance = 0
	OverheadUI.TextLabel.Visible = false
end)

task.spawn(updateAI)

it pretty much changed all core logic, surprised it works honestly but am also glad

1 Like