slice 7 regression. level_village_2..5.tscn each reference their own LevelResource for the death screen label, so pointing the resource back at the scene forms a cycle Godot cannot resolve - "Parse Error: Busy" and the level fails to load entirely. ScenePath was a string for exactly this reason. Reverted, and the rot it allowed (forest_1 aimed at a nonexistent scene) is now caught by LevelCatalogue.Validate checking every path resolves, plus a repo-level structural check - fixed by validation rather than by typing. project.godot: restores the C# feature flag and dialogue_manager uses_dotnet, which `godot --headless --import` strips on every run.
84 lines
3.1 KiB
C#
84 lines
3.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Godot;
|
|
using Godot.Collections;
|
|
using Mr.BrickAdventures.Progression;
|
|
|
|
namespace Mr.BrickAdventures.scripts.Resources;
|
|
|
|
/// <summary>
|
|
/// The authored list of Levels in campaign order, and the bridge from a <see cref="LevelId"/> back to
|
|
/// the Godot scene that implements it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 <see cref="Campaign"/>. Scene lookup is wanted solely by
|
|
/// the composition root, which is engine-side anyway.
|
|
/// </remarks>
|
|
[GlobalClass]
|
|
public partial class LevelCatalogue : Resource
|
|
{
|
|
/// <summary>
|
|
/// Levels in campaign order. A Level absent from this list is not reachable, even if its
|
|
/// <see cref="LevelResource"/> exists.
|
|
/// </summary>
|
|
[Export]
|
|
public Array<LevelResource> Levels { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// The ordering the domain works from. Throws if the authored data is inconsistent — a blank or
|
|
/// duplicated id cannot produce a <see cref="Campaign"/>, so a mistake fails at boot rather than
|
|
/// corrupting a save later.
|
|
/// </summary>
|
|
public Campaign ToCampaign()
|
|
{
|
|
var ids = new List<LevelId>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks every catalogued Level names a scene that actually exists, returning the problems found.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is what keeps <see cref="LevelResource.ScenePath"/> honest. The path cannot be a typed
|
|
/// <c>PackedScene</c> — 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.
|
|
/// </remarks>
|
|
public IReadOnlyList<string> Validate()
|
|
{
|
|
var problems = new List<string>();
|
|
|
|
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);
|
|
|
|
/// <summary>Loads a Level's scene on demand. Null when the Level is not catalogued.</summary>
|
|
public PackedScene SceneFor(LevelId id)
|
|
{
|
|
var path = Find(id)?.ScenePath;
|
|
return string.IsNullOrWhiteSpace(path) ? null : ResourceLoader.Load<PackedScene>(path);
|
|
}
|
|
|
|
public string DisplayNameKeyFor(LevelId id) => Find(id)?.LevelName ?? string.Empty;
|
|
}
|