Files
przygody-pana-cegly/Autoloads/ProgressionEventRepublisher.cs
Gabriel Kaszewski e8d1da8f89 feat: lives and death move to the new domain (commit C of the swap)
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.
2026-07-28 20:19:35 +02:00

89 lines
2.8 KiB
C#

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 AttemptFailed failed:
EventBus.EmitAttemptFailed(failed.Remaining.Value, failed.RunOver);
break;
case AchievementUnlocked unlocked:
EventBus.EmitAchievementUnlocked(unlocked.Achievement.Value);
break;
case LivesChanged lives:
EventBus.EmitLivesChanged(lives.Remaining.Value);
break;
case TotalCoinsChanged coins:
EventBus.EmitCoinsChanged(coins.Total.Value);
break;
case SkillSetChanged:
EventBus.EmitSkillSetChanged();
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;
}
}