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.
217 lines
6.6 KiB
C#
217 lines
6.6 KiB
C#
using System.Linq;
|
|
using Godot;
|
|
using Godot.Collections;
|
|
using Mr.BrickAdventures.Progression;
|
|
using Mr.BrickAdventures.scripts.components;
|
|
using Mr.BrickAdventures.scripts.interfaces;
|
|
using Mr.BrickAdventures.scripts.Resources;
|
|
|
|
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
|
|
{
|
|
private PlayerController _player;
|
|
|
|
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
|
|
|
public Dictionary ActiveComponents { get; private set; } = new();
|
|
|
|
private readonly System.Collections.Generic.Dictionary<string, int> _activeLevels = new();
|
|
|
|
public static SkillManager Instance { get; private set; }
|
|
|
|
[Signal]
|
|
public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent);
|
|
[Signal]
|
|
public delegate void SkillRemovedEventHandler(SkillData skillData);
|
|
|
|
public override void _Ready()
|
|
{
|
|
Instance = this;
|
|
EventBus.Instance.SkillSetChanged += Reconcile;
|
|
}
|
|
|
|
public override void _ExitTree()
|
|
{
|
|
if (EventBus.Instance != null)
|
|
EventBus.Instance.SkillSetChanged -= Reconcile;
|
|
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the PlayerController from its _Ready method to register itself with the manager.
|
|
/// </summary>
|
|
public void RegisterPlayer(PlayerController player)
|
|
{
|
|
if (_player == player) return;
|
|
|
|
if (_player != null && IsInstanceValid(_player))
|
|
UnregisterPlayer();
|
|
|
|
_player = player;
|
|
if (_player != null)
|
|
{
|
|
_player.TreeExiting += UnregisterPlayer;
|
|
Reconcile();
|
|
}
|
|
}
|
|
|
|
private void UnregisterPlayer()
|
|
{
|
|
if (_player != null && IsInstanceValid(_player))
|
|
{
|
|
_player.TreeExiting -= UnregisterPlayer;
|
|
RemoveAllActiveSkills();
|
|
}
|
|
_player = null;
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
if (_player == null || !IsInstanceValid(_player))
|
|
{
|
|
GD.Print("SkillManager: Player not available to add skill.");
|
|
return;
|
|
}
|
|
|
|
if (ActiveComponents.ContainsKey(skillData.Name))
|
|
return;
|
|
|
|
if (skillData.Type == SkillType.Throw)
|
|
{
|
|
foreach (var sd in AvailableSkills)
|
|
{
|
|
if (sd != skillData && sd?.Type == SkillType.Throw)
|
|
RemoveSkill(sd.Name);
|
|
}
|
|
}
|
|
|
|
var instance = skillData.Node.Instantiate();
|
|
if (instance is ISkill skill)
|
|
{
|
|
skill.Initialize(_player, skillData, level);
|
|
skill.Activate();
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!");
|
|
instance.QueueFree();
|
|
return;
|
|
}
|
|
|
|
_player.AddChild(instance);
|
|
ActiveComponents[skillData.Name] = instance;
|
|
_activeLevels[skillData.Name] = level;
|
|
|
|
if (instance is BrickThrowComponent btc)
|
|
EmitSignalActiveThrowSkillChanged(btc);
|
|
}
|
|
|
|
public void RemoveSkill(string skillName)
|
|
{
|
|
_activeLevels.Remove(skillName);
|
|
|
|
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
|
return;
|
|
|
|
if (component.AsGodotObject() is BrickThrowComponent)
|
|
EmitSignalActiveThrowSkillChanged(null);
|
|
|
|
var inst = (Node)component;
|
|
if (inst is ISkill skill)
|
|
skill.Deactivate();
|
|
|
|
if (IsInstanceValid(inst))
|
|
inst.QueueFree();
|
|
|
|
ActiveComponents.Remove(skillName);
|
|
var sd = GetSkillByName(skillName);
|
|
if (sd != null) EmitSignalSkillRemoved(sd);
|
|
}
|
|
|
|
private void RemoveAllActiveSkills()
|
|
{
|
|
foreach (var skillName in ActiveComponents.Keys.Select(k => k.ToString()).ToList())
|
|
RemoveSkill(skillName);
|
|
}
|
|
|
|
public SkillData GetSkillByName(string skillName)
|
|
{
|
|
foreach (var sd in AvailableSkills)
|
|
if (sd?.Name == skillName) return sd;
|
|
return null;
|
|
}
|
|
|
|
public bool IsSkillActive(SkillData skill) =>
|
|
skill != null && ActiveComponents.ContainsKey(skill.Name);
|
|
|
|
public void ActivateSkill(SkillData skill)
|
|
{
|
|
if (!ActiveComponents.ContainsKey(skill.Name))
|
|
AddSkill(skill);
|
|
}
|
|
|
|
public void DeactivateSkill(SkillData skill)
|
|
{
|
|
if (ActiveComponents.ContainsKey(skill.Name))
|
|
RemoveSkill(skill.Name);
|
|
}
|
|
|
|
public void ToggleSkillActivation(SkillData skill)
|
|
{
|
|
if (skill == null) return;
|
|
|
|
if (ActiveComponents.ContainsKey(skill.Name))
|
|
DeactivateSkill(skill);
|
|
else
|
|
ActivateSkill(skill);
|
|
}
|
|
}
|