Files
przygody-pana-cegly/Autoloads/ProgressionEventRepublisher.cs
Gabriel Kaszewski ca54bf0f82 feat(progression): read surface and change announcements (slice 10a)
reads for the UI: TotalCoins, Lives, IsRunOver, CurrentLevel,
LevelOf/IsOwned/CanAfford, HasCleared. The read half of candidate 01 -
callers get a narrow surface instead of reaching into state.

new domain events LivesChanged / TotalCoinsChanged / SkillSetChanged,
announced by comparing against the last announced value, so an operation
can neither forget to announce nor announce something unchanged.

HandlePlayerDeath now clears the spent attempt - it was still counting
towards TotalCoins after its ledger had been discarded.

republisher and EventBus extended; SkillSetChanged is what SkillManager
will reconcile against instead of the non-atomic call pairs. 65 tests.
2026-07-28 19:50:05 +02:00

85 lines
2.6 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 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;
}
}