该面板实为只读的形态技能一览(按形态翻页展示技能图标/名称/描述/消耗/冷却), 不含节点/前置/技能点/解锁交互。原名 SkillTree 易误解为可交互技能树,故改名为 FormSkillPanel 以反映真实职责。.meta GUID 保留,无 prefab/场景引用。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
221 lines
9.6 KiB
C#
221 lines
9.6 KiB
C#
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 — IntEventChannelSO,FormController 广播当前形态 index
|
||
/// </summary>
|
||
public class FormSkillPanel : MonoBehaviour
|
||
{
|
||
// ── 子数据结构 ────────────────────────────────────────────────────────
|
||
|
||
[System.Serializable]
|
||
public struct FormSkillConfig
|
||
{
|
||
[Tooltip("形态标题的本地化 Key,格式如 \"Form_SkyWarden_Label\",由 LocalizationManager.Get(…, LocalizationTable.UI) 查找。")]
|
||
public string formLabelKey;
|
||
[Tooltip("天魂/地魂/命魂的「魂技」")]
|
||
public FormSkillSO soulSkill;
|
||
[Tooltip("灵技 1(Y / Triangle / 手柄按钮)")]
|
||
public FormSkillSO spiritSkill1;
|
||
[Tooltip("灵技 2(Shift+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);
|
||
}
|
||
}
|
||
}
|