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