Game/Debris.cs
namespace Monolith;

/// <summary>
/// The cubes that just got destroyed, thrown outward in a hot colour so a blast reads as
/// something violent happening to the rock rather than voxels quietly vanishing.
///
/// Shares one procedural cube Model across every piece, so a burst costs a handful of
/// GameObjects and no mesh building.
/// </summary>
public sealed class Debris : Component
{
	private static Model cubeModel;
	private static int liveCount;

	/// <summary>Hard ceiling: at high fire rates this would otherwise flood the scene.</summary>
	private const int MaxLive = 160;

	private Vector3 velocity;
	private Vector3 spin;
	private float lifetime;
	private float startScale;
	private GameTimeSince timeSinceSpawn;
	private ModelRenderer renderer;
	private Color hot;

	/// <summary>Spawns a burst at a blast site. Count and spread scale with the blast.</summary>
	public static void Burst( Scene scene, Vector3 position, float worldRadius, bool isCharge )
	{
		if ( scene == null ) return;

		int count = Math.Clamp( (int)(worldRadius / 22f), 2, isCharge ? 14 : 6 );

		for ( int i = 0; i < count; i++ )
		{
			if ( liveCount >= MaxLive ) return;

			var obj = new GameObject( true, "Debris" );
			obj.NetworkMode = NetworkMode.Never;

			// Start scattered through the blast volume, not all from one point.
			obj.WorldPosition = position + Vector3.Random.Normal * (worldRadius * Game.Random.Float( 0.15f, 0.75f ));
			obj.WorldRotation = Rotation.Random;

			float scale = Tuning.VoxelSize * Game.Random.Float( 0.35f, 0.9f );
			obj.WorldScale = scale;

			var piece = obj.AddComponent<Debris>();
			piece.startScale = scale;
			piece.velocity = Vector3.Random.Normal * (worldRadius * Game.Random.Float( 1.6f, 4.2f ));
			piece.spin = Vector3.Random.Normal * Game.Random.Float( 120f, 520f );
			piece.lifetime = Game.Random.Float( 0.5f, 1.1f );

			// Charges throw white-hot, ordinary shots ember-orange.
			piece.hot = isCharge
				? Color.Lerp( new Color( 1f, 0.95f, 0.65f ), new Color( 1f, 0.55f, 0.12f ), Game.Random.Float() )
				: Color.Lerp( new Color( 1f, 0.62f, 0.18f ), new Color( 0.85f, 0.22f, 0.06f ), Game.Random.Float() );

			var mr = obj.AddComponent<ModelRenderer>();
			mr.Model = GetCubeModel();
			mr.Tint = piece.hot;

			piece.renderer = mr;
			liveCount++;
		}
	}

	protected override void OnStart()
	{
		timeSinceSpawn = 0;
	}

	protected override void OnDestroy()
	{
		liveCount = Math.Max( 0, liveCount - 1 );
	}

	protected override void OnUpdate()
	{
		// Frozen while a blocking screen or the pause menu is up. See GameTime.
		if ( GameTime.Paused )
			return;

		float t = timeSinceSpawn / lifetime;

		if ( t >= 1f )
		{
			GameObject.Destroy();
			return;
		}

		WorldPosition += velocity * Time.Delta;
		WorldRotation *= Rotation.From( spin.x * Time.Delta, spin.y * Time.Delta, spin.z * Time.Delta );

		// Drag, so pieces decelerate instead of sailing off forever.
		velocity = velocity.LerpTo( Vector3.Zero, 2.2f * Time.Delta );

		// Cool from hot to dark as it shrinks away.
		WorldScale = startScale * (1f - t);

		if ( renderer.IsValid() )
			renderer.Tint = Color.Lerp( hot, new Color( 0.15f, 0.05f, 0.03f ), t * t );
	}

	/// <summary>One unit cube, built once and shared by every piece of debris.</summary>
	private static Model GetCubeModel()
	{
		if ( cubeModel != null )
			return cubeModel;

		var vb = new VertexBuffer();
		vb.Init( true );

		// 6 faces of a unit cube centred on the origin.
		Vector3[] normals =
		{
			Vector3.Forward, Vector3.Backward, Vector3.Left,
			Vector3.Right, Vector3.Up, Vector3.Down,
		};

		int index = 0;

		foreach ( var n in normals )
		{
			// Build an orthonormal basis for this face.
			var tangent = MathF.Abs( n.z ) > 0.9f ? Vector3.Forward : Vector3.Up;
			var u = Vector3.Cross( n, tangent ).Normal;
			var v = Vector3.Cross( n, u ).Normal;

			var centre = n * 0.5f;

			var p0 = centre - u * 0.5f - v * 0.5f;
			var p1 = centre + u * 0.5f - v * 0.5f;
			var p2 = centre + u * 0.5f + v * 0.5f;
			var p3 = centre - u * 0.5f + v * 0.5f;

			vb.Add( new Vertex( p0, n, u, new Vector4( 0, 0, 0, 0 ) ) );
			vb.Add( new Vertex( p1, n, u, new Vector4( 1, 0, 0, 0 ) ) );
			vb.Add( new Vertex( p2, n, u, new Vector4( 1, 1, 0, 0 ) ) );
			vb.Add( new Vertex( p3, n, u, new Vector4( 0, 1, 0, 0 ) ) );

			vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 1 ); vb.AddRawIndex( index + 2 );
			vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 2 ); vb.AddRawIndex( index + 3 );

			index += 4;
		}

		var mesh = new Mesh( Material.Load( "materials/default.vmat" ) );
		mesh.CreateBuffers( vb );

		cubeModel = new ModelBuilder().AddMesh( mesh ).Create();
		return cubeModel;
	}
}