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;
}
}
}
}