feat(progression): composition root and event republisher (slice 9)

ProgressionRoot autoload builds the JSON stores from
GlobalizePath("user://"), loads the Profile, reads the authored upgrade
tables into a SkillCatalogue, and owns the ProgressionService. Every
mutating operation is wrapped so draining and republishing cannot be
forgotten - the one risk of the DrainEvents design, now confined to one
file.

ProgressionEventRepublisher turns domain records into EventBus signals
(ADR-0004), so existing subscribers and scene wiring keep working. New
RunEnded signal: GameOverScreen reads Lives directly today, which is the
last-life soft-lock.

Registered after SkillManager, since the skill catalogue is built from
AvailableSkills. GameStateStore stays until slice 10 moves the callers.

Transitional: LevelStarted is republished with a catalogue index because
GhostManager names files ghost_level_{index}.json.
This commit is contained in:
2026-07-28 19:43:04 +02:00
parent f85c16c641
commit 627900b2e8
5 changed files with 239 additions and 0 deletions

View File

@@ -154,6 +154,16 @@ public partial class EventBus : Node
[Signal] public delegate void CoinsChangedEventHandler(int totalCoins);
[Signal] public delegate void LivesChangedEventHandler(int lives);
/// <summary>
/// The Playthrough has no lives left. Raised as part of the same operation that spends the last
/// life, so a subscriber can never observe lives that have not been decremented yet — which is
/// the race that lets the death screen sit there forever on the last life.
/// </summary>
[Signal] public delegate void RunEndedEventHandler();
public static void EmitRunEnded()
=> Instance?.EmitSignal(SignalName.RunEnded);
public static void EmitCoinsChanged(int totalCoins)
=> Instance?.EmitSignal(SignalName.CoinsChanged, totalCoins);