ArenaGenerator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Facepunch.BombRoyale;
[Title( "Arena Generator" )]
[Category( "Bomb Royale" )]
public class ArenaGenerator : Component, IRestartable
{
[Property] public List<ArenaTheme> Themes { get; set; } = new();
[Property] public ModelRenderer GroundRenderer { get; set; }
[Property, Range( 0f, 1f )] public float FillPercentage { get; set; } = 0.65f;
[Property] public float PickupSpawnChance { get; set; } = 0.35f;
[Sync] private string ActiveThemePath { get; set; }
private ArenaTheme ActiveTheme { get; set; }
private const float GridSize = 32f;
private const int InnerExtent = 5;
private GameObject BreakablesContainer { get; set; }
private GameObject AmbienceInstance { get; set; }
void IRestartable.OnRestart()
{
Generate();
}
protected override void OnStart()
{
if ( !string.IsNullOrEmpty( ActiveThemePath ) )
ApplyTheme( ActiveThemePath );
}
private void Generate()
{
if ( !Networking.IsHost ) return;
PickTheme();
BroadcastTheme( ActiveThemePath );
SpawnAmbience();
DestroyBreakables();
GenerateBreakables();
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void BroadcastTheme( string path )
{
ActiveThemePath = path;
ApplyTheme( path );
}
private void ApplyTheme( string path )
{
ActiveTheme = ResolveTheme( path );
ApplyFloorMaterial();
ApplyBackgroundColor();
ApplySolidBlockModels();
}
private void ApplySolidBlockModels()
{
var models = ActiveTheme?.SolidBlockModels;
if ( models is null || models.Count == 0 ) return;
var themeSeed = ActiveTheme.ResourcePath?.GetHashCode() ?? 0;
foreach ( var solid in Scene.GetAllComponents<SolidBlock>() )
{
var seed = HashCode.Combine( solid.GameObject.Id, themeSeed );
var model = models[(int)((uint)seed % models.Count)];
var renderer = solid.Components.Get<ModelRenderer>();
if ( renderer.IsValid() )
renderer.Model = model;
var collider = solid.Components.Get<ModelCollider>();
if ( collider.IsValid() )
collider.Model = model;
}
}
private void SpawnAmbience()
{
if ( AmbienceInstance.IsValid() )
{
AmbienceInstance.Destroy();
AmbienceInstance = null;
}
var prefab = ActiveTheme?.AmbiencePrefab;
if ( prefab is null ) return;
AmbienceInstance = prefab.Clone();
AmbienceInstance.Name = "Ambience";
AmbienceInstance.NetworkSpawn();
}
private void PickTheme()
{
var theme = Themes.Count > 0 ? Game.Random.FromList( Themes ) : null;
ActiveThemePath = theme?.ResourcePath;
}
private ArenaTheme ResolveTheme( string path )
{
if ( string.IsNullOrEmpty( path ) ) return null;
return Themes.FirstOrDefault( t => t is not null && t.ResourcePath == path );
}
private void ApplyFloorMaterial()
{
if ( !GroundRenderer.IsValid() ) return;
if ( ActiveTheme?.FloorMaterial is null ) return;
GroundRenderer.MaterialOverride = ActiveTheme.FloorMaterial;
}
private void ApplyBackgroundColor()
{
var camera = Scene.Camera;
if ( !camera.IsValid() ) return;
if ( ActiveTheme is null ) return;
camera.BackgroundColor = ActiveTheme.BackgroundColor;
}
private void DestroyBreakables()
{
if ( BreakablesContainer.IsValid() )
{
BreakablesContainer.Destroy();
BreakablesContainer = null;
}
var existing = Scene.GetAllComponents<Bombable>().ToList();
foreach ( var bombable in existing )
{
bombable.GameObject.Destroy();
}
}
private void GenerateBreakables()
{
BreakablesContainer = new GameObject( true, "Breakables" )
{
WorldPosition = Vector3.Zero
};
var guaranteed = GetGuaranteedBreakablePositions();
var validPositions = GetValidBreakablePositions();
validPositions.RemoveAll( p => guaranteed.Contains( p ) );
var count = (int)( validPositions.Count * FillPercentage );
var chosen = validPositions.OrderBy( _ => Game.Random.Next() ).Take( count ).ToList();
foreach ( var gridPos in guaranteed )
{
var worldPos = new Vector3( gridPos.x * GridSize, gridPos.y * GridSize, 0f );
SpawnBreakable( worldPos, GetRandomBreakableModel() );
}
foreach ( var gridPos in chosen )
{
var worldPos = new Vector3( gridPos.x * GridSize, gridPos.y * GridSize, 0f );
SpawnBreakable( worldPos, GetRandomBreakableModel() );
}
BreakablesContainer.NetworkSpawn();
}
private void SpawnBreakable( Vector3 position, Model model )
{
var go = new GameObject( true, "Breakable" )
{
Parent = BreakablesContainer,
WorldPosition = position,
NetworkMode = NetworkMode.Object
};
var renderer = go.Components.Create<ModelRenderer>();
renderer.Model = model;
var collider = go.Components.Create<ModelCollider>();
collider.Model = Model.Load( "models/block_metal_a/block_metal_a.vmdl" );
collider.Static = false;
var bombable = go.Components.Create<Bombable>();
bombable.Renderer = renderer;
bombable.SpawnPickupChance = PickupSpawnChance;
}
private Model GetRandomBreakableModel()
{
var models = ActiveTheme?.BreakableBlockModels;
return models is { Count: > 0 } ? Game.Random.FromList( models ) : Model.Load( "models/block_a.vmdl" );
}
private List<Vector2Int> GetValidBreakablePositions()
{
var positions = new List<Vector2Int>();
var spawns = GetSpawnSafeZones();
var pillars = GetPillarPositions();
for ( var x = -InnerExtent; x <= InnerExtent; x++ )
{
for ( var y = -InnerExtent; y <= InnerExtent; y++ )
{
var pos = new Vector2Int( x, y );
if ( pillars.Contains( pos ) )
continue;
if ( spawns.Contains( pos ) )
continue;
positions.Add( pos );
}
}
return positions;
}
/// <summary>
/// Interior pillars at every even-even grid position.
/// </summary>
private static HashSet<Vector2Int> GetPillarPositions()
{
var pillars = new HashSet<Vector2Int>();
for ( var x = -InnerExtent + 1; x <= InnerExtent - 1; x++ )
{
for ( var y = -InnerExtent + 1; y <= InnerExtent - 1; y++ )
{
if ( x % 2 == 0 && y % 2 == 0 )
pillars.Add( new Vector2Int( x, y ) );
}
}
return pillars;
}
/// <summary>
/// Blocks that must always spawn at the edges of each spawn safe zone,
/// boxing the player into their starting L-shape.
/// </summary>
private static HashSet<Vector2Int> GetGuaranteedBreakablePositions()
{
var positions = new HashSet<Vector2Int>();
var spawnCorners = new Vector2Int[]
{
new( InnerExtent, -InnerExtent ),
new( InnerExtent, InnerExtent ),
new( -InnerExtent, InnerExtent ),
new( -InnerExtent, -InnerExtent )
};
foreach ( var corner in spawnCorners )
{
var signX = Math.Sign( corner.x );
var signY = Math.Sign( corner.y );
positions.Add( new Vector2Int( corner.x - signX * 2, corner.y ) );
positions.Add( new Vector2Int( corner.x, corner.y - signY * 2 ) );
}
return positions;
}
/// <summary>
/// Each spawn corner needs the spawn tile and the two adjacent tiles kept clear
/// so the player can always move out.
/// </summary>
private static HashSet<Vector2Int> GetSpawnSafeZones()
{
var safe = new HashSet<Vector2Int>();
var spawnCorners = new Vector2Int[]
{
new( InnerExtent, -InnerExtent ),
new( InnerExtent, InnerExtent ),
new( -InnerExtent, InnerExtent ),
new( -InnerExtent, -InnerExtent )
};
foreach ( var corner in spawnCorners )
{
safe.Add( corner );
var signX = Math.Sign( corner.x );
var signY = Math.Sign( corner.y );
safe.Add( new Vector2Int( corner.x - signX, corner.y ) );
safe.Add( new Vector2Int( corner.x, corner.y - signY ) );
}
return safe;
}
}