DiamondDebris.Sliding.cs
using System;
using System.Collections.Generic;
using Sandbox;
using static Diamonds.DiamondBoard;

namespace Diamonds;

public sealed partial class DiamondDebris
{
	readonly HashSet<int> slidingExcluded = new();
	void EmitSlidingSparks( DiamondBoard board, float delta )
	{
#if STANDALONE
#endif
		if ( !board.IsSliding || board.SlideSpeed < 1 ) return;
		var moving = board.SlidingCells;
		var excluded = slidingExcluded;
		excluded.Clear();
		for ( int i = 0; i < moving.Count; i++ ) excluded.Add( moving[i].Index );
		int direction = Math.Sign( moving[0].To.Lane - moving[0].From.Lane );
		var tangent = new Vector2( direction * Width, Height ).Normal;
		var normal = new Vector2( direction * Height, -Width ).Normal;
		foreach ( var move in moving )
		{
			var cell = board.Settled[move.Index];
			var center = new Vector2( LaneX( cell.Lane ), cell.Y );
			// The lower face behind the motion rubs the supporting slope. The
			// opposite face closes into obstacles and belongs to impact feedback.
			var side = center + new Vector2( -direction * Width * 0.5f, 0 );
			var bottom = center + new Vector2( 0, Height * 0.5f );
			for ( int i = 0; i < board.Settled.Count; i++ )
			{
				if ( excluded.Contains( i ) ) continue;
				var support = board.Settled[i];
				var top = new Vector2( LaneX( support.Lane ), support.Y - Height * 0.5f );
				var end = new Vector2( top.x + direction * Width * 0.5f, support.Y );
				EmitSlidingSeam( side, bottom, top, end, tangent, normal, board.SlideSpeed, delta, cell.ColorIndex, support.ColorIndex );
			}
			// Wall teeth provide the same rubbing slopes as stationary gems.
			foreach ( var support in BoundaryDiamonds( FillCornerPockets ) )
			{
				if ( (support.Lane < LaneCount / 2 ? 1 : -1) != direction ) continue;
				var top = new Vector2( LaneX( support.Lane ), support.Y - Height * 0.5f );
				var end = new Vector2( top.x + direction * Width * 0.5f, support.Y );
				EmitSlidingSeam( side, bottom, top, end, tangent, normal, board.SlideSpeed, delta, cell.ColorIndex );
			}
			// Floor peaks have the same slopes as gems, including partial contact
			// as a diamond travels from a peak into a valley.
			int segment = (int)MathF.Floor( center.x / (Width * 0.5f) );
			for ( int i = Math.Max( 0, segment - 1 ); i <= Math.Min( Valleys * 2 - 1, segment + 1 ); i++ )
			{
				if ( (i % 2 == 0 ? 1 : -1) != direction ) continue;
				var a = new Vector2( i * Width * 0.5f, BoardHeight - (i % 2 == 0 ? Height * 0.5f : 0) );
				var b = new Vector2( (i + 1) * Width * 0.5f, BoardHeight - (i % 2 == 0 ? 0 : Height * 0.5f) );
				EmitSlidingSeam( side, bottom, a, b, tangent, normal, board.SlideSpeed, delta, cell.ColorIndex );
			}
		}
	}

	void EmitSlidingSeam( Vector2 a, Vector2 b, Vector2 c, Vector2 d,
		Vector2 tangent, Vector2 normal, float speed, float delta, int color, int? supportColor = null )
	{
		float slope = (b.y - a.y) / (b.x - a.x);
		if ( MathF.Abs( c.y - (a.y + (c.x - a.x) * slope) ) > 0.05f ) return;
		float left = MathF.Max( MathF.Min( a.x, b.x ), MathF.Min( c.x, d.x ) );
		float right = MathF.Min( MathF.Max( a.x, b.x ), MathF.Max( c.x, d.x ) );
		if ( right - left < 0.5f ) return; // Point contacts alone produce no friction.
		// Distance-based emission is naturally sparse during the slow start and
		// grows with speed. Stochastic rounding avoids per-frame minimum bursts.
		float expected = MathF.Min( 18, speed * delta * 0.32f );
		int count = (int)expected + (random.NextDouble() < expected % 1 ? 1 : 0);
		for ( int spark = 0; spark < count; spark++ )
		{
			// Escape at an open end of the contact seam, never inside either gem.
			bool trailing = random.NextDouble() < 0.68;
			var escape = tangent * (trailing ? -1 : 1);
			float x = escape.x < 0 ? left : right;
			var origin = new Vector2( x, a.y + (x - a.x) * slope );
			float radius = Range( 0.7f, 1.6f );
			for ( int attempt = 0; attempt < 10; attempt++ )
			{
				// Separate the spawn clearance from its launch angle: nearly tangent
				// sparks still need enough room for their small collision fixture.
				float angle = Range( 0.015f, random.NextDouble() < 0.6 ? 0.24f : 0.9f );
				var outward = escape * MathF.Cos( angle ) + normal * MathF.Sin( angle );
				var position = origin + escape * Range( 4, 12 ) + normal * Range( 2.5f, 5 );
				if ( !HasRoom( position, radius + 0.5f ) ) continue;
				float floor = BoardHeight - Height * 0.5f * MathF.Abs( position.x / (Width * 0.5f) % 2 - 1 );
				if ( position.y + radius >= floor ) continue;
				// Start gently with the slide, retaining a bounded spray at chain speeds.
				float launchSpeed = MathF.Min( 370, speed * 0.45f ) * Range( 0.65f, 1 );
				int sparkColor = supportColor.HasValue && random.NextDouble() < 0.5 ? supportColor.Value : color;
				AddSpark( position, outward * launchSpeed, radius, sparkColor );
				break;
			}
		}
	}
}