Files
zeling_v2/Assets/_Game/Scripts/UI/CharmEquipPanel.cs
2026-05-25 13:21:41 +08:00

242 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}