Files
przygody-pana-cegly/progression/SkillCatalogue.cs
Gabriel Kaszewski 330e10880c feat(progression): use cases (slice 6)
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.
2026-07-28 19:10:06 +02:00

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;
}