Park/Rides/FlatRides/BumperCarsRide.cs
using HC3;
using System;

public sealed class BumperCarsRide : BasicRide
{
	[Property] public List<GameObject> Carts { get; set; } = new();
	[Property] Vector3 PlayAreaSize { get; set; } = new Vector3( 40, 20, 10 );
	private Dictionary<GameObject, Vector3> cartVelocities = new();
	private float speed = 80f;
	private float radius = 20.5f;

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( WorldPosition, PlayAreaSize ) );
		Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
		Gizmo.Draw.SolidBox( BBox.FromPositionAndSize( WorldPosition, PlayAreaSize ) );

		foreach ( var cart in Carts )
		{
			if ( !cart.IsValid() ) continue;
			Gizmo.Draw.Color = Color.Blue.WithAlpha( 0.5f );
			Gizmo.Draw.SolidSphere( cart.WorldPosition, radius * 2 );
		}
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();
		if ( !RideStarted ) return;

		var nextPositions = new Dictionary<GameObject, Vector3>();

		foreach ( var cart in Carts )
		{
			if ( !cart.IsValid() ) continue;

			var velocity = cartVelocities.GetValueOrDefault( cart, Vector3.Random.Normal.WithZ( 0 ) * speed );
			var newPos = cart.WorldPosition + velocity * Time.Delta;

			var local = newPos - WorldPosition;
			var half = PlayAreaSize / 2;

			// Bounce off walls
			if ( MathF.Abs( local.x ) > half.x )
			{
				velocity.x *= -1;
				local.x = MathF.Sign( local.x ) * half.x;

				Sound.Play( "bumper_car_hit_01", newPos );
			}
			if ( MathF.Abs( local.y ) > half.y )
			{
				velocity.y *= -1;
				local.y = MathF.Sign( local.y ) * half.y;
			}

			newPos = WorldPosition + local;

			cartVelocities[cart] = velocity;
			nextPositions[cart] = newPos;
		}

		for ( int i = 0; i < Carts.Count; i++ )
		{
			for ( int j = i + 1; j < Carts.Count; j++ )
			{
				var a = Carts[i];
				var b = Carts[j];

				if ( !a.IsValid() || !b.IsValid() ) continue;

				var posA = nextPositions[a];
				var posB = nextPositions[b];

				var delta = posB - posA;
				var distSq = delta.WithZ( 0 ).LengthSquared;
				var minDist = radius * 2;

				if ( distSq < minDist * minDist && distSq > 0.01f )
				{
					// Basic bounce: swap velocity
					var vA = cartVelocities[a];
					var vB = cartVelocities[b];

					cartVelocities[a] = vB;
					cartVelocities[b] = vA;
				}
			}
		}

		foreach ( var cart in Carts )
		{
			if ( !cart.IsValid() ) continue;

			cart.WorldPosition = nextPositions[cart];
			cart.WorldRotation = cart.WorldRotation.LerpTo( Rotation.LookAt( cartVelocities[cart].Normal, Vector3.Up ), Time.Delta * 10f );
		}
	}

	protected override void OnRideStarted()
	{
		base.OnRideStarted();

		cartVelocities.Clear();

		foreach ( var cart in Carts )
		{
			if ( !cart.IsValid() ) continue;
			var dir = Vector3.Random.Normal.WithZ( 0 ).Normal;
			cartVelocities[cart] = dir * speed;
		}
	}
}