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;
}
}