Player/Bot/PlayerBotController.Roam.cs
using System.Threading;

public partial class PlayerBotController
{
	private async Task<bool> Roam( CancellationToken token )
	{
		if ( FindAndUpdateTarget() != null )
		{
			// Switch to combat mode
			return false;
		}

		while ( !token.IsCancellationRequested )
		{
			bool result = await RunParallel(
				token,
				CheckForTarget,
				UpdateRotationToMatchMovement,
				token => RunSelector( token,
					PickupInventoryItem,
					GoSomewhereRandom
				)
			);

			if ( !result )
			{
				// still return true because we at least did some roaming
				return true;
			}
		}

		return false;
	}

	private async Task<bool> CheckForTarget( CancellationToken token )
	{
		while ( !token.IsCancellationRequested )
		{
			_currentTarget = FindAndUpdateTarget();

			if ( _currentTarget.IsValid() )
			{
				return false;
			}

			await Task.FixedUpdate();
		}

		return !token.IsCancellationRequested;
	}

	private async Task<bool> GoSomewhereRandom( CancellationToken token )
	{
		var randomPoint = Scene.NavMesh.GetRandomPoint();

		if ( !randomPoint.HasValue ) return false;

		TimeSince timeSinceLastConsumableSearch = 0;
		TimeSince timeSinceMoveStart = 0;
		const float MoveDuration = 10f;

		MeshAgent.MoveTo( randomPoint.Value );

		while ( timeSinceMoveStart < MoveDuration && !token.IsCancellationRequested )
		{
			if ( timeSinceLastConsumableSearch > 0.5f )
			{
				await TryDivertForConsumable(
					token: token
				);
				timeSinceLastConsumableSearch = 0;
			}

			MeshAgent.MoveTo( randomPoint.Value );
			await Task.FixedUpdate();
		}

		return !token.IsCancellationRequested;
	}

	private async Task<bool> PickupInventoryItem( CancellationToken token )
	{
		var inventoryPickupTarget = FindPickupsInRadius<InventoryPickup>(
			2000f,
			x => x.Items.Any( item => !Inventory.HasWeapon( item ) )
		).FirstOrDefault();

		if ( inventoryPickupTarget == null )
			return false;

		Vector3 inventoryPickupPosition = GetClosestPointOnPickupToNavMesh( inventoryPickupTarget, MeshAgent.Radius );
		TimeSince timeSinceLastConsumableSearch = 0;

		while ( IsPickupValid( inventoryPickupTarget ) && !token.IsCancellationRequested && inventoryPickupTarget.Items.Any( item => !Inventory.HasWeapon( item ) ) )
		{
			if ( timeSinceLastConsumableSearch > 0.5f )
			{
				await TryDivertForConsumable(
					token: token,
					mainTarget: inventoryPickupTarget.WorldPosition
				);
				timeSinceLastConsumableSearch = 0;
			}

			MeshAgent.MoveTo( inventoryPickupPosition );
			await Task.FixedUpdate();
		}

		return !token.IsCancellationRequested;
	}

	private async Task<bool> UpdateRotationToMatchMovement( CancellationToken token )
	{
		while ( !token.IsCancellationRequested )
		{
			var currentAimTarget = MeshAgent.GetLookAhead( 30.0f ).WithZ( Controller.EyePosition.z );

			var dir = currentAimTarget - Controller.EyePosition;

			if ( dir.LengthSquared > 0.01f * 0.01f )
			{
				var rotation = Rotation.LookAt( dir );
				Controller.EyeAngles = Controller.EyeAngles.LerpTo( rotation, 0.1f );
			}

			await Task.FixedUpdate();
		}

		return !token.IsCancellationRequested;
	}

}