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:
72
Autoloads/ProgressionEventRepublisher.cs
Normal file
72
Autoloads/ProgressionEventRepublisher.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
/// <summary>
|
||||
/// Translates domain events into Godot signals on the <see cref="EventBus"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The adapter half of ADR-0004. Aggregates raise plain records with no engine dependency; this is the
|
||||
/// single place that turns them into signals, which is what makes "state changed ⇒ signal fired"
|
||||
/// un-bypassable. Existing subscribers and scene wiring keep working unchanged.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Not a Node — it has no lifecycle of its own. The composition root owns one and hands it every
|
||||
/// batch of drained events.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ProgressionEventRepublisher
|
||||
{
|
||||
private readonly LevelCatalogue _catalogue;
|
||||
private readonly SceneTree _tree;
|
||||
|
||||
public ProgressionEventRepublisher(LevelCatalogue catalogue, SceneTree tree)
|
||||
{
|
||||
_catalogue = catalogue;
|
||||
_tree = tree;
|
||||
}
|
||||
|
||||
public void Publish(IReadOnlyList<ProgressionEvent> events)
|
||||
{
|
||||
foreach (var @event in events)
|
||||
Publish(@event);
|
||||
}
|
||||
|
||||
private void Publish(ProgressionEvent @event)
|
||||
{
|
||||
switch (@event)
|
||||
{
|
||||
case LevelStarted started:
|
||||
// The existing signal is keyed by array index because GhostManager names its replay
|
||||
// files ghost_level_{index}.json. Transitional: once ghosts key on LevelId this
|
||||
// becomes a straight pass-through and the index lookup goes away.
|
||||
EventBus.EmitLevelStarted(IndexOf(started.Level), _tree.CurrentScene);
|
||||
break;
|
||||
|
||||
case RunEnded:
|
||||
EventBus.EmitRunEnded();
|
||||
break;
|
||||
|
||||
case AchievementUnlocked unlocked:
|
||||
EventBus.EmitAchievementUnlocked(unlocked.Achievement.Value);
|
||||
break;
|
||||
|
||||
default:
|
||||
GD.PushWarning($"No signal is published for domain event {@event.GetType().Name}.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private int IndexOf(LevelId level)
|
||||
{
|
||||
for (var i = 0; i < _catalogue.Levels.Count; i++)
|
||||
if (_catalogue.Levels[i]?.Id == level.Value)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user