Help with angle wrapping

I’m dealing with a trig issue about angle wrapping, where if an angle crosses the bounds in range [-180, 180), it wraps around. For example, if an angle exceeds 180, it will wrap back around to -180, which is causing the following issue:


The red line in the circle represents -180 and 180. Note how the target radians are being outputted.

This is what I am trying to accomplish:


See how the NPC’s head only wraps when you are directly behind them? How can I accomplish that, no matter what direction the NPC is facing?

Here is the function:

function lookAt(npcCharacter)
	
	local INIT_TARG_RX = npcCharacter.Head.Orientation.X
	local INIT_TARG_RY = npcCharacter.Head.Orientation.Y
	local MAX_X, MAX_Y = math.rad(30), math.rad(60)
	local LOOK_DIST = 10
	local FRICTION = 1
	
	local rx, ry = 0, math.rad(INIT_TARG_RY)
	local targRX, targRY = 0, math.rad(INIT_TARG_RY)
	
	local function moveHead(initCFrame)
		
		rx += (targRX - rx) * FRICTION
		ry += (targRY - ry) * FRICTION
		
		local offset = CFrame.new(0, 0.5, 0) -- 0.5 for half the head's size
		local neckCFrame = CFrame.new((initCFrame * CFrame.new(0, 1, 0)).Position)
		local newCFrame = neckCFrame * CFrame.fromOrientation(rx, ry, 0) * offset
		
		npcCharacter.Head.CFrame = newCFrame
	end
	
	while task.wait() do
		
		-- Don't run if the player don't exist
		Character = Player.Character
		if not Character then continue end
		
		-- The player's RootPart is required
		RootPart = Character:FindFirstChild("HumanoidRootPart")
		if not RootPart then continue end
		
		local initCFrame = npcCharacter.Torso.CFrame
		local initRX, initRY = initCFrame:ToOrientation()
		
		local magnitude = (RootPart.Position + Vector3.new(0, 1.5, 0) - npcCharacter.Head.Position).Magnitude
		if magnitude > LOOK_DIST then -- Is the player out of range?
			
			-- Player is out of range, reset rotation
			targRX = math.rad(INIT_TARG_RX)
			targRY = math.rad(INIT_TARG_RY)
			
			moveHead(initCFrame)
			
			continue
		end
		
		-- Player is in range, stare into the player's soul
		local lookAt = RootPart.Position
		local rotatedCFrame = CFrame.lookAt(initCFrame.Position, lookAt)
		
		targRX, targRY = rotatedCFrame:ToOrientation()
		targRX = math.clamp(targRX, -MAX_X, MAX_X)
		targRY = math.clamp(targRY, initRY - MAX_Y, initRY + MAX_Y)
		
		moveHead(initCFrame)
	end
end

In the last bit of lines, I am getting the target angles “targRX, targRY” (the angles of which the NPC’s head points towards the player) by using :ToOrientation() (which I’m assuming is the problem because I have to deal with it’s boundaries). I then clamp these angles so that way the NPC’s aren’t snapping their heads around to look at you.

Maybe I’m asking for a function that normalizes these angles? Or maybe this is too extra and I should just use :Lerp()? I looked around the dev forum but only found a single post about angle wrapping, which the solution was using :Lerp() instead, but that solution doesn’t deal with clamping like I have here. I looked on Stack Overflow but that only taught me the terminology of this mess lol.

How do I fix this angle wrapping?

Why not just convert you angle into a 0-360 range?

local angle180 = -180
local angle360 = angle180 < 0 and angle180 + 360 or angle180