Editor/Wall/ArchWallModPart.cs

Defines editor data types for wall modifiers. Contains enums for modifier kinds and placement side, extension methods giving behaviour, labels and glyphs, and a plain data class ArchWallModPart that stores a modifier's authored properties (id, name, kind, side, station along the wall, size, field and palette).

Reflection
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Placed work on a wall. A pilaster run is a rhythm the whole wall wears; a modifier stands at a
// station, can be selected, moved, disabled and deleted on its own.
//
// APPEND ONLY - a member inserted mid-list renumbers every authored modifier after it.
public enum ArchWallMod
{
	Pilaster,
	Quoin,
	Buttress,
	Indent,
	Sign
}

// Which elevation the block belongs to. A shopfront's corner pier reads on both; a recess only ever
// bites the face it was cut into. OnSurface is a placement mode - the drag decides, and what gets
// authored is Outside or Inside - so a placed part never carries it.
public enum ArchWallSide
{
	Outside,
	Inside,
	Both,
	OnSurface
}

// What a kind of modifier actually is - asked here, never re-tested inline in a generator or a form.
public static class ArchWallModKinds
{
	// How tall a plate stands when the author has not said. A fascia board, not a storey.
	public const float PlateHeight = 24f;

	public static bool Carves( this ArchWallMod kind ) => kind == ArchWallMod.Indent;

	// A rusticated corner is a STACK of blocks, alternately long and short.
	public static bool Coursed( this ArchWallMod kind ) => kind == ArchWallMod.Quoin;

	// A buttress leans back as it climbs; everything else stands square.
	public static bool Tapers( this ArchWallMod kind ) => kind == ArchWallMod.Buttress;

	// A plate is a board at a station rather than a strip dressing the whole elevation, so it is authored
	// with a band of its own - a sign climbing four storeys is a billboard nobody asked for.
	public static bool Plated( this ArchWallMod kind ) => kind == ArchWallMod.Sign;

	// Where a plate seats when nothing is authored: its head on the wall's, where a fascia board goes.
	public static float Seat( this ArchWallMod kind, float authored, float wallHeight )
	{
		if ( !kind.Plated() || authored > 0.5f )
		{
			return authored;
		}

		return MathF.Max( 0f, wallHeight - PlateHeight );
	}

	public static float Rise( this ArchWallMod kind, float authored )
	{
		if ( !kind.Plated() || authored > 0.5f )
		{
			return authored;
		}

		return PlateHeight;
	}

	// Only a sign comes out as a piece of its own: it is the one modifier an author hides, collides and
	// repaints apart from the elevation carrying it. Null leaves the block on the wall's fittings.
	public static string Piece( this ArchWallMod kind ) => kind == ArchWallMod.Sign ? ArchPieces.Sign : null;

	public static ArchSurface Field( this ArchWallMod kind ) => kind switch
	{
		ArchWallMod.Indent => ArchSurface.WallBase,
		ArchWallMod.Quoin => ArchSurface.WallBase,
		ArchWallMod.Sign => ArchSurface.Sign,
		_ => ArchSurface.Pillar
	};

	public static string Label( this ArchWallMod kind ) => kind switch
	{
		ArchWallMod.Pilaster => "Pilaster — an engaged strip standing proud of the elevation",
		ArchWallMod.Quoin => "Quoin — rusticated blocks, alternately long and short",
		ArchWallMod.Buttress => "Buttress — a proud pier leaning back as it climbs",
		ArchWallMod.Sign => "Sign — a proud plate saying what the building was",
		_ => "Indent — a recess bitten out of the outside face"
	};

	public static string Glyph( this ArchWallMod kind ) => kind switch
	{
		ArchWallMod.Pilaster => "view_column",
		ArchWallMod.Quoin => "grid_view",
		ArchWallMod.Buttress => "signal_cellular_4_bar",
		ArchWallMod.Sign => "label",
		_ => "check_box_outline_blank"
	};

	public static string Label( this ArchWallSide side ) => side switch
	{
		ArchWallSide.Outside => "Outside — stands on the street elevation",
		ArchWallSide.Inside => "Inside — stands in the room",
		ArchWallSide.Both => "Both — a pier reading on either face",
		_ => "On surface — whichever face the drag lands on"
	};

	public static string Glyph( this ArchWallSide side ) => side switch
	{
		ArchWallSide.Outside => "villa",
		ArchWallSide.Inside => "flip_to_back",
		ArchWallSide.Both => "flip",
		_ => "ads_click"
	};
}

public sealed class ArchWallModPart : IArchPainted, IArchNamed
{
	public int Id { get; set; }
	public string Name { get; set; } = "Pilaster";
	public ArchWallMod Kind { get; set; } = ArchWallMod.Pilaster;
	public ArchWallSide Side { get; set; } = ArchWallSide.Outside;
	// A STATION on the wall run - never a world point, so the modifier survives the wall being dragged.
	// The station is what is clamped, not the block: one standing ON the end wraps the corner, which is
	// how a shopfront pier reads across two elevations.
	public float Along { get; set; }
	public float Width { get; set; } = 16f;
	public float Base { get; set; }
	// Zero climbs to the wall head, so one preset dresses walls of several heights.
	public float Height { get; set; }
	public float Depth { get; set; } = 3f;
	public ArchSurface Field { get; set; } = ArchSurface.Pillar;
	public ArchPalette Palette { get; set; } = new();

	public bool Carves => Kind.Carves();

	public float Left => Along - Width * 0.5f;

	public float Right => Along + Width * 0.5f;
}