ProgressionService holds the loaded Profile, active Playthrough and active Attempt, so "exactly one active Attempt" has a testable home. EnterLevel always announces LevelStarted - the reason ghost recording and splits never worked on level 1 or after a Continue. Refuses unreachable levels. Does not touch campaign position, so a Replay leaves it alone. SkillCatalogue returns null for a skill with no authored upgrades, killing the Upgrades[0].Cost overrun. PurchaseSkill reads the current level from the ledger first, so one Attempt can unlock then upgrade. HandlePlayerDeath now persists - death was free if you quit. SkillLevel.First/Next driven out here by a real caller. 53 tests.
41 lines
1.8 KiB
C#
41 lines
1.8 KiB
C#
namespace Mr.BrickAdventures.Progression;
|
|
|
|
/// <summary>
|
|
/// The authored upgrade table for every Skill: what each level costs to reach. Built from
|
|
/// <c>SkillData</c> resources by an adapter — same pattern as <see cref="Campaign"/>, so no port is
|
|
/// needed and a test can construct one directly.
|
|
/// </summary>
|
|
public sealed class SkillCatalogue
|
|
{
|
|
private readonly Dictionary<SkillId, IReadOnlyList<Coins>> _costs;
|
|
|
|
private SkillCatalogue(Dictionary<SkillId, IReadOnlyList<Coins>> costs) => _costs = costs;
|
|
|
|
public static SkillCatalogue Empty() => new(new Dictionary<SkillId, IReadOnlyList<Coins>>());
|
|
|
|
/// <summary>
|
|
/// Builds a catalogue. Each Skill's list is the cost to reach level 1, level 2, and so on, so a
|
|
/// Skill with an empty list cannot be bought at all.
|
|
/// </summary>
|
|
public static SkillCatalogue Of(IReadOnlyDictionary<SkillId, IReadOnlyList<Coins>> costsByLevel) =>
|
|
new(costsByLevel.ToDictionary(e => e.Key, e => e.Value));
|
|
|
|
/// <summary>
|
|
/// The highest level this Skill can reach, or null when it has no authored upgrades and so
|
|
/// cannot be bought. Returning null is what stops <c>Upgrades[0].Cost</c> from throwing on the
|
|
/// five Skills that currently have no upgrade entries.
|
|
/// </summary>
|
|
public SkillLevel? MaxLevelOf(SkillId skill) =>
|
|
_costs.TryGetValue(skill, out var costs) && costs.Count > 0
|
|
? SkillLevel.Of(costs.Count)
|
|
: null;
|
|
|
|
/// <summary>
|
|
/// What it costs to reach <paramref name="level"/>, or null if this Skill does not go that high.
|
|
/// </summary>
|
|
public Coins? CostToReach(SkillId skill, SkillLevel level) =>
|
|
_costs.TryGetValue(skill, out var costs) && level.Value <= costs.Count
|
|
? costs[level.Value - 1]
|
|
: null;
|
|
}
|