AI/Guests/GuestManager.cs
using Sandbox.Diagnostics;
using System;

namespace HC3;

/// <summary>
/// Events related to guests
/// </summary>
public interface IGuestEvents : ISceneEvent<IGuestEvents>
{
	/// <summary>
	/// Called when a guest enters the park.
	/// </summary>
	/// <param name="guest"></param>
	public void OnGuestEnter( Guest guest );

	/// <summary>
	/// Called when a guest leaves the park.
	/// </summary>
	/// <param name="guest"></param>
	public void OnGuestLeave( Guest guest /*, Reason? */ );
}

public sealed partial class GuestManager : Component
{
	/// <summary>
	/// Singleton
	/// </summary>
	public static GuestManager Instance { get; private set; }

	/// <summary>
	/// Max amount of guests in the park
	/// </summary>
	[ConVar( "hc3.debug.guest.max", ConVarFlags.Cheat )]
	public static int MaxGuests { get; set; } = 1000;

	/// <summary>
	/// A multiplier for guests so we can test high guest count
	/// </summary>
	[ConVar( "hc3.debug.guest.mult", ConVarFlags.Cheat )]
	public static float GuestMultiplier { get; set; } = 1f;

	/// <summary>
	/// How frequently should a guest enter the park
	/// </summary>
	[ConVar( "hc3.debug.guest.stagger", ConVarFlags.Cheat )]
	public static float GuestStaggerTime { get; set; } = 1f;

	/// <summary>
	/// The guest prefab
	/// </summary>
	[Property]
	public GameObject GuestPrefab { get; set; }

	/// <summary>
	/// All guests currently in the park?
	/// </summary>
	public IEnumerable<Guest> GuestList => _guests;
	private HashSet<Guest> _guests = new HashSet<Guest>();

	/// <summary>
	/// How many guests are currently in the park?
	/// </summary>
	public int GuestCount => _guests.Count;

	/// <summary>
	/// How many guests are currently alive?
	/// </summary>
	public int AliveCount => _guests.Count;

	/// <summary>
	/// A quick reference to the <see cref="ParkManager"/>
	/// </summary>
	public ParkManager ParkManager => ParkManager.Instance;

	/// <summary>
	/// How long until we create a new guest (if at all)
	/// </summary>
	TimeUntil timeUntilNextGuest;

	/// <summary>
	/// The park entrance, just find one in the scene
	/// </summary>
	private ParkEntrance _cachedEntrance;
	private ParkEntrance Entrance => _cachedEntrance ??= Scene.GetAll<ParkEntrance>().FirstOrDefault();

	protected override void OnStart()
	{
		Instance = this;
	}

	protected override void OnUpdate()
	{
		// Don't do anything if we aren't the host
		if ( !Networking.IsHost )
			return;

		// Don't bother adding new guests if we're closed
		if ( !ParkManager.Instance?.IsOpen() ?? false )
			return;

		// Don't create guests if we meet or exceed the target
		if ( AliveCount >= GetGuestTarget() )
			return;

		if ( timeUntilNextGuest )
		{
			CreateGuest();
			timeUntilNextGuest = GuestStaggerTime;
		}
	}

	/// <summary>
	/// Returns a target guest count, so we'll strive to spawn this many guests in the park over time
	/// </summary>
	/// <returns></returns>
	public int GetGuestTarget()
	{
		// Maximum possible number of guests

		var baseTarget = 0;
		baseTarget += Building.ActiveBuildings.Sum( x => x.AddedGuests );

		// Park rating can increase max guests
		var ratingFactor = ParkManager.Rating / 5f;
		baseTarget *= Math.Max( 1, ratingFactor.FloorToInt() );

		// Admission fee can decrease max guests, and *slightly* increase if cheaper
		var admissionFactor = (ParkManager.AdmissionFee / 50f);
		if ( admissionFactor < 1f )
		{
			admissionFactor *= 3f;
		}
		baseTarget = (int)MathF.Floor( baseTarget / MathF.Max( 0.25f, admissionFactor ) );

		if ( DayNightController.IsPeakSeason() )
		{
			// Double the influx of guests in peak
			baseTarget *= 2;
		}

		baseTarget *= GuestMultiplier.CeilToInt();

		return Math.Clamp( baseTarget, 0, MaxGuests );
	}

	/// <summary>
	/// Spawns a guest
	/// </summary>
	[Button( "Enter a Guest", "arrow_forward_ios" )]
	public void CreateGuest()
	{
		CreateGuestCore();
	}

	/// <summary>
	/// Cached spawn points
	/// </summary>
	private List<SpawnPoint> _cachedSpawnPoints;

	private void CreateGuestCore( Action<Guest> onInit = null )
	{
		_cachedSpawnPoints ??= Scene.GetAll<SpawnPoint>().ToList();
		var target = _cachedSpawnPoints.OrderBy( x => Random.Shared.Next() ).FirstOrDefault()?.GameObject.WorldPosition ?? Entrance.WorldPosition;

		var guestObject = GuestPrefab?.Clone( new CloneConfig()
		{
			Transform = new Transform( target, Entrance.WorldRotation, 1f ),
			StartEnabled = false
		} );

		Assert.True( guestObject.IsValid(), "Guest prefab not hooked up to GuestManager" );

		var guest = guestObject.GetComponent<Guest>( true );

		Assert.True( guest.IsValid(), "Guest prefab does not have a Guest component" );

		onInit?.Invoke( guest );

		guestObject.Enabled = true;
		guestObject.NetworkSpawn();
	}

	/// <summary>
	/// Guest has entered the park
	/// </summary>
	public bool RegisterGuest( Guest guest, bool isNewGuest )
	{
		if ( !_guests.Add( guest ) ) return false;
		if ( !isNewGuest ) return true;

		// Can't afford to get in (admission fee went up while guest is walking to enter)
		if ( guest.Money < ParkManager.AdmissionFee )
		{
			return false;
		}

		ParkManager.GiveMoney( ParkManager.AdmissionFee, "Admission Fee" );
		IGuestEvents.Post( x => x.OnGuestEnter( guest ) );

		MoneyEffect.Broadcast( guest.WorldPosition + Vector3.Random * 10f + Vector3.Up * 50f, $"${ParkManager.AdmissionFee:N0}", Color.Green );

		if ( Stats.Get( "park.max_guests" ) < GuestCount )
		{
			Stats.Set( "park.max_guests", GuestCount );
		}

		return true;
	}

	/// <summary>
	/// Guest has left the park
	/// </summary>
	public bool UnregisterGuest( Guest guest )
	{
		if ( !_guests.Remove( guest ) ) return false;

		IGuestEvents.Post( x => x.OnGuestLeave( guest ) );
		return true;
	}
}