Editor/Roof/ArchRoofWalls.cs

Editor code that generates and manages roof parapet walls for the architecture system. It computes a common seat height for roof walls, creates a scoped deck host (room and connection service) for roof walls, files walls onto a plan, and converts a uniform parapet ring into individual walls with parapet-specific defaults.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// The deck a roof wall is generated against, resolved once so the room and the joins can never disagree about
// which walls are up there or how high they stand.
public readonly struct ArchRoofDeckHost
{
	public ArchRoom Room { get; init; }
	public ArchConnectionService Connections { get; init; }

	public float Seat => Room.BaseHeight;
}

// A WALL STANDING ON A DECK: a parapet that varies edge by edge, a party wall between two rooftops, a screen
// round plant. It is an ordinary ArchWall, so it wears every treatment, band, coping, rhythm and opening a wall
// on a storey wears - it is merely filed on the roof, because no room stands up there to own it.
//
// roof.Parapet is still the quick uniform ring. StandParapet turns that ring into walls, which is the only way a
// height, a treatment or a gap can differ from one edge to the next - the same idiom as breaking an opening
// rhythm by dragging one of its units.
public static class ArchRoofWalls
{
	public const float ParapetHeight = 18f;

	public static bool Stands( ArchRoofPart roof ) => ArchPlanStore.FiledOn( ArchKind.Wall, roof ).Any();

	// THE seat every wall on this roof stands on, read by the generator, the ghost and the report alike: the
	// LOWEST deck surface under the section, so a wall on a rake buries its uphill end instead of floating its
	// downhill one. ONE height for the whole roof, because two walls meeting at a corner have to arrive there at
	// the same height or neither mitres.
	public static float Seat( ArchRoofPart roof, ArchKit kit )
	{
		var lowest = float.MaxValue;

		foreach ( var corner in roof.Outline() )
		{
			lowest = MathF.Min( lowest, ArchAsks.RoofSeat( roof, kit, corner ) );
		}

		return lowest < float.MaxValue ? lowest : roof.BaseHeight;
	}

	// The one place a stand-in room is made, and it is scoped to the deck alone: a storey above the roof's own
	// level so nothing in the building under it reads as covering these walls, and a connection service holding
	// only this room so the roof's walls mitre with each other and with nothing downstairs.
	public static ArchRoofDeckHost Deck( ArchRoofPart roof, ArchKit kit )
	{
		var room = new ArchRoom
		{
			Id = roof.Id,
			Name = roof.Name,
			Floor = roof.Level + 1,
			BaseHeight = Seat( roof, kit ),
			WallHeight = ParapetHeight,
			HasFloor = false,
			HasCeiling = false,
			RaisedFoundation = false,
			Palette = roof.Palette,
			Walls = ArchPlanStore.FiledOn( ArchKind.Wall, roof ).OfType<ArchWall>().ToList()
		};

		var deck = new ArchBuilding { Id = roof.Id, Name = roof.Name, Rooms = new List<ArchRoom> { room } };

		return new ArchRoofDeckHost { Room = room, Connections = new ArchConnectionService( deck, kit ) };
	}

	// Whatever style: a parapet is a wall standing at a seat, and every deck has one. Only what is CUT OUT of a deck
	// needs the axis-aligned patches ArchRoofPlane.Planar gates on.
	public static ArchRoofPart Under( ArchPlan plan, int level, Vector2 point ) => ArchRoofLight.Over( plan, level, point );

	public static ArchWall Add( ArchPlan plan, ArchRoofPart roof, Vector2 from, Vector2 to, ArchWallPreset treatment )
	{
		if ( roof is null || (to - from).Length < 1f )
		{
			return null;
		}

		var wall = Parapeted( treatment.Spawn( plan.AllocateId(), from, to ) );

		plan.File( ArchKind.Wall, roof, wall );

		return wall;
	}

	// The ring stood as real walls, one per edge of the deck, so what was one number becomes a wall each author
	// can raise, drop, dress or delete on its own. The uniform ring is switched off in the same breath, or the
	// deck carries both at once.
	public static IReadOnlyList<ArchWall> StandParapet( ArchPlan plan, ArchRoofPart roof, ArchKit kit, ArchWallPreset treatment )
	{
		var outline = ArchFootprint.Wind( roof.Outline() );
		var height = roof.Parapet ? MathF.Max( 4f, roof.ParapetHeight ) : ParapetHeight;
		var stood = new List<ArchWall>();
		var kinds = ArchKinds.Load();

		for ( var index = 0; index < outline.Count; index++ )
		{
			var from = outline[index];
			var to = outline[(index + 1) % outline.Count];

			if ( (to - from).Length < 1f )
			{
				continue;
			}

			var wall = Parapeted( treatment.Spawn( plan.AllocateId(), from, to ) );
			wall.Height = height;
			wall.Thickness = MathF.Max( 2f, kit.WallThickness );

			plan.File( ArchKind.Wall, roof, wall, kinds );
			stood.Add( wall );
		}

		roof.Parapet = false;

		return stood;
	}

	// What a wall standing on a deck IS, as opposed to one standing in a room: both faces are weather, its head
	// is finished by a coping rather than run under an eave, and there is no floor for a skirting to die into.
	static ArchWall Parapeted( ArchWall wall )
	{
		wall.Height = wall.Height > 0.5f ? wall.Height : ParapetHeight;
		wall.Exterior = true;
		wall.Baseboard = false;
		wall.Cap = true;

		return wall;
	}
}