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:
@@ -116,15 +116,13 @@ public partial class GameManager : Node
|
|||||||
var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||||
LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null;
|
LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null;
|
||||||
|
|
||||||
// Old path: coins and skills earned this attempt.
|
|
||||||
Store?.CommitSessionCoins();
|
|
||||||
Store?.CommitSessionSkills();
|
|
||||||
|
|
||||||
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
|
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
|
||||||
|
|
||||||
// New path: banks, marks cleared, and advances campaign position if this was a first clear.
|
// Banks the attempt's coins and skills, marks the level cleared, and advances campaign
|
||||||
|
// position if this was a First Clear.
|
||||||
Progression.FinishLevel(time);
|
Progression.FinishLevel(time);
|
||||||
Store?.ResetSession();
|
|
||||||
|
// Still needed for lives, which move with the death path in the next commit.
|
||||||
SaveSystem.Instance.SaveGame();
|
SaveSystem.Instance.SaveGame();
|
||||||
|
|
||||||
if (Progression.CurrentLevel is { } next && !next.Equals(finished))
|
if (Progression.CurrentLevel is { } next && !next.Equals(finished))
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.scripts.components;
|
||||||
using Mr.BrickAdventures.scripts.interfaces;
|
using Mr.BrickAdventures.scripts.interfaces;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the live skill components attached to the player, and keeps them matching what the Playthrough
|
||||||
|
/// says is owned.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Reconciles from <see cref="EventBus.SkillSetChanged"/> rather than being told by each caller. The old
|
||||||
|
/// arrangement had four call sites doing "record it in the store, then tell SkillManager" as two
|
||||||
|
/// separate calls — anything between them, including <c>AddSkill</c>'s own early return when no player
|
||||||
|
/// is registered, left a skill owned but inert.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Tracks the level each component was built at, so an upgrade rebuilds it. Previously
|
||||||
|
/// <c>AddSkill</c> returned early for an already-active skill, and the upgrade was applied by the
|
||||||
|
/// purchase code reaching into the component instead.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public partial class SkillManager : Node
|
public partial class SkillManager : Node
|
||||||
{
|
{
|
||||||
private PlayerController _player;
|
private PlayerController _player;
|
||||||
private GameStateStore Store => GameStateStore.Instance;
|
|
||||||
|
|
||||||
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
||||||
|
|
||||||
public Dictionary ActiveComponents { get; private set; } = new();
|
public Dictionary ActiveComponents { get; private set; } = new();
|
||||||
|
|
||||||
|
private readonly System.Collections.Generic.Dictionary<string, int> _activeLevels = new();
|
||||||
|
|
||||||
public static SkillManager Instance { get; private set; }
|
public static SkillManager Instance { get; private set; }
|
||||||
|
|
||||||
[Signal]
|
[Signal]
|
||||||
@@ -27,10 +45,14 @@ public partial class SkillManager : Node
|
|||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
|
EventBus.Instance.SkillSetChanged += Reconcile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
|
if (EventBus.Instance != null)
|
||||||
|
EventBus.Instance.SkillSetChanged -= Reconcile;
|
||||||
|
|
||||||
if (Instance == this) Instance = null;
|
if (Instance == this) Instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,24 +63,17 @@ public partial class SkillManager : Node
|
|||||||
{
|
{
|
||||||
if (_player == player) return;
|
if (_player == player) return;
|
||||||
|
|
||||||
// If a player is already registered (e.g., from a previous scene), unregister it first.
|
|
||||||
if (_player != null && IsInstanceValid(_player))
|
if (_player != null && IsInstanceValid(_player))
|
||||||
{
|
|
||||||
UnregisterPlayer();
|
UnregisterPlayer();
|
||||||
}
|
|
||||||
|
|
||||||
_player = player;
|
_player = player;
|
||||||
if (_player != null)
|
if (_player != null)
|
||||||
{
|
{
|
||||||
// Automatically unregister when the player node is removed from the scene.
|
|
||||||
_player.TreeExiting += UnregisterPlayer;
|
_player.TreeExiting += UnregisterPlayer;
|
||||||
ApplyUnlockedSkills();
|
Reconcile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cleans up skills and references related to the current player.
|
|
||||||
/// </summary>
|
|
||||||
private void UnregisterPlayer()
|
private void UnregisterPlayer()
|
||||||
{
|
{
|
||||||
if (_player != null && IsInstanceValid(_player))
|
if (_player != null && IsInstanceValid(_player))
|
||||||
@@ -69,9 +84,37 @@ public partial class SkillManager : Node
|
|||||||
_player = null;
|
_player = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddSkill(SkillData skillData)
|
/// <summary>
|
||||||
|
/// Brings the live components in line with what is owned: adds what is missing, rebuilds anything
|
||||||
|
/// whose level changed, removes what is no longer owned.
|
||||||
|
/// </summary>
|
||||||
|
public void Reconcile()
|
||||||
|
{
|
||||||
|
if (_player == null || !IsInstanceValid(_player)) return;
|
||||||
|
|
||||||
|
var progression = ProgressionRoot.Instance;
|
||||||
|
if (progression == null) return;
|
||||||
|
|
||||||
|
foreach (var skill in AvailableSkills)
|
||||||
|
{
|
||||||
|
if (skill == null || string.IsNullOrWhiteSpace(skill.Name)) continue;
|
||||||
|
|
||||||
|
var owned = progression.LevelOf(SkillId.Of(skill.Name))?.Value;
|
||||||
|
var active = _activeLevels.TryGetValue(skill.Name, out var at) ? at : (int?)null;
|
||||||
|
|
||||||
|
if (owned == active) continue;
|
||||||
|
|
||||||
|
if (active is not null) RemoveSkill(skill.Name);
|
||||||
|
if (owned is not null) AddSkill(skill, owned.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kept for the marketplace's activate/deactivate toggle, which is a display concern.</summary>
|
||||||
|
public void AddSkill(SkillData skillData) =>
|
||||||
|
AddSkill(skillData, ProgressionRoot.Instance?.LevelOf(SkillId.Of(skillData.Name))?.Value ?? 1);
|
||||||
|
|
||||||
|
private void AddSkill(SkillData skillData, int level)
|
||||||
{
|
{
|
||||||
// Ensure a valid player is registered before adding a skill.
|
|
||||||
if (_player == null || !IsInstanceValid(_player))
|
if (_player == null || !IsInstanceValid(_player))
|
||||||
{
|
{
|
||||||
GD.Print("SkillManager: Player not available to add skill.");
|
GD.Print("SkillManager: Player not available to add skill.");
|
||||||
@@ -83,10 +126,9 @@ public partial class SkillManager : Node
|
|||||||
|
|
||||||
if (skillData.Type == SkillType.Throw)
|
if (skillData.Type == SkillType.Throw)
|
||||||
{
|
{
|
||||||
// Remove any other active throw skill before adding the new one
|
|
||||||
foreach (var sd in AvailableSkills)
|
foreach (var sd in AvailableSkills)
|
||||||
{
|
{
|
||||||
if (sd != skillData && sd.Type == SkillType.Throw)
|
if (sd != skillData && sd?.Type == SkillType.Throw)
|
||||||
RemoveSkill(sd.Name);
|
RemoveSkill(sd.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,8 +136,7 @@ public partial class SkillManager : Node
|
|||||||
var instance = skillData.Node.Instantiate();
|
var instance = skillData.Node.Instantiate();
|
||||||
if (instance is ISkill skill)
|
if (instance is ISkill skill)
|
||||||
{
|
{
|
||||||
// Initialize the skill with the registered player instance.
|
skill.Initialize(_player, skillData, level);
|
||||||
skill.Initialize(_player, skillData);
|
|
||||||
skill.Activate();
|
skill.Activate();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -105,31 +146,27 @@ public partial class SkillManager : Node
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the skill node as a child of the player.
|
|
||||||
_player.AddChild(instance);
|
_player.AddChild(instance);
|
||||||
ActiveComponents[skillData.Name] = instance;
|
ActiveComponents[skillData.Name] = instance;
|
||||||
|
_activeLevels[skillData.Name] = level;
|
||||||
|
|
||||||
if (instance is BrickThrowComponent btc)
|
if (instance is BrickThrowComponent btc)
|
||||||
{
|
|
||||||
EmitSignalActiveThrowSkillChanged(btc);
|
EmitSignalActiveThrowSkillChanged(btc);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveSkill(string skillName)
|
public void RemoveSkill(string skillName)
|
||||||
{
|
{
|
||||||
|
_activeLevels.Remove(skillName);
|
||||||
|
|
||||||
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (component.AsGodotObject() is BrickThrowComponent)
|
if (component.AsGodotObject() is BrickThrowComponent)
|
||||||
{
|
|
||||||
EmitSignalActiveThrowSkillChanged(null);
|
EmitSignalActiveThrowSkillChanged(null);
|
||||||
}
|
|
||||||
|
|
||||||
var inst = (Node)component;
|
var inst = (Node)component;
|
||||||
if (inst is ISkill skill)
|
if (inst is ISkill skill)
|
||||||
{
|
|
||||||
skill.Deactivate();
|
skill.Deactivate();
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInstanceValid(inst))
|
if (IsInstanceValid(inst))
|
||||||
inst.QueueFree();
|
inst.QueueFree();
|
||||||
@@ -141,57 +178,31 @@ public partial class SkillManager : Node
|
|||||||
|
|
||||||
private void RemoveAllActiveSkills()
|
private void RemoveAllActiveSkills()
|
||||||
{
|
{
|
||||||
// Create a copy of keys to avoid modification during iteration
|
foreach (var skillName in ActiveComponents.Keys.Select(k => k.ToString()).ToList())
|
||||||
var keys = ActiveComponents.Keys.ToArray();
|
|
||||||
var skillNames = keys.Select(key => key.ToString()).ToList();
|
|
||||||
|
|
||||||
foreach (var skillName in skillNames)
|
|
||||||
{
|
|
||||||
RemoveSkill(skillName);
|
RemoveSkill(skillName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyUnlockedSkills()
|
|
||||||
{
|
|
||||||
if (_player == null || !IsInstanceValid(_player)) return;
|
|
||||||
if (Store == null) return;
|
|
||||||
|
|
||||||
foreach (var sd in AvailableSkills)
|
|
||||||
{
|
|
||||||
if (Store.IsSkillUnlocked(sd))
|
|
||||||
CallDeferred(MethodName.AddSkill, sd);
|
|
||||||
else
|
|
||||||
RemoveSkill(sd.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SkillData GetSkillByName(string skillName)
|
public SkillData GetSkillByName(string skillName)
|
||||||
{
|
{
|
||||||
foreach (var sd in AvailableSkills)
|
foreach (var sd in AvailableSkills)
|
||||||
if (sd.Name == skillName) return sd;
|
if (sd?.Name == skillName) return sd;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSkillActive(SkillData skill)
|
public bool IsSkillActive(SkillData skill) =>
|
||||||
{
|
skill != null && ActiveComponents.ContainsKey(skill.Name);
|
||||||
return skill != null && ActiveComponents.ContainsKey(skill.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ActivateSkill(SkillData skill)
|
public void ActivateSkill(SkillData skill)
|
||||||
{
|
{
|
||||||
if (!ActiveComponents.ContainsKey(skill.Name))
|
if (!ActiveComponents.ContainsKey(skill.Name))
|
||||||
{
|
|
||||||
AddSkill(skill);
|
AddSkill(skill);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void DeactivateSkill(SkillData skill)
|
public void DeactivateSkill(SkillData skill)
|
||||||
{
|
{
|
||||||
if (ActiveComponents.ContainsKey(skill.Name))
|
if (ActiveComponents.ContainsKey(skill.Name))
|
||||||
{
|
|
||||||
RemoveSkill(skill.Name);
|
RemoveSkill(skill.Name);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleSkillActivation(SkillData skill)
|
public void ToggleSkillActivation(SkillData skill)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Mr.BrickAdventures.Autoloads;
|
using Mr.BrickAdventures.Autoloads;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.scripts.Events;
|
namespace Mr.BrickAdventures.scripts.Events;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles coin collection events and updates the GameStateStore.
|
/// Records collected coins against the current Attempt.
|
||||||
/// Replaces the manual coin logic in GameManager.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Coins are banked only by a First Clear, so dying loses what was found this attempt — including
|
||||||
|
/// anything spent from it. See docs/adr/0002-attempt-is-a-ledger.md.
|
||||||
|
/// </remarks>
|
||||||
public partial class CoinStateHandler : Node
|
public partial class CoinStateHandler : Node
|
||||||
{
|
{
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
@@ -17,13 +21,13 @@ public partial class CoinStateHandler : Node
|
|||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
if (EventBus.Instance != null)
|
if (EventBus.Instance != null)
|
||||||
{
|
|
||||||
EventBus.Instance.CoinCollected -= OnCoinCollected;
|
EventBus.Instance.CoinCollected -= OnCoinCollected;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCoinCollected(int amount, Vector2 position)
|
private void OnCoinCollected(int amount, Vector2 position)
|
||||||
{
|
{
|
||||||
GameStateStore.Instance?.AddSessionCoins(amount);
|
if (amount <= 0) return;
|
||||||
|
|
||||||
|
ProgressionRoot.Instance?.RecordCoinsFound(Coins.Of(amount));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Mr.BrickAdventures.Autoloads;
|
using Mr.BrickAdventures.Autoloads;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.scripts.Events;
|
namespace Mr.BrickAdventures.scripts.Events;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles skill collection events and unlocks skills via GameStateStore.
|
/// Grants a skill picked up in a level.
|
||||||
/// Skills are immediately activated but only persisted on level complete.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only records that it is owned. Attaching the live component is SkillManager's job, driven by
|
||||||
|
/// SkillSetChanged — previously this did both as two separate calls, and the second could silently do
|
||||||
|
/// nothing if no player was registered yet, leaving the skill owned but inert.
|
||||||
|
/// </remarks>
|
||||||
public partial class SkillCollectHandler : Node
|
public partial class SkillCollectHandler : Node
|
||||||
{
|
{
|
||||||
private SkillManager SkillManager => SkillManager.Instance;
|
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
EventBus.Instance.SkillCollected += OnSkillCollected;
|
EventBus.Instance.SkillCollected += OnSkillCollected;
|
||||||
@@ -20,22 +23,13 @@ public partial class SkillCollectHandler : Node
|
|||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
if (EventBus.Instance != null)
|
if (EventBus.Instance != null)
|
||||||
{
|
|
||||||
EventBus.Instance.SkillCollected -= OnSkillCollected;
|
EventBus.Instance.SkillCollected -= OnSkillCollected;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSkillCollected(SkillData skill, Vector2 position)
|
private void OnSkillCollected(SkillData skill, Vector2 position)
|
||||||
{
|
{
|
||||||
if (skill == null) return;
|
if (skill == null || string.IsNullOrWhiteSpace(skill.Name)) return;
|
||||||
|
|
||||||
// Unlock in session (will be committed on level complete, lost on death)
|
ProgressionRoot.Instance?.GrantSkill(SkillId.Of(skill.Name));
|
||||||
GameStateStore.Instance?.UnlockSkillInSession(skill);
|
|
||||||
|
|
||||||
// Immediately activate the skill for the player
|
|
||||||
SkillManager?.AddSkill(skill);
|
|
||||||
|
|
||||||
// Emit skill unlocked event for UI/achievements
|
|
||||||
EventBus.EmitSkillUnlocked(skill.Name, skill.Level);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ public partial class SkillData : Resource
|
|||||||
[Export] public string Name { get; set; } = "New Skill";
|
[Export] public string Name { get; set; } = "New Skill";
|
||||||
[Export] public string Description { get; set; } = "New Skill";
|
[Export] public string Description { get; set; } = "New Skill";
|
||||||
[Export] public Texture2D Icon { get; set; }
|
[Export] public Texture2D Icon { get; set; }
|
||||||
[Export] public int Level { get; set; } = 1;
|
|
||||||
[Export] public SkillType Type { get; set; } = SkillType.Throw;
|
[Export] public SkillType Type { get; set; } = SkillType.Throw;
|
||||||
[Export] public PackedScene Node { get; set; }
|
[Export] public PackedScene Node { get; set; }
|
||||||
[Export] public Array<SkillUpgrade> Upgrades { get; set; } = [];
|
[Export] public Array<SkillUpgrade> Upgrades { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Highest level the authored upgrade table defines. Zero means this skill cannot be bought.
|
||||||
|
/// </summary>
|
||||||
public int MaxLevel => Upgrades.Count;
|
public int MaxLevel => Upgrades.Count;
|
||||||
}
|
}
|
||||||
@@ -14,16 +14,32 @@ public partial class Hud : Control
|
|||||||
|
|
||||||
private GameStateStore Store => GameStateStore.Instance;
|
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)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
SetHealthBar();
|
SetHealthBar();
|
||||||
SetLivesLabel();
|
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()
|
private void SetLivesLabel()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Godot;
|
|||||||
using Godot.Collections;
|
using Godot.Collections;
|
||||||
using Mr.BrickAdventures.Autoloads;
|
using Mr.BrickAdventures.Autoloads;
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.scripts.UI;
|
namespace Mr.BrickAdventures.scripts.UI;
|
||||||
@@ -18,7 +19,7 @@ public partial class Marketplace : Control
|
|||||||
[Export] public PackedScene MarketplaceButtonScene { get; set; }
|
[Export] public PackedScene MarketplaceButtonScene { get; set; }
|
||||||
[Export] public PackedScene SkillButtonScene { 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 SkillManager SkillManager => SkillManager.Instance;
|
||||||
private readonly List<Button> _unlockButtons = [];
|
private readonly List<Button> _unlockButtons = [];
|
||||||
private readonly List<SkillButton> _skillButtons = [];
|
private readonly List<SkillButton> _skillButtons = [];
|
||||||
@@ -33,11 +34,12 @@ public partial class Marketplace : Control
|
|||||||
|
|
||||||
foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill);
|
foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill);
|
||||||
|
|
||||||
var unlockedSkills = Store.GetAllUnlockedSkills();
|
foreach (var skill in Skills)
|
||||||
foreach (var skill in unlockedSkills) CreateSkillButton(skill);
|
if (skill != null && Progression?.IsOwned(SkillId.Of(skill.Name)) == true)
|
||||||
|
CreateSkillButton(skill);
|
||||||
|
|
||||||
SkillUnlockerComponent.SkillUnlocked += OnSkillUnlocked;
|
// One announcement covers pickups, purchases and upgrades alike.
|
||||||
EventBus.Instance.SkillCollected += OnGlobalSkillCollected;
|
EventBus.Instance.SkillSetChanged += OnSkillSetChanged;
|
||||||
|
|
||||||
if (InputDeviceManager.Instance != null)
|
if (InputDeviceManager.Instance != null)
|
||||||
InputDeviceManager.Instance.DeviceChanged += OnDeviceChanged;
|
InputDeviceManager.Instance.DeviceChanged += OnDeviceChanged;
|
||||||
@@ -45,11 +47,8 @@ public partial class Marketplace : Control
|
|||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
SkillUnlockerComponent.SkillUnlocked -= OnSkillUnlocked;
|
|
||||||
if (EventBus.Instance != null)
|
if (EventBus.Instance != null)
|
||||||
{
|
EventBus.Instance.SkillSetChanged -= OnSkillSetChanged;
|
||||||
EventBus.Instance.SkillCollected -= OnGlobalSkillCollected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (InputDeviceManager.Instance != null)
|
if (InputDeviceManager.Instance != null)
|
||||||
InputDeviceManager.Instance.DeviceChanged -= OnDeviceChanged;
|
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 skill in Skills)
|
||||||
foreach (var existingButton in _skillButtons)
|
|
||||||
{
|
{
|
||||||
if (existingButton.Name == skill.Name)
|
if (skill == null || Progression?.IsOwned(SkillId.Of(skill.Name)) != true) continue;
|
||||||
{
|
if (_skillButtons.Exists(b => b.Data == skill)) continue;
|
||||||
buttonExists = true;
|
|
||||||
break;
|
CreateSkillButton(skill);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!buttonExists) CreateSkillButton(skill);
|
RefreshActivationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshActivationState()
|
||||||
|
{
|
||||||
foreach (var btn in _skillButtons)
|
foreach (var btn in _skillButtons)
|
||||||
{
|
{
|
||||||
if (SkillManager.IsSkillActive(btn.Data)) btn.Activate();
|
if (SkillManager.IsSkillActive(btn.Data)) btn.Activate();
|
||||||
@@ -154,41 +158,19 @@ public partial class Marketplace : Control
|
|||||||
ToUnlockGrid.QueueSort();
|
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)
|
private void OnUpgradeButtonPressed(SkillData skill)
|
||||||
{
|
{
|
||||||
if (Store.IsSkillUnlocked(skill))
|
if (SkillUnlockerComponent.TryBuy(skill) == PurchaseOutcome.AlreadyAtTopLevel)
|
||||||
{
|
SkillManager.ToggleSkillActivation(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSkillButtonPressed(SkillButton button)
|
private void OnSkillButtonPressed(SkillButton button)
|
||||||
{
|
{
|
||||||
SkillUnlockerComponent.SkillManager.ToggleSkillActivation(button.Data);
|
SkillManager.ToggleSkillActivation(button.Data);
|
||||||
|
RefreshActivationState();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Mr.BrickAdventures;
|
|
||||||
using Mr.BrickAdventures.Autoloads;
|
using Mr.BrickAdventures.Autoloads;
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.scripts.UI;
|
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
|
public partial class MarketplaceButton : Button
|
||||||
{
|
{
|
||||||
[Export] public SkillData Data { get; set; }
|
[Export] public SkillData Data { get; set; }
|
||||||
@@ -13,81 +20,70 @@ public partial class MarketplaceButton : Button
|
|||||||
[Export] public Texture2D LockedSkillIcon { get; set; }
|
[Export] public Texture2D LockedSkillIcon { get; set; }
|
||||||
[Export] public Container SkillLevelContainer { get; set; }
|
[Export] public Container SkillLevelContainer { get; set; }
|
||||||
|
|
||||||
private SkillUnlockerComponent _skillUnlockerComponent;
|
private static ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||||
private SkillManager _skillManager;
|
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
var player = GameManager.Instance?.Player;
|
if (EventBus.Instance != null)
|
||||||
if (player == null) return;
|
|
||||||
|
|
||||||
_skillUnlockerComponent = player.GetNodeOrNull<SkillUnlockerComponent>("SkillUnlockerComponent");
|
|
||||||
if (_skillUnlockerComponent != null)
|
|
||||||
{
|
{
|
||||||
_skillUnlockerComponent.SkillUnlocked += OnSkillStateChanged;
|
EventBus.Instance.SkillSetChanged += UpdateButtonState;
|
||||||
|
EventBus.Instance.CoinsChanged += OnCoinsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
_skillManager = SkillManager.Instance;
|
|
||||||
_skillManager.SkillRemoved += OnSkillStateChanged;
|
|
||||||
|
|
||||||
UpdateButtonState();
|
UpdateButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
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)
|
if (Data == null || Progression == null || string.IsNullOrWhiteSpace(Data.Name)) return 0;
|
||||||
{
|
|
||||||
UpdateButtonState();
|
return Progression.LevelOf(SkillId.Of(Data.Name))?.Value ?? 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateButtonState()
|
private void UpdateButtonState()
|
||||||
{
|
{
|
||||||
if (Data == null || Data.Upgrades.Count == 0)
|
if (Data == null || Data.Upgrades.Count == 0)
|
||||||
{
|
{
|
||||||
|
// No authored upgrades means it cannot be bought at any price.
|
||||||
Visible = false;
|
Visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isUnlocked = GameStateStore.Instance?.IsSkillUnlocked(Data) ?? false;
|
var owned = OwnedLevel();
|
||||||
|
|
||||||
for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++)
|
for (var i = 0; i < SkillLevelContainer.GetChildCount(); i++)
|
||||||
{
|
|
||||||
SkillLevelContainer.GetChild(i).QueueFree();
|
SkillLevelContainer.GetChild(i).QueueFree();
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < Data.MaxLevel; i++)
|
for (var i = 0; i < Data.MaxLevel; i++)
|
||||||
{
|
{
|
||||||
var icon = new TextureRect()
|
SkillLevelContainer.AddChild(new TextureRect
|
||||||
{
|
{
|
||||||
Texture = (isUnlocked && i < Data.Level) ? UnlockedSkillIcon : LockedSkillIcon,
|
Texture = i < owned ? UnlockedSkillIcon : LockedSkillIcon,
|
||||||
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional
|
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional,
|
||||||
};
|
});
|
||||||
SkillLevelContainer.AddChild(icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUnlocked)
|
if (owned >= Data.MaxLevel)
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
Text = $"{Tr(Data.Name)} (MAX)";
|
Text = $"{Tr(Data.Name)} (MAX)";
|
||||||
Disabled = true;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,9 @@ public partial class BrickArmorSkillComponent : SkillComponentBase
|
|||||||
private HealthComponent _healthComponent;
|
private HealthComponent _healthComponent;
|
||||||
private float _armorBonus = 0;
|
private float _armorBonus = 0;
|
||||||
|
|
||||||
public override void Initialize(Node owner, SkillData data)
|
public override void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
base.Initialize(owner, data);
|
base.Initialize(owner, data, level);
|
||||||
if (Player != null)
|
if (Player != null)
|
||||||
{
|
{
|
||||||
_healthComponent = Player.GetNode<HealthComponent>("HealthComponent");
|
_healthComponent = Player.GetNode<HealthComponent>("HealthComponent");
|
||||||
@@ -21,8 +21,8 @@ public partial class BrickArmorSkillComponent : SkillComponentBase
|
|||||||
|
|
||||||
public override void Activate()
|
public override void Activate()
|
||||||
{
|
{
|
||||||
if (_healthComponent == null || Data == null) return;
|
if (_healthComponent == null) return;
|
||||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
ApplyCurrentUpgrade();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Deactivate()
|
public override void Deactivate()
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ public partial class BrickShieldSkillComponent : SkillComponentBase
|
|||||||
{
|
{
|
||||||
if (Data != null && _skillManager != null)
|
if (Data != null && _skillManager != null)
|
||||||
{
|
{
|
||||||
GameStateStore.Instance?.RemoveUnlockedSkill(Data.Name);
|
// Revoking is enough: SkillManager reconciles the live component away from SkillSetChanged.
|
||||||
_skillManager.RemoveSkill(Data.Name);
|
ProgressionRoot.Instance?.RevokeSkill(Progression.SkillId.Of(Data.Name));
|
||||||
}
|
}
|
||||||
_shieldInstance = null;
|
_shieldInstance = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,16 +81,13 @@ public partial class BrickThrowComponent : SkillComponentBase
|
|||||||
_timer.Start();
|
_timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(Node owner, SkillData data)
|
public override void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
base.Initialize(owner, data);
|
base.Initialize(owner, data, level);
|
||||||
|
|
||||||
ThrowInputBehavior = (ThrowInputResource)ThrowInputBehavior?.Duplicate();
|
ThrowInputBehavior = (ThrowInputResource)ThrowInputBehavior?.Duplicate();
|
||||||
|
|
||||||
if (Data.Level > 0 && Data.Upgrades.Count >= Data.Level)
|
ApplyCurrentUpgrade();
|
||||||
{
|
|
||||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Activate()
|
public override void Activate()
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ public partial class GroundPoundSkillComponent : SkillComponentBase
|
|||||||
private PlayerInputHandler _input;
|
private PlayerInputHandler _input;
|
||||||
private bool _isPounding = false;
|
private bool _isPounding = false;
|
||||||
|
|
||||||
public override void Initialize(Node owner, SkillData data)
|
public override void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
base.Initialize(owner, data);
|
base.Initialize(owner, data, level);
|
||||||
if (Player != null)
|
if (Player != null)
|
||||||
{
|
{
|
||||||
_input = Player.GetNode<PlayerInputHandler>("PlayerInputHandler");
|
_input = Player.GetNode<PlayerInputHandler>("PlayerInputHandler");
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ public partial class MagneticSkillComponent : SkillComponentBase
|
|||||||
collectable.GlobalPosition += direction.Normalized() * speed;
|
collectable.GlobalPosition += direction.Normalized() * speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(Node owner, SkillData data)
|
public override void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
base.Initialize(owner, data);
|
base.Initialize(owner, data, level);
|
||||||
|
|
||||||
if (Player == null)
|
if (Player == null)
|
||||||
{
|
{
|
||||||
@@ -94,10 +94,7 @@ public partial class MagneticSkillComponent : SkillComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Data.Level > 0 && Data.Upgrades.Count >= Data.Level)
|
ApplyCurrentUpgrade();
|
||||||
{
|
|
||||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Activate()
|
public override void Activate()
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ using Mr.BrickAdventures.scripts.Resources;
|
|||||||
namespace Mr.BrickAdventures.scripts.components;
|
namespace Mr.BrickAdventures.scripts.components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for all skill components to reduce boilerplate.
|
/// Base class for all skill components, to reduce boilerplate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract partial class SkillComponentBase : Node, ISkill
|
public abstract partial class SkillComponentBase : Node, ISkill
|
||||||
{
|
{
|
||||||
protected PlayerController Player { get; private set; }
|
protected PlayerController Player { get; private set; }
|
||||||
protected SkillData Data { get; private set; }
|
protected SkillData Data { get; private set; }
|
||||||
|
|
||||||
public virtual void Initialize(Node owner, SkillData data)
|
/// <summary>
|
||||||
|
/// How far this skill is upgraded, as owned by the Playthrough. Never read from
|
||||||
|
/// <see cref="Data"/> — see <see cref="ISkill.Initialize"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected int Level { get; private set; } = 1;
|
||||||
|
|
||||||
|
public virtual void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
Player = owner as PlayerController;
|
Player = owner as PlayerController;
|
||||||
Data = data;
|
Data = data;
|
||||||
|
Level = level;
|
||||||
|
|
||||||
if (Player == null)
|
if (Player == null)
|
||||||
{
|
{
|
||||||
@@ -23,6 +30,21 @@ public abstract partial class SkillComponentBase : Node, ISkill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the upgrade matching the level this skill was initialised at, if the authored table
|
||||||
|
/// goes that far.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Lives here because three components had this guard copy-pasted and one of them — the brick
|
||||||
|
/// armor — was missing it, indexing <c>Upgrades[Level - 1]</c> unchecked.
|
||||||
|
/// </remarks>
|
||||||
|
protected void ApplyCurrentUpgrade()
|
||||||
|
{
|
||||||
|
if (Data == null || Level < 1 || Data.Upgrades.Count < Level) return;
|
||||||
|
|
||||||
|
ApplyUpgrade(Data.Upgrades[Level - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void Activate();
|
public abstract void Activate();
|
||||||
public abstract void Deactivate();
|
public abstract void Deactivate();
|
||||||
public virtual void ApplyUpgrade(SkillUpgrade upgrade) { }
|
public virtual void ApplyUpgrade(SkillUpgrade upgrade) { }
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
|
||||||
using Mr.BrickAdventures.Autoloads;
|
using Mr.BrickAdventures.Autoloads;
|
||||||
using Mr.BrickAdventures.scripts.interfaces;
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.scripts.components;
|
namespace Mr.BrickAdventures.scripts.components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buys skills on the player's behalf.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Now a thin adapter. Unlocking and upgrading used to be two methods that each checked affordability,
|
||||||
|
/// debited coins, mutated <c>SkillData.Level</c> and told <c>SkillManager</c> separately — four steps a
|
||||||
|
/// caller had to get right, in order. <c>PurchaseSkill</c> is one operation covering both; it charges
|
||||||
|
/// the Attempt's ledger, so dying undoes the spend and the skill together, and SkillManager reconciles
|
||||||
|
/// itself rather than being told.
|
||||||
|
/// </remarks>
|
||||||
[GlobalClass]
|
[GlobalClass]
|
||||||
public partial class SkillUnlockerComponent : Node
|
public partial class SkillUnlockerComponent : Node
|
||||||
{
|
{
|
||||||
@@ -14,59 +23,36 @@ public partial class SkillUnlockerComponent : Node
|
|||||||
[Signal]
|
[Signal]
|
||||||
public delegate void SkillUnlockedEventHandler(SkillData skill);
|
public delegate void SkillUnlockedEventHandler(SkillData skill);
|
||||||
|
|
||||||
private GameStateStore Store => GameStateStore.Instance;
|
private static ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
SkillManager = SkillManager.Instance;
|
SkillManager = SkillManager.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasEnoughCoins(int amount) => (Store?.GetTotalCoins() ?? 0) >= amount;
|
/// <summary>Buys the next level of a skill, whether that is the first or an upgrade.</summary>
|
||||||
|
public PurchaseOutcome TryBuy(SkillData skill)
|
||||||
public bool TryUnlockSkill(SkillData skill)
|
|
||||||
{
|
{
|
||||||
if (Store == null) return false;
|
if (skill == null || Progression == null) return PurchaseOutcome.NotPlaying;
|
||||||
if (Store.IsSkillUnlocked(skill)) return false;
|
|
||||||
if (!HasEnoughCoins(skill.Upgrades[0].Cost)) return false;
|
|
||||||
|
|
||||||
skill.Level = 1;
|
var outcome = Progression.PurchaseSkill(SkillId.Of(skill.Name));
|
||||||
Store.RemoveCoins(skill.Upgrades[0].Cost);
|
if (outcome == PurchaseOutcome.Purchased)
|
||||||
Store.UnlockSkillInSession(skill);
|
|
||||||
SkillManager.AddSkill(skill);
|
|
||||||
EmitSignalSkillUnlocked(skill);
|
EmitSignalSkillUnlocked(skill);
|
||||||
|
|
||||||
return true;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Grants every purchasable skill, ignoring cost. Console only.</summary>
|
||||||
public void UnlockAllSkills()
|
public void UnlockAllSkills()
|
||||||
{
|
{
|
||||||
var availableSkills = SkillManager.AvailableSkills;
|
if (SkillManager == null || Progression == null) return;
|
||||||
|
|
||||||
foreach (var skill in availableSkills)
|
foreach (var skill in SkillManager.AvailableSkills)
|
||||||
{
|
{
|
||||||
Store?.UnlockSkillPermanently(skill);
|
if (skill == null) continue;
|
||||||
|
|
||||||
|
Progression.GrantSkill(SkillId.Of(skill.Name));
|
||||||
EmitSignalSkillUnlocked(skill);
|
EmitSignalSkillUnlocked(skill);
|
||||||
}
|
}
|
||||||
|
|
||||||
SkillManager.ApplyUnlockedSkills();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryUpgradeSkill(SkillData skill)
|
|
||||||
{
|
|
||||||
if (Store == null) return false;
|
|
||||||
if (!Store.IsSkillUnlocked(skill)) return false;
|
|
||||||
if (skill.Level >= skill.MaxLevel) return false;
|
|
||||||
if (!HasEnoughCoins(skill.Upgrades[skill.Level].Cost)) return false;
|
|
||||||
|
|
||||||
Store.RemoveCoins(skill.Upgrades[skill.Level].Cost);
|
|
||||||
skill.Level++;
|
|
||||||
if (SkillManager.ActiveComponents.TryGetValue(skill.Name, out Variant componentVariant))
|
|
||||||
{
|
|
||||||
var component = componentVariant.AsGodotObject();
|
|
||||||
if (component is ISkill skillInstance)
|
|
||||||
skillInstance.ApplyUpgrade(skill.Upgrades[skill.Level - 1]);
|
|
||||||
}
|
|
||||||
EmitSignalSkillUnlocked(skill);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ public partial class XRayVisionSkillComponent : SkillComponentBase
|
|||||||
private uint _originalVisibilityLayer;
|
private uint _originalVisibilityLayer;
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
|
|
||||||
public override void Initialize(Node owner, SkillData data)
|
public override void Initialize(Node owner, SkillData data, int level)
|
||||||
{
|
{
|
||||||
base.Initialize(owner, data);
|
base.Initialize(owner, data, level);
|
||||||
_viewport = GetViewport();
|
_viewport = GetViewport();
|
||||||
_camera = GetViewport().GetCamera2D();
|
_camera = GetViewport().GetCamera2D();
|
||||||
_timer = new Timer { OneShot = true };
|
_timer = new Timer { OneShot = true };
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ namespace Mr.BrickAdventures.scripts.interfaces;
|
|||||||
|
|
||||||
public interface ISkill
|
public interface ISkill
|
||||||
{
|
{
|
||||||
void Initialize(Node owner, SkillData data);
|
/// <summary>
|
||||||
|
/// Sets the skill up on its owner at a given level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="level">
|
||||||
|
/// How far this skill is upgraded. Passed in rather than read from <paramref name="data"/>: the
|
||||||
|
/// level belongs to the Playthrough, whereas <c>SkillData</c> is a shared cached resource — one
|
||||||
|
/// instance per process — so keeping it there never persisted and would leak between save slots.
|
||||||
|
/// </param>
|
||||||
|
void Initialize(Node owner, SkillData data, int level);
|
||||||
|
|
||||||
void Activate();
|
void Activate();
|
||||||
void Deactivate();
|
void Deactivate();
|
||||||
|
|||||||
Reference in New Issue
Block a user