242 lines
9.6 KiB
C#
242 lines
9.6 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 护符装备面板。
|
||
///
|
||
/// 布局:
|
||
/// 顶部 — 标题 + 凹槽容量进度条(已用/总数)
|
||
/// 左侧 — 已装备护符区域(动态格子列表)
|
||
/// 右侧 — 已收集护符目录(可滚动,点击即装备/卸下)
|
||
///
|
||
/// 数据来源:ServiceLocator 获取 EquipmentManager,读取 Collected / Equipped 列表。
|
||
/// 反应式更新:订阅 _onEquipmentChanged VoidEventChannelSO,变更时重建两侧列表。
|
||
///
|
||
/// Inspector 必填:
|
||
/// _notchText — "X / Y 格子" 文本
|
||
/// _equippedContainer — 已装备列表父节点
|
||
/// _catalogContainer — 收藏目录列表父节点
|
||
/// _charmCardTemplate — 护符卡片预制(kept inactive)
|
||
/// _btnClose — 关闭按钮
|
||
/// _onEquipmentChanged — EquipmentManager 广播的装备变更事件频道
|
||
/// </summary>
|
||
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<GameObject> _equippedCards = new();
|
||
private readonly List<GameObject> _catalogCards = new();
|
||
private readonly Queue<GameObject> _cardPool = new(); // O(1) 取用归还
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||
|
||
private void OnEnable()
|
||
{
|
||
_manager = ServiceLocator.GetOrDefault<IEquipmentService>();
|
||
|
||
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<CharmCardView>();
|
||
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);
|
||
|
||
/// <summary>
|
||
/// 兼容旧 Prefab(未挂载 <see cref="CharmCardView"/>)的反射绑定。
|
||
/// 仅作为过渡方案;首次开发完成后建议在 Editor 中强制要求新组件。
|
||
/// </summary>
|
||
private void FallbackBindByReflection(GameObject go, CharmSO charm, bool isEquipped)
|
||
{
|
||
var images = go.GetComponentsInChildren<Image>(includeInactive: true);
|
||
if (images.Length > 0 && charm.icon != null)
|
||
images[0].sprite = charm.icon;
|
||
|
||
var texts = go.GetComponentsInChildren<TMP_Text>(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<Button>(includeInactive: true);
|
||
if (btn != null)
|
||
{
|
||
btn.onClick.RemoveAllListeners();
|
||
CharmSO captured = charm;
|
||
if (!isEquipped)
|
||
btn.onClick.AddListener(() => OnEquipClicked(captured));
|
||
else
|
||
btn.onClick.AddListener(() => OnUnequipClicked(captured));
|
||
if (texts.Length > 3)
|
||
texts[3].text = isEquipped ? "✓" : string.Empty;
|
||
}
|
||
}
|
||
|
||
// ── 回收 ─────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>取队列头第一个非空实例;队列为空时返回 false。</summary>
|
||
private bool TryDequeueCard(out GameObject go)
|
||
{
|
||
while (_cardPool.Count > 0)
|
||
{
|
||
go = _cardPool.Dequeue();
|
||
if (go != null) return true;
|
||
}
|
||
go = null;
|
||
return false;
|
||
}
|
||
|
||
private void RecycleCards(List<GameObject> cards)
|
||
{
|
||
foreach (var card in cards)
|
||
{
|
||
if (card == null) continue;
|
||
card.SetActive(false);
|
||
_cardPool.Enqueue(card);
|
||
}
|
||
cards.Clear();
|
||
}
|
||
|
||
private void OnCloseBtnClicked()
|
||
{
|
||
var uiMgr = ServiceLocator.GetOrDefault<IUIManager>();
|
||
if (uiMgr != null) uiMgr.CloseTopPanel();
|
||
else gameObject.SetActive(false);
|
||
}
|
||
|
||
// ── IFocusable ────────────────────────────────────────────────────────
|
||
|
||
/// <summary>面板恢复为栈顶时将焦点移回关闭按鈕。</summary>
|
||
public void OnFocusRestored()
|
||
=> EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject);
|
||
}
|
||
}
|