Rides/TrackRides/ITrackSection.cs
using Sandbox.Utility;
using System;
namespace HC3.Rides;
#nullable enable
/// <summary>
/// Helper struct wrapping a <see cref="TrackElement"/> with information about its place in a <see cref="ITrackSection"/>.
/// </summary>
/// <param name="Index">Index of the element in <see cref="ITrackSection.Elements"/></param>
/// <param name="Element">The element in question.</param>
/// <param name="StartDistance">Distance along the track at the start of the element.</param>
/// <param name="EndDistance">Distance along the track at the end of the element.</param>
public readonly record struct PlacedTrackElement(
int Index,
TrackElement Element,
float StartDistance,
float EndDistance )
{
public TrackFeature Feature => Element.Feature;
}
/// <summary>
/// Interface for a continuous section of track, made out of a sequence of <see cref="TrackElement"/>s.
/// </summary>
public interface ITrackSection
{
/// <summary>
/// Position and orientation of the track between each element.
/// </summary>
IReadOnlyList<TrackNode> Nodes { get; }
/// <summary>
/// Describes the shape of the track, and what special features it has.
/// </summary>
IReadOnlyList<TrackElement> Elements { get; }
/// <summary>
/// Total length of track in game units.
/// </summary>
float Length { get; }
/// <summary>
/// Gets the total length of the track from the first node to the given <paramref name="nodeIndex"/>.
/// </summary>
float GetDistanceAtNode( int nodeIndex );
/// <summary>
/// Does track obstruct the given <paramref name="trackGridPos"/>, optionally ignoring elements that neighbour <paramref name="ignoreNodes"/>.
/// </summary>
public bool HasObstruction( Vector3Int trackGridPos, IReadOnlySet<TrackNode>? ignoreNodes = null ) => true;
Transform SampleAtDistance( float distance );
}
public static class TrackSectionExtensions
{
/// <summary>
/// Is this track a closed loop?
/// </summary>
public static bool IsCycle( this ITrackSection track ) =>
track.Nodes.Count > 1 && track.Nodes[0] == track.Nodes[^1];
public static int GetElementIndex( this ITrackSection track, float distance )
{
if ( track.Elements.Count < 1 ) return -1;
if ( distance < 0f ) return 0;
if ( distance >= track.Length ) return track.Elements.Count - 1;
var minIndex = 0;
var maxIndex = track.Elements.Count;
while ( maxIndex - minIndex > 1 )
{
var midIndex = (maxIndex + minIndex) / 2;
var midDistance = track.GetDistanceAtNode( midIndex );
if ( midDistance.AlmostEqual( distance ) )
{
return midIndex;
}
if ( midDistance > distance )
{
maxIndex = midIndex;
}
else
{
minIndex = midIndex;
}
}
return Math.Clamp( minIndex, 0, track.Elements.Count - 1 );
}
public static Transform SampleAtNode( this ITrackSection track, int nodeIndex ) =>
track.SampleAtDistance( track.GetDistanceAtNode( nodeIndex ) );
public static Transform SampleAtDistance( this ITrackSection track, float distance, float sampleOffset )
{
var frontSample = track.SampleAtDistance( distance + sampleOffset );
var backSample = track.SampleAtDistance( distance - sampleOffset );
var position = (frontSample.Position + backSample.Position) * 0.5f;
var rotation = Rotation.Slerp( frontSample.Rotation, backSample.Rotation, 0.5f );
return new Transform( position, rotation );
}
public static float NormalizeDistance( this ITrackSection track, float distance )
{
var trackLength = track.Length;
return track.IsCycle()
? distance - MathF.Floor( distance / trackLength ) * trackLength
: Math.Clamp( distance, 0f, trackLength );
}
public static float NormalizeDelta( this ITrackSection track, float delta )
{
var trackLength = track.Length;
return track.IsCycle()
? delta - MathF.Floor( delta / trackLength + 0.5f ) * trackLength
: delta;
}
public static Transform SampleAtDistance( this ITrackSection track, TrackDefinition? trackDef, Spline spline, float distance )
{
if ( spline.PointCount < 2 ) return Transform.Zero;
distance = track.NormalizeDistance( distance );
var elementIndex = track.GetElementIndex( distance );
var prevDist = track.GetDistanceAtNode( elementIndex );
var nextDist = track.GetDistanceAtNode( elementIndex + 1 );
var element = track.Elements[elementIndex];
var prevNode = track.Nodes[elementIndex];
var nextNode = track.Nodes[elementIndex + 1];
var invertedRoll = element.Direction is TurnDirection.Right ? 180f : -180f;
return SampleTransform( spline, trackDef,
prevNode.Orientation.Banking.Roll() + (prevNode.Orientation.Inverted ? invertedRoll : 0f),
nextNode.Orientation.Banking.Roll() + (nextNode.Orientation.Inverted ? invertedRoll : 0f),
distance, prevDist, nextDist );
}
private static Transform SampleTransform( Spline spline, TrackDefinition? trackDef,
float prevRoll, float nextRoll,
float distance, float prevDist, float nextDist )
{
var sample = spline.SampleAtDistance( distance );
var pivotOffset = (trackDef?.RailHeight ?? 0f) + 8f;
if ( nextRoll > prevRoll + 270f )
{
nextRoll -= 360f;
}
else if ( nextRoll < prevRoll - 270f )
{
nextRoll += 360f;
}
// Make roll super smooth instead of linearly interpolating
var t = (distance - prevDist) / (nextDist - prevDist);
if ( prevRoll is < -95f or > 95f )
{
t = Easing.SineEaseOut( t );
}
else if ( nextRoll is < -95f or > 95f )
{
t = Easing.SineEaseIn( t );
}
else
{
t = Easing.SineEaseInOut( t );
}
var roll = MathX.Lerp( prevRoll, nextRoll, t );
var rotation = Rotation.LookAt( sample.Tangent, sample.Up ) * Rotation.FromRoll( roll );
return new Transform( Vector3.Up * pivotOffset ).ToWorld( new Transform( sample.Position - rotation.Up * pivotOffset, rotation ) );
}
public static PlacedTrackElement? GetElements( this ITrackSection track, float headPosition, float length, List<PlacedTrackElement> elements )
{
if ( track.Elements.Count == 0 ) return null;
var headIndex = track.GetElementIndex( track.NormalizeDistance( headPosition ) );
var tailIndex = track.GetElementIndex( track.NormalizeDistance( headPosition - length ) );
if ( tailIndex > headIndex )
{
headIndex += track.Elements.Count;
}
for ( var i = headIndex; i >= tailIndex; --i )
{
var index = i % track.Elements.Count;
elements.Add( new PlacedTrackElement( index, track.Elements[index],
track.GetDistanceAtNode( index ),
track.GetDistanceAtNode( index + 1 ) ) );
}
if ( !track.IsCycle() && headIndex >= elements.Count - 1 )
{
return null;
}
var nextIndex = (headIndex + 1) % track.Elements.Count;
return new PlacedTrackElement( nextIndex, track.Elements[nextIndex],
track.GetDistanceAtNode( nextIndex ),
track.GetDistanceAtNode( nextIndex + 1 ) );
}
}