Editor/Services/ArchWingGeneration.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchWingGeneration {
	readonly ArchPlan plan;
	readonly ArchBuilding building;
	readonly ArchKit kit;
	int level;
	float baseHeight;
	Vector2 min;
	Vector2 max;
	SectionRoof choice;
	float drop;
	bool floor;
	bool gutters;
	RidgeRun ridge;

	public ArchWingGeneration( ArchPlan plan, ArchBuilding building, ArchKit kit ) {
		this.plan = plan;
		this.building = building;
		this.kit = kit;
	}

	public ArchWingGeneration On( int level, float baseHeight, Vector2 min, Vector2 max ) {
		this.level = level;
		this.baseHeight = baseHeight;
		this.min = min;
		this.max = max;
		return this;
	}

	public ArchWingGeneration WithRoof( SectionRoof choice, float drop, bool gutters, RidgeRun ridge ) {
		this.choice = choice;
		this.drop = drop;
		this.gutters = gutters;
		this.ridge = ridge;
		return this;
	}

	public ArchWingGeneration WithFloor( bool floor ) {
		this.floor = floor;
		return this;
	}

	public ArchSection Create() {
		var resolved = new ArchBoundaryPlacement( plan, kit ).Outside( level, min, max, building );

		if ( !resolved.IsUsable || !resolved.TouchesHost ) {
			return null;
		}

		min = resolved.Min;
		max = resolved.Max;
		var placement = new ArchWingPlacement( building, kit, level );

		var continuing = choice == SectionRoof.Continue;
		var host = continuing ? placement.Host( min, max ) : null;
		var merge = host is not null;
		var wallHeight = merge
			? MathF.Max( 32f, host.BaseHeight - baseHeight )
			: MathF.Max( 32f, kit.WallHeight - (continuing ? 0f : MathF.Max( 0f, drop )) );
		var party = new List<(Vector2 From, Vector2 To)>();
		var wing = ArchBuild.CreateRoom( plan, building, $"Wing{building.Rooms.Count}", level, baseHeight, wallHeight, min, max, floor, party, kit.WallThickness );
		var connections = new ArchConnections( plan, kit );

		foreach ( var edge in party ) {
			connections.OpenRooms( wing, building, edge.From, edge.To, wallHeight, wing.Floor );
		}

		// The breach took the host's wall out of the span, so the wing's own must not stand in its place.
		foreach ( var (from, to) in party ) {
			var partyWall = wing.Walls.FirstOrDefault( wall => SameRun( wall, from, to ) );

			if ( partyWall is not null ) {
				plan.Unfile( partyWall );
			}
		}

		connections.NormalizeBoundaries();

		if ( merge ) {
			var merged = ArchFootprint.Merge( host.Outline(), ArchFootprint.Rect( min, max ) );

			if ( merged is not null ) {
				host.Reshape( merged );
				host.Style = host.Style == RoofStyle.Flat ? RoofStyle.Flat : RoofStyle.Hip;

				return new ArchSection { Room = wing, Roof = host, SharedWalls = party.Count, Merged = true };
			}
		}

		if ( choice == SectionRoof.None ) {
			return new ArchSection { Room = wing, SharedWalls = party.Count };
		}

		var roof = ArchBuild.Roof( plan, kit, level, ArchBuild.Winged( choice ), min, max, baseHeight + wallHeight, gutters, choice == SectionRoof.Capped, ridge );
		plan.File( ArchKind.Roof, building, roof );

		return new ArchSection { Room = wing, Roof = roof, SharedWalls = party.Count };
	}

	static bool SameRun( ArchWall wall, Vector2 from, Vector2 to ) {
		return (wall.Start - from).Length <= 0.01f && (wall.End - to).Length <= 0.01f
			|| (wall.Start - to).Length <= 0.01f && (wall.End - from).Length <= 0.01f;
	}
}