Park/Rides/Queues/QueueGuestSystem.cs
using HC3.Terrain;
namespace HC3;
/// <summary>
/// A queue system set up on a ride that allocates guests, and positions them.
/// </summary>
public sealed class QueueGuestSystem : Component
{
[ConVar( "hc3.debug.queue" )]
public static bool DebugDraw { get; set; } = false;
[RequireComponent] public BasicRide Ride { get; set; }
[Property] public int GuestsPerTile { get; set; } = 4;
[Property] public float GuestSpacing { get; set; } = 16f;
public QueueSign Signage { get; private set; }
private List<Guest> _guests = new();
public List<Queue> _queuePath = new();
public bool IsActive() => _queuePath.Count > 0;
/// <summary>
/// Maximum number of guests this queue can currently hold.
/// </summary>
public int MaxGuests => GuestsPerTile * _queuePath.Count;
public bool IsFull()
{
return _guests.Count >= MaxGuests;
}
protected override void OnDisabled()
{
base.OnDisabled();
if ( Signage.IsValid() )
{
Signage.DestroyGameObject();
}
foreach ( var path in _queuePath )
{
if ( !path.IsValid() ) continue;
path.QueueSystem = null;
}
}
public void BuildQueuePath()
{
List<Queue> removed = new List<Queue>( _queuePath );
_queuePath.Clear();
var start = Ride.GetQueueStart();
if ( start is not null )
{
var visited = new HashSet<Queue>();
var current = start;
Queue previous = null;
while ( current != null )
{
removed.Remove( current );
_queuePath.Add( current );
var next = current.Connections
.OfType<Queue>()
.FirstOrDefault( x => x.IsValid() && x != previous && !visited.Contains( x ) );
if ( next.IsValid() )
{
if ( next.QueueSystem.IsValid() && next.QueueSystem != this )
break;
}
previous = current;
current = next;
}
}
foreach ( var path in removed )
{
if ( !path.IsValid() ) continue;
path.QueueSystem = null;
}
foreach ( var path in _queuePath )
{
if ( !path.IsValid() ) continue;
path.QueueSystem = this;
}
UpdatePositions();
UpdateSignage();
}
private void UpdateSignage()
{
if ( !Signage.IsValid() )
{
var obj = GameObject.Clone( "prefabs/queues/path_queue_sign.prefab" );
Signage = obj.GetComponent<QueueSign>();
Signage.SetRide( Ride );
}
Signage.UpdateText();
var path = _queuePath.LastOrDefault();
if ( path.IsValid() )
{
Signage.WorldPosition = path.WorldPosition;
// work out which way it should face
foreach ( var connection in path.Connections )
{
if ( connection.IsValid() && !_queuePath.Contains( connection ) && connection != Ride.Entrance )
{
var next = connection.WorldPosition;
Signage.WorldRotation = Rotation.LookAt( (path.WorldPosition - next).WithZ( 0 ) );
var edge = Signage.WorldRotation.ToTileEdge();
Signage.WorldPosition += Vector3.Up * path.GetHeightOffset( edge.GetOpposite() ) * GridManager.HeightStep;
return;
}
}
{
// not conneted to any other path piece, just use the last one (or entrance)
var prev = _queuePath.Count > 1 ? _queuePath.ElementAt( _queuePath.Count - 2 ).WorldPosition : Ride.Entrance.WorldPosition;
Signage.WorldRotation = Rotation.LookAt( (prev - path.WorldPosition).WithZ( 0 ) );
var edge = Signage.WorldRotation.ToTileEdge();
Signage.WorldPosition += Vector3.Up * path.GetHeightOffset( edge.GetOpposite() ) * GridManager.HeightStep;
}
}
else
{
if ( Ride.Entrance.IsValid() )
{
Signage.WorldPosition = Ride.Entrance.WorldPosition;
Signage.WorldRotation = Ride.Entrance.WorldRotation;
}
}
}
public bool Join( Guest guest, int? index = null )
{
if ( !_guests.Contains( guest ) )
{
if ( index.HasValue && index.Value < _guests.Count )
_guests.Insert( index.Value, guest );
else
_guests.Add( guest );
UpdatePositions();
return true;
}
return false;
}
public bool Leave( Guest guest )
{
if ( _guests.Remove( guest ) )
{
UpdatePositions();
return true;
}
return false;
}
public void Clear()
{
_guests.Clear();
UpdatePositions();
}
public bool Contains( Guest guest ) => _guests.Contains( guest );
public int GetPosition( Guest guest ) => _guests.IndexOf( guest );
public int GetGuestCount() => _guests.Count;
private void UpdatePositions()
{
if ( _queuePath.Count == 0 )
return;
// Could've been deleted at some point
_guests = _guests.Where( x => x.IsValid() ).ToList();
for ( int i = 0; i < _guests.Count; i++ )
{
var guest = _guests[i];
var spot = GetQueueSpot( i );
guest.WorldPosition = spot.Position;
guest.WorldRotation = spot.Rotation;
}
}
private Transform GetQueueSpot( int i )
{
float distance = i * GuestSpacing;
int segIndex = (int)(distance / GridManager.GridSize);
float t = (distance % GridManager.GridSize) / GridManager.GridSize;
segIndex = segIndex.Clamp( 0, _queuePath.Count - 1 );
var start = segIndex > 0 && _queuePath[segIndex - 1].IsValid() ? _queuePath[segIndex - 1].WorldPosition : Ride.Entrance.WorldPosition;
var end = _queuePath[segIndex].IsValid() ? _queuePath[segIndex].WorldPosition : start;
var position = Vector3.Lerp( start, end, t );
float z = start.z;
if ( segIndex > 0 )
{
// make sure we're giving the right Z pos at the point we've worked out above
var current = (t < 0.5f ? _queuePath[segIndex - 1] : _queuePath[segIndex]);
if ( current.IsValid() )
{
z = current.GetHeightAt( position );
}
}
var dir = (start - end).Normal;
return new Transform( position.WithZ( z ), Rotation.LookAt( dir.WithZ( 0 ) ) );
}
/// <summary>
/// Gets a position along the queue path based on how many guests are currently in line.
/// This is where the next guest should walk to.
/// </summary>
public Vector3 GetQueuePosition()
{
if ( _queuePath.Count == 0 )
return Vector3.Zero;
return GetQueueSpot( _guests.Count ).Position;
}
protected override void OnUpdate()
{
if ( !DebugDraw || !Ride.HasEntrance ) return;
var start = Ride.Entrance.WorldPosition;
var offset = new Vector3( 0, 0, 4 );
for ( int i = 0; i < _queuePath.Count; i++ )
{
var path = _queuePath[i];
if ( !path.IsValid() )
return;
Gizmo.Draw.Color = Color.Green;
Gizmo.Draw.LineThickness = 5;
Gizmo.Draw.Arrow( path.WorldPosition + offset, start + offset );
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.Text( i.ToString(), new Transform( path.WorldPosition + offset ), size: 16 );
start = path.WorldPosition;
}
Gizmo.Draw.Color = Color.Orange;
Gizmo.Draw.SolidSphere( GetQueuePosition(), 5 );
}
}