DiamondDebris.Crushing.cs
using System;
using System.Collections.Generic;
using Sandbox;
using static Diamonds.DiamondBoard;
namespace Diamonds;
public sealed partial class DiamondDebris
{
readonly record struct CrushFace( Vector2 Normal, float Distance, Vector2 Velocity, int Owner );
readonly List<CrushFace> crushFaces = new();
readonly record struct CrushDiamond( Vector2 Center, Vector2 Velocity );
readonly List<CrushDiamond> crushDiamonds = new();
bool crushDiamondsMoving;
Vector2 crushPosition, crushVelocity;
float crushDelta;
void CacheCrushDiamonds()
{
#if STANDALONE
#endif
// Cross the native boundary once per diamond per step, not once for
// every shard/face pair. Refresh after stepping so enclosure stays exact.
crushDiamonds.Clear();
crushDiamondsMoving = false;
foreach ( var body in settledBodies ) Cache( body );
foreach ( var body in activeBodies ) Cache( body );
void Cache( PhysicsBody body )
{
var position = body.Position;
var velocity = body.Velocity;
crushDiamonds.Add( new( new( position.x, position.y ), new( velocity.x, velocity.y ) ) );
crushDiamondsMoving |= velocity.LengthSquared > 1;
}
}
void CrushTrappedChips( float delta )
{
#if STANDALONE
#endif
// Rebuilt lock/shatter colliders can already enclose a fragment before
// their first step, with no closing velocity left for the seam test.
CrushEmbeddedShards();
// Stationary gems/walls cannot close a press, even in a dense shard pile.
if ( !crushDiamondsMoving ) return;
crushDelta = delta;
for ( int i = particles.Count - 1; i >= 0; i-- )
{
var chip = particles[i];
if ( chip.IsSpark ) continue;
crushPosition = chip.Position;
var velocity = chip.Body.Velocity;
crushVelocity = new Vector2( velocity.x, velocity.y );
crushFaces.Clear();
int owner = 0;
foreach ( var diamond in crushDiamonds ) AddDiamondFaces( chip, diamond, ++owner );
for ( int edge = 0; edge < Valleys * 2; edge++ )
{
float x = edge * Width * 0.5f;
AddFace( chip, new Vector2( x, BoardHeight - (edge % 2 == 0 ? Height * 0.5f : 0) ),
new Vector2( x + Width * 0.5f, BoardHeight - (edge % 2 == 0 ? 0 : Height * 0.5f) ), Vector2.Zero, -1 );
}
foreach ( var tooth in SideWallTeeth() )
{
float x = LaneX( tooth.Lane );
bool left = tooth.Lane < 0;
var top = new Vector2( x, tooth.Y - Height * 0.5f );
var tip = new Vector2( x + (left ? 1 : -1) * Width * 0.5f, tooth.Y );
var bottom = new Vector2( x, tooth.Y + Height * 0.5f );
AddFace( chip, left ? top : bottom, tip, Vector2.Zero, left ? -2 : -3 );
AddFace( chip, tip, left ? bottom : top, Vector2.Zero, left ? -2 : -3 );
}
foreach ( var corner in CornerFills( FillCornerPockets ) )
AddDiamondFaces( chip, new( new Vector2( LaneX( corner.Lane ), corner.Y ), Vector2.Zero ), corner.Lane == 0 ? -2 : -3 );
AddFace( chip, new Vector2( BoardWidth, 0 ), new Vector2( 0, 0 ), Vector2.Zero, -4 );
if ( !IsPressedFlat( chip, delta ) ) continue;
AwardCrush( chip );
EmitSmoke( chip, true );
chip.Body.Remove();
particles.RemoveAt( i );
}
}
void CrushEmbeddedShards()
{
#if STANDALONE
#endif
Span<Vector2> polygon = stackalloc Vector2[8];
for ( int i = particles.Count - 1; i >= 0; i-- )
{
var shard = particles[i];
if ( shard.IsSpark ) continue;
var position = shard.Position;
var vertices = polygon[..shard.Vertices.Length];
bool transformed = false;
float coveredArea = 0;
bool embedded = false;
foreach ( var diamond in crushDiamonds )
{
if ( !NearDiamond( position, shard.Radius, diamond.Center ) ) continue;
if ( EnclosesShard( diamond.Center, position, shard.Radius ) ) { embedded = true; break; }
if ( !transformed ) { TransformShard( shard, vertices ); transformed = true; }
coveredArea += CoveredArea( vertices, diamond.Center );
}
// Board diamonds have disjoint interiors: adding their clipped areas
// detects a fragment buried across shared faces or corner junctions.
// A centroid-only check misses precisely those internal seams.
if ( transformed && coveredArea > PolygonArea( vertices ) * 0.55f ) embedded = true;
if ( !embedded )
{
if ( !transformed || ClearOfDiamonds( vertices, position, shard.Radius ) )
{
shard.LastClearTransform = shard.Body.Transform;
shard.HasClearTransform = true;
}
continue;
}
// Other shards can force a solver overlap without a diamond closing
// the pocket. Recover the last clear pose if it is still available.
// A newly placed/moving gem covering that pose still causes a crush.
if ( RestoreClearShardPose( shard, vertices ) ) continue;
AwardCrush( shard );
EmitSmoke( shard, true );
shard.Body.Remove();
particles.RemoveAt( i );
}
}
void AwardCrush( Particle shard )
{
if ( currentBoard.GameOver ) return;
EmitCrushSunburst( shard );
crushAwards.Add( new( shard.Position, shard.ColorIndex, currentBoard.NextShardPoints ) );
currentBoard.AwardShardCrush( 1 );
CrushedThisFrame++;
}
static bool EnclosesShard( Vector2 center, Vector2 position, float radius )
{
var offset = position - center;
// Distance from the centroid to the nearest diamond face. Tolerate
// shallow solver overlap, but never let a gem contain a shard's core.
float penetration = (1 - MathF.Abs( offset.x ) / (Width / 2) - MathF.Abs( offset.y ) / (Height / 2))
/ MathF.Sqrt( 4 / (Width * Width) + 4 / (Height * Height) );
return penetration > MathF.Max( 1.5f, radius * 0.2f );
}
void AddDiamondFaces( Particle chip, CrushDiamond diamond, int owner )
{
var center = diamond.Center;
var difference = crushPosition - center;
float reach = chip.Radius * 2.5f + (diamond.Velocity - crushVelocity).Length * crushDelta;
if ( MathF.Abs( difference.x ) > Width * 0.5f + reach ||
MathF.Abs( difference.y ) > Height * 0.5f + reach ) return;
var velocity = diamond.Velocity;
var top = center + new Vector2( 0, -Height * 0.5f );
var right = center + new Vector2( Width * 0.5f, 0 );
var bottom = center + new Vector2( 0, Height * 0.5f );
var left = center + new Vector2( -Width * 0.5f, 0 );
AddFace( chip, top, right, velocity, owner );
AddFace( chip, right, bottom, velocity, owner );
AddFace( chip, bottom, left, velocity, owner );
AddFace( chip, left, top, velocity, owner );
}
void AddFace( Particle chip, Vector2 a, Vector2 b, Vector2 velocity, int owner )
{
var edge = b - a;
float length = edge.Length;
var tangent = edge / length;
var offset = crushPosition - a;
float along = Vector2.Dot( offset, tangent );
// Stay on the actual segment; extrapolated planes at tips aren't presses.
if ( along < 0 || along > length ) return;
var normal = new Vector2( tangent.y, -tangent.x );
float distance = Vector2.Dot( offset, normal );
float reach = chip.Radius * 2.5f + (velocity - crushVelocity).Length * crushDelta;
if ( distance < -chip.Radius || distance > reach ) return;
crushFaces.Add( new( normal, distance, velocity, owner ) );
}
bool IsPressedFlat( Particle chip, float delta )
{
for ( int a = 0; a < crushFaces.Count; a++ )
for ( int b = a + 1; b < crushFaces.Count; b++ )
{
var first = crushFaces[a];
var second = crushFaces[b];
// Adjacent faces of one gem and zigzag valley supports cannot crush.
if ( first.Owner == second.Owner || Vector2.Dot( first.Normal, second.Normal ) > -0.95f ) continue;
float min = float.PositiveInfinity, max = float.NegativeInfinity;
var rotation = chip.Body.Rotation;
foreach ( var vertex in chip.Vertices )
{
var rotated = rotation * new Vector3( vertex.x, vertex.y, 0 );
float projection = Vector2.Dot( new Vector2( rotated.x, rotated.y ), first.Normal );
min = MathF.Min( min, projection ); max = MathF.Max( max, projection );
}
float thickness = max - min;
float gap = first.Distance + second.Distance;
float closing = Vector2.Dot( first.Velocity - second.Velocity, first.Normal );
// Test the substep's approaching surfaces before Box2D ejects a trapped
// chip. Small solver overlaps are harmless; require >55% compression.
if ( closing > 1 && gap - closing * delta < thickness * 0.45f ) return true;
}
return false;
}
}