Angular limits on raycasting?

Hey, and thanks for reading in advance.

I’m helping a buddy out with his Outbreak Survival look-alike game called Mutation. We’ve been working on it for a bit, and have decided to advertise once we’ve made a short, visual-learning tutorial.

The Zombie half of the tutorial includes a section that requires the player to overcome 3 - 4 AI controlled humans that present a rather difficult challenge that the player must overcome by experimenting with various mutations and seeing what works.

I’m currently coding the guidance system for the AI that manages vision and detection, but the way it works now - the AI has eyes in the back of its head. I want to impose angular limits on the vision ray it casts so that it can see at about 60 degrees in either direction and 70 degrees up or down, but I’m terrible with trig - if anyone has something I could use as a reference or a simple formula, I’d very much appreciate it!

Relevant Code:

local Character = script.Parent
local Humanoid = Character.Humanoid

local Root = Character:WaitForChild("HumanoidRootPart")
local Head = Character:WaitForChild("Head")

--
local rotator = Instance.new("BodyGyro")
rotator.MaxTorque = Vector3.new(0, 0, 0)
rotator.P = 10000; rotator.D = 400
rotator.CFrame = Root.CFrame
rotator.Parent = Root

--
local CURRENT_AGGRO = 0
local DETECT_THRESH = 200
local ACTIVE_MODE = false

local AGGRO_FALLPOINT = 5
local LAST_SIGHTING = tick()/2

--
local detectParams = RaycastParams.new()
detectParams.FilterDescendantsInstances = {Character}
detectParams.FilterType = Enum.RaycastFilterType.Blacklist
detectParams.IgnoreWater = true

--
function speak(text, color)
	game.ReplicatedStorage.Remotes.ServerMessage:FireAllClients(
		text, color or Color3.new(1, 1, 1), Character.Name
	)
end

-- PRIMARY LOOP
spawn(function()
	while Humanoid.Health > 0 and Character.Parent and wait() do
		local searchFor = {}; local fireAt = {}
		
		for _,player in pairs(game.Players:GetPlayers()) do
			if player.Team == game.Teams.Zombies then
				if player.Character and player.Character:IsDescendantOf(workspace)
					and player.Character:FindFirstChild("Head")
					and player.Character:FindFirstChild("HumanoidRootPart")
					and player.Character:FindFirstChild("Humanoid")
					and player.Character.Humanoid.Health > 0 then
					searchFor[#searchFor + 1] = player.Character
				end
			end
		end
		
		if #searchFor > 0 then
			for _,target in pairs(searchFor) do
				local visionCheck = workspace:Raycast(Head.Position, (target.Head.Position - Head.Position).Unit * 200, detectParams)
				
				if visionCheck and visionCheck.Instance:IsDescendantOf(target) then
					LAST_SIGHTING = tick()
					
					if ACTIVE_MODE then
						fireAt[#fireAt + 1] = target
					else
						local drawDistance = (visionCheck.Position - Head.Position).Magnitude
						CURRENT_AGGRO += DETECT_THRESH * (1 - (drawDistance / 200))
						
						if CURRENT_AGGRO >= DETECT_THRESH then
							ACTIVE_MODE = true; speak("Undead spotted!")
							rotator.MaxTorque = Vector3.new(0, math.huge, 0)
							fireAt[#fireAt + 1] = target
						end
					end
				end
			end
			
			if ACTIVE_MODE then
				if #fireAt > 0 then
					table.sort(fireAt, function(a, b)
						local distA = (a.Head.Position - Head.Position).Magnitude
						local distB = (b.Head.Position - Head.Position).Magnitude
						
						return distA < distB
					end)
					
					local target = fireAt[1]
					local t_head = target.Head
					local t_root = target.HumanoidRootPart
					
					rotator.CFrame = CFrame.new(Root.Position, Vector3.new(t_root.Position.X, Root.Position.Y, t_root.Position.Z))
					
				elseif (tick() - LAST_SIGHTING) >= AGGRO_FALLPOINT then
					CURRENT_AGGRO = math.ceil((CURRENT_AGGRO * .92) - 1) * (1/15)

					if CURRENT_AGGRO <= 0 then
						ACTIVE_MODE = false; speak("I lost 'em..")
						rotator.MaxTorque = Vector3.new(0, 0, 0)
						rotator.CFrame = Root.CFrame
					end
				end
			end
		end
	end
end)