Make a Group of Pathfinding NPCs Move in Formation

After I fixed my pathfinding issue, I wanted to make all of my NPCs move in a line together. For example, if the player is in one part of the map, the NPCs form a line formation and march to the player
image
So far, the pathfinding is buggy, inefficient, and costly. I want to know if there is a better way to do this.

local PS = game:GetService("PathfindingService")
local ENEMY_FOLDER = workspace:WaitForChild("Bots")
local DESTINATION = Vector3.new(50, 0, 110)
local AGENT_RADIUS = 2
local FORMATION_RADIUS = 5
local WAYPOINT_REACH_DISTANCE = 2

local IDLE_ANIMATION_ID = "rbxassetid://109580015110725"
local WALK_ANIMATION_ID = "rbxassetid://108528753842263"

local function createDebugHitbox(size, position)
	local debugPart = Instance.new("Part")
	debugPart.Size = size
	debugPart.Position = position
	debugPart.Transparency = 0.6
	debugPart.Anchored = true
	debugPart.CanCollide = false
	debugPart.BrickColor = BrickColor.new("Bright red")
	debugPart.Parent = workspace
	task.delay(2, function()
		if debugPart then debugPart:Destroy() end
	end)
end

local function loadAnimation(npc, animationId)
	local humanoid = npc:FindFirstChild("Humanoid")
	if humanoid then
		local animation = Instance.new("Animation")
		animation.AnimationId = animationId
		local animator = humanoid:FindFirstChild("Animator") or humanoid:WaitForChild("Animator")
		return animator:LoadAnimation(animation)
	end
	return nil
end

local function playAnimation(animationTrack, shouldLoop)
	if animationTrack then
		animationTrack.Looped = shouldLoop
		animationTrack:Play()
	end
end

local function stopAnimation(animationTrack)
	if animationTrack then
		animationTrack:Stop()
	end
end

local function calculateFormationCenter(npcs)
	local totalPosition = Vector3.new(0, 0, 0)
	for _, npc in pairs(npcs) do
		totalPosition = totalPosition + npc.PrimaryPart.Position
	end
	return totalPosition / #npcs
end

local function calculateRelativePositions(npcs, center)
	local relativePositions = {}
	for _, npc in pairs(npcs) do
		relativePositions[npc] = npc.PrimaryPart.Position - center
	end
	return relativePositions
end

local function generatePath(startPos, endPos)
	local path = PS:CreatePath({
		AgentRadius = AGENT_RADIUS,
		AgentHeight = 5,
		AgentCanJump = true,
		AgentJumpHeight = 5,
		AgentMaxSlope = 45
	})
	path:ComputeAsync(startPos, endPos)
	return path
end

local function moveNPC(npc, targetPosition, idleTrack, walkTrack)
	local humanoid = npc:FindFirstChild("Humanoid")
	if humanoid then
		playAnimation(walkTrack, true)
		stopAnimation(idleTrack)
		humanoid:MoveTo(targetPosition)
		createDebugHitbox(Vector3.new(2, 2, 2), targetPosition)
		local reached = humanoid.MoveToFinished:Wait()
		playAnimation(idleTrack, true)
		stopAnimation(walkTrack)
		return reached
	end
end

local function followFormationPath(npcs, relativePositions, path, animationTracks)
	local waypoints = path:GetWaypoints()
	for _, waypoint in ipairs(waypoints) do
		local center = waypoint.Position
		local tasks = {}
		for npc, relativePosition in pairs(relativePositions) do
			local targetPosition = center + relativePosition
			local idleTrack = animationTracks[npc]["Idle"]
			local walkTrack = animationTracks[npc]["Walk"]
			table.insert(tasks, coroutine.create(function()
				moveNPC(npc, targetPosition, idleTrack, walkTrack)
			end))
		end
		for _, task in ipairs(tasks) do
			coroutine.resume(task)
		end
		task.wait(0.5)
	end
end

local function handleBots()
	local npcs = {}
	local animationTracks = {}
	for _, npc in pairs(ENEMY_FOLDER:GetChildren()) do
		if npc:IsA("Model") and npc:FindFirstChild("Humanoid") and npc:FindFirstChild("HumanoidRootPart") then
			npc.PrimaryPart = npc:FindFirstChild("HumanoidRootPart")
			table.insert(npcs, npc)
			animationTracks[npc] = {
				Idle = loadAnimation(npc, IDLE_ANIMATION_ID),
				Walk = loadAnimation(npc, WALK_ANIMATION_ID)
			}
		end
	end

	if #npcs == 0 then
		warn("No NPCs found in the 'Bots' folder.")
		return
	end

	while true do
		local formationCenter = calculateFormationCenter(npcs)
		local relativePositions = calculateRelativePositions(npcs, formationCenter)
		local path = generatePath(formationCenter, DESTINATION)

		if path.Status == Enum.PathStatus.Success then
			followFormationPath(npcs, relativePositions, path, animationTracks)
		else
			warn("Pathfinding failed.")
		end

		task.wait(2)
	end
end

handleBots()