Help with managing multiple weeping angel NPCs

I have been trying to put together a weeping angel ai that works with multiple players and weeping angels for a few days now. In my current system what I am doing is having alocal script inside starterCharacter scripts loop through each angel to check and see if it is on the players screen and if it is then checking to see if a raycast from the players camera can reach it. If botha are true the script fires an event meant to anchor all parts of the angel. The angels themselves are always pathfinding to the player and in addition to that have a part change its position to whatever the angel’s next waypoint to detect whether the player will see it if it moves to said waypoint(This is to prevent the angels from moving on the players screen before they get a chance to anchor upon detection) The current issue is that when I use multiple Angel NPCs often times they can still move even when in the players view, and in addition to that it seems that they all freeze only when a certain few are in the players view when the ones out of the players view should still be moving. This is the problem I am trying to solve. I will leave links to videos down below as well as the scripts used.

Video With a single NPC
Video With Multiple NPCS

**Local Script: **

local runService = game:GetService("RunService")
local collectionService = game:GetService("CollectionService")
local workService = game:GetService("Workspace") 
local players = game:GetService("Players")
local RS = game:GetService("ReplicatedStorage")
local player = players.LocalPlayer
local camera = workService.CurrentCamera
--local statue = game.Workspace:WaitForChild("statues"):WaitForChild("statueTester")
--local gazeDetection = statue:WaitForChild("gazeDetection")
--local gazeDetection2 = statue:WaitForChild("gazeDetection2")
--local statueHRP = statue:WaitForChild("HumanoidRootPart")
local seenEvent = RS:WaitForChild("Events"):WaitForChild("seenEvent")
local statues = collectionService:GetTagged("statue")
local rayResult = false

