StageDoc.cs
namespace PixelPusher;
public sealed class StageEntity
{
public string Kind { get; set; }
public int Tx { get; set; }
public int Ty { get; set; }
public string Data { get; set; }
}
public sealed class StageDoc
{
public string Name { get; set; } = "untitled stage";
public int Width { get; private set; }
public int Height { get; private set; }
public int TileSize { get; set; } = 16;
public int[] Tiles { get; private set; }
public List<StageEntity> Entities { get; private set; } = new();
public int Version { get; private set; }
public float Gravity { get; set; } = 1800f;
public float MaxRun { get; set; } = 140f;
public float JumpVel { get; set; } = 420f;
public string Goal { get; set; } = "exit";
const int MaxUndo = 100;
readonly List<StageSnap> _undo = new();
readonly List<StageSnap> _redo = new();
public StageDoc( int width, int height, int tileSize = 16 )
{
Width = Math.Clamp( width, 8, 256 );
Height = Math.Clamp( height, 8, 128 );
TileSize = tileSize;
Tiles = new int[Width * Height];
}
public int TileAt( int tx, int ty )
{
if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
return 0;
return Tiles[ty * Width + tx];
}
public void SetTile( int tx, int ty, int id )
{
if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
return;
var i = ty * Width + tx;
if ( Tiles[i] == id )
return;
Tiles[i] = id;
Version++;
}
public void FillRect( int x0, int y0, int x1, int y1, int id )
{
if ( x0 > x1 ) (x0, x1) = (x1, x0);
if ( y0 > y1 ) (y0, y1) = (y1, y0);
x0 = Math.Clamp( x0, 0, Width - 1 );
x1 = Math.Clamp( x1, 0, Width - 1 );
y0 = Math.Clamp( y0, 0, Height - 1 );
y1 = Math.Clamp( y1, 0, Height - 1 );
for ( var y = y0; y <= y1; y++ )
for ( var x = x0; x <= x1; x++ )
Tiles[y * Width + x] = id;
Version++;
}
public void BeginStroke()
{
_undo.Add( Capture() );
if ( _undo.Count > MaxUndo )
_undo.RemoveAt( 0 );
_redo.Clear();
}
public bool CanUndo => _undo.Count > 0;
public bool CanRedo => _redo.Count > 0;
public void Undo()
{
if ( _undo.Count == 0 )
return;
_redo.Add( Capture() );
Restore( _undo[^1] );
_undo.RemoveAt( _undo.Count - 1 );
Version++;
}
public void Redo()
{
if ( _redo.Count == 0 )
return;
_undo.Add( Capture() );
Restore( _redo[^1] );
_redo.RemoveAt( _redo.Count - 1 );
Version++;
}
public void Flood( int tx, int ty, int id )
{
if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
return;
var target = Tiles[ty * Width + tx];
if ( target == id )
return;
var stack = new Stack<(int x, int y)>();
stack.Push( (tx, ty) );
var seen = new bool[Width * Height];
while ( stack.Count > 0 )
{
var (cx, cy) = stack.Pop();
if ( (uint)cx >= (uint)Width || (uint)cy >= (uint)Height )
continue;
var i = cy * Width + cx;
if ( seen[i] || Tiles[i] != target )
continue;
seen[i] = true;
Tiles[i] = id;
stack.Push( (cx + 1, cy) );
stack.Push( (cx - 1, cy) );
stack.Push( (cx, cy + 1) );
stack.Push( (cx, cy - 1) );
}
Version++;
}
public void StrokeLine( int x0, int y0, int x1, int y1, int id )
{
var dx = Math.Abs( x1 - x0 );
var dy = Math.Abs( y1 - y0 );
var sx = x0 < x1 ? 1 : -1;
var sy = y0 < y1 ? 1 : -1;
var err = dx - dy;
while ( true )
{
SetTile( x0, y0, id );
if ( x0 == x1 && y0 == y1 )
break;
var e2 = 2 * err;
if ( e2 > -dy ) { err -= dy; x0 += sx; }
if ( e2 < dx ) { err += dx; y0 += sy; }
}
}
public void PlaceEntity( string kind, int tx, int ty, string data = null )
{
if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
return;
if ( kind == "start" )
Entities.RemoveAll( e => e.Kind == "start" );
var existing = Entities.Find( e => e.Tx == tx && e.Ty == ty && e.Kind == kind );
if ( existing is not null )
{
if ( kind == "sign" && data is not null )
{
existing.Data = data;
Version++;
return;
}
Entities.Remove( existing );
Version++;
return;
}
Entities.RemoveAll( e => e.Tx == tx && e.Ty == ty );
Entities.Add( new StageEntity { Kind = kind, Tx = tx, Ty = ty, Data = data } );
Version++;
}
public void RemoveAt( int tx, int ty )
{
var n = Entities.RemoveAll( e => e.Tx == tx && e.Ty == ty );
if ( TileAt( tx, ty ) != 0 )
{
SetTile( tx, ty, 0 );
return;
}
if ( n > 0 )
Version++;
}
public StageEntity Find( string kind ) => Entities.Find( e => e.Kind == kind );
public StageSave ToSave()
{
return new StageSave
{
Name = Name,
Width = Width,
Height = Height,
TileSize = TileSize,
Tiles = CopyInts( Tiles ),
Entities = Entities.Select( CloneEntity ).ToList(),
Gravity = Gravity,
MaxRun = MaxRun,
JumpVel = JumpVel,
Goal = Goal
};
}
StageSnap Capture() => new()
{
Tiles = CopyInts( Tiles ),
Entities = Entities.Select( CloneEntity ).ToList(),
Goal = Goal
};
void Restore( StageSnap snap )
{
if ( snap.Tiles is { Length: > 0 } )
{
var n = Math.Min( Tiles.Length, snap.Tiles.Length );
Array.Copy( snap.Tiles, Tiles, n );
}
Entities = snap.Entities ?? new List<StageEntity>();
if ( !string.IsNullOrWhiteSpace( snap.Goal ) )
Goal = snap.Goal;
}
static StageEntity CloneEntity( StageEntity e ) => new()
{
Kind = e.Kind,
Tx = e.Tx,
Ty = e.Ty,
Data = e.Data
};
static int[] CopyInts( int[] src )
{
if ( src is null )
return new int[0];
var dst = new int[src.Length];
Array.Copy( src, dst, src.Length );
return dst;
}
public static StageDoc FromSave( StageSave save )
{
if ( save is null )
return StarterPack.TutorialStage();
var doc = new StageDoc( save.Width, save.Height, save.TileSize > 0 ? save.TileSize : 16 )
{
Name = save.Name ?? "untitled stage",
Gravity = save.Gravity > 0 ? save.Gravity : 1800f,
MaxRun = save.MaxRun > 0 ? save.MaxRun : 140f,
JumpVel = save.JumpVel > 0 ? save.JumpVel : 420f,
Goal = string.IsNullOrWhiteSpace( save.Goal ) ? "exit" : save.Goal
};
if ( save.Tiles is { Length: > 0 } )
{
var n = Math.Min( doc.Tiles.Length, save.Tiles.Length );
Array.Copy( save.Tiles, doc.Tiles, n );
}
if ( save.Entities is not null )
doc.Entities = save.Entities;
return doc;
}
}
public sealed class StageSave
{
public string Name { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int TileSize { get; set; }
public int[] Tiles { get; set; }
public List<StageEntity> Entities { get; set; }
public float Gravity { get; set; }
public float MaxRun { get; set; }
public float JumpVel { get; set; }
public string Goal { get; set; }
}
sealed class StageSnap
{
public int[] Tiles { get; set; }
public List<StageEntity> Entities { get; set; }
public string Goal { get; set; }
}