Park/Rides/BasicRide.EntranceExit.cs
using HC3.Terrain;
using System;
namespace HC3;
/// <summary>
/// A ride's entrance or exit point.
/// </summary>
public sealed class RideEntranceExit : Building, IGridObjectEvent
{
[Property]
public bool IsExit { get; set; }
/// <summary>
/// Which ride is this bound to?
/// </summary>
[Property]
public BasicRide Ride { get; set; }
public Path Path { get; set; }
protected override void OnValidate()
{
base.OnValidate();
Cost = 0;
GuestCost = 0;
Need = null;
BuildingType = BuildingType.Special;
IsWalkable = true;
}
public void RotateToFaceRideEdge( TileEdge edge )
{
var dir = edge.GetDirection();
WorldRotation = Rotation.LookAt( -new Vector3( dir.x, dir.y ) );
}
void IGridObjectEvent.NeighborsChanged( GridCell cell )
{
UpdatePath();
if ( Ride.IsValid() )
{
Ride.QueueSystem.BuildQueuePath();
}
}
/// <summary>
/// Look for an associated path
/// </summary>
public void UpdatePath()
{
foreach ( var edge in PathMask.GetEdges() )
{
var neighbour = GridManager.Instance.GetCell( GridObject.GridBounds.Position + edge.RotateDegrees( -WorldRotation.Yaw() ).GetDirection() );
if ( neighbour is null ) continue;
if ( neighbour.GetComponents<Path>().FirstOrDefault( x => x.IsValid() && !x.GhostPreview ) is { } found )
{
Path = found;
break;
}
}
}
}
/// <summary>
/// Handles the exit and entrance point for a <see cref="BasicRide"/> -- we'll prompt the player to spawn a entrance and exit as part of the placement flow.
/// </summary>
partial class BasicRide : IGridObjectEvent
{
[RequireComponent, Property]
public QueueGuestSystem QueueSystem { get; set; } = new();
[Sync( SyncFlags.FromHost )]
private RideEntranceExit _entrance { get; set; }
[Sync( SyncFlags.FromHost )]
private RideEntranceExit _exit { get; set; }
public bool HasEntrance => _entrance.IsValid();
public bool HasExit => _exit.IsValid();
public RideEntranceExit Entrance => _entrance!;
public RideEntranceExit Exit => _exit!;
public bool HasEntranceExit => HasEntrance && HasExit;
/// <summary>
/// How many guests are currently queueing for this ride.
/// </summary>
public int QueueingCount => QueueSystem?.GetGuestCount() ?? 0;
public override bool IsAvailable()
{
return !IsBroken && HasEntranceExit && OpenState is OpenState.Open;
}
public override Vector3 GetEntrance()
{
if ( HasEntranceExit )
{
// Do we have queues?
if ( QueueSystem.IsActive() )
return QueueSystem.GetQueuePosition();
return _entrance!.WorldPosition;
}
return WorldPosition;
}
protected override void OnStart()
{
if ( IsProxy )
{
UpdateEntranceExit();
}
base.OnStart();
}
public Vector3 GetExit()
{
return HasEntranceExit ? _exit!.WorldPosition : WorldPosition;
}
public virtual IEnumerable<(Vector3Int GridPos, TileEdge Edge)> ValidEntranceExitPositions
{
get
{
var bounds = GridObject.GridBounds;
var height = (int)MathF.Round( WorldPosition.z / GridManager.HeightStep );
for ( var x = bounds.Left; x < bounds.Right; x++ )
{
yield return (new Vector3Int( x, bounds.Top - 1, height ), TileEdge.YMin);
yield return (new Vector3Int( x, bounds.Bottom, height ), TileEdge.YMax);
}
for ( var y = bounds.Top; y < bounds.Bottom; y++ )
{
yield return (new Vector3Int( bounds.Left - 1, y, height ), TileEdge.XMin);
yield return (new Vector3Int( bounds.Right, y, height ), TileEdge.XMax);
}
}
}
void IGridObjectEvent.NeighborsChanged( GridCell cell )
{
UpdateEntranceExit();
// Rebuild queue path
QueueSystem.BuildQueuePath();
}
public bool AddToQueue( Guest guest )
{
if ( !QueueSystem.IsActive() )
return false;
// These delinquents will skip the queue! Naughty.
// TODO: other guests may report this action.
const float skipQueueChance = 0.8f;
var hasQueueSkip = guest.Delinquency > 0.6f && Game.Random.Float() < skipQueueChance;
int? queueIndex = hasQueueSkip ? 0 : null;
return QueueSystem.Join( guest, queueIndex );
}
public bool RemoveFromQueue( Guest guest )
{
return QueueSystem.Leave( guest );
}
protected void UpdateEntranceExit()
{
// Check the entrance/exits are still in valid positions
if ( _entrance.IsValid() )
{
var gridPos = GridManager.WorldToGridPosition3D( _entrance.WorldPosition );
if ( !ValidEntranceExitPositions.Any( x => x.GridPos == gridPos ) )
{
_entrance.DestroyGameObject();
_entrance = null;
QueueSystem?.Signage?.DestroyGameObject();
}
}
if ( _exit.IsValid() )
{
var gridPos = GridManager.WorldToGridPosition3D( _exit.WorldPosition );
if ( !ValidEntranceExitPositions.Any( x => x.GridPos == gridPos ) )
{
_exit.DestroyGameObject();
_exit = null;
}
}
// Don't need to do anything
if ( HasEntranceExit ) return;
foreach ( var (gridPos, edge) in ValidEntranceExitPositions )
{
FindEntranceExit( gridPos, edge );
}
DirtyErrors();
}
private void FindEntranceExit( Vector3Int gridPos, TileEdge edge )
{
var gridPos2d = new Vector2Int( gridPos.x, gridPos.y );
var entranceExit = GridManager.Instance.GetCell( gridPos2d )?
.GetComponents<RideEntranceExit>()
.FirstOrDefault();
if ( !entranceExit.IsValid() ) return;
if ( entranceExit.Ride.IsValid() ) return;
entranceExit.RotateToFaceRideEdge( edge );
if ( entranceExit.IsExit && !_exit.IsValid() )
{
_exit = entranceExit;
entranceExit.Ride = this;
}
else if ( !entranceExit.IsExit && !_entrance.IsValid() )
{
_entrance = entranceExit;
entranceExit.Ride = this;
}
foreach ( var kv in queueColors )
{
UpdateQueueColor( kv.Value, kv.Key );
}
GenerateDecorations();
}
public override void OnUnloaded( Guest guest )
{
base.OnUnloaded( guest );
guest.SetSequenceOverride( null );
guest.WorldPosition = GetExit();
guest.Transform.ClearInterpolation();
guest.ActionController.ClearAction();
}
}