Park/Rides/BasicRide.cs
using System;
using HC3.Terrain;
namespace HC3;
/// <summary>
/// A basic ride. The difference between this and a building is that rides have loadable slots and can start/end rides.
/// They can also have queues.
/// </summary>
public partial class BasicRide : Building, ITintMaskEvents
{
[Property, Feature( "Ride" )] public int GuestLimit { get; set; } = 10;
[Title( "Wait For" ), Inspectable] public LoadMode MinimumLoad { get; set; } = LoadMode.NotEmpty;
[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" )] public float RideLength { get; set; } = 30.0f;
[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" ), MinMax( 5, 120 )] public float MinimumLoadTime { get; set; } = 5.0f;
[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" ), MinMax( 5, 120 )] public float MaximumLoadTime { get; set; } = 60.0f;
[Sync( SyncFlags.FromHost )]
public bool RideStarted { get; private set; }
[Sync( SyncFlags.FromHost )]
public float Durability { get; private set; } = 1f;
[Sync( SyncFlags.FromHost ), Change( nameof( OnIsBrokenChanged ) )]
public bool IsBroken { get; private set; }
public MusicPlayer MusicPlayer { get; private set; }
public TimeSince RideTime { get; set; } = 0.0f;
public TimeSince LoadingTime { get; set; } = 0f;
public override Group Group => new( "Building", BuildingType.ToString() );
/// <summary>
/// Tint channels for this building. These are used to tint the building's sections.
/// </summary>
public TintChannelData QueueRedChannel { get; set; } = new( true, Color.White );
public TintChannelData QueueGreenChannel { get; set; } = new( false, Color.White );
public TintChannelData QueueBlueChannel { get; set; } = new( false, Color.White );
public TintChannelData QueueAlphaChannel { get; set; } = new( false, Color.White );
protected override void OnEnabled()
{
base.OnEnabled();
MusicPlayer = GetOrAddComponent<MusicPlayer>();
}
protected override void OnUpdate()
{
if ( !Networking.IsHost )
return;
if ( !RideStarted )
{
if ( LoadingTime < MinimumLoadTime )
return;
int slotCount = 0;
int occupiedCount = 0;
foreach ( var s in GetLoadableSlots() )
{
slotCount++;
if ( !s.IsFree() ) occupiedCount++;
}
if ( occupiedCount >= GuestLimit || (MinimumLoad.IsSatisfied( occupiedCount, slotCount ) && LoadingTime >= MaximumLoadTime) )
{
StartRide();
}
}
else if ( RideLength > 0 && RideTime >= RideLength )
{
EndRide();
}
if ( RideStarted )
{
var guests = GetGuests();
foreach ( var guest in guests )
{
guest.Needs.Happiness += Time.Delta * 0.015f;
}
// Rides start to lose durability while they are being used
Durability -= Time.Delta * 0.003f;
Durability = Durability.Clamp( 0f, 1f );
}
else
{
if ( Durability <= 0f && !IsBroken )
{
BreakDown();
}
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if ( !Networking.IsHost )
return;
if ( _entrance.IsValid() )
_entrance.DestroyGameObject();
if ( _exit.IsValid() )
_exit.DestroyGameObject();
}
public override bool CanEnter( Guest guest )
{
if ( RideStarted ) return false;
return base.CanEnter( guest );
}
/// <summary>
/// What sequence to run while on this ride?
/// </summary>
[Property] public string LoadedSequence { get; set; } = "SitPose_Default";
public override void OnLoadedIn( Guest guest )
{
if ( !string.IsNullOrEmpty( LoadedSequence ) )
guest.SetSequenceOverride( LoadedSequence );
if ( GetOccupiedSlots() == 1 )
{
LoadingTime = 0f;
}
}
public override string GetInUseDescription( Guest guest )
{
return $"Riding {Title}";
}
public override string GetStatus()
{
if ( OpenState is OpenState.Open )
{
if ( RideStarted )
return "Running";
else
return "Waiting for guests";
}
return base.GetStatus();
}
protected override void OpenStateChanged()
{
base.OpenStateChanged();
LoadingTime = 0;
if ( OpenState is not OpenState.Open )
{
EndRide();
UnloadAll( false );
}
}
protected virtual bool CanStart()
{
return HasEntranceExit && OpenState is OpenState.Open;
}
public void Repair()
{
if ( !IsBroken )
return;
Durability = 1f;
IsBroken = false;
MusicPlayer.IsBrokenDown = false;
OnRideRepaired();
Stats.Increment( "staff.rides_repaired" );
}
public void Damage( float amount )
{
Durability = Math.Clamp( Durability - amount, 0f, 1f );
}
public void BreakDown()
{
IsBroken = true;
MusicPlayer.IsBrokenDown = true;
Durability = 0f;
OnRideBroken();
}
private void OnIsBrokenChanged()
{
DirtyErrors();
}
[Button( "Test Ride" )]
protected void StartRide()
{
if ( RideStarted ) return;
if ( !CanStart() ) return;
RideStarted = true;
RideTime = 0f;
OnRideStarted();
}
protected void EndRide()
{
if ( !RideStarted ) return;
RideStarted = false;
LoadingTime = 0;
var guestCount = GetOccupiedSlots();
OnRideEnded();
Uses += guestCount;
}
[Rpc.Broadcast( NetFlags.HostOnly )]
protected virtual void OnRideStarted()
{
}
[Rpc.Broadcast( NetFlags.HostOnly )]
protected virtual void OnRideEnded()
{
// Eject all guests
UnloadAll( true );
}
protected virtual void OnRideBroken()
{
}
protected virtual void OnRideRepaired()
{
}
protected override void GenerateDecorations()
{
base.GenerateDecorations();
var offset = new Vector3( -Size * GridManager.GridSize / 2.0f, 0 );
// TODO: do flooring type from the exit? no filled pieces right now so not bothering
/*
for ( int x = 0; x < Size.x; x++ )
{
for ( int y = 0; y < Size.y; y++ )
{
var position = offset + (new Vector3( x, y, 0 ) * GridManager.GridSize) + GridManager.CentreOffset;
var path = GameObject.Clone( "prefabs/paths/path_single.prefab", global::Transform.Zero, GameObject );
path.Flags = GameObjectFlags.NotSaved;
path.Tags.Add( "decor_generated" );
path.WorldRotation = Rotation.Identity;
path.LocalPosition = position;
}
}
*/
foreach ( var edge in GridManager.AllEdges )
{
int length = edge is TileEdge.Left or TileEdge.Right ? Size.y : Size.x;
Vector2Int dir = edge.GetDirection();
for ( int i = 0; i < length; i++ )
{
Vector2Int pos = edge switch
{
TileEdge.Left => new Vector2Int( 0, i ),
TileEdge.Right => new Vector2Int( Size.x - 1, i ),
TileEdge.Down => new Vector2Int( i, 0 ),
TileEdge.Up => new Vector2Int( i, Size.y - 1 ),
_ => default
};
Vector3 localPosition = offset + new Vector3( pos.x, pos.y ) * GridManager.GridSize + GridManager.CentreOffset;
Vector3 worldPosition = Transform.World.PointToWorld( localPosition );
Vector3Int gridAdjacent = GridManager.WorldToGridPosition( worldPosition ) + edge.RotateDegrees( -WorldRotation.Yaw() ).GetDirection();
// skip entrance/exit adjacent tiles
if ( (_entrance.IsValid() && GridManager.WorldToGridPosition( _entrance.WorldPosition ) == gridAdjacent) ||
(_exit.IsValid() && GridManager.WorldToGridPosition( _exit.WorldPosition ) == gridAdjacent) )
{
continue;
}
var fence = GameObject.Clone( "prefabs/fences/fence_small.prefab", global::Transform.Zero, GameObject );
fence.Tags.Add( "decor_generated" );
fence.Flags = GameObjectFlags.NotSaved;
fence.LocalPosition = localPosition;
var facing = edge.GetOpposite().GetDirection();
fence.LocalRotation = Rotation.LookAt( new Vector3( facing.x, facing.y ) );
}
}
}
/// <summary>
/// The start of the queue (where the entrance is) if it exists
/// </summary>
public Queue GetQueueStart()
{
if ( !_entrance.IsValid() ) return null;
return _entrance.Path as Queue;
}
/// <summary>
/// The end of the queue (where the sign is) if it exists
/// </summary>
public Queue GetQueueEnd()
{
return QueueSystem._queuePath.LastOrDefault() ?? GetQueueStart();
}
public TintMaskComponent GetQueueTintComponent()
{
if ( !_entrance.IsValid() ) return null;
var queue = GetQueueStart();
if ( !queue.IsValid() ) return null;
var tintComp = queue.GameObject.GetComponentsInChildren<TintMaskComponent>().FirstOrDefault();
if ( tintComp is not null )
{
return tintComp;
}
return null;
}
private Dictionary<TintChannel, Color> queueColors { get; set; } = new();
public Dictionary<TintChannel, Color> QueueColors => queueColors;
public void UpdateQueueColor( Color color, TintChannel channel )
{
var end = _entrance;
if ( !end.IsValid() ) return;
queueColors[channel] = color;
foreach ( var path in end.GetComponentsInChildren<TintMaskComponent>() )
{
if ( !path.IsValid() ) continue;
if ( channel == TintChannel.Primary )
path.PrimaryColor = color;
else if ( channel == TintChannel.Secondary )
path.SecondaryColor = color;
else if ( channel == TintChannel.Accent )
path.AccentColor = color;
else if ( channel == TintChannel.Detail )
path.DetailColor = color;
}
if ( !QueueSystem.IsActive() ) return;
foreach ( var path in QueueSystem._queuePath )
{
if ( !path.IsValid() ) continue;
path.UpdateTint( channel, color );
}
}
void ITintMaskEvents.OnColorsChanged( TintMaskComponent component )
{
var tintMask = GetTintComponent();
if ( tintMask != component )
return;
var red = QueueRedChannel;
red.Color = component.PrimaryColor;
QueueRedChannel = red;
var green = QueueGreenChannel;
green.Color = component.SecondaryColor;
QueueGreenChannel = green;
var blue = QueueBlueChannel;
blue.Color = component.AccentColor;
QueueBlueChannel = blue;
var alpha = QueueAlphaChannel;
alpha.Color = component.DetailColor;
QueueAlphaChannel = alpha;
}
}