DiamondPanel.Shine.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	static Color ShineTint( Color color ) =>
		new( 0.85f + color.r * 0.15f, 0.85f + color.g * 0.15f, 0.85f + color.b * 0.15f );

	static Fill ShineReflection( Vector2 start, Vector2 end, Color tint, float alpha ) =>
		Fill.LinearGradient( start, end, [
			new( 0, Color.Transparent ), new( 0.15f, tint.WithAlpha( alpha * 0.12f ) ),
			new( 0.43f, Color.White.WithAlpha( alpha ) ), new( 0.6f, tint.WithAlpha( alpha * 0.85f ) ),
			new( 0.7f, Color.Transparent ), new( 0.79f, tint.WithAlpha( alpha * 0.65f ) ),
			new( 0.86f, Color.Transparent ), new( 1, Color.Transparent )] );

	void DrawShardShine( Painter p, DiamondDebris.Particle chip )
	{
		float t = chip.ShineProgress;
		if ( t < 0 || t >= 1 ) return;
		var direction = new Vector2( chip.ShineDirection, 1 ).Normal;
		float min = float.PositiveInfinity, max = float.NegativeInfinity;
		foreach ( var point in chip.Vertices )
		{
			float projection = Vector2.Dot( point, direction );
			min = MathF.Min( min, projection ); max = MathF.Max( max, projection );
		}
		float halfWidth = (max - min) * 0.3f;
		float sweep = min - halfWidth + (max - min + halfWidth * 2) * t;
		// Fill the actual rotating triangle so the reflection never spills outside it.
		p.Fill = ShineReflection( direction * (sweep - halfWidth), direction * (sweep + halfWidth),
			ShineTint( Palette[chip.ColorIndex] ), chip.Opacity );
		p.Polygon( chip.Vertices );
	}

	Vector2 DamageShakeOffset( int index )
	{
		// Reuse the real-time damage clock and its survivor index remapping.
		// This is presentation only: collision and settlement positions stay exact.
		for ( int i = Board.DamageShines.Count - 1; i >= 0; i-- )
		{
			var shine = Board.DamageShines[i];
			if ( !shine.SettledIndices.Contains( index ) ) continue;
			float t = Math.Clamp( shine.Age / DiamondBoard.DamageShineDuration, 0, 1 );
			// Lerp the magnitude from 8 to zero with quadratic ease-out.
			float magnitude = 8 * (1 - t) * (1 - t);
			return new Vector2( MathF.Cos( shine.ShakeAngle ), MathF.Sin( shine.ShakeAngle ) ) * magnitude;
		}
		return Vector2.Zero;
	}

	readonly List<DiamondBoard.Diamond> damageShakeCells = new();
	IReadOnlyList<DiamondBoard.Diamond> DamageShakeCells()
	{
		if ( Board.DamageShines.Count == 0 ) return Board.Settled;
		var cells = damageShakeCells;
		cells.Clear();
		for ( int i = 0; i < Board.Settled.Count; i++ )
		{
			var offset = DamageShakeOffset( i );
			var cell = Board.Settled[i];
			cells.Add( cell with { Lane = cell.Lane + offset.x / (DiamondBoard.Width * 0.5f), Y = cell.Y + offset.y } );
		}
		return cells;
	}

	void DrawDamageShines( Painter p )
	{
		using var scope = p.Scope();
		p.Translate( new Vector2( Left, Top ) );
		p.Stroke = Stroke.None;
		float w = DiamondBoard.Width * 0.5f, h = DiamondBoard.Height * 0.5f;
		Vector2[] corners = [new( 0, -h ), new( w, 0 ), new( 0, h ), new( -w, 0 )];
		foreach ( var shine in Board.DamageShines )
		{
			// A single board-space gradient sweeps the union of the affected cells.
			// Polygon fills are the mask: gaps, concavities and other gems stay clear.
			var polygons = new List<(Vector2[] Points, int ColorIndex, float Opacity)>();
			foreach ( int index in shine.SettledIndices )
			{
				if ( index < 0 || index >= Board.Settled.Count ) continue;
				var cell = Board.Settled[index];
				var center = new Vector2( DiamondBoard.LaneX( cell.Lane ), cell.Y ) + DamageShakeOffset( index );
				polygons.Add( (corners.Select( c => center + c ).ToArray(), cell.ColorIndex, 1) );
			}
			foreach ( int index in shine.ShatteringIndices )
			{
				if ( !Board.IsDamagePaused || index < 0 || index >= Board.Shattering.Count ) continue;
				var cell = Board.Shattering[index];
				var center = new Vector2( DiamondBoard.LaneX( cell.Lane ), cell.Y );
				polygons.Add( (corners.Select( c => center + c ).ToArray(), cell.ColorIndex, 1) );
			}
			if ( polygons.Count == 0 ) continue;
			var direction = new Vector2( shine.Direction, 1 ).Normal;
			float min = float.PositiveInfinity, max = float.NegativeInfinity;
			foreach ( var polygon in polygons )
			foreach ( var point in polygon.Points )
			{
				float projection = Vector2.Dot( point, direction );
				min = MathF.Min( min, projection ); max = MathF.Max( max, projection );
			}
			float t = Math.Clamp( shine.Age / DiamondBoard.DamageShineDuration, 0, 1 );
			float halfWidth = 14 + MathF.Min( 14, (max - min) * 0.08f );
			float sweep = min - halfWidth + (max - min + halfWidth * 2) * t;
			var start = direction * (sweep - halfWidth);
			var end = direction * (sweep + halfWidth);
			foreach ( var polygon in polygons )
			{
				// Brief whole-shape flash under a broad glass reflection and a sharp,
				// offset secondary streak. Orientation never changes during this hit.
				var color = Palette[polygon.ColorIndex];
				// A pure-white core with only a faint gem tint in the surrounding reflection.
				var tint = ShineTint( color );
				float flash = 0.22f * MathF.Pow( MathF.Max( 0, 1 - t * 3 ), 2 ) * polygon.Opacity;
				p.Fill = tint.WithAlpha( flash );
				p.Polygon( polygon.Points );
				p.Fill = ShineReflection( start, end, tint, polygon.Opacity );
				p.Polygon( polygon.Points );
			}
		}
	}
}