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;
///
/// Owns the live skill components attached to the player, and keeps them matching what the Playthrough
/// says is owned.
///
///
///
/// Reconciles from 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 AddSkill's own early return when no player
/// is registered, left a skill owned but inert.
///
///
/// Tracks the level each component was built at, so an upgrade rebuilds it. Previously
/// AddSkill returned early for an already-active skill, and the upgrade was applied by the
/// purchase code reaching into the component instead.
///
///
public partial class SkillManager : Node
{
private PlayerController _player;
[Export] public Array AvailableSkills { get; set; } = [];
public Dictionary ActiveComponents { get; private set; } = new();
private readonly System.Collections.Generic.Dictionary _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;
}
///
/// Called by the PlayerController from its _Ready method to register itself with the manager.
///
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;
}
///
/// 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.
///
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);
}
}
/// Kept for the marketplace's activate/deactivate toggle, which is a display concern.
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);
}
}