Editor/Barriers/ArchBarrier.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchBarrierPost {
public ArchFrame Frame { get; init; }
public float Seat { get; init; }
public float Foot { get; init; }
public float Lean { get; init; }
public bool End { get; init; }
public Vector2 Flat => new( Frame.Position.x, Frame.Position.y );
}
// Seats differ only on a raking run - a stepped run levels them.
public sealed class ArchBarrierBayShape {
const float StrandChord = 48f;
public ArchBarrierPost From { get; init; }
public ArchBarrierPost To { get; init; }
public float SeatFrom { get; init; }
public float SeatTo { get; init; }
public float Ground { get; init; }
public int Courses { get; init; }
public bool Gate { get; init; }
public bool Missing { get; init; }
public float Lean { get; init; }
public float Slip { get; init; }
public float Sag { get; init; }
public int Index { get; init; }
public float Span => (To.Flat - From.Flat).Length;
// The one sagging line. The drop was resolved once, on the bay, so the ghost walks exactly the points the
// generator builds and neither can re-derive a curve of its own.
public List<Vector3> Strand( float from, float to ) {
var chords = Math.Max( 1, ArchDivide.AtMost( Span, StrandChord ).Count );
var path = new List<Vector3>();
var span = To.Flat - From.Flat;
for ( var chord = 0; chord <= chords; chord++ ) {
var fraction = chord / (float)chords;
var flat = From.Flat + span * fraction;
var level = MathX.Lerp( from, to, fraction );
path.Add( new Vector3( flat.x, flat.y, level - Sag * 4f * fraction * (1f - fraction) ) );
}
return path;
}
public Vector2 Along {
get {
var span = To.Flat - From.Flat;
return span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;
}
}
public float SeatAt( float fraction ) => MathX.Lerp( SeatFrom, SeatTo, fraction );
}
// One resolve, read by the generator AND the placement ghost, so the posts drawn are the posts built.
public sealed class ArchBarrierShape {
public List<ArchBarrierPost> Posts { get; init; } = new();
public List<ArchBarrierBayShape> Bays { get; init; } = new();
public bool IsUsable => Bays.Count > 0;
public static ArchBarrierShape Resolve( ArchCurve curve, ArchBarrierSpec spec, float offset = 0f, float lift = 0f, ArchKit kit = null ) {
var shape = new ArchBarrierShape();
if ( curve is null || !curve.IsUsable || spec is null ) {
return shape;
}
var length = curve.Length;
if ( length < 4f ) {
return shape;
}
var stations = Stations( curve, spec, length, offset, lift );
if ( stations.Count < 2 ) {
return shape;
}
var seats = Seats( stations, spec );
var gate = spec.HasGate ? (spec.GateAt, spec.GateAt + spec.GateWidth) : (0f, 0f);
for ( var index = 0; index < stations.Count; index++ ) {
var neighbours = new List<float> { seats[Math.Max( 0, index - 1 )], seats[Math.Min( seats.Count - 1, index )] };
shape.Posts.Add( new ArchBarrierPost {
Frame = stations[index],
Seat = neighbours.Max(),
Foot = MathF.Min( stations[index].Position.z, neighbours.Min() ),
Lean = Signed( spec.Seed, index, 11 ) * spec.Wear * 2.5f,
End = !curve.Closed && (index == 0 || index == stations.Count - 1)
} );
}
for ( var index = 0; index < seats.Count; index++ ) {
var from = shape.Posts[index];
var to = shape.Posts[index + 1];
var authored = spec.Bays.ElementAtOrDefault( index );
var midpoint = (from.Frame.Distance + to.Frame.Distance) * 0.5f;
shape.Bays.Add( new ArchBarrierBayShape {
From = from,
To = to,
SeatFrom = spec.Ground == BarrierGround.Follow ? from.Frame.Position.z : seats[index],
SeatTo = spec.Ground == BarrierGround.Follow ? to.Frame.Position.z : seats[index],
Ground = MathF.Min( from.Frame.Position.z, to.Frame.Position.z ),
Courses = Courses( spec, index, authored ),
Gate = authored?.Gate == true || (spec.HasGate && midpoint >= gate.Item1 && midpoint <= gate.Item2),
Missing = spec.Broken > 0.001f && Noise( spec.Seed, index, 23 ) < spec.Broken,
Lean = Signed( spec.Seed, index, 31 ) * spec.Wear * 3.5f,
// Down only - a panel settles; a lifted one would top the wall's height line.
Slip = -Noise( spec.Seed, index, 47 ) * spec.Wear * 2.5f,
Sag = Sagged( spec, kit, (to.Flat - from.Flat).Length, index ),
Index = index
} );
}
return shape;
}
static List<ArchFrame> Stations( ArchCurve curve, ArchBarrierSpec spec, float length, float offset, float lift ) {
var distances = Distances( spec, length );
var stations = new List<ArchFrame>();
foreach ( var distance in distances ) {
if ( !curve.Sample( Math.Clamp( distance, 0f, length ), out var frame ) ) {
continue;
}
stations.Add( offset == 0f && lift == 0f ? frame : Shifted( frame, offset, lift ) );
}
return stations;
}
static List<float> Distances( ArchBarrierSpec spec, float length ) {
var panel = MathF.Max( 12f, spec.PanelLength );
if ( spec.Bays.Count == 0 ) {
return spec.EvenBays
? ArchDivide.AtMost( length, panel ).Nodes.ToList()
: ArchDivide.Fixed( length, panel );
}
var distances = new List<float> { 0f };
var travelled = 0f;
foreach ( var bay in spec.Bays ) {
travelled += MathF.Max( 12f, bay.Span > 0.5f ? bay.Span : panel );
if ( travelled >= length - 6f ) {
break;
}
distances.Add( travelled );
}
// A run extended past its own bay list carries on at the panel length rather than ending in one enormous bay.
while ( length - travelled > panel * 1.15f ) {
travelled += panel;
distances.Add( travelled );
}
distances.Add( length );
return distances;
}
// Sits on the higher end; the plinth makes up the difference.
static List<float> Seats( IReadOnlyList<ArchFrame> stations, ArchBarrierSpec spec ) {
var seats = new List<float>();
var highest = stations.Max( station => station.Position.z );
for ( var index = 0; index < stations.Count - 1; index++ ) {
seats.Add( spec.Ground switch {
BarrierGround.Level => highest,
BarrierGround.Step => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z ),
_ => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z )
} );
}
return seats;
}
// A FRACTION of the span, so one kit number sags a short spur and a long crossing alike, and wear deepens it.
static float Sagged( ArchBarrierSpec spec, ArchKit kit, float span, int index ) {
if ( spec.Style != BarrierStyle.Catenary ) {
return 0f;
}
var fraction = MathF.Max( 0f, kit?.CatenarySag ?? 0f );
return span * fraction * (1f + Noise( spec.Seed, index, 113 ) * spec.Wear);
}
// Every emitted vertex canonicalizes onto the finest rung, so a strand thinner than TWO rungs has both
// sides of its section land on one of them and the whole prism collapses to nothing. The kit may ask for
// a hair-thin wire; the mesh cannot hold one, and a line nobody can see is worse than one a touch fat.
public static float WireHalf( ArchKit kit ) {
return MathF.Max( ArchGridService.FinestSize, (kit?.WireThickness ?? 0f) * 0.5f );
}
static int Courses( ArchBarrierSpec spec, int index, ArchBarrierBay authored ) {
if ( authored is { Courses: > 0 } ) {
return authored.Courses;
}
var courses = Math.Max( 1, spec.Courses );
if ( spec.CourseVary <= 0 ) {
return courses;
}
// Down only - Courses is the height line; a bay falls short, never over.
return Math.Max( 1, courses - (int)MathF.Round( Noise( spec.Seed, index, 59 ) * spec.CourseVary ) );
}
static ArchFrame Shifted( ArchFrame frame, float offset, float lift ) {
return new ArchFrame {
Position = frame.Side( offset, lift ),
Along = frame.Along,
Across = frame.Across,
Up = frame.Up,
Distance = frame.Distance,
WidthScale = frame.WidthScale
};
}
// Seeded, never Game.Random: a run that reshuffled itself on every hotload could not be reviewed.
public static float Noise( int seed, int index, int salt ) {
var hash = unchecked((uint)(seed * 374761393 + index * 668265263 + salt * 2246822519));
hash ^= hash >> 13;
hash = unchecked(hash * 1274126177);
hash ^= hash >> 16;
return (hash & 0xFFFFFF) / (float)0xFFFFFF;
}
public static float Signed( int seed, int index, int salt ) => Noise( seed, index, salt ) * 2f - 1f;
}