Editor/Stair/ArchStairPart.cs

Editor data classes for stair pieces and a floor cutout used by the architecture editor. Defines enums for stair properties, ArchStairLeg for legacy leg data, ArchStairPart which stores stair configuration, derived properties and a ReverseChain method, and ArchFloorCutout which stores a hole/loop footprint and utility methods.

File AccessNetworking
using System.Text.Json.Serialization;

namespace Sunless.Architecture;

public enum StairTurn
{
	None,
	Left,
	Right,
	Back
}

public enum StairSupport
{
	ClosedString,
	OpenString
}

public enum StairUnder
{
	Open,
	Filled,
	Cupboard
}

public enum StairGuard
{
	None,
	Left,
	Right,
	Both
}

// A flight that carries its own walls up the well, so it reads as a stair core rather than a rake
// standing in the open. This is what the Stairwell shaft's enclosure used to stand.
public enum StairWrap
{
	None,
	Walls
}

// What a stair was drawn as before the core: a chain of world-space boxes that had to OVERLAP, where the square
// two of them shared was the landing. Kept only so a plan authored that way still opens - ArchPlan.Normalize reads
// it once through ArchStairLanes.FromLegs and clears it. Nothing else in the tool may touch it.
public sealed class ArchStairLeg
{
	public Vector2 Start { get; set; }
	public Vector2 End { get; set; }
	public float Width { get; set; }
	public bool Landing { get; set; }

	public Vector2 Span => End - Start;

	public float Length => Span.Length;

	public float Yaw => MathF.Atan2( Span.y, Span.x ).RadianToDegree();

	public ArchStairAxes Axes => new() { Origin = Start, Yaw = Yaw };
}

public sealed class ArchStairPart : IArchCollides, IArchPainted, IArchNamed
{
	// Overrides the kit for THIS part alone: one wall that needs its exact holes, one tower that needs none.
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Stair";
	public float BaseHeight { get; set; }

	// The shaft, and every flight standing in it. Both are the stair - there is nothing else to author.
	public ArchStairCore Core { get; set; } = new();
	public List<ArchStairLane> Lanes { get; set; } = new();

	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
	public List<ArchStairLeg> Legs { get; set; }

	// The climb, from before it belonged to the shaft. It keeps the OLD json name, which is the whole point:
	// ArchPlan.Normalize lifts it onto the core and clears it, and nothing else may read it.
	[JsonPropertyName( "TotalRise" )]
	[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
	public float StoredRise { get; set; }

	// The width a fresh lane takes, and the fallback for one authored at nothing. A lane keeps its own.
	public float Width { get; set; } = 48f;
	public float StepRise { get; set; } = 8f;
	public float StepGoing { get; set; } = 12f;
	public float TreadThickness { get; set; } = 3f;
	public float TreadNosing { get; set; } = 1f;

	public StairSupport Support { get; set; } = StairSupport.ClosedString;
	public StairUnder Under { get; set; } = StairUnder.Open;
	public StairGuard Guard { get; set; } = StairGuard.Both;

	// What a guarded side stands, per side, so a flight can carry a full balustrade down one flank and a bare
	// rake on newels down the other without a railing row being authored on every step to say so.
	public StairRailing LeftRailing { get; set; } = StairRailing.Balustrade;
	public StairRailing RightRailing { get; set; } = StairRailing.Balustrade;

	// Whether a step with no railing rows of its own gets one from Guard and WallRail at all. Off, NOTHING is
	// derived: only the railings actually placed on a step stand, which is what makes each of them a thing you
	// put somewhere rather than a side effect of a setting.
	public bool AutoRailings { get; set; } = true;
	public bool WellGuard { get; set; } = true;
	// The handrail bolted to the plaster on a side that runs against a wall - the side a balustrade never stands on.
	public bool WallRail { get; set; } = true;

	public StairWrap Wrap { get; set; }
	public float WrapThickness { get; set; } = 8f;

	// Zero takes the kit's.
	public float BalusterSpacing { get; set; }

	// Authored, not derived - it's what a handrail needs to turn through.
	public float WellGap { get; set; } = 4f;
	public bool TopLanding { get; set; } = true;
	public float TopLandingDepth { get; set; } = 48f;

	public string CupboardOpening { get; set; } = "door";
	public ArchPalette Palette { get; set; } = new();

	public bool Risers => Support == StairSupport.ClosedString;

	// The climb the stair actually stands: the core's, unless the flights pinned to more than it holds - a box
	// follows what is inside it, so a flight dragged to a height climbs to that height.
	[JsonIgnore]
	public float TotalRise => ArchStairLanes.Climb( Core, Lanes );

	[JsonIgnore]
	public int StepCount => ArchStairLanes.Steps( TotalRise, StepRise, Core?.Length ?? 0f, StepGoing );

	[JsonIgnore]
	public Vector2 Origin => Core?.Origin ?? Vector2.Zero;

	[JsonIgnore]
	public float Yaw => Core?.Yaw ?? 0f;

	[JsonIgnore]
	public float HeadHeight => BaseHeight + TotalRise;

	// The last flight becomes the first and every walk turns round, so the climb flows the other way.
	public void ReverseChain()
	{
		Lanes.Reverse();

		foreach ( var lane in Lanes )
		{
			lane.Walk = lane.Reversed;
		}
	}
}

// The loop is what's tested, so a turned flight cuts its own shape.
public sealed class ArchFloorCutout
{
	public int Id { get; set; }
	public string Name { get; set; } = "Stairwell";
	public int Level { get; set; }
	// Deleting the piercer takes its holes - no shaft left by a deleted stair.
	public int OwnerId { get; set; }
	public Vector2 Min { get; set; }
	public Vector2 Max { get; set; }
	public List<Vector2> Loop { get; set; } = new();

	// Derived from the cut that opened it, never authored: a stored stairwell is not a ruin.
	[JsonIgnore]
	public ArchCarveBreak Break { get; set; }

	public bool HasLoop => Loop.Count >= 3;

	// The one answer to what this hole's footprint is: its authored loop when it has one, otherwise the
	// rect its bounds describe. Slabs, planks, foundations and the overlay all read this, so a circle
	// cut and a box cut are the same question to every consumer - the way ArchCutSegment.Outline
	// answers for a cut's own legs.
	public List<Vector2> Outline()
	{
		return HasLoop ? Loop : ArchFootprint.Rect( Min, Max );
	}

	public void Reshape( IEnumerable<Vector2> loop )
	{
		Loop = loop.ToList();

		Min = new Vector2( Loop.Min( point => point.x ), Loop.Min( point => point.y ) );
		Max = new Vector2( Loop.Max( point => point.x ), Loop.Max( point => point.y ) );
	}

	public bool Contains( Vector2 point ) => ArchFootprint.Contains( Outline(), point );
}