Component that picks which level to play. It discovers .level resources or uses a manual ArcadeLevels list, provides Weekly and Arcade selection logic, builds a shuffled arcade order, and exposes accessors for current/weekly levels and weekly seed.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.UI;
namespace Breakout;
/// <summary>
/// Owns which level to play: runtime discovery of all .level resources, the Arcade shuffle
/// order, and the deterministic Weekly selection. BreakoutGame delegates here.
/// </summary>
[Title( "Level Director" ), Category( "Breakout" ), Icon( "playlist_play" )]
public sealed class LevelDirector : Component
{
/// <summary>
/// Optional manual override. When empty, the pool is discovered from every .level resource.
/// </summary>
[Property] public List<LevelResource> ArcadeLevels { get; set; } = new();
private List<LevelResource> _discoveredLevels;
private readonly List<int> arcadeOrder = new();
private int arcadeOrderPos;
private int lastArcadeIndex = -1;
/// <summary>
/// The pool of levels: the manual ArcadeLevels list if set, otherwise every .level asset in the project.
/// </summary>
public IReadOnlyList<LevelResource> Levels
{
get
{
if ( ArcadeLevels is { Count: > 0 } )
return ArcadeLevels;
if ( _discoveredLevels is not { Count: > 0 } )
{
_discoveredLevels = ResourceLibrary.GetAll<LevelResource>()
.OrderBy( l => l.ResourcePath, StringComparer.OrdinalIgnoreCase )
.ToList();
}
return _discoveredLevels;
}
}
/// <summary>
/// Which level index this week's challenge uses (the same for everyone playing that week).
/// </summary>
public int WeeklyLevelIndex => Levels is { Count: > 0 }
? ((BreakoutGame.WeeklyId % Levels.Count) + Levels.Count) % Levels.Count
: 0;
/// <summary>
/// A per-week, per-level random seed, so the Weekly board's brick mix matches for everyone that week.
/// </summary>
public int WeeklySeed( int level ) => BreakoutGame.WeeklyId * 397 + level;
/// <summary>
/// The level resource to load for the given mode and level number.
/// </summary>
public LevelResource CurrentLevel( GameMode mode, int level )
{
var levels = Levels;
if ( levels is not { Count: > 0 } )
return null;
int idx = mode switch
{
GameMode.Weekly => WeeklyLevelIndex,
GameMode.Arcade when arcadeOrder.Count > 0 => arcadeOrder[Math.Clamp( arcadeOrderPos, 0, arcadeOrder.Count - 1 )],
_ => 0
};
return levels[Math.Clamp( idx, 0, levels.Count - 1 )];
}
/// <summary>
/// Starts a fresh Arcade run: forget the previously played board, then shuffle.
/// </summary>
public void BeginArcadeRun()
{
lastArcadeIndex = -1;
BuildArcadeOrder();
}
/// <summary>
/// Steps to the next level in the shuffled Arcade order, reshuffling once the list runs out.
/// </summary>
public void AdvanceArcadeOrder()
{
if ( arcadeOrder.Count == 0 )
{
BuildArcadeOrder();
return;
}
lastArcadeIndex = arcadeOrder[Math.Clamp( arcadeOrderPos, 0, arcadeOrder.Count - 1 )];
arcadeOrderPos++;
if ( arcadeOrderPos >= arcadeOrder.Count )
BuildArcadeOrder();
}
private void BuildArcadeOrder()
{
arcadeOrder.Clear();
arcadeOrderPos = 0;
var levels = Levels;
if ( levels is not { Count: > 0 } )
return;
arcadeOrder.AddRange( Enumerable.Range( 0, levels.Count ).OrderBy( _ => Game.Random.Float( 0f, 1f ) ) );
if ( arcadeOrder.Count > 1 && arcadeOrder[0] == lastArcadeIndex )
(arcadeOrder[0], arcadeOrder[1]) = (arcadeOrder[1], arcadeOrder[0]);
}
public LevelResource GetWeeklyLevel()
{
var levels = Levels;
if ( levels is not { Count: > 0 } )
return null;
return levels[Math.Clamp( WeeklyLevelIndex, 0, levels.Count - 1 )];
}
}