runService.Heartbeat:Connect(function(deltaTime: number) 
	for i, value in pairs(statues) do
		local _, onScreenGaze = camera:WorldToViewportPoint(value:WaitForChild("gazeDetection").Position) 
		local _, OSGx = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(3,0,0))
		local _, OSGy = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(0,3,0))
		local _, OSGz = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(0,0,2))
		local _, OSGx2 = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(-3,0,0))
		local _, OSGy2 = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(0,-3,0))
		local _, OSGz2 = camera:WorldToViewportPoint(value.gazeDetection.Position + Vector3.new(0,0,-3))
		local _, onScreenLArm = camera:WorldToViewportPoint(value["Left Arm"].Position)
		local _, onScreenRArm = camera:WorldToViewportPoint(value["Right Arm"].Position)
		local _, onScreenHead = camera:WorldToViewportPoint(value.Head.Position)
		local _, onScreenHRP = camera:WorldToViewportPoint(value.HumanoidRootPart.Position) 
		local _, onScreenTorso = camera:WorldToViewportPoint(value.Torso.Position)
		local onScreen = onScreenGaze or OSGx or OSGy or OSGz or OSGx2 or OSGy2 or OSGz2 or onScreenHead or onScreenLArm or onScreenRArm or onScreenHRP or onScreenTorso
		local rayOrigin = camera.CFrame.Position
		local rayParams = RaycastParams.new()
		rayParams.FilterDescendantsInstances = {player.Character}--filters you
		rayParams.FilterType = Enum.RaycastFilterType.Exclude
		for i, v in pairs(statues) do
			if v ~= value then
				table.insert(rayParams.FilterDescendantsInstances, v)--filters other statues
			end
		end
		-- raycasts to each part of statue from camera
		if onScreen then
			local rayCastGaze = workService:Raycast(rayOrigin, (value.gazeDetection.Position - rayOrigin).Unit * (value.gazeDetection.Position-rayOrigin).Magnitude, rayParams) 
				or workService:Raycast(rayOrigin, ((value.gazeDetection.Position + Vector3.new(3,0,0)) - rayOrigin).Unit * ((value.gazeDetection.Position + Vector3.new(2,0,0)) - rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, ((value.gazeDetection.Position + Vector3.new(0,3,0)) - rayOrigin).Unit * ((value.gazeDetection.Position + Vector3.new(0,2,0)) - rayOrigin).Magnitude, rayParams)  
				or workService:Raycast(rayOrigin, ((value.gazeDetection.Position + Vector3.new(0,0,3)) - rayOrigin).Unit * ((value.gazeDetection.Position + Vector3.new(0,0,2)) - rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, ((value.gazeDetection.Position + Vector3.new(-3,0,0)) - rayOrigin).Unit * ((value.gazeDetection.Position + Vector3.new(2,0,0)) - rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, ((value.gazeDetection.Position + Vector3.new(0,-3,0)) - rayOrigin).Unit * ((value.gazeDetection.Position + Vector3.new(0,2,0)) - rayOrigin).Magnitude, rayParams)
			local otherRays = workService:Raycast(rayOrigin, (value["Left Arm"].Position - rayOrigin).Unit * (value["Left Arm"].Position-rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, (value["Right Arm"].Position - rayOrigin).Unit * (value["Right Arm"].Position-rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, (value.Head.Position - rayOrigin).Unit * (value.Head.Position-rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, (value.HumanoidRootPart.Position - rayOrigin).Unit * (value.HumanoidRootPart.Position-rayOrigin).Magnitude, rayParams)
				or workService:Raycast(rayOrigin, (value.Torso.Position - rayOrigin).Unit * (value.Torso.Position-rayOrigin).Magnitude, rayParams)

			rayResult = rayCastGaze.Instance or otherRays.Instance
		end
		if onScreen and rayResult:FindFirstAncestor("statueTester") then
			seenEvent:FireServer("Freeze", value)
		else
			seenEvent:FireServer("Move", value)
		end	
	end
end)

**Anchor Script: **

local rs = game:GetService("ReplicatedStorage")
local seenEvent = rs:WaitForChild("Events"):WaitForChild("seenEvent")
local statue = script.Parent.Parent
local moveValue = statue:WaitForChild("moveValue")
local looking = {}
statue.PrimaryPart:SetNetworkOwner(nil)


seenEvent.OnServerEvent:Connect(function (player, status, value)
	print(value, " ", statue)
	
	if status == "Freeze" and statue == value then
		if not table.find(looking, player.UserId) then
			table.insert(looking, player.UserId)
		end
		if #looking > 0 then
			moveValue.Value = false
			for i, v in pairs(statue:GetChildren()) do
				if v:IsA("BasePart") and v.Name ~= "gazeDetection" then
					v.Transparency = 0
					v.Anchored = true
				end
			end
		end
	elseif status == "Move" and statue == value  then
		if table.find(looking, player.UserId) then
			table.remove(looking, table.find(looking, player.UserId))
		end
		if #looking <= 0 then
			moveValue.Value = true
			for i, v in pairs(statue:GetChildren()) do
				if v:IsA("BasePart")  and v.Name ~= "gazeDetection" then
					v.Transparency = 1
					v.Anchored = false
				end
			end
		end
	
	end
	
end)

**Move Script: **

local PFS = game:GetService("PathfindingService")
local players = game:GetService("Players")
local RS = game:GetService("RunService")
local statue = script.Parent.Parent
local humanoid = statue:WaitForChild("Humanoid")
local gazeDetection = statue:WaitForChild("gazeDetection")
local moveValue = statue:WaitForChild("moveValue")

statue.PrimaryPart:SetNetworkOwner(nil)

local walkSpeed = statue:GetAttribute("WalkSpeed")
local sprintSpeed = statue:GetAttribute("SprintSpeed")
local damage = statue:GetAttribute("Damage")
local waypoints = game.Workspace.waypoints:GetChildren()


local function getPath(destination)
	local path = PFS:CreatePath({
		AgentHeight = 6;
		AgentRadius = 6;
		AgentCanJump = false;
		AgentCanClimb = false;
		Costs =  {
			--adding things later
		}
		
	})
	path:ComputeAsync(statue.HumanoidRootPart.Position, destination.Position)
	return path
end

local function findNearestTarget()
	local nearestTarget
	local maxDistance = 1000
	for index, player in pairs(players:GetPlayers()) do
		if player.Character then
			if player.Character:FindFirstChild("HumanoidRootPart") then
				local target = player.Character:FindFirstChild("HumanoidRootPart")
				local distance = (target.Position - statue.HumanoidRootPart.Position).Magnitude
				if distance < maxDistance then
					nearestTarget = target
					maxDistance = distance
				end
			end
		end
	end
	if nearestTarget == nil then
		local waypoints = game.Workspace:WaitForChild("waypoints"):GetChildren()
		nearestTarget = waypoints[math.random(1,#waypoints)]
	end
	return nearestTarget
end

local function attack(target)
	local distance = (statue.HumanoidRootPart.Position - target.Position).Magnitude
	if distance > 3 then
		humanoid:MoveTo(target.Position)
	elseif distance < 3 then
		target.Parent.Humanoid:TakeDamage(damage)
	end
end

local function walkTo(destination)
	local path = getPath(destination)
	if path.Status == Enum.PathStatus.Success then
	    local pathWay = path:GetWaypoints()
		for index, waypoint in pairs(pathWay) do
			local target = findNearestTarget()
			if (statue.HumanoidRootPart.Position - target.Position).Magnitude < 15 and target.Parent:FindFirstChild("Humanoid") then
				statue.Humanoid.WalkSpeed = walkSpeed
				attack(target)
			else
				if moveValue.Value == true then
					if pathWay[index+2]~= nil and (statue.HumanoidRootPart.Position - target.Position).Magnitude > 15 then
						gazeDetection.Position = pathWay[index+2].Position + Vector3.new(0,3,0)
						statue.Humanoid.WalkSpeed = sprintSpeed
						humanoid:MoveTo(waypoint.Position)
						humanoid.MoveToFinished:Wait()
					elseif pathWay[index+1] ~= nil and (statue.HumanoidRootPart.Position - target.Position).Magnitude > 10 then
						gazeDetection.Position = pathWay[index+1].Position + Vector3.new(0,3,0)
						statue.Humanoid.WalkSpeed = sprintSpeed
						humanoid:MoveTo(waypoint.Position)
						humanoid.MoveToFinished:Wait()
					else
						gazeDetection.Position = waypoint.Position + Vector3.new(0,3,0)
						statue.Humanoid.WalkSpeed = sprintSpeed
						humanoid:MoveTo(waypoint.Position)
						humanoid.MoveToFinished:Wait()
					end
				end
				--if moveValue.Value == true then
				--	statue.Humanoid.WalkSpeed = sprintSpeed
				--	humanoid:MoveTo(waypoint.Position)
				--	humanoid.MoveToFinished:Wait()
				--end				
			end
		end
	else
		humanoid:MoveTo(destination.Position - (statue.HumanoidRootPart.CFrame.LookVector*10))
	end
end

while wait(0.01) do
	local target = findNearestTarget()
	walkTo(target)
end

I think the most obvious problem here is that your oring together all the raycast calls is just going to short-circuit as soon as a ray hits anything, whether it’s part of a statue or not, and skip the rest of the raycasts. Instead of using or, you need to do the raycasts in a loop, and do your FindFirstAncestor("statueTester") test on the result.Instance for each raycast, and early-exit from the loop with break as soon as you find a ray that reaches the statue. You could technically do this with or, but you’d need the or between all the calls to FindFirstAncestor("statueTester"), not just between the Raycast calls, because you don’t care only that there is a result, you need to know whether the ray hit the statue or something else in the environment.

Personally, there are a few other things I’d do differently:

I would only use the seenEvent.OnServerEvent to update the visibility bool for each player, I would not have the setting of transparency or anchoring happen with every event. Instead, have the server update all the statues just once per frame, based on if anyone is looking at them. Your code is updating each statue as many as N times per frame, where N is the number of players.

Secondly, because of server-client latency, it will always be possible for a player to see a statue update while they are looking at it, if they move their camera to point to it after the server has move it, but before the move has replicated to their client. Preventing this is not trivial; if you made all the statues local models, players would never see them move, but you’d have another problem to solve which is that clients could get out of sync with each other. A statue that moved for other players but whose update has been deferred on one client (because they looked at the spot right before the move), will be in a different position on that one client until they look away and allow the move to happen.

Lastly, if you are going to use Player.UserId in your looking table, be sure that you are removing players when they disconnect, otherwise if someone disconnects while looking at a statue, it will get stuck with that player’s UserId in the looking array, never able to move again.