no LoseLife (ADR-0005). SettleDeath / SettleCompletion defer to the Attempt's terminal outcome, so both are atomic and idempotent. SettleCompletion branches on HasCleared itself - a Replay cannot bank or move campaign position backwards (ADR-0003). Campaign value object holds authored level order; ReachableLevels is derived, replacing the stored UnlockedLevels. Coins.IsAtLeast driven out by cross-scope affordability. 29 tests. CONTEXT.md: Campaign, Settle.
38 lines
1.2 KiB
C#
38 lines
1.2 KiB
C#
namespace Mr.BrickAdventures.Progression;
|
|
|
|
/// <summary>
|
|
/// The authored ordering of Levels that defines progression. Shared by every Playthrough — it is
|
|
/// authored data, not per-Playthrough state, and carries no scene or presentation detail.
|
|
/// </summary>
|
|
public sealed class Campaign
|
|
{
|
|
private readonly List<LevelId> _levels;
|
|
|
|
private Campaign(List<LevelId> levels) => _levels = levels;
|
|
|
|
public static Campaign Of(IEnumerable<LevelId> levels)
|
|
{
|
|
var ordered = levels.ToList();
|
|
|
|
if (ordered.Count == 0)
|
|
throw new ArgumentException("A campaign needs at least one level.", nameof(levels));
|
|
|
|
if (ordered.Distinct().Count() != ordered.Count)
|
|
throw new ArgumentException("A campaign cannot list the same level twice.", nameof(levels));
|
|
|
|
return new Campaign(ordered);
|
|
}
|
|
|
|
public LevelId First => _levels[0];
|
|
|
|
/// <summary>
|
|
/// The Level after <paramref name="level"/>, or null if it is the last one or not in this
|
|
/// Campaign at all.
|
|
/// </summary>
|
|
public LevelId? Next(LevelId level)
|
|
{
|
|
var at = _levels.IndexOf(level);
|
|
return at >= 0 && at + 1 < _levels.Count ? _levels[at + 1] : null;
|
|
}
|
|
}
|