Editor/Barriers/ArchRetaining.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One station of a retaining run: the level it holds, the grade it stands open to, and which way the fill lies.
// Shared by the two bays either side of it, so a section cannot step across a joint.
public readonly struct ArchRetainingStation {
public Vector2 At { get; init; }
public float Head { get; init; }
public float Grade { get; init; }
public Vector2 Fill { get; init; }
public bool End { get; init; }
public float Retains => Head - Grade;
}
public readonly struct ArchRetainingBay {
public ArchRetainingStation From { get; init; }
public ArchRetainingStation To { get; init; }
public int Index { get; init; }
public float Span => (To.At - From.At).Length;
public float Retains => MathF.Max( From.Retains, To.Retains );
}
// A freestanding retaining wall: the barrier spline says where it runs, ArchGround says what it holds, and where
// the two faces meet there is no fill and no wall. One resolve, read by the generator, the ghost and the report.
public sealed class ArchRetainingShape {
// The thinnest wall worth pouring, and the floor every wing thickness in the tool already takes.
const float ThinnestStem = 8f;
public List<ArchRetainingBay> Bays { get; init; } = new();
public List<ArchWingWall> Returns { get; init; } = new();
public float Stem { get; init; }
public float Lean { get; init; }
public bool IsUsable => Bays.Count > 0;
public float Retains => Bays.Count == 0 ? 0f : Bays.Max( bay => bay.Retains );
public static ArchRetainingShape Resolve( ArchBarrierShape barrier, ArchKit kit, ArchGround ground ) {
if ( barrier is null || !barrier.IsUsable ) {
return new ArchRetainingShape();
}
var stations = barrier.Posts.Select( post => Station( post, ground ) ).ToList();
// A drop shallower than the wall's own footing is a kerb, not something that retains anything.
var least = ArchGround.Embedment( kit );
var bays = new List<ArchRetainingBay>();
foreach ( var bay in barrier.Bays ) {
if ( bay.Missing || bay.Index + 1 >= stations.Count ) {
continue;
}
var standing = new ArchRetainingBay { From = stations[bay.Index], To = stations[bay.Index + 1], Index = bay.Index };
if ( standing.Retains < least || standing.Span < 1f ) {
continue;
}
bays.Add( standing );
}
var retains = bays.Count == 0 ? 0f : bays.Max( bay => bay.Retains );
var batter = MathF.Max( 0.02f, kit?.RetainingBatter ?? 0.08f );
// One section for the whole wall, off its deepest exposure: sized per bay, a joint steps mid-run.
var stem = MathF.Max( ThinnestStem, retains * batter );
return new ArchRetainingShape {
Bays = bays,
Returns = Ends( bays, ground ),
Stem = stem,
Lean = Math.Clamp( retains * batter, 0f, stem * 0.5f )
};
}
// The head is the fill's own level, never the line the author drew: a wall dragged along the toe of a bank
// still comes out as tall as what it holds.
static ArchRetainingStation Station( ArchBarrierPost post, ArchGround ground ) {
var across = ArchBarrierGen.Across( post.Frame );
var seat = post.Seat;
var right = ground.Under( post.Flat + across * ArchWing.ProbeStep, seat );
var left = ground.Under( post.Flat - across * ArchWing.ProbeStep, seat );
return new ArchRetainingStation {
At = post.Flat,
Head = MathF.Max( seat, MathF.Max( right, left ) ),
Grade = MathF.Min( right, left ),
Fill = right >= left ? across : -across,
End = post.End
};
}
// Each open end carries on into the bank and dies where it meets its own head - ArchWing's rule, and its
// refusal: no fill, no wing.
static List<ArchWingWall> Ends( IReadOnlyList<ArchRetainingBay> bays, ArchGround ground ) {
var returns = new List<ArchWingWall>();
if ( bays.Count == 0 ) {
return returns;
}
Return( returns, bays[0].From, Heading( bays[0], false ), ground );
Return( returns, bays[^1].To, Heading( bays[^1], true ), ground );
return returns;
}
static void Return( List<ArchWingWall> into, ArchRetainingStation station, Vector2 heading, ArchGround ground ) {
if ( !station.End ) {
return;
}
var died = ArchWing.Dies( station.At, heading, station.Retains, station.Head, ground, station.Grade );
if ( died < ArchWing.ProbeStep ) {
return;
}
var end = station.At + heading * died;
into.Add( new ArchWingWall {
From = station.At,
To = end,
Head = station.Head,
Ground = MathF.Min( station.Head, ground.Under( end, station.Grade ) ),
Buried = station.Grade
} );
}
static Vector2 Heading( ArchRetainingBay bay, bool forward ) {
var span = bay.To.At - bay.From.At;
var along = span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;
return forward ? along : -along;
}
}