using System.Collections.Generic; using System.Linq; using Godot; using Godot.Collections; using Mr.BrickAdventures.Progression; namespace Mr.BrickAdventures.scripts.Resources; /// /// The authored list of Levels in campaign order, and the bridge from a back to /// the Godot scene that implements it. /// /// /// Not a domain port. Nothing in the progression domain resolves a Level to a scene — the domain only /// needs the ordering, which it takes as a . Scene lookup is wanted solely by /// the composition root, which is engine-side anyway. /// [GlobalClass] public partial class LevelCatalogue : Resource { /// /// Levels in campaign order. A Level absent from this list is not reachable, even if its /// exists. /// [Export] public Array Levels { get; set; } = []; /// /// The ordering the domain works from. Throws if the authored data is inconsistent — a blank or /// duplicated id cannot produce a , so a mistake fails at boot rather than /// corrupting a save later. /// public Campaign ToCampaign() { var ids = new List(); for (var i = 0; i < Levels.Count; i++) { if (Levels[i] == null) throw new System.InvalidOperationException($"LevelCatalogue has an empty slot at index {i}."); ids.Add(LevelId.Of(Levels[i].Id)); } return Campaign.Of(ids); } /// /// Checks every catalogued Level names a scene that actually exists, returning the problems found. /// /// /// This is what keeps honest. The path cannot be a typed /// PackedScene — several level scenes reference their own LevelResource, so a reference back /// to the scene is a cycle Godot reports as "Parse Error: Busy" and the level fails to load. A /// string plus this check gets the safety without the cycle. /// public IReadOnlyList Validate() { var problems = new List(); foreach (var level in Levels.Where(l => l != null)) { if (string.IsNullOrWhiteSpace(level.ScenePath)) problems.Add($"level '{level.Id}' has no scene path"); else if (!ResourceLoader.Exists(level.ScenePath)) problems.Add($"level '{level.Id}' points at '{level.ScenePath}', which does not exist"); } return problems; } public LevelResource Find(LevelId id) => Levels.FirstOrDefault(level => level != null && level.Id == id.Value); /// Loads a Level's scene on demand. Null when the Level is not catalogued. public PackedScene SceneFor(LevelId id) { var path = Find(id)?.ScenePath; return string.IsNullOrWhiteSpace(path) ? null : ResourceLoader.Load(path); } public string DisplayNameKeyFor(LevelId id) => Find(id)?.LevelName ?? string.Empty; }