feat: coins and skills move to the new domain (commit B of the swap)
atomic pair - purchasing couples them, so neither moves alone. SkillData.Level is gone. It was an [Export] on a cached Resource: one instance per process, so the level never persisted, leaked between save slots, and made "unowned" indistinguishable from "level 1". Level now comes from the Playthrough and is passed to ISkill.Initialize. SkillManager reconciles from SkillSetChanged instead of being told by each caller. The old shape was "record it, then tell SkillManager" as two calls at four sites; AddSkill's early return when no player is registered left the skill owned but inert. Reconciliation also tracks the level each component was built at, so an upgrade rebuilds it rather than being patched in from outside. SkillUnlockerComponent collapses to one TryBuy - unlock and upgrade were two methods each doing affordability, debit, level mutation and manager notification in order. The guard three components copy-pasted around Upgrades[Level - 1] - and which BrickArmor omitted - now lives once in SkillComponentBase. Hud reads coins from CoinsChanged instead of rebuilding translated strings every frame. Lives still polls; it moves with the death path.
This commit is contained in:
@@ -14,16 +14,32 @@ public partial class Hud : Control
|
||||
|
||||
private GameStateStore Store => GameStateStore.Instance;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (EventBus.Instance != null)
|
||||
EventBus.Instance.CoinsChanged += OnCoinsChanged;
|
||||
|
||||
SetCoinsLabel(ProgressionRoot.Instance?.TotalCoins.Value ?? 0);
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (EventBus.Instance != null)
|
||||
EventBus.Instance.CoinsChanged -= OnCoinsChanged;
|
||||
}
|
||||
|
||||
// Lives still polls the old store; it moves with the death path in the next commit.
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
SetHealthBar();
|
||||
SetLivesLabel();
|
||||
SetCoinsLabel();
|
||||
}
|
||||
|
||||
private void SetCoinsLabel()
|
||||
private void OnCoinsChanged(int totalCoins) => SetCoinsLabel(totalCoins);
|
||||
|
||||
private void SetCoinsLabel(int totalCoins)
|
||||
{
|
||||
CoinsLabel.Text = Tr("COINS_LABEL") + ": " + (Store?.GetTotalCoins() ?? 0);
|
||||
CoinsLabel.Text = Tr("COINS_LABEL") + ": " + totalCoins;
|
||||
}
|
||||
|
||||
private void SetLivesLabel()
|
||||
|
||||
@@ -3,6 +3,7 @@ using Godot;
|
||||
using Godot.Collections;
|
||||
using Mr.BrickAdventures.Autoloads;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.UI;
|
||||
@@ -18,7 +19,7 @@ public partial class Marketplace : Control
|
||||
[Export] public PackedScene MarketplaceButtonScene { get; set; }
|
||||
[Export] public PackedScene SkillButtonScene { get; set; }
|
||||
|
||||
private GameStateStore Store => GameStateStore.Instance;
|
||||
private static ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||
private SkillManager SkillManager => SkillManager.Instance;
|
||||
private readonly List<Button> _unlockButtons = [];
|
||||
private readonly List<SkillButton> _skillButtons = [];
|
||||
@@ -33,11 +34,12 @@ public partial class Marketplace : Control
|
||||
|
||||
foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill);
|
||||
|
||||
var unlockedSkills = Store.GetAllUnlockedSkills();
|
||||
foreach (var skill in unlockedSkills) CreateSkillButton(skill);
|
||||
foreach (var skill in Skills)
|
||||
if (skill != null && Progression?.IsOwned(SkillId.Of(skill.Name)) == true)
|
||||
CreateSkillButton(skill);
|
||||
|
||||
SkillUnlockerComponent.SkillUnlocked += OnSkillUnlocked;
|
||||
EventBus.Instance.SkillCollected += OnGlobalSkillCollected;
|
||||
// One announcement covers pickups, purchases and upgrades alike.
|
||||
EventBus.Instance.SkillSetChanged += OnSkillSetChanged;
|
||||
|
||||
if (InputDeviceManager.Instance != null)
|
||||
InputDeviceManager.Instance.DeviceChanged += OnDeviceChanged;
|
||||
@@ -45,11 +47,8 @@ public partial class Marketplace : Control
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
SkillUnlockerComponent.SkillUnlocked -= OnSkillUnlocked;
|
||||
if (EventBus.Instance != null)
|
||||
{
|
||||
EventBus.Instance.SkillCollected -= OnGlobalSkillCollected;
|
||||
}
|
||||
EventBus.Instance.SkillSetChanged -= OnSkillSetChanged;
|
||||
|
||||
if (InputDeviceManager.Instance != null)
|
||||
InputDeviceManager.Instance.DeviceChanged -= OnDeviceChanged;
|
||||
@@ -108,20 +107,25 @@ public partial class Marketplace : Control
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSkillUnlocked(SkillData skill)
|
||||
/// <summary>
|
||||
/// Rebuilds the owned-skill row from what the Playthrough owns, rather than trying to patch it per
|
||||
/// event. Cheap at this size, and it cannot drift out of step with the truth.
|
||||
/// </summary>
|
||||
private void OnSkillSetChanged()
|
||||
{
|
||||
var buttonExists = false;
|
||||
foreach (var existingButton in _skillButtons)
|
||||
foreach (var skill in Skills)
|
||||
{
|
||||
if (existingButton.Name == skill.Name)
|
||||
{
|
||||
buttonExists = true;
|
||||
break;
|
||||
}
|
||||
if (skill == null || Progression?.IsOwned(SkillId.Of(skill.Name)) != true) continue;
|
||||
if (_skillButtons.Exists(b => b.Data == skill)) continue;
|
||||
|
||||
CreateSkillButton(skill);
|
||||
}
|
||||
|
||||
if (!buttonExists) CreateSkillButton(skill);
|
||||
RefreshActivationState();
|
||||
}
|
||||
|
||||
private void RefreshActivationState()
|
||||
{
|
||||
foreach (var btn in _skillButtons)
|
||||
{
|
||||
if (SkillManager.IsSkillActive(btn.Data)) btn.Activate();
|
||||
@@ -154,41 +158,19 @@ public partial class Marketplace : Control
|
||||
ToUnlockGrid.QueueSort();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One call covers unlocking and upgrading — the Playthrough works out which. At the top level the
|
||||
/// button instead toggles the skill on or off, which is a display concern rather than a purchase.
|
||||
/// </summary>
|
||||
private void OnUpgradeButtonPressed(SkillData skill)
|
||||
{
|
||||
if (Store.IsSkillUnlocked(skill))
|
||||
{
|
||||
if (skill.Level < skill.MaxLevel)
|
||||
{
|
||||
SkillUnlockerComponent.TryUpgradeSkill(skill);
|
||||
if (!SkillUnlockerComponent.SkillManager.IsSkillActive(skill)) SkillUnlockerComponent.SkillManager.ToggleSkillActivation(skill);
|
||||
}
|
||||
else
|
||||
{
|
||||
SkillUnlockerComponent.SkillManager.ToggleSkillActivation(skill);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SkillUnlockerComponent.TryUnlockSkill(skill);
|
||||
}
|
||||
if (SkillUnlockerComponent.TryBuy(skill) == PurchaseOutcome.AlreadyAtTopLevel)
|
||||
SkillManager.ToggleSkillActivation(skill);
|
||||
}
|
||||
|
||||
private void OnSkillButtonPressed(SkillButton button)
|
||||
{
|
||||
SkillUnlockerComponent.SkillManager.ToggleSkillActivation(button.Data);
|
||||
|
||||
foreach (var btn in _skillButtons)
|
||||
{
|
||||
if (SkillUnlockerComponent.SkillManager.IsSkillActive(btn.Data))
|
||||
btn.Activate();
|
||||
else
|
||||
btn.Deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGlobalSkillCollected(SkillData skill, Vector2 position)
|
||||
{
|
||||
OnSkillUnlocked(skill);
|
||||
SkillManager.ToggleSkillActivation(button.Data);
|
||||
RefreshActivationState();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
using Godot;
|
||||
using Mr.BrickAdventures;
|
||||
using Mr.BrickAdventures.Autoloads;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.UI;
|
||||
|
||||
/// <summary>
|
||||
/// One buy/upgrade button in the marketplace.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reads the owned level from the Playthrough and the costs from the authored upgrade table — the split
|
||||
/// <c>SkillData.Level</c> used to blur. It also listens to <see cref="EventBus.SkillSetChanged"/> rather
|
||||
/// than to a signal on a component it had to locate by node path on the player.
|
||||
/// </remarks>
|
||||
public partial class MarketplaceButton : Button
|
||||
{
|
||||
[Export] public SkillData Data { get; set; }
|
||||
@@ -13,81 +20,70 @@ public partial class MarketplaceButton : Button
|
||||
[Export] public Texture2D LockedSkillIcon { get; set; }
|
||||
[Export] public Container SkillLevelContainer { get; set; }
|
||||
|
||||
private SkillUnlockerComponent _skillUnlockerComponent;
|
||||
private SkillManager _skillManager;
|
||||
private static ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var player = GameManager.Instance?.Player;
|
||||
if (player == null) return;
|
||||
|
||||
_skillUnlockerComponent = player.GetNodeOrNull<SkillUnlockerComponent>("SkillUnlockerComponent");
|
||||
if (_skillUnlockerComponent != null)
|
||||
if (EventBus.Instance != null)
|
||||
{
|
||||
_skillUnlockerComponent.SkillUnlocked += OnSkillStateChanged;
|
||||
EventBus.Instance.SkillSetChanged += UpdateButtonState;
|
||||
EventBus.Instance.CoinsChanged += OnCoinsChanged;
|
||||
}
|
||||
|
||||
_skillManager = SkillManager.Instance;
|
||||
_skillManager.SkillRemoved += OnSkillStateChanged;
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (_skillUnlockerComponent != null)
|
||||
if (EventBus.Instance != null)
|
||||
{
|
||||
_skillUnlockerComponent.SkillUnlocked -= OnSkillStateChanged;
|
||||
EventBus.Instance.SkillSetChanged -= UpdateButtonState;
|
||||
EventBus.Instance.CoinsChanged -= OnCoinsChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSkillStateChanged(SkillData skill)
|
||||
private void OnCoinsChanged(int totalCoins) => UpdateButtonState();
|
||||
|
||||
/// <summary>The level the player owns this skill at, or 0 when they do not own it.</summary>
|
||||
private int OwnedLevel()
|
||||
{
|
||||
if (skill.Name == Data.Name)
|
||||
{
|
||||
UpdateButtonState();
|
||||
}
|
||||
if (Data == null || Progression == null || string.IsNullOrWhiteSpace(Data.Name)) return 0;
|
||||
|
||||
return Progression.LevelOf(SkillId.Of(Data.Name))?.Value ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateButtonState()
|
||||
{
|
||||
if (Data == null || Data.Upgrades.Count == 0)
|
||||
{
|
||||
// No authored upgrades means it cannot be bought at any price.
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var isUnlocked = GameStateStore.Instance?.IsSkillUnlocked(Data) ?? false;
|
||||
var owned = OwnedLevel();
|
||||
|
||||
for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++)
|
||||
{
|
||||
SkillLevelContainer.GetChild(i).QueueFree();
|
||||
}
|
||||
|
||||
for (var i = 0; i < Data.MaxLevel; i++)
|
||||
{
|
||||
var icon = new TextureRect()
|
||||
SkillLevelContainer.AddChild(new TextureRect
|
||||
{
|
||||
Texture = (isUnlocked && i < Data.Level) ? UnlockedSkillIcon : LockedSkillIcon,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional
|
||||
};
|
||||
SkillLevelContainer.AddChild(icon);
|
||||
Texture = i < owned ? UnlockedSkillIcon : LockedSkillIcon,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isUnlocked)
|
||||
{
|
||||
Text = $"{Tr(Data.Name)} ({Data.Upgrades[0].Cost})";
|
||||
Disabled = false;
|
||||
}
|
||||
else if (Data.Level < Data.MaxLevel)
|
||||
{
|
||||
Text = $"{Tr(Data.Name)} ({Data.Upgrades[Data.Level].Cost})";
|
||||
Disabled = false;
|
||||
}
|
||||
else
|
||||
if (owned >= Data.MaxLevel)
|
||||
{
|
||||
Text = $"{Tr(Data.Name)} (MAX)";
|
||||
Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Upgrades[n] is the cost to reach level n + 1, so the next level costs Upgrades[owned].
|
||||
Text = $"{Tr(Data.Name)} ({Data.Upgrades[owned].Cost})";
|
||||
Disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user