Files
zeling_v2/Assets/_Game/Scripts/UI/SkillTreePanel.cs
2026-05-25 11:54:37 +08:00

221 lines
9.5 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 UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Skills;
using BaseGames.Localization;
namespace BaseGames.UI
{
/// <summary>
/// 技能树面板(架构 10_UIModule §Panel
/// 全屏面板,按形态展示各技能槽的技能资料(名称、描述、消耗、冷却)。
/// 支持左右翻页浏览各形态,并高亮当前实际使用的形态。
///
/// Inspector 必填:
/// _formSkillSets — 每个形态对应一条配置soulSkill + spirit1 + spirit2
/// _formTitleText — 显示当前形态标签名的文本
/// _prevFormBtn / _nextFormBtn — 翻页按钮
/// _activeFormIndicator — 高亮"当前形态"的标志(可选,如小圆点)
/// _skillSlots — 3 个 SkillSlotView 引用Soul / Spirit1 / Spirit2
/// _onFormChanged — IntEventChannelSOFormController 广播当前形态 index
/// </summary>
public class SkillTreePanel : MonoBehaviour
{
// ── 子数据结构 ────────────────────────────────────────────────────────
[System.Serializable]
public struct FormSkillConfig
{
[Tooltip("形态标题的本地化 Key格式如 \"Form_SkyWarden_Label\",由 LocalizationManager.Get(…, LocalizationTable.UI) 查找。")]
public string formLabelKey;
[Tooltip("天魂/地魂/命魂的「魂技」")]
public FormSkillSO soulSkill;
[Tooltip("灵技 1Y / Triangle / 手柄按钮)")]
public FormSkillSO spiritSkill1;
[Tooltip("灵技 2Shift+Y / L2+Triangle 等)")]
public FormSkillSO spiritSkill2;
}
[System.Serializable]
public struct SkillSlotView
{
[Tooltip("技能图标")]
public Image icon;
[Tooltip("技能名称文本")]
public TMP_Text nameText;
[Tooltip("技能描述文本")]
public TMP_Text descText;
[Tooltip("消耗数值文本(如 \"魂力 25\"")]
public TMP_Text costText;
[Tooltip("冷却数值文本(如 \"CD 3.5s\"")]
public TMP_Text cooldownText;
[Tooltip("无技能时隐藏的根 GameObject")]
public GameObject root;
}
// ── Inspector 字段 ────────────────────────────────────────────────────
[Header("形态技能配置")]
[Tooltip("每个元素对应一种形态的三项技能,顺序应与 FormType 枚举序号一致。")]
[SerializeField] private FormSkillConfig[] _formSkillSets;
[Header("UI 导航")]
[SerializeField] private TMP_Text _formTitleText;
[SerializeField] private Button _prevFormBtn;
[SerializeField] private Button _nextFormBtn;
[Tooltip("高亮当前实际装备形态的指示器(可选)")]
[SerializeField] private GameObject _activeFormIndicator;
[Header("技能卡槽Soul / Spirit1 / Spirit2")]
[SerializeField] private SkillSlotView _soulSlot;
[SerializeField] private SkillSlotView _spiritSlot1;
[SerializeField] private SkillSlotView _spiritSlot2;
[Header("关闭按钮")]
[SerializeField] private Button _btnClose;
[Header("Event Channels")]
[Tooltip("FormController 广播当前形态切换payload = FormType 的整数序号0=天魂,1=地魂,2=命魂)")]
[SerializeField] private IntEventChannelSO _onFormChanged;
// ── 私有状态 ──────────────────────────────────────────────────────────
private int _viewIndex = 0; // 当前正在查看的形态
private int _activeIndex = 0; // 玩家当前实际使用的形态
private readonly CompositeDisposable _subs = new();
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
_prevFormBtn?.onClick.AddListener(PrevForm);
_nextFormBtn?.onClick.AddListener(NextForm);
_btnClose?.onClick.AddListener(OnCloseClicked);
}
private void OnEnable()
{
_onFormChanged?.Subscribe(OnFormChanged).AddTo(_subs);
// 打开时自动跳到当前形态
_viewIndex = _activeIndex;
Refresh();
}
private void OnDisable()
{
_subs.Clear();
}
// ── 事件处理 ──────────────────────────────────────────────────────────
private void OnFormChanged(int formIndex)
{
_activeIndex = formIndex;
if (_activeFormIndicator != null)
_activeFormIndicator.SetActive(_viewIndex == _activeIndex);
}
// ── 导航 ──────────────────────────────────────────────────────────────
private void PrevForm()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = (_viewIndex - 1 + _formSkillSets.Length) % _formSkillSets.Length;
Refresh();
}
private void NextForm()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = (_viewIndex + 1) % _formSkillSets.Length;
Refresh();
}
// ── 显示刷新 ──────────────────────────────────────────────────────────
private void Refresh()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = Mathf.Clamp(_viewIndex, 0, _formSkillSets.Length - 1);
var cfg = _formSkillSets[_viewIndex];
// 标题走本地化管道fallback 为 key 本身)
if (_formTitleText != null)
{
string label = !string.IsNullOrEmpty(cfg.formLabelKey)
? LocalizationManager.Get(cfg.formLabelKey, LocalizationTable.UI)
: string.Empty;
_formTitleText.text = string.IsNullOrEmpty(label) || label == cfg.formLabelKey
? cfg.formLabelKey // fallback直接显示 Key
: label;
}
// 当前形态高亮指示器
if (_activeFormIndicator != null)
_activeFormIndicator.SetActive(_viewIndex == _activeIndex);
// 翻页按钮可见性(只有一种形态时隐藏)
bool multiForm = _formSkillSets.Length > 1;
if (_prevFormBtn != null) _prevFormBtn.gameObject.SetActive(multiForm);
if (_nextFormBtn != null) _nextFormBtn.gameObject.SetActive(multiForm);
// 技能卡槽
FillSlot(ref _soulSlot, cfg.soulSkill);
FillSlot(ref _spiritSlot1, cfg.spiritSkill1);
FillSlot(ref _spiritSlot2, cfg.spiritSkill2);
}
private static void FillSlot(ref SkillSlotView slot, FormSkillSO skill)
{
bool hasSkill = skill != null;
if (slot.root != null) slot.root.SetActive(hasSkill);
if (!hasSkill) return;
if (slot.icon != null)
{
slot.icon.sprite = skill.icon;
slot.icon.enabled = skill.icon != null;
}
if (slot.nameText != null)
{
string name = LocalizationManager.Get(skill.displayNameKey, LocalizationTable.UI);
slot.nameText.text = string.IsNullOrEmpty(name) || name == skill.displayNameKey
? skill.skillId
: name;
}
if (slot.descText != null)
{
string desc = LocalizationManager.Get(skill.descriptionKey, LocalizationTable.UI);
slot.descText.text = string.IsNullOrEmpty(desc) || desc == skill.descriptionKey
? string.Empty
: desc;
}
if (slot.costText != null)
slot.costText.text = skill.baseCost > 0
? $"{skill.resourceType} {skill.baseCost}"
: string.Empty;
if (slot.cooldownText != null)
slot.cooldownText.text = skill.cooldown > 0f
? $"CD {skill.cooldown:0.#}s"
: string.Empty;
}
// ── 关闭 ──────────────────────────────────────────────────────────────
private void OnCloseClicked()
{
var uiMgr = ServiceLocator.GetOrDefault<UIManager>();
if (uiMgr != null) uiMgr.CloseTopPanel();
else gameObject.SetActive(false);
}
}
}