Editor/Layers/ArchBuildingStamp.cs
using Sandbox;
namespace Sunless.Architecture;
public readonly record struct ArchAssetShape( IReadOnlyList<Vector2> Loop, float Bottom, float Top );
// What a saved building looks like before it exists, worked out once when it is chosen rather than per
// frame: the hover redraws every frame and a footprint derived from every room and deck each time is a
// tool that stutters while you aim it.
public sealed class ArchAssetGhost {
public List<ArchAssetShape> Shapes { get; init; } = new();
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
}
// Placing a saved building back into a plan. The copy is exact - every layer and every subtool standing in
// it - so the only things that change are the ids it answers to and where on the map it stands.
public static class ArchBuildingStamp {
public static ArchBuilding Place( ArchPlan plan, ArchBuildingAsset asset, Vector2 at, float degrees ) {
if ( plan is null || asset is not { IsUsable: true } ) {
return null;
}
var text = ArchStorage.Serialize( asset );
if ( !ArchLayerIds.Range( text, out var lowest, out var highest ) ) {
return null;
}
var delta = plan.NextId - lowest;
if ( ArchStorage.Deserialize<ArchBuildingAsset>( ArchLayerIds.Shifted( text, delta ) ) is not { IsUsable: true } copy ) {
return null;
}
var unit = copy.Unit;
Bounds( unit, out var min, out var max );
ArchCarry.Unit( unit, Landing( min, max, at, degrees ) );
unit.Facing = degrees;
unit.Name = Unique( plan, string.IsNullOrWhiteSpace( asset.Title ) ? unit.Name : asset.Title );
plan.Units.Add( unit );
plan.Layers.AddRange( copy.Layers );
plan.Parts.AddRange( copy.Parts );
plan.Links.AddRange( copy.Links );
plan.NextId = Math.Max( plan.NextId, highest + delta + 1 );
return unit;
}
// The cursor holds the corner of the TURNED shape, not the corner it was authored at: spinning about the
// authored corner walks the building off to one side of the pointer, further with every turn.
public static ArchRemap Landing( Vector2 min, Vector2 max, Vector2 at, float degrees ) {
var size = max - min;
var turned = ArchFootprint
.Turned( new[] { Vector2.Zero, size, new Vector2( size.x, 0f ), new Vector2( 0f, size.y ) }, Vector2.Zero, degrees )
.Aggregate( Vector2.Min );
return ArchRemap.Spun( min, degrees, at - turned );
}
public static ArchRemap Landing( ArchAssetGhost ghost, Vector2 at, float degrees ) => Landing( ghost.Min, ghost.Max, at, degrees );
// CARRYING holds the centre, where stamping holds a corner. A house already standing is grabbed and taken
// somewhere, so it has to spin in place under the pointer: held by a corner, every quarter threw it across
// the screen because the corner the cursor holds is a different corner after the turn.
public static ArchRemap Carried( Vector2 min, Vector2 max, Vector2 at, float degrees ) {
return ArchRemap.Spun( (min + max) * 0.5f, degrees, at );
}
public static ArchRemap Carried( ArchAssetGhost ghost, Vector2 at, float degrees ) => Carried( ghost.Min, ghost.Max, at, degrees );
public static ArchAssetGhost Ghost( ArchBuildingAsset asset ) {
return asset is { IsUsable: true } ? Ghost( asset.Unit ) : new ArchAssetGhost();
}
// The same ghost a STANDING building offers, because carrying a house that already exists is the same
// gesture as stamping one that does not - and two ghosts of one shape is two shapes.
public static ArchAssetGhost Ghost( ArchBuilding unit ) {
if ( unit is null ) {
return new ArchAssetGhost();
}
var shapes = new List<ArchAssetShape>();
foreach ( var room in unit.Rooms ) {
var loop = ArchFloorGen.Footprint( room );
if ( loop.Count >= 3 ) {
shapes.Add( new ArchAssetShape( loop, room.BaseHeight, room.BaseHeight + room.WallHeight ) );
}
}
// The deck reads as a plate rather than a solid - a prism the depth of a roof over the walls under it
// hides the very footprint the hover is for.
foreach ( var roof in unit.Roofs ) {
var loop = roof.Outline();
if ( loop.Count >= 3 ) {
shapes.Add( new ArchAssetShape( loop, roof.BaseHeight, roof.BaseHeight + MathF.Max( 1f, roof.Thickness ) ) );
}
}
Bounds( unit, out var min, out var max );
return new ArchAssetGhost { Shapes = shapes, Min = min, Max = max };
}
// Card art off the saved plan itself, so what is browsed is what will stand.
public static ArchStaged Stage( ArchBuildingAsset asset ) {
if ( asset is not { IsUsable: true } ||
ArchStorage.Deserialize<ArchBuilding>( ArchStorage.Serialize( asset.Unit ) ) is not { } unit ) {
return new ArchStaged( null, Vector3.Zero, 480f );
}
var plan = new ArchPlan();
plan.Units.Add( unit );
plan.Normalize();
Bounds( unit, out var min, out var max );
var size = max - min;
var centre = (min + max) * 0.5f;
var height = Standing( unit );
return new ArchStaged(
plan,
new Vector3( centre.x, centre.y, height * 0.55f ),
MathF.Max( 520f, MathF.Max( MathF.Max( size.x, size.y ), height ) * 1.9f ) );
}
public static string Identity( ArchBuildingAsset asset ) => ArchStageKey.Of( "building", asset.Name, asset );
public static string Describe( ArchBuildingAsset asset ) {
if ( asset is not { IsUsable: true } ) {
return "empty";
}
Bounds( asset.Unit, out var min, out var max );
var size = max - min;
var rooms = asset.Unit.Rooms.Count;
return $"{size.x:0} x {size.y:0} — {rooms} room{(rooms == 1 ? "" : "s")}";
}
static float Standing( ArchBuilding unit ) {
var height = 128f;
foreach ( var room in unit.Rooms ) {
height = MathF.Max( height, room.BaseHeight + room.WallHeight );
}
return height;
}
static void Bounds( ArchBuilding unit, out Vector2 min, out Vector2 max ) {
if ( ArchHandles.Bounds( unit, out min, out max ) ) {
return;
}
min = Vector2.Zero;
max = Vector2.Zero;
}
static string Unique( ArchPlan plan, string wanted ) {
var taken = plan.Units.Select( unit => unit.Name ).ToHashSet( StringComparer.OrdinalIgnoreCase );
if ( !taken.Contains( wanted ) ) {
return wanted;
}
for ( var suffix = 2; suffix < 1000; suffix++ ) {
if ( !taken.Contains( $"{wanted} {suffix}" ) ) {
return $"{wanted} {suffix}";
}
}
return wanted;
}
}