ArenaLayout.cs
using Sandbox;
using System;
[Flags]
public enum ArenaLayoutUsage
{
None = 0,
Standard = 1 << 0,
Lobby = 1 << 1,
HighNoon = 1 << 2
}
public enum ArenaLayoutSection
{
Spawn,
Center
}
/// <summary>
/// Metadata for one arena-module layout root.
///
/// Put this component on the ROOT of each arena layout prefab.
///
/// Arena System v2.1 keeps layout prefabs out of main.scene. ArenaController
/// stores prefab references in its catalog, reads this metadata from those
/// assets, and locally clones only the currently selected Spawn A / Center /
/// Spawn B layouts at runtime.
///
/// LayoutId is stable identity; it is not a list position.
///
/// The authoring gizmos are editor-only visual guides. They do not add gameplay
/// geometry, collision, networking, or runtime presentation.
/// </summary>
[Title( "Arena Layout" )]
[Category( "One More Round" )]
[Icon( "view_quilt" )]
public sealed class ArenaLayout : Component
{
[Property, Group( "Identity" )]
public int LayoutId { get; set; } = 0;
[Property, Group( "Identity" )]
public string DisplayName { get; set; } = "";
[Property, Group( "Usage" )]
public ArenaLayoutUsage Usage { get; set; } =
ArenaLayoutUsage.Standard;
// ---------------------------------------------------------------------
// Authoring
// ---------------------------------------------------------------------
/// <summary>
/// Which module this prefab is authored for.
/// Spawn layouts are authored from Spawn A's canonical orientation and
/// Spawn B mirrors them by rotating its module 180 degrees around Yaw.
/// </summary>
[Property, Group( "Authoring" )]
public ArenaLayoutSection Section { get; set; } =
ArenaLayoutSection.Spawn;
/// <summary>
/// Draw module bounds, spawn references, clearance zones and facing arrows
/// while this layout (or one of its children) is selected in the editor.
/// </summary>
[Property, Group( "Authoring" )]
public bool ShowAuthoringGuides { get; set; } = true;
/// <summary>
/// Visual no-build radius around a player spawn.
/// This is intentionally larger than the physical player radius.
/// </summary>
[Property, Group( "Authoring" ), Range( 24f, 160f )]
public float SpawnClearanceRadius { get; set; } = 64f;
/// <summary>
/// Show the real opposing spawn references while editing a layout.
/// Especially useful for checking long sight lines from Center layouts.
/// </summary>
[Property, Group( "Authoring" )]
public bool ShowRemoteSpawnReferences { get; set; } = true;
// Current OMR arena/player measurements.
// These are authoring references only.
// Matches the existing Guide_SpawnA / Guide_Center meshes in main.scene:
// X = -480..+480, so the playable/module width is 960 units.
private const float ArenaWidth = 960f;
private const float SpawnSectionLength = 384f;
private const float CenterSectionLength = 512f;
private const float SpawnDistanceFromArenaCenter = 448f;
private const float SpawnRootHeight = 16f;
private const float PlayerBodyRadius = 20f;
private const float PlayerBodyHeight = 72f;
public bool Supports(
ArenaLayoutUsage usage
)
{
return (
Usage & usage
) != 0;
}
public string GetDisplayName()
{
if ( !string.IsNullOrWhiteSpace( DisplayName ) )
return DisplayName;
if ( GameObject is null )
return $"LAYOUT {LayoutId}";
return GameObject.Name
.Replace( "_", " " );
}
protected override void DrawGizmos()
{
base.DrawGizmos();
if ( !ShowAuthoringGuides )
return;
// When the prefab asset itself is open for authoring, keep the guides
// visible continuously so they remain useful while moving/resizing child
// geometry. In main.scene the layout is a prefab instance, so fall back
// to selected-only drawing to avoid filling the arena with every layout's
// authoring guides at once.
if ( GameObject.IsPrefabInstance )
{
if ( !Gizmo.IsSelected && !Gizmo.IsChildSelected )
return;
}
Gizmo.Transform = WorldTransform;
Gizmo.Draw.IgnoreDepth = true;
DrawSectionBounds();
if ( Section == ArenaLayoutSection.Spawn )
{
DrawSpawnLayoutGuides();
}
else
{
DrawCenterLayoutGuides();
}
}
private void DrawSectionBounds()
{
float length =
Section == ArenaLayoutSection.Spawn
? SpawnSectionLength
: CenterSectionLength;
var size = new Vector3(
ArenaWidth,
length,
4f
);
var box = BBox.FromPositionAndSize(
new Vector3( 0f, 0f, -2f ),
size
);
Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.10f );
Gizmo.Draw.SolidBox( box );
Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.8f );
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.LineBBox( box );
// X centerline.
Gizmo.Draw.Color = Color.White.WithAlpha( 0.25f );
Gizmo.Draw.LineThickness = 1f;
Gizmo.Draw.Line(
new Vector3( 0f, -length * 0.5f, 2f ),
new Vector3( 0f, length * 0.5f, 2f )
);
}
private void DrawSpawnLayoutGuides()
{
var localSpawn = new Vector3(
0f,
0f,
SpawnRootHeight
);
// Canonical Spawn A faces toward arena center (-Y).
DrawSpawnReference(
localSpawn,
new Vector3( 0f, -1f, 0f ),
Color.Red,
drawClearance: true
);
// Highlight the edge of this module that leads into Center.
float centerEdgeY = -SpawnSectionLength * 0.5f;
Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.9f );
Gizmo.Draw.LineThickness = 3f;
Gizmo.Draw.Line(
new Vector3( -ArenaWidth * 0.5f, centerEdgeY, 3f ),
new Vector3( ArenaWidth * 0.5f, centerEdgeY, 3f )
);
// Direction-to-center arrow.
Gizmo.Draw.Color = Color.Yellow;
Gizmo.Draw.Arrow(
new Vector3( 0f, -48f, 36f ),
new Vector3( 0f, -176f, 36f ),
18f,
8f
);
if ( !ShowRemoteSpawnReferences )
return;
// Opposing spawn relative to canonical Spawn A layout root:
// +448 world Y -> -448 world Y = -896 local Y.
DrawSpawnReference(
new Vector3(
0f,
-SpawnDistanceFromArenaCenter * 2f,
SpawnRootHeight
),
new Vector3( 0f, 1f, 0f ),
Color.Yellow,
drawClearance: false
);
}
private void DrawCenterLayoutGuides()
{
if ( !ShowRemoteSpawnReferences )
return;
// Center module is at world origin, so the two real round spawn points
// are directly representable in its local space.
DrawSpawnReference(
new Vector3(
0f,
SpawnDistanceFromArenaCenter,
SpawnRootHeight
),
new Vector3( 0f, -1f, 0f ),
Color.Cyan,
drawClearance: false
);
DrawSpawnReference(
new Vector3(
0f,
-SpawnDistanceFromArenaCenter,
SpawnRootHeight
),
new Vector3( 0f, 1f, 0f ),
Color.Yellow,
drawClearance: false
);
}
private void DrawSpawnReference(
Vector3 spawnPosition,
Vector3 facingDirection,
Color color,
bool drawClearance
)
{
var bodyBottom = new Vector3(
spawnPosition.x,
spawnPosition.y,
0f
);
var bodyTop = new Vector3(
spawnPosition.x,
spawnPosition.y,
PlayerBodyHeight
);
// Physical player footprint reference.
Gizmo.Draw.Color = color.WithAlpha( 0.9f );
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.LineCylinder(
bodyBottom,
bodyTop,
PlayerBodyRadius,
PlayerBodyRadius,
20
);
// Exact round-spawn root.
Gizmo.Draw.LineSphere(
new Sphere(
spawnPosition,
8f
),
12
);
// Facing direction.
Gizmo.Draw.Arrow(
new Vector3(
spawnPosition.x,
spawnPosition.y,
36f
),
new Vector3(
spawnPosition.x + facingDirection.x * 96f,
spawnPosition.y + facingDirection.y * 96f,
36f
),
14f,
6f
);
if ( !drawClearance )
return;
// Authoring-only no-build zone around the real spawn.
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.18f );
Gizmo.Draw.SolidBox(
BBox.FromPositionAndSize(
new Vector3(
spawnPosition.x,
spawnPosition.y,
2f
),
new Vector3(
SpawnClearanceRadius * 2f,
SpawnClearanceRadius * 2f,
4f
)
)
);
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.95f );
Gizmo.Draw.LineBBox(
BBox.FromPositionAndSize(
new Vector3(
spawnPosition.x,
spawnPosition.y,
2f
),
new Vector3(
SpawnClearanceRadius * 2f,
SpawnClearanceRadius * 2f,
4f
)
)
);
}
}