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); } }