Editor procedural prop generator. Defines shared prop settings and a PropBuilder, then street, interior, industrial and defense generators that assemble props from boxes, cylinders and profiles.
namespace AdvancedMappingTools;
/// <summary>
/// What all placed props share. Which prop it is comes from the settings of each family.
/// </summary>
public abstract class PropSettings : GeneratorSettings
{
[Property, Group( "Prop" ), Range( 0.1f, 16, slider: false ), WideMode, Description( "Size of the whole prop." )]
public float Scale { get; set; } = 1.0f;
[Property, Group( "Prop" ), Range( 0, 1 ), WideMode, Description( "How run down it is. Smaller parts go missing, and a wreck loses more of them." )]
public float Wear { get; set; } = 0.0f;
[Property, Group( "Materials" ), Title( "Main" ), ResourceType( "vmat" ), WideMode]
public string MainMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Metal" ), ResourceType( "vmat" ), WideMode]
public string MetalMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Details" ), ResourceType( "vmat" ), WideMode]
public string DetailMaterial { get; set; }
/// <summary>
/// The prop this settings object stands for, as shown to the mapper.
/// </summary>
public abstract string PropName { get; }
}
/// <summary>
/// Builds a prop out of independent parts. Everything is drawn at full size and scaled here,
/// so a prop only says what it looks like. Parts only ever touch: a new part starts where the last one ends.
/// </summary>
public sealed class PropBuilder( PropSettings settings, GenContext context, PartPlan root, string category, string prefix = "", float yaw = 0 )
{
const int RoundSides = 12;
const float WearShare = 0.6f;
public Material Main { get; } = context.Material( settings.MainMaterial );
public Material Metal { get; } = context.Material( settings.MetalMaterial );
public Material Detail { get; } = context.Material( settings.DetailMaterial );
public GenContext Context => context;
public PropBuilder Rotated( float angle ) => new( settings, context, root, category, prefix, yaw + angle );
readonly float _scale = MathF.Max( settings.Scale, 0.01f );
public void Part( string id, string label, Material material, Action<MeshKit> build )
{
root.Add( context.Solid( prefix + id, label, "PropPart", category, material, kit =>
{
kit.Frame = new Transform( Vector3.Zero, Rotation.FromYaw( yaw ), _scale );
build( kit );
} ) );
}
/// <summary>
/// A part that only exists while the prop is in good enough shape.
/// </summary>
public void Loose( string id, string label, Material material, Action<MeshKit> build )
{
if ( !context.Stream( category, prefix + id ).Chance( settings.Wear * WearShare ) )
Part( id, label, material, build );
}
public void Box( string id, string label, Material material, Vector3 mins, Vector3 maxs ) =>
Part( id, label, material, kit => kit.Box( Vector3.Min( mins, maxs ), Vector3.Max( mins, maxs ) ) );
public void LooseBox( string id, string label, Material material, Vector3 mins, Vector3 maxs ) =>
Loose( id, label, material, kit => kit.Box( Vector3.Min( mins, maxs ), Vector3.Max( mins, maxs ) ) );
/// <summary>
/// A standing round body, narrower at the top when the radii differ.
/// </summary>
public void Round( string id, string label, Material material, float x, float y, float bottom, float top, float z0, float z1, int sides = RoundSides ) =>
Part( id, label, material, kit => kit.Cylinder( new Vector3( x, y, 0.0f ), bottom, top, z0, z1, sides ) );
/// <summary>
/// A shape drawn from the side, in x and z, reaching from y0 to y1.
/// </summary>
public void Side( string id, string label, Material material, IReadOnlyList<Vector2> shape, float y0, float y1 ) =>
Part( id, label, material, kit => kit.Profile( shape, y0, y1 ) );
/// <summary>
/// A wheel or a lying drum seen from the side.
/// </summary>
public void Disc( string id, string label, Material material, float x, float z, float radius, float y0, float y1, int sides = RoundSides ) =>
Side( id, label, material, Circle( x, z, radius, sides ), y0, y1 );
public static List<Vector2> Circle( float x, float z, float radius, int sides = RoundSides )
{
var points = new List<Vector2>( sides );
for ( var i = 0; i < sides; i++ )
{
var angle = i * MathF.PI * 2.0f / sides;
points.Add( new Vector2( x + MathF.Cos( angle ) * radius, z + MathF.Sin( angle ) * radius ) );
}
return points;
}
/// <summary>
/// Legs under a corner frame, one at each corner of a rectangle.
/// </summary>
public void Legs( string id, Material material, float x, float y, float size, float z0, float z1 )
{
var names = new[] { "front left", "front right", "back left", "back right" };
var corners = new[] { (-x, -y), (x - size, -y), (-x, y - size), (x - size, y - size) };
for ( var i = 0; i < corners.Length; i++ )
{
var (cx, cy) = corners[i];
Box( $"{id}{i}", $"Leg {names[i]}", material, new Vector3( cx, cy, z0 ), new Vector3( cx + size, cy + size, z1 ) );
}
}
}
/// <summary>
/// A family of small props. Each kind is a few lines that say which parts it has.
/// </summary>
public abstract class PropGenerator : IPartGenerator
{
public const string PropCategory = "prop";
public abstract string Id { get; }
public abstract string Title { get; }
public abstract Type SettingsType { get; }
public IReadOnlyList<string> SeedNames => [PropCategory];
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (PropSettings)settings;
var root = context.Group( "root", s.PropName, "Building", PropCategory );
Build( s, new PropBuilder( s, context, root, PropCategory ) );
return root;
}
/// <summary>
/// A prop as a piece of something bigger. Its ids start with the id of its group, so two lamps along a road stay two lamps.
/// </summary>
public PartPlan Inside( PropSettings settings, GenContext context, string id, string name, string category )
{
var group = context.Group( id, name, "Prop", category );
Build( settings, new PropBuilder( settings, context, group, category, $"{id}/" ) );
return group;
}
protected abstract void Build( PropSettings settings, PropBuilder b );
}
public enum StreetProp
{
[Title( "Street Lamp" )] Lamp,
[Title( "Double Street Lamp" )] DoubleLamp,
[Title( "Traffic Light" )] TrafficLight,
Bench,
[Title( "Bus Stop" )] BusStop,
[Title( "Trash Can" )] TrashCan,
Dumpster,
[Title( "Road Sign" )] RoadSign,
Hydrant,
Mailbox,
[Title( "Phone Booth" )] PhoneBooth,
[Title( "Parking Meter" )] ParkingMeter,
Bollard,
[Title( "Traffic Cone" )] Cone,
[Title( "Jersey Barrier" )] JerseyBarrier,
[Title( "Power Pole" )] PowerPole,
Planter,
[Title( "Bike Rack" )] BikeRack,
Billboard,
[Title( "Car Wreck" )] Car,
[Title( "Fire Barrel" )] FireBarrel
}
public sealed class StreetSettings : PropSettings
{
[Property, Group( "Prop" ), Title( "Street Prop" ), WideMode]
public StreetProp Kind { get; set; } = StreetProp.Lamp;
public override string PropName => Kind.ToString();
}
/// <summary>
/// What stands along a street.
/// </summary>
public sealed class StreetGenerator : PropGenerator
{
public override string Id => "street";
public override string Title => "Street Prop";
public override Type SettingsType => typeof( StreetSettings );
protected override void Build( PropSettings settings, PropBuilder b )
{
switch ( ((StreetSettings)settings).Kind )
{
case StreetProp.Lamp: Lamp( b, false ); break;
case StreetProp.DoubleLamp: Lamp( b, true ); break;
case StreetProp.TrafficLight: TrafficLight( b ); break;
case StreetProp.Bench: Bench( b ); break;
case StreetProp.BusStop: BusStop( b ); break;
case StreetProp.TrashCan: TrashCan( b ); break;
case StreetProp.Dumpster: Dumpster( b ); break;
case StreetProp.RoadSign: RoadSign( b ); break;
case StreetProp.Hydrant: Hydrant( b ); break;
case StreetProp.Mailbox: Mailbox( b ); break;
case StreetProp.PhoneBooth: PhoneBooth( b ); break;
case StreetProp.ParkingMeter: ParkingMeter( b ); break;
case StreetProp.Bollard: Bollard( b ); break;
case StreetProp.Cone: Cone( b ); break;
case StreetProp.JerseyBarrier: JerseyBarrier( b ); break;
case StreetProp.PowerPole: PowerPole( b ); break;
case StreetProp.Planter: Planter( b ); break;
case StreetProp.BikeRack: BikeRack( b ); break;
case StreetProp.Billboard: Billboard( b ); break;
case StreetProp.Car: Car( b ); break;
case StreetProp.FireBarrel: FireBarrel( b ); break;
}
}
static void Lamp( PropBuilder b, bool both )
{
b.Round( "base", "Base", b.Metal, 0, 0, 7, 6, 0, 10 );
b.Round( "pole", "Pole", b.Metal, 0, 0, 2.5f, 1.8f, 10, 180 );
b.Box( "arm", "Arm", b.Metal, new( both ? -54 : -2.5f, -1.5f, 180 ), new( 54, 1.5f, 183 ) );
foreach ( var (side, name) in both ? new[] { (1.0f, "right"), (-1.0f, "left") } : new[] { (1.0f, "right") } )
{
b.Box( $"head_{name}", "Lamp Head", b.Metal, new( side * 36, -5, 176 ), new( side * 54, 5, 180 ) );
b.LooseBox( $"glass_{name}", "Lamp Glass", b.Detail, new( side * 38, -4, 175 ), new( side * 52, 4, 176 ) );
}
}
static void TrafficLight( PropBuilder b )
{
b.Round( "base", "Base", b.Metal, 0, 0, 8, 6, 0, 8 );
b.Round( "pole", "Pole", b.Metal, 0, 0, 3, 2.5f, 8, 170 );
b.Box( "mast", "Mast Arm", b.Metal, new( -3, -2, 170 ), new( 110, 2, 174 ) );
b.Box( "head", "Signal Head", b.Main, new( 80, -5, 134 ), new( 92, 5, 170 ) );
for ( var i = 0; i < 3; i++ )
b.LooseBox( $"lamp{i}", "Signal Lamp", b.Detail, new( 82, -6, 137 + i * 11 ), new( 90, -5, 145 + i * 11 ) );
}
static void Bench( PropBuilder b )
{
foreach ( var (x, name) in new[] { (-34f, "left"), (30f, "right") } )
{
b.Box( $"leg_{name}", "Leg", b.Metal, new( x, -9, 0 ), new( x + 4, 9, 16 ) );
b.Box( $"post_{name}", "Back Post", b.Metal, new( x, 9, 0 ), new( x + 4, 11, 34 ) );
}
for ( var i = 0; i < 3; i++ )
b.LooseBox( $"seat{i}", "Seat Plank", b.Main, new( -36, -9 + i * 6.2f, 16 ), new( 36, -4 + i * 6.2f, 18 ) );
b.LooseBox( "back0", "Back Plank", b.Main, new( -36, 11, 22 ), new( 36, 12, 26 ) );
b.LooseBox( "back1", "Back Plank", b.Main, new( -36, 11, 28 ), new( 36, 12, 32 ) );
}
static void BusStop( PropBuilder b )
{
b.Box( "roof", "Roof", b.Main, new( -80, -30, 96 ), new( 80, 30, 100 ) );
foreach ( var (x, name) in new[] { (-80f, "left"), (-2f, "middle"), (76f, "right") } )
b.Box( $"post_{name}", "Post", b.Metal, new( x, 26, 0 ), new( x + 4, 30, 96 ) );
b.LooseBox( "glass_left", "Glass", b.Detail, new( -76, 27.5f, 12 ), new( -2, 28.5f, 84 ) );
b.LooseBox( "glass_right", "Glass", b.Detail, new( 2, 27.5f, 12 ), new( 76, 28.5f, 84 ) );
b.Box( "seat", "Seat", b.Main, new( -56, 10, 16 ), new( 56, 26, 18 ) );
b.Box( "seat_leg_left", "Seat Leg", b.Metal, new( -52, 10, 0 ), new( -48, 26, 16 ) );
b.Box( "seat_leg_right", "Seat Leg", b.Metal, new( 48, 10, 0 ), new( 52, 26, 16 ) );
b.Box( "sign", "Sign", b.Metal, new( -30, -30, 100 ), new( 30, -28, 112 ) );
}
static void TrashCan( PropBuilder b )
{
b.Round( "body", "Body", b.Metal, 0, 0, 10, 11, 0, 34, 16 );
b.Loose( "lid", "Lid", b.Metal, kit => kit.Cylinder( new Vector3( 0, 0, 0 ), 11.5f, 11.5f, 34, 37, 16 ) );
b.LooseBox( "handle", "Handle", b.Detail, new( -4, -1, 37 ), new( 4, 1, 39 ) );
}
static void Dumpster( PropBuilder b )
{
b.Side( "body", "Body", b.Metal, [new( -36, 6 ), new( 36, 6 ), new( 42, 48 ), new( -42, 48 )], -28, 28 );
b.LooseBox( "lid_left", "Lid", b.Main, new( -42, -28, 48 ), new( 0, 28, 50 ) );
b.LooseBox( "lid_right", "Lid", b.Main, new( 0, -28, 48 ), new( 42, 28, 50 ) );
b.Legs( "wheel", b.Detail, 34, 26, 6, 0, 6 );
}
static void RoadSign( PropBuilder b )
{
b.Box( "pole", "Pole", b.Metal, new( -1, -1, 0 ), new( 1, 1, 108 ) );
var plate = PropBuilder.Circle( 0, 96, 13, 8 );
b.Side( "plate", "Plate", b.Detail, plate, -2, -1 );
}
static void Hydrant( PropBuilder b )
{
b.Round( "foot", "Foot", b.Metal, 0, 0, 6, 6, 0, 2 );
b.Round( "body", "Body", b.Main, 0, 0, 4, 4, 2, 20 );
b.Round( "top", "Top", b.Main, 0, 0, 4, 1, 20, 25 );
b.LooseBox( "cap_left", "Cap", b.Metal, new( -7, -1.5f, 12 ), new( -4, 1.5f, 15 ) );
b.LooseBox( "cap_right", "Cap", b.Metal, new( 4, -1.5f, 12 ), new( 7, 1.5f, 15 ) );
}
static void Mailbox( PropBuilder b )
{
b.Legs( "leg", b.Metal, 9, 8, 2, 0, 12 );
b.Box( "body", "Body", b.Main, new( -9, -8, 12 ), new( 9, 8, 38 ) );
List<Vector2> top = [new( -9, 38 ), new( 9, 38 ), .. MeshKit.ArchCurve( -9, 9, 38, 9, ArchStyle.Round, 8 ).Reverse().Skip( 1 ).Take( 7 )];
b.Side( "top", "Top", b.Main, top, -8, 8 );
b.LooseBox( "flap", "Flap", b.Detail, new( -6, -9, 30 ), new( 6, -8, 34 ) );
}
static void PhoneBooth( PropBuilder b )
{
b.Box( "floor", "Floor", b.Metal, new( -18, -18, 0 ), new( 18, 18, 2 ) );
b.Legs( "post", b.Metal, 18, 18, 2, 2, 92 );
b.Box( "roof", "Roof", b.Main, new( -19, -19, 92 ), new( 19, 19, 96 ) );
b.Box( "back", "Back Panel", b.Main, new( -16, 16, 2 ), new( 16, 18, 92 ) );
b.LooseBox( "phone", "Phone", b.Detail, new( -6, 13, 44 ), new( 6, 16, 60 ) );
b.LooseBox( "glass_left", "Glass", b.Detail, new( -17.5f, -16, 16 ), new( -16.5f, 16, 84 ) );
b.LooseBox( "glass_right", "Glass", b.Detail, new( 16.5f, -16, 16 ), new( 17.5f, 16, 84 ) );
}
static void ParkingMeter( PropBuilder b )
{
b.Round( "pole", "Pole", b.Metal, 0, 0, 1.2f, 1.2f, 0, 42 );
b.Box( "head", "Head", b.Main, new( -4, -2, 42 ), new( 4, 2, 54 ) );
}
static void Bollard( PropBuilder b )
{
b.Round( "body", "Body", b.Metal, 0, 0, 4, 4, 0, 32 );
b.Round( "cap", "Cap", b.Detail, 0, 0, 4.5f, 3, 32, 36 );
}
static void Cone( PropBuilder b )
{
b.Box( "foot", "Foot", b.Main, new( -8, -8, 0 ), new( 8, 8, 1.5f ) );
b.Round( "cone", "Cone", b.Detail, 0, 0, 6, 1, 1.5f, 28 );
}
static void JerseyBarrier( PropBuilder b )
{
b.Part( "barrier", "Barrier", b.Main, kit =>
{
kit.Frame = kit.Frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90 ) ) );
kit.Profile( [new( -12, 0 ), new( 12, 0 ), new( 12, 3 ), new( 5, 14 ), new( 3, 32 ), new( -3, 32 ), new( -5, 14 ), new( -12, 3 )], -48, 48 );
} );
}
static void PowerPole( PropBuilder b )
{
b.Round( "pole", "Pole", b.Main, 0, 0, 6, 5, 0, 340 );
b.Box( "crossarm", "Crossarm", b.Main, new( -48, -3, 340 ), new( 48, 3, 346 ) );
foreach ( var (x, name) in new[] { (-40f, "left"), (0f, "middle"), (40f, "right") } )
b.Loose( $"insulator_{name}", "Insulator", b.Detail, kit => kit.Cylinder( new Vector3( x, 0, 0 ), 2, 1, 346, 354, 8 ) );
b.LooseBox( "bracket", "Bracket", b.Metal, new( 5.3f, -2, 280 ), new( 9, 2, 284 ) );
b.Loose( "transformer", "Transformer", b.Metal, kit => kit.Cylinder( new Vector3( 19, 0, 0 ), 10, 10, 270, 300, 12 ) );
}
static void Planter( PropBuilder b )
{
b.Box( "wall_front", "Wall", b.Main, new( -24, -12, 0 ), new( 24, -9, 16 ) );
b.Box( "wall_back", "Wall", b.Main, new( -24, 9, 0 ), new( 24, 12, 16 ) );
b.Box( "wall_left", "Wall", b.Main, new( -24, -9, 0 ), new( -21, 9, 16 ) );
b.Box( "wall_right", "Wall", b.Main, new( 21, -9, 0 ), new( 24, 9, 16 ) );
b.Box( "soil", "Soil", b.Detail, new( -21, -9, 0 ), new( 21, 9, 13 ) );
b.Loose( "shrub", "Shrub", b.Detail, kit => kit.Cylinder( new Vector3( 0, 0, 0 ), 8, 2, 13, 34, 8 ) );
}
static void BikeRack( PropBuilder b )
{
b.Box( "rail_front", "Rail", b.Metal, new( -40, -12, 0 ), new( 40, -10, 2 ) );
b.Box( "rail_back", "Rail", b.Metal, new( -40, 10, 0 ), new( 40, 12, 2 ) );
for ( var i = 0; i < 4; i++ )
{
var x = -32 + i * 20;
b.Box( $"hoop{i}_front", "Hoop", b.Metal, new( x, -12, 2 ), new( x + 2, -10, 28 ) );
b.Box( $"hoop{i}_back", "Hoop", b.Metal, new( x, 10, 2 ), new( x + 2, 12, 28 ) );
b.Box( $"hoop{i}_top", "Hoop", b.Metal, new( x, -12, 28 ), new( x + 2, 12, 30 ) );
}
}
static void Billboard( PropBuilder b )
{
b.Box( "leg_left", "Leg", b.Metal, new( -70, -4, 0 ), new( -60, 4, 132 ) );
b.Box( "leg_right", "Leg", b.Metal, new( 60, -4, 0 ), new( 70, 4, 132 ) );
b.Box( "panel", "Panel", b.Main, new( -112, -4, 132 ), new( 112, 4, 244 ) );
b.LooseBox( "walkway", "Walkway", b.Metal, new( -112, -20, 126 ), new( 112, -4, 128 ) );
b.LooseBox( "lamp", "Lamp Bar", b.Detail, new( -90, -12, 246 ), new( 90, -4, 248 ) );
}
static void Car( PropBuilder b )
{
b.Side( "hull", "Body", b.Main, [new( -86, 12 ), new( 86, 12 ), new( 88, 30 ), new( -88, 30 )], -32, 32 );
b.Side( "cabin", "Cabin", b.Main, [new( -58, 30 ), new( 60, 30 ), new( 38, 52 ), new( -30, 52 )], -28, 28 );
foreach ( var (y, name) in new[] { (28f, "left"), (-28.4f, "right") } )
b.Loose( $"window_{name}", "Side Window", b.Detail, kit => kit.Profile( [new( -48, 32 ), new( 52, 32 ), new( 34, 49 ), new( -26, 49 )], y, y + 0.4f ) );
foreach ( var (x, front) in new[] { (-52f, "rear"), (52f, "front") } )
{
b.Loose( $"wheel_{front}_left", "Wheel", b.Detail, kit => kit.Profile( PropBuilder.Circle( x, 12, 12 ), 32, 38 ) );
b.Loose( $"wheel_{front}_right", "Wheel", b.Detail, kit => kit.Profile( PropBuilder.Circle( x, 12, 12 ), -38, -32 ) );
}
}
static void FireBarrel( PropBuilder b )
{
var levels = new[] { 0f, 10, 12, 22, 24, 34 };
for ( var i = 0; i + 1 < levels.Length; i++ )
{
var rib = i % 2 == 1;
b.Round( $"section{i}", rib ? "Rib" : "Section", b.Metal, 0, 0, rib ? 11.6f : 11, rib ? 11.6f : 11, levels[i], levels[i + 1], 16 );
}
}
}
public enum InteriorProp
{
Table,
Chair,
Desk,
Bed,
[Title( "Bunk Bed" )] BunkBed,
Shelf,
Bookcase,
Cabinet,
Locker,
Counter,
Sofa,
[Title( "Kitchen Unit" )] KitchenUnit,
Fridge,
Stove,
Toilet,
Sink,
Wardrobe
}
public sealed class InteriorSettings : PropSettings
{
[Property, Group( "Prop" ), Title( "Furniture" ), WideMode]
public InteriorProp Kind { get; set; } = InteriorProp.Table;
public override string PropName => Kind.ToString();
}
/// <summary>
/// What stands inside a building.
/// </summary>
public sealed class InteriorGenerator : PropGenerator
{
public override string Id => "interior";
public override string Title => "Furniture";
public override Type SettingsType => typeof( InteriorSettings );
protected override void Build( PropSettings settings, PropBuilder b )
{
switch ( ((InteriorSettings)settings).Kind )
{
case InteriorProp.Table: Table( b ); break;
case InteriorProp.Chair: Chair( b ); break;
case InteriorProp.Desk: Desk( b ); break;
case InteriorProp.Bed: Bed( b, 0, "" ); break;
case InteriorProp.BunkBed: BunkBed( b ); break;
case InteriorProp.Shelf: Shelf( b, false ); break;
case InteriorProp.Bookcase: Shelf( b, true ); break;
case InteriorProp.Cabinet: Doors( b, 36, 18, 36, 2 ); break;
case InteriorProp.Locker: Doors( b, 45, 18, 72, 3 ); break;
case InteriorProp.Counter: Counter( b ); break;
case InteriorProp.Sofa: Sofa( b ); break;
case InteriorProp.KitchenUnit: KitchenUnit( b ); break;
case InteriorProp.Fridge: Fridge( b ); break;
case InteriorProp.Stove: Stove( b ); break;
case InteriorProp.Toilet: Toilet( b ); break;
case InteriorProp.Sink: Sink( b ); break;
case InteriorProp.Wardrobe: Doors( b, 48, 24, 80, 2 ); break;
}
}
static void Table( PropBuilder b )
{
b.Box( "top", "Top", b.Main, new( -30, -18, 28 ), new( 30, 18, 30 ) );
b.Legs( "leg", b.Main, 28, 16, 2.5f, 0, 28 );
}
static void Chair( PropBuilder b )
{
b.Box( "seat", "Seat", b.Main, new( -9, -9, 17 ), new( 9, 9, 19 ) );
b.Legs( "leg", b.Main, 9, 9, 2, 0, 17 );
b.LooseBox( "post_left", "Back Post", b.Main, new( -9, 7, 19 ), new( -7, 9, 36 ) );
b.LooseBox( "post_right", "Back Post", b.Main, new( 7, 7, 19 ), new( 9, 9, 36 ) );
b.LooseBox( "back", "Back", b.Main, new( -7, 7.5f, 26 ), new( 7, 8.5f, 36 ) );
}
static void Desk( PropBuilder b )
{
b.Box( "top", "Top", b.Main, new( -30, -14, 28 ), new( 30, 14, 30 ) );
b.Box( "side", "Side", b.Main, new( -30, -14, 0 ), new( -28, 14, 28 ) );
b.Box( "drawers", "Drawers", b.Main, new( 12, -14, 0 ), new( 30, 14, 28 ) );
for ( var i = 0; i < 3; i++ )
b.LooseBox( $"drawer{i}", "Drawer Front", b.Detail, new( 14, -14.5f, 2 + i * 9 ), new( 28, -14, 10 + i * 9 ) );
}
static void Bed( PropBuilder b, float z, string id )
{
b.Box( $"{id}frame", "Frame", b.Main, new( -40, -20, z + 8 ), new( 40, 20, z + 12 ) );
b.Box( $"{id}mattress", "Mattress", b.Detail, new( -40, -20, z + 12 ), new( 40, 20, z + 19 ) );
b.LooseBox( $"{id}pillow", "Pillow", b.Detail, new( -38, -14, z + 19 ), new( -24, 14, z + 22 ) );
if ( z > 0.0f ) return;
b.Legs( "leg", b.Main, 40, 20, 3, 0, 8 );
b.Box( "head", "Head Board", b.Main, new( -43, -20, 0 ), new( -40, 20, 34 ) );
}
static void BunkBed( PropBuilder b )
{
b.Legs( "post", b.Metal, 42, 22, 2, 0, 74 );
foreach ( var (z, id) in new[] { (0f, "lower_"), (40f, "upper_") } )
{
b.Box( $"{id}frame", "Frame", b.Main, new( -40, -20, z + 8 ), new( 40, 20, z + 12 ) );
b.Box( $"{id}mattress", "Mattress", b.Detail, new( -40, -20, z + 12 ), new( 40, 20, z + 19 ) );
b.Box( $"{id}rail_head", "Rail", b.Metal, new( -40, -22, z + 8 ), new( -38, -20, z + 12 ) );
}
for ( var i = 0; i < 4; i++ )
b.LooseBox( $"rung{i}", "Ladder Rung", b.Metal, new( 28, -23, 12 + i * 11 ), new( 40, -22, 14 + i * 11 ) );
}
static void Shelf( PropBuilder b, bool books )
{
const float Width = 36, Height = 72;
b.Box( "side_left", "Side", b.Main, new( -Width * 0.5f, -7, 0 ), new( -Width * 0.5f + 1.5f, 7, Height ) );
b.Box( "side_right", "Side", b.Main, new( Width * 0.5f - 1.5f, -7, 0 ), new( Width * 0.5f, 7, Height ) );
for ( var level = 0; level < 4; level++ )
{
var z = 2 + level * 17.5f;
b.Box( $"board{level}", "Board", b.Main, new( -Width * 0.5f + 1.5f, -7, z ), new( Width * 0.5f - 1.5f, 7, z + 1.5f ) );
if ( !books ) continue;
var stream = b.Context.Stream( PropCategory, $"books{level}" );
var x = -Width * 0.5f + 2.0f;
for ( var i = 0; x < Width * 0.5f - 6.0f; i++ )
{
var thick = stream.Range( 1.2f, 3.0f );
var tall = stream.Range( 9.0f, 14.0f );
var at = x;
b.LooseBox( $"book{level}_{i}", "Book", b.Detail, new( at, -5, z + 1.5f ), new( at + thick, 5, z + 1.5f + tall ) );
x += thick + 0.1f;
}
}
b.Box( "top", "Top", b.Main, new( -Width * 0.5f, -7, Height ), new( Width * 0.5f, 7, Height + 1.5f ) );
}
static void Doors( PropBuilder b, float width, float depth, float height, int doors )
{
b.Box( "body", "Body", b.Main, new( -width * 0.5f, -depth * 0.5f, 0 ), new( width * 0.5f, depth * 0.5f, height ) );
var each = width / doors;
for ( var i = 0; i < doors; i++ )
{
var x = -width * 0.5f + each * i;
b.LooseBox( $"door{i}", "Door", b.Detail, new( x + 0.5f, -depth * 0.5f - 0.75f, 2 ), new( x + each - 0.5f, -depth * 0.5f, height - 2 ) );
}
}
static void Counter( PropBuilder b )
{
b.Box( "body", "Body", b.Main, new( -48, -12, 0 ), new( 48, 12, 34 ) );
b.Box( "top", "Top", b.Detail, new( -50, -14, 34 ), new( 50, 14, 36 ) );
}
static void Sofa( PropBuilder b )
{
b.Box( "base", "Base", b.Main, new( -36, -16, 3 ), new( 36, 16, 12 ) );
b.Box( "back", "Back", b.Main, new( -36, 8, 12 ), new( 36, 16, 32 ) );
b.Box( "arm_left", "Arm", b.Main, new( -42, -16, 3 ), new( -36, 16, 24 ) );
b.Box( "arm_right", "Arm", b.Main, new( 36, -16, 3 ), new( 42, 16, 24 ) );
b.Legs( "foot", b.Metal, 42, 16, 3, 0, 3 );
for ( var i = 0; i < 3; i++ )
b.LooseBox( $"cushion{i}", "Cushion", b.Detail, new( -36 + i * 24, -16, 12 ), new( -12 + i * 24, 8, 17 ) );
}
static void KitchenUnit( PropBuilder b )
{
b.Box( "base", "Base Unit", b.Main, new( -48, -12, 4 ), new( 48, 12, 34 ) );
b.Box( "plinth", "Plinth", b.Metal, new( -48, -10, 0 ), new( 48, 12, 4 ) );
b.Box( "top", "Worktop", b.Detail, new( -49, -13, 34 ), new( 49, 12, 36 ) );
b.Box( "wall_unit", "Wall Unit", b.Main, new( -48, 2, 54 ), new( 48, 12, 80 ) );
b.Box( "back", "Splash Back", b.Metal, new( -48, 11, 36 ), new( 48, 12, 54 ) );
for ( var i = 0; i < 4; i++ )
{
b.LooseBox( $"door{i}", "Door", b.Detail, new( -47 + i * 24, -12.5f, 6 ), new( -25 + i * 24, -12, 32 ) );
b.LooseBox( $"upper_door{i}", "Door", b.Detail, new( -47 + i * 24, 1.5f, 56 ), new( -25 + i * 24, 2, 78 ) );
}
}
static void Fridge( PropBuilder b )
{
b.Box( "body", "Body", b.Main, new( -15, -14, 0 ), new( 15, 14, 70 ) );
b.Box( "door_low", "Lower Door", b.Main, new( -14.5f, -16, 2 ), new( 14.5f, -14, 48 ) );
b.Box( "door_high", "Upper Door", b.Main, new( -14.5f, -16, 49 ), new( 14.5f, -14, 69 ) );
b.LooseBox( "handle_low", "Handle", b.Metal, new( 10, -17, 30 ), new( 12, -16, 46 ) );
b.LooseBox( "handle_high", "Handle", b.Metal, new( 10, -17, 50 ), new( 12, -16, 62 ) );
}
static void Stove( PropBuilder b )
{
b.Box( "body", "Body", b.Main, new( -15, -13, 0 ), new( 15, 13, 34 ) );
b.Box( "hob", "Hob", b.Metal, new( -15, -13, 34 ), new( 15, 13, 35 ) );
b.Box( "oven", "Oven Door", b.Detail, new( -13, -14, 6 ), new( 13, -13, 26 ) );
for ( var i = 0; i < 4; i++ )
{
var x = i % 2 == 0 ? -7f : 7f;
var y = i < 2 ? -6f : 6f;
b.Loose( $"plate{i}", "Hot Plate", b.Detail, kit => kit.Cylinder( new Vector3( x, y, 0 ), 5, 5, 35, 35.5f, 10 ) );
}
}
static void Toilet( PropBuilder b )
{
b.Round( "foot", "Foot", b.Main, 0, -4, 6, 7, 0, 10, 10 );
b.Round( "bowl", "Bowl", b.Main, 0, -4, 8.5f, 9.5f, 10, 16, 12 );
b.Loose( "lid", "Lid", b.Detail, kit => kit.Cylinder( new Vector3( 0, -4, 0 ), 9.5f, 9.5f, 16, 17, 12 ) );
b.Box( "tank", "Tank", b.Main, new( -9, 6, 0 ), new( 9, 13, 30 ) );
}
static void Sink( PropBuilder b )
{
b.Round( "foot", "Pedestal", b.Main, 0, 0, 5, 3.5f, 0, 26, 10 );
b.Box( "basin", "Basin", b.Main, new( -11, -8, 26 ), new( 11, 8, 32 ) );
b.LooseBox( "tap", "Tap", b.Metal, new( -1, 4, 32 ), new( 1, 6, 37 ) );
}
}
public enum IndustrialProp
{
Barrel,
[Title( "Barrel Group" )] BarrelGroup,
Pallet,
[Title( "Pallet Stack" )] PalletStack,
Generator,
[Title( "Roof AC Unit" )] AirConditioner,
[Title( "Fuel Tank" )] FuelTank,
[Title( "Water Tower" )] WaterTower,
[Title( "Electrical Cabinet" )] ElectricalCabinet,
Workbench,
Scaffold,
[Title( "Cable Drum" )] CableDrum,
[Title( "Gas Cylinders" )] GasCylinders,
[Title( "Roof Vent" )] RoofVent,
Crate,
[Title( "Pipe Spool" )] PipeSpool,
[Title( "Jersey Barrier" )] JerseyBarrier,
[Title( "Waste Container" )] WasteContainer,
[Title( "Cable Tray" )] CableTray,
[Title( "Pipe Rack" )] PipeRack
}
public sealed class IndustrialSettings : PropSettings
{
[Property, Group( "Prop" ), Title( "Equipment" ), WideMode]
public IndustrialProp Kind { get; set; } = IndustrialProp.Barrel;
public override string PropName => Kind.ToString();
}
/// <summary>
/// What stands around factories, yards and roofs.
/// </summary>
public sealed class IndustrialGenerator : PropGenerator
{
public override string Id => "industrial";
public override string Title => "Equipment";
public override Type SettingsType => typeof( IndustrialSettings );
protected override void Build( PropSettings settings, PropBuilder b )
{
switch ( ((IndustrialSettings)settings).Kind )
{
case IndustrialProp.Barrel: Barrel( b, "", 0, 0, 0 ); break;
case IndustrialProp.BarrelGroup: BarrelGroup( b ); break;
case IndustrialProp.Pallet: Pallet( b, "", 0 ); break;
case IndustrialProp.PalletStack: PalletStack( b ); break;
case IndustrialProp.Generator: PowerUnit( b ); break;
case IndustrialProp.AirConditioner: AirConditioner( b ); break;
case IndustrialProp.FuelTank: FuelTank( b ); break;
case IndustrialProp.WaterTower: WaterTower( b ); break;
case IndustrialProp.ElectricalCabinet: ElectricalCabinet( b ); break;
case IndustrialProp.Workbench: Workbench( b ); break;
case IndustrialProp.Scaffold: Scaffold( b ); break;
case IndustrialProp.CableDrum: CableDrum( b ); break;
case IndustrialProp.GasCylinders: GasCylinders( b ); break;
case IndustrialProp.RoofVent: RoofVent( b ); break;
case IndustrialProp.Crate: Crate( b ); break;
case IndustrialProp.PipeSpool: PipeSpool( b ); break;
case IndustrialProp.JerseyBarrier: JerseyBarrier( b ); break;
case IndustrialProp.WasteContainer: WasteContainer( b ); break;
case IndustrialProp.CableTray: CableTray( b ); break;
case IndustrialProp.PipeRack: PipeRack( b ); break;
}
}
static void Barrel( PropBuilder b, string id, float x, float y, float z )
{
var levels = new[] { 0f, 1, 13, 14, 26, 27, 34 };
for ( var i = 0; i + 1 < levels.Length; i++ )
{
var rib = i % 2 == 0 && i < levels.Length - 2;
b.Round( $"{id}section{i}", rib ? "Rib" : "Section", b.Metal, x, y, rib ? 11.6f : 11, rib ? 11.6f : 11, z + levels[i], z + levels[i + 1], 16 );
}
}
static void BarrelGroup( PropBuilder b )
{
Pallet( b, "pallet_", 0 );
Barrel( b, "a_", -13, -13, 5.5f );
Barrel( b, "b_", 13, -13, 5.5f );
Barrel( b, "c_", -13, 13, 5.5f );
Barrel( b, "d_", 13, 13, 5.5f );
}
static void Pallet( PropBuilder b, string id, float z )
{
foreach ( var (y, name) in new[] { (-20f, "front"), (-2f, "middle"), (16f, "back") } )
b.Box( $"{id}runner_{name}", "Runner", b.Main, new( -24, y, z ), new( 24, y + 4, z + 4 ) );
for ( var i = 0; i < 7; i++ )
b.LooseBox( $"{id}plank{i}", "Plank", b.Main, new( -24 + i * 7, -20, z + 4 ), new( -19 + i * 7, 20, z + 5 ) );
}
static void PalletStack( PropBuilder b )
{
for ( var i = 0; i < 6; i++ )
Pallet( b, $"pallet{i}_", i * 5.0f );
}
static void PowerUnit( PropBuilder b )
{
b.Box( "skid", "Skid", b.Metal, new( -36, -18, 0 ), new( 36, 18, 3 ) );
b.Box( "engine", "Engine", b.Main, new( -30, -14, 3 ), new( 8, 14, 30 ) );
b.Box( "alternator", "Alternator", b.Main, new( 8, -10, 3 ), new( 30, 10, 24 ) );
b.Loose( "tank", "Tank", b.Detail, kit => kit.Profile( PropBuilder.Circle( -11, 36, 6 ), -12, 12 ) );
b.Loose( "exhaust", "Exhaust", b.Metal, kit => kit.Cylinder( new Vector3( 20, 0, 0 ), 2, 2, 24, 44, 8 ) );
}
static void AirConditioner( PropBuilder b )
{
b.Box( "base", "Frame", b.Metal, new( -24, -18, 0 ), new( 24, 18, 3 ) );
b.Box( "body", "Body", b.Main, new( -22, -16, 3 ), new( 22, 16, 26 ) );
b.Loose( "fan", "Fan Cover", b.Detail, kit => kit.Cylinder( new Vector3( 0, 0, 0 ), 11, 11, 26, 28, 16 ) );
for ( var i = 0; i < 6; i++ )
b.LooseBox( $"fin{i}", "Fin", b.Metal, new( -20 + i * 7, -17, 6 ), new( -17 + i * 7, -16, 22 ) );
}
static void FuelTank( PropBuilder b )
{
b.Part( "tank", "Tank", b.Main, kit =>
{
kit.Frame = kit.Frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90 ) ) );
kit.Profile( PropBuilder.Circle( 0, 40, 28, 16 ), -70, 70 );
} );
// A saddle carries the tank along its lower faces, so it follows the same corners.
var ring = PropBuilder.Circle( 0, 40, 28, 16 );
List<Vector2> saddle = [new( -24, 0 ), new( 24, 0 ), new( 24, ring[10].y ), ring[14], ring[13], ring[12], ring[11], ring[10], new( -24, ring[10].y )];
foreach ( var (y, name) in new[] { (-50f, "left"), (46f, "right") } )
{
b.Part( $"saddle_{name}", "Saddle", b.Metal, kit =>
{
kit.Frame = kit.Frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90 ) ) );
kit.Profile( MeshKit.Separate( saddle ), y, y + 4 );
} );
}
}
static void WaterTower( PropBuilder b )
{
b.Legs( "leg", b.Metal, 44, 44, 5, 0, 220 );
b.Box( "deck", "Deck", b.Metal, new( -50, -50, 220 ), new( 50, 50, 226 ) );
b.Round( "tank", "Tank", b.Main, 0, 0, 44, 44, 226, 310, 20 );
b.Round( "roof", "Roof", b.Main, 0, 0, 46, 4, 310, 336, 20 );
}
static void ElectricalCabinet( PropBuilder b )
{
b.Box( "foot", "Foot", b.Metal, new( -16, -8, 0 ), new( 16, 8, 3 ) );
b.Box( "body", "Body", b.Main, new( -15, -7, 3 ), new( 15, 7, 58 ) );
b.Box( "door_left", "Door", b.Detail, new( -14, -8, 5 ), new( -0.5f, -7, 56 ) );
b.Box( "door_right", "Door", b.Detail, new( 0.5f, -8, 5 ), new( 14, -7, 56 ) );
b.Loose( "conduit", "Conduit", b.Metal, kit => kit.Cylinder( new Vector3( 8, 0, 0 ), 1.5f, 1.5f, 58, 96, 8 ) );
}
static void Workbench( PropBuilder b )
{
b.Box( "top", "Top", b.Main, new( -36, -16, 32 ), new( 36, 16, 35 ) );
b.Legs( "leg", b.Metal, 34, 14, 3, 0, 32 );
b.Box( "shelf", "Shelf", b.Main, new( -31, -11, 10 ), new( 31, 11, 12 ) );
b.LooseBox( "vise", "Vise", b.Metal, new( 24, -18, 35 ), new( 32, -8, 41 ) );
b.LooseBox( "toolbox", "Tool Box", b.Detail, new( -28, -8, 35 ), new( -10, 4, 43 ) );
}
static void Scaffold( PropBuilder b )
{
const float Width = 48, Depth = 32, Height = 96;
for ( var level = 0; level < 2; level++ )
{
var z = level * Height;
b.Legs( $"post{level}_", b.Metal, Width, Depth, 2, z, z + Height - 2 );
b.Box( $"beam{level}_front", "Beam", b.Metal, new( -Width, -Depth, z + Height - 2 ), new( Width, -Depth + 2, z + Height ) );
b.Box( $"beam{level}_back", "Beam", b.Metal, new( -Width, Depth - 2, z + Height - 2 ), new( Width, Depth, z + Height ) );
b.Box( $"beam{level}_left", "Beam", b.Metal, new( -Width, -Depth + 2, z + Height - 2 ), new( -Width + 2, Depth - 2, z + Height ) );
b.Box( $"beam{level}_right", "Beam", b.Metal, new( Width - 2, -Depth + 2, z + Height - 2 ), new( Width, Depth - 2, z + Height ) );
for ( var plank = 0; plank < 5; plank++ )
b.LooseBox( $"plank{level}_{plank}", "Plank", b.Main, new( -Width + 2, -Depth + 2 + plank * 12, z + Height ), new( Width - 2, -Depth + 13 + plank * 12, z + Height + 1 ) );
}
}
static void CableDrum( PropBuilder b )
{
b.Disc( "side_left", "Side", b.Main, 0, 24, 24, -14, -12, 16 );
b.Disc( "core", "Core", b.Metal, 0, 24, 12, -12, 12, 16 );
b.Disc( "side_right", "Side", b.Main, 0, 24, 24, 12, 14, 16 );
}
static void GasCylinders( PropBuilder b )
{
b.Box( "base", "Base", b.Metal, new( -20, -8, 0 ), new( 20, 8, 2 ) );
b.Box( "rail", "Rail", b.Metal, new( -20, 6, 2 ), new( 20, 8, 44 ) );
for ( var i = 0; i < 3; i++ )
{
var x = -13 + i * 13;
b.Round( $"cylinder{i}", "Cylinder", b.Main, x, -0.5f, 6, 6, 2, 46, 12 );
b.Round( $"neck{i}", "Neck", b.Main, x, -0.5f, 6, 2, 46, 50, 12 );
b.Loose( $"valve{i}", "Valve", b.Detail, kit => kit.Cylinder( new Vector3( x, -0.5f, 0 ), 1.5f, 1.5f, 50, 54, 8 ) );
}
}
static void WasteContainer( PropBuilder b )
{
b.Box( "body", "Body", b.Main, new( -32, -20, 8 ), new( 32, 20, 52 ) );
b.Box( "lid", "Lid", b.Detail, new( -33, -21, 52 ), new( 33, 21, 56 ) );
b.LooseBox( "handle", "Handle", b.Metal, new( -10, -24, 53 ), new( 10, -21, 56 ) );
foreach ( var (x, y, name) in new[] { (-24f, -14f, "fl"), (24f, -14f, "fr"), (-24f, 14f, "bl"), (24f, 14f, "br") } )
b.LooseBox( $"wheel_{name}", "Wheel", b.Metal, new( x - 4, y - 4, 0 ), new( x + 4, y + 4, 8 ) );
}
static void CableTray( PropBuilder b )
{
const float Length = 96.0f, HalfDepth = 14.0f, Rail = 12.0f;
b.Box( "floor", "Tray", b.Metal, new( -Length, -HalfDepth + 2, 8 ), new( Length, HalfDepth - 2, 10 ) );
b.Box( "rail_left", "Rail", b.Metal, new( -Length, -HalfDepth, 8 ), new( Length, -HalfDepth + 2, 8 + Rail ) );
b.Box( "rail_right", "Rail", b.Metal, new( -Length, HalfDepth - 2, 8 ), new( Length, HalfDepth, 8 + Rail ) );
foreach ( var (x, name) in new[] { (-Length + 8.0f, "left"), (0.0f, "mid"), (Length - 8.0f, "right") } )
b.LooseBox( $"support_{name}", "Support", b.Metal, new( x - 2, -HalfDepth, 0 ), new( x + 2, HalfDepth, 8 ) );
}
static void PipeRack( PropBuilder b )
{
b.Legs( "post", b.Metal, 40, 16, 3, 0, 64 );
b.Box( "beam_low_front", "Beam", b.Metal, new( -37, -16, 26 ), new( 37, -13, 30 ) );
b.Box( "beam_low_back", "Beam", b.Metal, new( -37, 13, 26 ), new( 37, 16, 30 ) );
b.Box( "beam_low_left", "Beam", b.Metal, new( -40, -13, 26 ), new( -37, 13, 30 ) );
b.Box( "beam_low_right", "Beam", b.Metal, new( 37, -13, 26 ), new( 40, 13, 30 ) );
b.Box( "beam_top", "Top Beam", b.Metal, new( -40, -16, 64 ), new( 40, 16, 67 ) );
}
static void JerseyBarrier( PropBuilder b )
{
b.Box( "base", "Base", b.Main, new( -32, -12, 0 ), new( 32, 12, 6 ) );
b.Box( "body", "Body", b.Main, new( -24, -8, 6 ), new( 24, 8, 24 ) );
b.Box( "top", "Top", b.Main, new( -16, -6, 24 ), new( 16, 6, 32 ) );
b.Box( "cap", "Cap", b.Detail, new( -18, -7, 32 ), new( 18, 7, 34 ) );
}
static void Crate( PropBuilder b )
{
b.Box( "base", "Base", b.Main, new( -24, -16, 0 ), new( 24, 16, 4 ) );
b.Box( "body", "Body", b.Main, new( -22, -14, 4 ), new( 22, 14, 34 ) );
b.Box( "lid", "Lid", b.Detail, new( -24, -16, 34 ), new( 24, 16, 38 ) );
foreach ( var (x, name) in new[] { (-20f, "left"), (20f, "right") } )
{
b.LooseBox( $"band_{name}_front", "Band", b.Metal, new( x - 2, -15, 4 ), new( x + 2, -14, 34 ) );
b.LooseBox( $"band_{name}_back", "Band", b.Metal, new( x - 2, 14, 4 ), new( x + 2, 15, 34 ) );
}
}
static void PipeSpool( PropBuilder b )
{
b.Loose( "pipe", "Pipe", b.Main, kit => kit.Cylinder( Vector3.Zero, 12, 12, 2, 62, 16 ) );
b.Loose( "flange_bottom", "Flange", b.Metal, kit => kit.Cylinder( Vector3.Zero, 17, 17, 0, 2, 16 ) );
b.Loose( "flange_top", "Flange", b.Metal, kit => kit.Cylinder( Vector3.Zero, 17, 17, 62, 64, 16 ) );
}
static void RoofVent( PropBuilder b )
{
b.Box( "curb", "Curb", b.Metal, new( -14, -14, 0 ), new( 14, 14, 6 ) );
b.Round( "stack", "Stack", b.Main, 0, 0, 8, 8, 6, 22, 12 );
b.Round( "hood", "Hood", b.Metal, 0, 0, 13, 2, 26, 34, 12 );
foreach ( var (x, name) in new[] { (-9f, "left"), (7f, "right") } )
b.Box( $"strut_{name}", "Strut", b.Metal, new( x, -1, 22 ), new( x + 2, 1, 26 ) );
}
}
public enum DefenseProp
{
[Title( "Wood Barricade" )] WoodBarricade,
[Title( "Boarded Window" )] BoardedWindow,
[Title( "Plank Wall" )] PlankWall,
[Title( "Concrete Blocks" )] ConcreteBlocks,
[Title( "Hesco Barrier" )] Hesco,
[Title( "Guard Booth" )] GuardBooth,
[Title( "Road Spikes" )] RoadSpikes
}
public sealed class DefenseSettings : PropSettings
{
[Property, Group( "Prop" ), Title( "Barricade" ), WideMode]
public DefenseProp Kind { get; set; } = DefenseProp.WoodBarricade;
[Property, Group( "Prop" ), Title( "Opening Width" ), Range( 16, 512, slider: false ), WideMode, Description( "How wide a boarded window or plank wall is." )]
public float Width { get; set; } = 64.0f;
[Property, Group( "Prop" ), Title( "Opening Height" ), Range( 16, 512, slider: false ), WideMode]
public float Height { get; set; } = 72.0f;
public override string PropName => Kind.ToString();
}
/// <summary>
/// What survivors put up. Everything nailed together lies in layers, one board in front of the last.
/// </summary>
public sealed class DefenseGenerator : PropGenerator
{
const float BoardThickness = 1.5f;
public override string Id => "defense";
public override string Title => "Barricade";
public override Type SettingsType => typeof( DefenseSettings );
protected override void Build( PropSettings settings, PropBuilder b )
{
var s = (DefenseSettings)settings;
switch ( s.Kind )
{
case DefenseProp.WoodBarricade: WoodBarricade( b ); break;
case DefenseProp.BoardedWindow: Boards( b, s.Width, s.Height, true ); break;
case DefenseProp.PlankWall: Boards( b, s.Width, s.Height, false ); break;
case DefenseProp.ConcreteBlocks: ConcreteBlocks( b ); break;
case DefenseProp.Hesco: Hesco( b ); break;
case DefenseProp.GuardBooth: GuardBooth( b ); break;
case DefenseProp.RoadSpikes: RoadSpikes( b ); break;
}
}
static void Beam( PropBuilder b, string id, string label, Material material, Vector3 from, Vector3 to, float width, float height, bool loose = false )
{
Action<MeshKit> build = kit => kit.Beam( from, to, width, height );
if ( loose ) b.Loose( id, label, material, build );
else b.Part( id, label, material, build );
}
static void WoodBarricade( PropBuilder b )
{
foreach ( var (x, name) in new[] { (-48f, "left"), (48f, "right") } )
{
Beam( b, $"cross_{name}_a", "Trestle", b.Main, new( x - 20, -8, 1.1f ), new( x + 20, -8, 44 ), 3, 3 );
Beam( b, $"cross_{name}_b", "Trestle", b.Main, new( x + 20, -4.9f, 1.1f ), new( x - 20, -4.9f, 44 ), 3, 3 );
}
for ( var i = 0; i < 4; i++ )
Beam( b, $"plank{i}", "Plank", b.Main, new( -76, -2.2f, 12 + i * 8 ), new( 76, -2.2f, 13 + i * 8 ), 2, 5, true );
}
static void Boards( PropBuilder b, float width, float height, bool gaps )
{
var stream = b.Context.Stream( PropCategory, "boards" );
var count = Math.Max( (int)(height / (gaps ? 12.0f : 8.0f)), 2 );
if ( !gaps )
{
b.Box( "post_left", "Post", b.Main, new( -width * 0.5f, 0, 0 ), new( -width * 0.5f + 4, 4, height ) );
b.Box( "post_right", "Post", b.Main, new( width * 0.5f - 4, 0, 0 ), new( width * 0.5f, 4, height ) );
}
for ( var i = 0; i < count; i++ )
{
// Every board has its own layer, so crooked boards can cross without passing through each other.
var y = -(i + 1) * BoardThickness - 0.2f;
var z = (i + 0.5f) * height / count;
var tall = gaps ? 7.0f : 7.5f;
var room = MathF.Max( z - tall * 0.5f - 0.1f, 0.0f );
var tilt = Math.Clamp( gaps ? stream.Range( -8.0f, 8.0f ) : stream.Range( -1.0f, 1.0f ), -room, room );
var half = width * 0.5f * (gaps ? 1.15f : 1.05f);
Beam( b, $"board{i}", "Board", b.Main, new( -half, y, z - tilt ), new( half, y, z + tilt ), BoardThickness, tall, true );
}
}
static void ConcreteBlocks( PropBuilder b )
{
for ( var row = 0; row < 3; row++ )
{
for ( var i = 0; i < 4 - row; i++ )
{
var x = -72 + row * 24 + i * 48;
b.Box( $"block{row}_{i}", "Block", b.Main, new( x, -12, row * 24 ), new( x + 47.9f, 12, row * 24 + 24 ) );
}
}
}
static void Hesco( PropBuilder b )
{
for ( var i = 0; i < 3; i++ )
{
var x = -72 + i * 48;
b.Box( $"cage{i}", "Cage", b.Main, new( x, -24, 0 ), new( x + 47.9f, 24, 56 ) );
b.Box( $"soil{i}", "Fill", b.Detail, new( x + 1, -23, 56 ), new( x + 46.9f, 23, 58 ) );
}
}
static void GuardBooth( PropBuilder b )
{
b.Box( "floor", "Floor", b.Main, new( -30, -30, 0 ), new( 30, 30, 4 ) );
b.Box( "wall_back", "Back Wall", b.Main, new( -30, 26, 4 ), new( 30, 30, 96 ) );
b.Box( "wall_left", "Left Wall", b.Main, new( -30, -30, 4 ), new( -26, 26, 96 ) );
b.Box( "wall_right", "Right Wall", b.Main, new( 26, -30, 4 ), new( 30, 26, 96 ) );
b.Box( "sill", "Sill Wall", b.Main, new( -26, -30, 4 ), new( 26, -26, 44 ) );
b.Box( "roof", "Roof", b.Metal, new( -36, -36, 96 ), new( 36, 36, 102 ) );
}
static void RoadSpikes( PropBuilder b )
{
b.Box( "strip", "Strip", b.Metal, new( -60, -5, 0 ), new( 60, 5, 1.5f ) );
for ( var i = 0; i < 12; i++ )
b.Loose( $"spike{i}", "Spike", b.Metal, kit => kit.Cylinder( new Vector3( -55 + i * 10, 0, 0 ), 2, 0, 1.5f, 8, 6 ) );
}
}