Demos/TarkovInventory/Grid.cs
using System.Collections.Generic;
namespace Sandbox.TarkovInventory;
// Anchor (top-left) + footprint. Rotation is a (W,H) swap about the anchor cell.
public readonly record struct GridRect(int X, int Y, int W, int H)
{
// Rotate 90°: swap W/H, keep the top-left anchor (pivot is the anchor cell, not the centre).
public GridRect Rotated() => this with { W = H, H = W };
}
// Pure occupancy model: no Panel, no UI state. T = item key (string id in the demo).
public sealed class Grid<T> where T : notnull
{
readonly Dictionary<T, GridRect> _placed = new();
readonly Dictionary<(int x, int y), T> _occ = new();
public Grid( int cols, int rows )
{
Cols = cols;
Rows = rows;
}
public int Cols { get; }
public int Rows { get; }
public IReadOnlyDictionary<T, GridRect> Placed => _placed;
public bool CanPlace( GridRect r ) => CanPlaceCore( r, hasIgnore: false, default! );
public bool CanPlaceIgnoring( GridRect r, T ignore ) => CanPlaceCore( r, hasIgnore: true, ignore );
bool CanPlaceCore( GridRect r, bool hasIgnore, T ignore )
{
if ( r.W <= 0 || r.H <= 0 ) return false;
if ( r.X < 0 || r.Y < 0 || r.X + r.W > Cols || r.Y + r.H > Rows ) return false;
var cmp = EqualityComparer<T>.Default;
for ( int y = r.Y; y < r.Y + r.H; y++ )
for ( int x = r.X; x < r.X + r.W; x++ )
if ( _occ.TryGetValue( (x, y), out var occ ) && !(hasIgnore && cmp.Equals( occ, ignore )) )
return false;
return true;
}
public bool TryPlace( T item, GridRect r )
{
if ( _placed.ContainsKey( item ) ) return false;
if ( !CanPlace( r ) ) return false;
Occupy( item, r );
return true;
}
public bool TryMove( T item, int x, int y )
{
if ( !_placed.TryGetValue( item, out var cur ) ) return false;
return TryMove( item, cur with { X = x, Y = y } );
}
// Move + optionally reshape (rotation): validates the NEW footprint, ignoring the item's own cells.
public bool TryMove( T item, GridRect dest )
{
if ( !_placed.TryGetValue( item, out var cur ) ) return false;
if ( !CanPlaceIgnoring( dest, item ) ) return false;
Free( cur );
Occupy( item, dest );
return true;
}
public void Remove( T item )
{
if ( !_placed.TryGetValue( item, out var r ) ) return;
Free( r );
_placed.Remove( item );
}
public GridRect? FindFirstFree( int w, int h )
{
for ( int y = 0; y + h <= Rows; y++ )
for ( int x = 0; x + w <= Cols; x++ )
{
var r = new GridRect( x, y, w, h );
if ( CanPlace( r ) ) return r;
}
return null;
}
public T? ItemAt( int x, int y ) => _occ.TryGetValue( (x, y), out var t ) ? t : default;
void Occupy( T item, GridRect r )
{
_placed[item] = r;
for ( int y = r.Y; y < r.Y + r.H; y++ )
for ( int x = r.X; x < r.X + r.W; x++ )
_occ[(x, y)] = item;
}
void Free( GridRect r )
{
for ( int y = r.Y; y < r.Y + r.H; y++ )
for ( int x = r.X; x < r.X + r.W; x++ )
_occ.Remove( (x, y) );
}
}