fixes the regression commit B introduced: coins and skills lived in the Attempt ledger, but nothing discarded it, because LivesStateHandler still called the old store and HandlePlayerDeath was never invoked. Dying kept everything earned that level. Discard-on-death is the ledger's purpose, so it could not be separated from it. new AttemptFailed(remaining, runOver) event, raised after the life is spent and the ledger discarded. DeathScreen and GameOverScreen react to that instead of being wired to HealthComponent.Death by scene connections which fired *before* the life was charged - on the last life game-over saw one life left and showed nothing, then the death screen saw zero and refused to reload. 18 scene connections deleted; the race is now unrepresentable rather than fixed. DeathScreen also had two read paths for lives - a signal and a direct store read - because neither was reliable alone. Now one. Hud reads both facts from signals; only health still polls, and that belongs to the component rather than to progression. GameManager no longer touches GameStateStore or SaveSystem at all.
55 lines
2.7 KiB
C#
55 lines
2.7 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>
|
|
/// An Attempt ended in death and has been settled: the life is already spent and the ledger already
|
|
/// discarded when this is raised.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Exists so no screen has to read <c>Lives</c> for itself. The death and game-over screens were wired
|
|
/// straight to <c>HealthComponent.Death</c> by scene connections, which fire before the life is charged
|
|
/// — so on the last life the game-over screen saw one life left and showed nothing, and the death
|
|
/// screen then saw zero and refused to reload. Carrying the settled values removes the race entirely.
|
|
/// </remarks>
|
|
public sealed record AttemptFailed(Lives Remaining, bool RunOver) : 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;
|
|
|
|
/// <summary>
|
|
/// Lives remaining changed. Raised by the same operation that changed them, so no reader can observe
|
|
/// a stale value between the write and the announcement.
|
|
/// </summary>
|
|
public sealed record LivesChanged(Lives Remaining) : ProgressionEvent;
|
|
|
|
/// <summary>
|
|
/// Spendable coins changed — the Bank plus the current Attempt's takings, less what it has spent.
|
|
/// A single fact for readers, so nothing has to know the Attempt and Bank are separate.
|
|
/// </summary>
|
|
public sealed record TotalCoinsChanged(Coins Total) : ProgressionEvent;
|
|
|
|
/// <summary>
|
|
/// The set of owned Skills, or the level of one of them, changed. What <c>SkillManager</c> reconciles
|
|
/// its live components against, instead of being told separately by every caller.
|
|
/// </summary>
|
|
public sealed record SkillSetChanged : ProgressionEvent;
|