Editor/Data/ArchArchetypeRules.cs

Architecture editor rules for building archetypes. Applies section-level authored rules and kit defaults to rooms, walls and roofs, copies palette materials and texture settings, and computes standing wall/plate heights.

File Access
using System;
using Sandbox;

namespace Sunless.Architecture;

// The only place a type touches geometry; a building keeps obeying its type.
public static class ArchArchetypeRules
{
	// An unstated wall height is the kit's storey, never "leave what is standing" - a type that says
	// nothing has to be able to take a taller type's walls back down when it is adopted.
	public static float Standing( float authored, ArchKit kit ) => authored > 1f ? authored : kit.WallHeight;

	public static float Standing( ArchSectionRules rules, ArchKit kit ) => Standing( rules.WallHeight, kit );

	public static void Apply( ArchSection section, ArchSectionRules rules, ArchKit kit )
	{
		if ( section?.Room is null || rules is null )
		{
			return;
		}

		// Merged wings keep the height Extend already measured, else they'd stand open along their eave.
		if ( !section.Merged )
		{
			section.Room.WallHeight = Standing( rules, kit );
		}

		section.Room.HasCeiling = rules.Ceiling ?? section.Room.HasCeiling;
		section.Room.HasFloor = rules.Floor ?? section.Room.HasFloor;
		section.Room.RaisedFoundation = rules.Foundation ?? section.Room.RaisedFoundation;
		section.Room.Collision = rules.Collision ?? section.Room.Collision;
		section.Room.FloorBoards = rules.FloorBoards;
		section.Room.FloorBoardYaw = rules.FloorBoardYaw;

		// One copier: a band added later reaches old elevations; no shared band list.
		if ( rules.Wall is { } treatment )
		{
			foreach ( var wall in section.Room.Walls )
			{
				ArchWallTreatments.Dress( treatment, wall );
			}
		}

		Skin( section.Room.Palette, rules.Palette );

		if ( section.Roof is null )
		{
			return;
		}

		if ( !section.Merged )
		{
			section.Room.WallHeight = Plate( section.Room );
			section.Roof.BaseHeight = section.Room.BaseHeight + section.Room.WallHeight;
		}

		Roof( section.Roof, rules );
	}

	// The roof sits on the walls that actually stand - a treatment height moves it too.
	static float Plate( ArchRoom room )
	{
		var authored = 0f;

		foreach ( var wall in room.Walls )
		{
			authored = MathF.Max( authored, wall.Height );
		}

		return authored > 1f ? authored : room.WallHeight;
	}

	public static void Roof( ArchRoofPart roof, ArchSectionRules rules )
	{
		if ( rules.RoofPitch > 0.5f )
		{
			roof.Pitch = rules.RoofPitch;
		}

		if ( rules.Overhang > 0.01f )
		{
			roof.Overhang = rules.Overhang;
		}

		if ( rules.FrameSpacing > 4f )
		{
			roof.FrameSpacing = rules.FrameSpacing;
		}

		if ( rules.ParapetHeight > 0.5f )
		{
			roof.ParapetHeight = rules.ParapetHeight;
		}

		if ( rules.GuardHeight > 0.5f )
		{
			roof.GuardHeight = rules.GuardHeight;
		}

		roof.Ceiling = rules.Ceiling ?? roof.Ceiling;
		roof.Gutters = rules.Gutters ?? roof.Gutters;
		roof.Fascia = rules.Fascia ?? roof.Fascia;
		roof.Soffit = rules.Soffit ?? roof.Soffit;
		roof.Frame = rules.Frame ?? roof.Frame;
		roof.Parapet = rules.Parapet ?? roof.Parapet;
		roof.Guardrail = rules.Guardrail ?? roof.Guardrail;

		Skin( roof.Palette, rules.Palette );
	}

	// Copied, not assigned - a shared dictionary would silently re-skin every part.
	public static void Skin( ArchPalette target, ArchPalette source )
	{
		foreach ( var entry in source.Materials )
		{
			target.Materials[entry.Key] = entry.Value;
		}

		foreach ( var entry in source.TexelScales )
		{
			target.TexelScales[entry.Key] = entry.Value;
		}

		foreach ( var entry in source.TextureOffsets ?? new Dictionary<string, Vector2>() )
		{
			target.TextureOffsets[entry.Key] = entry.Value;
		}
	}
}