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.
25 lines
1.2 KiB
C#
25 lines
1.2 KiB
C#
namespace Mr.BrickAdventures.Progression;
|
|
|
|
/// <summary>
|
|
/// Something that happened to player progression, raised by an aggregate as part of the operation
|
|
/// that caused it. Plain records with no engine dependency — an adapter republishes them as Godot
|
|
/// signals (ADR-0004), which is what makes "state changed ⇒ signal fired" un-bypassable.
|
|
/// </summary>
|
|
public abstract record ProgressionEvent;
|
|
|
|
/// <summary>Raised when a Playthrough loses its last life.</summary>
|
|
public sealed record RunEnded : ProgressionEvent;
|
|
|
|
/// <summary>
|
|
/// Raised the first time an Achievement is unlocked, and only then — so a subscriber that shows a
|
|
/// popup or pushes to Steam cannot fire twice however often it is told.
|
|
/// </summary>
|
|
public sealed record AchievementUnlocked(AchievementId Achievement) : ProgressionEvent;
|
|
|
|
/// <summary>
|
|
/// Raised whenever a Level is entered — every time, including the first Level of a new Playthrough
|
|
/// and the first Level after resuming one. Ghost recording and the speedrun timer both start from
|
|
/// this, so an entry path that does not announce is an entry path where neither works.
|
|
/// </summary>
|
|
public sealed record LevelStarted(LevelId Level) : ProgressionEvent;
|