using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UI; using TMPro; using UnityEngine.EventSystems; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Equipment; using BaseGames.Localization; namespace BaseGames.UI { /// /// 护符装备面板。 /// /// 布局: /// 顶部 — 标题 + 凹槽容量进度条(已用/总数) /// 左侧 — 已装备护符区域(动态格子列表) /// 右侧 — 已收集护符目录(可滚动,点击即装备/卸下) /// /// 数据来源:ServiceLocator 获取 EquipmentManager,读取 Collected / Equipped 列表。 /// 反应式更新:订阅 _onEquipmentChanged VoidEventChannelSO,变更时重建两侧列表。 /// /// Inspector 必填: /// _notchText — "X / Y 格子" 文本 /// _equippedContainer — 已装备列表父节点 /// _catalogContainer — 收藏目录列表父节点 /// _charmCardTemplate — 护符卡片预制(kept inactive) /// _btnClose — 关闭按钮 /// _onEquipmentChanged — EquipmentManager 广播的装备变更事件频道 /// public class CharmEquipPanel : MonoBehaviour, IFocusable { // ── Inspector 字段 ──────────────────────────────────────────────────── [Header("UI 根节点")] [SerializeField] private TMP_Text _notchText; [SerializeField] private Image _notchBarFill; [SerializeField] private Transform _equippedContainer; [SerializeField] private Transform _catalogContainer; [SerializeField] private GameObject _charmCardTemplate; // kept inactive [SerializeField] private Button _btnClose; [Header("Event Channels")] [SerializeField] private VoidEventChannelSO _onEquipmentChanged; // ── 私有状态 ────────────────────────────────────────────────────────── private IEquipmentService _manager; private readonly List _equippedCards = new(); private readonly List _catalogCards = new(); private readonly Queue _cardPool = new(); // O(1) 取用归还 private readonly CompositeDisposable _subs = new(); // ── 生命周期 ────────────────────────────────────────────────────────── private void OnEnable() { _manager = ServiceLocator.GetOrDefault(); if (_btnClose != null) { _btnClose.onClick.RemoveAllListeners(); _btnClose.onClick.AddListener(OnCloseBtnClicked); } _onEquipmentChanged?.Subscribe(Rebuild).AddTo(_subs); Rebuild(); // 手柄导航:面板打开时将焦点置于关闭按钮 EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject); } private void OnDisable() { _subs.Clear(); RecycleCards(_equippedCards); RecycleCards(_catalogCards); } // ── 重建 UI ─────────────────────────────────────────────────────────── private void Rebuild() { if (_manager == null) return; RefreshNotchBar(); RebuildEquippedList(); RebuildCatalogList(); } private void RefreshNotchBar() { int used = _manager.UsedNotches; int total = _manager.TotalNotches; if (_notchText != null) _notchText.text = $"{used} / {total}"; if (_notchBarFill != null) _notchBarFill.fillAmount = total > 0 ? (float)used / total : 0f; } private void RebuildEquippedList() { RecycleCards(_equippedCards); if (_equippedContainer == null) return; foreach (var charm in _manager.Equipped) { var card = SpawnCard(_equippedContainer, charm, isEquipped: true); if (card != null) _equippedCards.Add(card); } } private void RebuildCatalogList() { RecycleCards(_catalogCards); if (_catalogContainer == null) return; foreach (var charm in _manager.Collected) { bool isEquipped = _manager.Equipped.Contains(charm); var card = SpawnCard(_catalogContainer, charm, isEquipped); if (card != null) _catalogCards.Add(card); } } // ── 卡片生成 ────────────────────────────────────────────────────────── private GameObject SpawnCard(Transform parent, CharmSO charm, bool isEquipped) { if (_charmCardTemplate == null || parent == null) return null; if (!TryDequeueCard(out GameObject go)) go = Instantiate(_charmCardTemplate); go.transform.SetParent(parent, worldPositionStays: false); go.SetActive(true); // 优先使用预绑定视图组件(推荐);若 Prefab 未挂载则回落到反射查找以保证向后兼容。 var view = go.GetComponent(); if (view != null) { view.Bind(charm, isEquipped, OnEquipClicked, OnUnequipClicked); } else { FallbackBindByReflection(go, charm, isEquipped); } return go; } private void OnEquipClicked(CharmSO charm) { string err = _manager?.TryEquipCharm(charm); if (err != null) Debug.LogWarning($"[CharmEquipPanel] 装备失败: {err}"); } private void OnUnequipClicked(CharmSO charm) => _manager?.UnequipCharm(charm); /// /// 兼容旧 Prefab(未挂载 )的反射绑定。 /// 仅作为过渡方案;首次开发完成后建议在 Editor 中强制要求新组件。 /// private void FallbackBindByReflection(GameObject go, CharmSO charm, bool isEquipped) { var images = go.GetComponentsInChildren(includeInactive: true); if (images.Length > 0 && charm.icon != null) images[0].sprite = charm.icon; var texts = go.GetComponentsInChildren(includeInactive: true); if (texts.Length > 0) { string name = LocalizationManager.Get(charm.displayNameKey, LocalizationTable.Items); texts[0].text = string.IsNullOrEmpty(name) || name == charm.displayNameKey ? charm.charmId : name; } if (texts.Length > 1) texts[1].text = charm.notchCost.ToString(); if (texts.Length > 2) { string desc = LocalizationManager.Get(charm.descriptionKey, LocalizationTable.Items); texts[2].text = string.IsNullOrEmpty(desc) || desc == charm.descriptionKey ? string.Empty : desc; } var btn = go.GetComponentInChildren