UI 系统
This commit is contained in:
35
Assets/_Game/Scripts/UI/AbilityCellView.cs
Normal file
35
Assets/_Game/Scripts/UI/AbilityCellView.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力总览单元格视图:图标 + 名称 + 已解锁/未解锁视觉(未解锁时暗化 + 显示锁定遮罩)。
|
||||
/// 由 <see cref="DataDrivenAbilityPanel"/> 据 AbilityMetaSO 实例化并 Bind。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class AbilityCellView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private LocalizedText _label;
|
||||
[Tooltip("未解锁时显示(锁图标 / 暗罩,可空)。")]
|
||||
[SerializeField] private GameObject _lockedOverlay;
|
||||
[Tooltip("整格暗化用(可空)。")]
|
||||
[SerializeField] private CanvasGroup _group;
|
||||
|
||||
public void Bind(string labelKey, Sprite icon, bool unlocked)
|
||||
{
|
||||
if (_label != null) _label.SetKey(labelKey);
|
||||
if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; }
|
||||
SetUnlocked(unlocked);
|
||||
}
|
||||
|
||||
/// <summary>只更新解锁态(保留图标/名称)。设备/解锁事件刷新时调用。</summary>
|
||||
public void SetUnlocked(bool unlocked)
|
||||
{
|
||||
if (_lockedOverlay != null) _lockedOverlay.SetActive(!unlocked);
|
||||
if (_group != null) _group.alpha = unlocked ? 1f : 0.4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/AbilityCellView.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/AbilityCellView.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 759137176b2cfdd47b53d655d10b170f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Assets/_Game/Scripts/UI/AbilityMetaSO.cs
Normal file
31
Assets/_Game/Scripts/UI/AbilityMetaSO.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力总览数据表(策划编辑):每个 <see cref="AbilityType"/> → 名称/说明/图标。
|
||||
/// <see cref="DataDrivenAbilityPanel"/> 据此列出全部能力 + 已解锁/未解锁态。
|
||||
/// 改本表即增删/重排能力项、改标签/图标,零代码。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/UI/Ability Meta", fileName = "UI_AbilityMeta")]
|
||||
public class AbilityMetaSO : ScriptableObject
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct Item
|
||||
{
|
||||
[Tooltip("此条目对应的能力位。")]
|
||||
public AbilityType type;
|
||||
[Tooltip("能力名标签本地化 Key(UI 表,如 ABL_DASH)。")]
|
||||
public string labelKey;
|
||||
[Tooltip("能力说明本地化 Key(可空)。")]
|
||||
public string descKey;
|
||||
[Tooltip("能力图标(可空)。")]
|
||||
public Sprite icon;
|
||||
}
|
||||
|
||||
[SerializeField] private Item[] _items;
|
||||
|
||||
public Item[] Items => _items;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/AbilityMetaSO.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/AbilityMetaSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 381fcefcad0b4544c88188bf7e8d695e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -21,6 +21,7 @@
|
||||
"BaseGames.Spells",
|
||||
"BaseGames.Quest",
|
||||
"BaseGames.Skills",
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Inventory",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager",
|
||||
|
||||
46
Assets/_Game/Scripts/UI/BestiaryCellView.cs
Normal file
46
Assets/_Game/Scripts/UI/BestiaryCellView.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 图鉴单元格视图(仿 <see cref="AbilityCellView"/>):肖像 + 名称 + 已发现/未发现视觉。
|
||||
/// 未发现时:名称显示 "???"(BESTIARY_LOCKED 文案)、肖像剪影化、暗罩、整格暗化。
|
||||
/// 由 <see cref="BestiaryPanel"/> 据 <see cref="BestiaryDatabaseSO"/> 实例化并 Bind。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class BestiaryCellView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private LocalizedText _label;
|
||||
[Tooltip("未发现时显示的暗罩 / 锁图标(可空)。")]
|
||||
[SerializeField] private GameObject _lockedOverlay;
|
||||
[Tooltip("整格暗化用(可空)。")]
|
||||
[SerializeField] private CanvasGroup _group;
|
||||
[Tooltip("点击选中此格以在详情区展示(可空)。")]
|
||||
[SerializeField] private Button _selectButton;
|
||||
|
||||
private const string LockedKey = "BESTIARY_LOCKED";
|
||||
private string _nameKey;
|
||||
|
||||
public Button SelectButton => _selectButton;
|
||||
|
||||
public void Bind(string nameKey, Sprite icon, bool discovered)
|
||||
{
|
||||
_nameKey = nameKey;
|
||||
if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; }
|
||||
SetDiscovered(discovered);
|
||||
}
|
||||
|
||||
/// <summary>只更新发现态(保留图标/名称)。EVT_BestiaryUpdated 刷新时调用。</summary>
|
||||
public void SetDiscovered(bool discovered)
|
||||
{
|
||||
if (_label != null) _label.SetKey(discovered ? _nameKey : LockedKey);
|
||||
if (_lockedOverlay != null) _lockedOverlay.SetActive(!discovered);
|
||||
// 未发现:肖像剪影化(压暗);已发现:原色
|
||||
if (_icon != null) _icon.color = discovered ? Color.white : new Color(0.04f, 0.04f, 0.06f, 1f);
|
||||
if (_group != null) _group.alpha = discovered ? 1f : 0.55f;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/BestiaryCellView.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/BestiaryCellView.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7734d0120f3b75b41905933b325a823e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
41
Assets/_Game/Scripts/UI/BestiaryDatabaseSO.cs
Normal file
41
Assets/_Game/Scripts/UI/BestiaryDatabaseSO.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 图鉴数据表(策划单表编辑,仿 <see cref="AbilityMetaSO"/>)。每条目对应一个敌人,
|
||||
/// 以 <c>enemyId</c> 与 <c>EnemyBase._enemyId</c> 关联。自包含显示数据(名/图标/描述),
|
||||
/// 不引用敌人程序集(BaseGames.UI 不依赖 BaseGames.Enemies,故显示数据放此处)。
|
||||
/// 改本表即增删 / 重排敌人条目、改名/图标,零代码。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/UI/Bestiary Database", fileName = "UI_BestiaryDatabase")]
|
||||
public class BestiaryDatabaseSO : ScriptableObject
|
||||
{
|
||||
public enum Tier { Normal, Elite, Boss }
|
||||
|
||||
[System.Serializable]
|
||||
public struct Entry
|
||||
{
|
||||
[Tooltip("敌人唯一 ID,须与 EnemyBase._enemyId 一致(如 Enemy_SpiderGuard)。解锁靠此 id 匹配。")]
|
||||
public string enemyId;
|
||||
[Tooltip("敌人名本地化 Key(UI 表,如 ENM_E001_NAME)。")]
|
||||
public string displayNameKey;
|
||||
[Tooltip("敌人描述 / 背景本地化 Key(可空)。")]
|
||||
public string descKey;
|
||||
[Tooltip("敌人肖像(可空)。")]
|
||||
public Sprite icon;
|
||||
[Tooltip("分级(普通 / 精英 / Boss),用于分类与排序。")]
|
||||
public Tier tier;
|
||||
[Tooltip("所属区域 ID(可空,用于筛选)。")]
|
||||
public string region;
|
||||
[Tooltip("解锁完整词条所需累计击杀数(<=1 即首杀全解锁)。")]
|
||||
public int fullLoreKillCount;
|
||||
[Tooltip("展示用最大生命(策划手填;0 = 不显示)。")]
|
||||
public int displayMaxHP;
|
||||
}
|
||||
|
||||
[SerializeField] private Entry[] _entries;
|
||||
|
||||
public Entry[] Entries => _entries;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/BestiaryDatabaseSO.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/BestiaryDatabaseSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d7e33770928593459417c4dab65ced5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
168
Assets/_Game/Scripts/UI/BestiaryPanel.cs
Normal file
168
Assets/_Game/Scripts/UI/BestiaryPanel.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 图鉴面板(数据驱动,仿 <see cref="DataDrivenAbilityPanel"/>)。据 <see cref="BestiaryDatabaseSO"/> 列出全部敌人,
|
||||
/// 经 <see cref="IBestiaryService"/>(ServiceLocator) 读已发现态:已发现亮显,未发现 "???"+剪影+锁。
|
||||
/// 订阅 EVT_BestiaryUpdated 实时刷新;点击格子在右侧详情区展示名/描述/数值/击杀数;显示完成度 X/Y。
|
||||
/// 菜单态(无游戏,service=null)下全部显示未发现。
|
||||
/// </summary>
|
||||
public class BestiaryPanel : MonoBehaviour, IFocusable
|
||||
{
|
||||
[Header("数据 / 单元格")]
|
||||
[SerializeField] private BestiaryDatabaseSO _database;
|
||||
[Tooltip("敌人格的父节点(通常挂 GridLayoutGroup)。")]
|
||||
[SerializeField] private Transform _container;
|
||||
[SerializeField] private BestiaryCellView _cellPrefab;
|
||||
|
||||
[Header("头部 / 关闭")]
|
||||
[Tooltip("完成度文本(X / Y)。")]
|
||||
[SerializeField] private TMP_Text _completionText;
|
||||
[SerializeField] private Button _btnClose;
|
||||
|
||||
[Header("详情区")]
|
||||
[SerializeField] private Image _detailIcon;
|
||||
[SerializeField] private LocalizedText _detailName;
|
||||
[SerializeField] private TMP_Text _detailDesc;
|
||||
[SerializeField] private TMP_Text _detailStats;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[Tooltip("图鉴更新广播(payload=enemyId),用于实时刷新。对应 EVT_BestiaryUpdated。")]
|
||||
[SerializeField] private StringEventChannelSO _onBestiaryUpdated;
|
||||
|
||||
private readonly List<(BestiaryCellView view, BestiaryDatabaseSO.Entry entry)> _cells = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private IBestiaryService _service;
|
||||
|
||||
private void Awake() => _btnClose?.onClick.AddListener(OnClose);
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onBestiaryUpdated?.Subscribe(_ => RefreshStates()).AddTo(_subs);
|
||||
_service = ServiceLocator.GetOrDefault<IBestiaryService>(); // 菜单态为 null → 全部未发现
|
||||
BuildList();
|
||||
ShowDetail(-1);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── 列表构建 ──────────────────────────────────────────────────────────
|
||||
public void BuildList()
|
||||
{
|
||||
ClearList();
|
||||
if (_database == null || _container == null || _cellPrefab == null) return;
|
||||
var entries = _database.Entries;
|
||||
if (entries == null) return;
|
||||
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
var cell = Instantiate(_cellPrefab, _container);
|
||||
cell.gameObject.SetActive(true);
|
||||
cell.Bind(entry.displayNameKey, entry.icon, IsDiscovered(entry));
|
||||
int idx = i;
|
||||
if (cell.SelectButton != null)
|
||||
cell.SelectButton.onClick.AddListener(() => ShowDetail(idx));
|
||||
_cells.Add((cell, entry));
|
||||
}
|
||||
UpdateCompletion();
|
||||
}
|
||||
|
||||
private void RefreshStates()
|
||||
{
|
||||
if (_service == null) _service = ServiceLocator.GetOrDefault<IBestiaryService>();
|
||||
foreach (var (view, entry) in _cells)
|
||||
if (view != null) view.SetDiscovered(IsDiscovered(entry));
|
||||
UpdateCompletion();
|
||||
}
|
||||
|
||||
private bool IsDiscovered(BestiaryDatabaseSO.Entry entry)
|
||||
=> _service != null && _service.IsDiscovered(entry.enemyId);
|
||||
|
||||
private void UpdateCompletion()
|
||||
{
|
||||
if (_completionText == null) return;
|
||||
int total = _database != null && _database.Entries != null ? _database.Entries.Length : 0;
|
||||
int found = 0;
|
||||
foreach (var (_, entry) in _cells) if (IsDiscovered(entry)) found++;
|
||||
_completionText.text = $"{found} / {total}";
|
||||
}
|
||||
|
||||
// ── 详情 ──────────────────────────────────────────────────────────────
|
||||
private void ShowDetail(int index)
|
||||
{
|
||||
bool valid = index >= 0 && index < _cells.Count;
|
||||
var entry = valid ? _cells[index].entry : default;
|
||||
bool discovered = valid && IsDiscovered(entry);
|
||||
|
||||
if (_detailIcon != null)
|
||||
{
|
||||
_detailIcon.sprite = discovered ? entry.icon : null;
|
||||
_detailIcon.enabled = discovered && entry.icon != null;
|
||||
}
|
||||
if (_detailName != null)
|
||||
_detailName.SetKey(!valid ? "" : (discovered ? entry.displayNameKey : "BESTIARY_LOCKED"));
|
||||
|
||||
if (_detailDesc != null)
|
||||
{
|
||||
if (!valid) _detailDesc.text = "";
|
||||
else if (!discovered) _detailDesc.text = LocalizationManager.Get("BESTIARY_LOCKED_DESC", LocalizationTable.UI);
|
||||
else
|
||||
{
|
||||
string d = LocalizationManager.Get(entry.descKey, LocalizationTable.UI);
|
||||
_detailDesc.text = string.IsNullOrEmpty(d) || d == entry.descKey ? "" : d;
|
||||
}
|
||||
}
|
||||
|
||||
if (_detailStats != null)
|
||||
{
|
||||
if (!valid || !discovered) { _detailStats.text = ""; }
|
||||
else
|
||||
{
|
||||
int kills = _service != null ? _service.GetKillCount(entry.enemyId) : 0;
|
||||
string hpLabel = LocalizationManager.Get("BESTIARY_HP", LocalizationTable.UI);
|
||||
string killLabel = LocalizationManager.Get("BESTIARY_KILLS", LocalizationTable.UI);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
if (entry.displayMaxHP > 0) sb.Append(hpLabel).Append(' ').Append(entry.displayMaxHP).Append(" ");
|
||||
sb.Append(killLabel).Append(' ').Append(kills);
|
||||
_detailStats.text = sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearList()
|
||||
{
|
||||
_cells.Clear();
|
||||
if (_container == null) return;
|
||||
for (int i = _container.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var c = _container.GetChild(i).gameObject;
|
||||
if (Application.isPlaying) Destroy(c); else DestroyImmediate(c);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
var uiMgr = ServiceLocator.GetOrDefault<UIManager>();
|
||||
if (uiMgr != null) uiMgr.CloseTopPanel();
|
||||
else gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── IFocusable ────────────────────────────────────────────────────────
|
||||
public void OnFocusRestored()
|
||||
{
|
||||
GameObject target = _cells.Count > 0 && _cells[0].view != null && _cells[0].view.SelectButton != null
|
||||
? _cells[0].view.SelectButton.gameObject
|
||||
: _btnClose != null ? _btnClose.gameObject : null;
|
||||
if (target != null) EventSystem.current?.SetSelectedGameObject(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/BestiaryPanel.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/BestiaryPanel.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fbb26cf6309a0749b96d6137a9b1b61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
87
Assets/_Game/Scripts/UI/DataDrivenAbilityPanel.cs
Normal file
87
Assets/_Game/Scripts/UI/DataDrivenAbilityPanel.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力解锁总览面板(数据驱动)。据 <see cref="AbilityMetaSO"/> 列出全部能力 + 已解锁/未解锁态。
|
||||
/// 在场景中找 <see cref="PlayerStats"/> 读 <see cref="PlayerStats.UnlockedAbilities"/>;
|
||||
/// 订阅 <see cref="AbilityTypeEventChannelSO"/>(EVT_AbilityUnlocked) 实时刷新解锁态。
|
||||
/// 策划改 UI_AbilityMeta 即增删/重排能力、改标签图标;美术改 UI_AbilityCell / 面板预制件改样式。
|
||||
/// </summary>
|
||||
public class DataDrivenAbilityPanel : MonoBehaviour
|
||||
{
|
||||
[Header("数据 / 单元格")]
|
||||
[SerializeField] private AbilityMetaSO _meta;
|
||||
[Tooltip("能力格的父节点(通常挂 GridLayoutGroup)。")]
|
||||
[SerializeField] private Transform _container;
|
||||
[SerializeField] private AbilityCellView _cellPrefab;
|
||||
|
||||
[Header("引用")]
|
||||
[SerializeField] private Button _btnClose;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[Tooltip("能力解锁广播(AbilityType payload),用于实时刷新。对应 EVT_AbilityUnlocked。")]
|
||||
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
|
||||
|
||||
private readonly List<(AbilityCellView view, AbilityType type)> _cells = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private PlayerStats _stats;
|
||||
|
||||
private void Awake() => _btnClose?.onClick.AddListener(OnClose);
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onAbilityUnlocked?.Subscribe(_ => RefreshStates()).AddTo(_subs);
|
||||
_stats = FindObjectOfType<PlayerStats>(true); // 游戏内有玩家;菜单态为 null(全部显示未解锁)
|
||||
BuildList();
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
/// <summary>据 AbilityMetaSO 重建能力格(public 以便编辑器/测试)。</summary>
|
||||
public void BuildList()
|
||||
{
|
||||
ClearList();
|
||||
if (_meta == null || _container == null || _cellPrefab == null) return;
|
||||
foreach (var item in _meta.Items)
|
||||
{
|
||||
var cell = Instantiate(_cellPrefab, _container);
|
||||
cell.gameObject.SetActive(true);
|
||||
cell.Bind(item.labelKey, item.icon, IsUnlocked(item.type));
|
||||
_cells.Add((cell, item.type));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStates()
|
||||
{
|
||||
if (_stats == null) _stats = FindObjectOfType<PlayerStats>(true);
|
||||
foreach (var (view, type) in _cells)
|
||||
if (view != null) view.SetUnlocked(IsUnlocked(type));
|
||||
}
|
||||
|
||||
private bool IsUnlocked(AbilityType type) => _stats != null && _stats.HasAbility(type);
|
||||
|
||||
private void ClearList()
|
||||
{
|
||||
_cells.Clear();
|
||||
if (_container == null) return;
|
||||
for (int i = _container.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var c = _container.GetChild(i).gameObject;
|
||||
if (Application.isPlaying) Destroy(c); else DestroyImmediate(c);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
var uiMgr = ServiceLocator.GetOrDefault<UIManager>();
|
||||
if (uiMgr != null) uiMgr.CloseTopPanel();
|
||||
else gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/DataDrivenAbilityPanel.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/DataDrivenAbilityPanel.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba7e35964864d7d44b021a24a7213c52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,8 +14,9 @@ namespace BaseGames.UI
|
||||
/// 只读面板:按形态展示各技能槽的技能资料(名称、描述、消耗、冷却),不含任何解锁/技能点交互。
|
||||
/// 支持左右翻页浏览各形态,并高亮当前实际使用的形态。
|
||||
///
|
||||
/// 数据驱动:技能数据读 <see cref="FormSkillDatabaseSO"/>(与 SkillManager 共享的权威源),不再复制配置。
|
||||
/// Inspector 必填:
|
||||
/// _formSkillSets — 每个形态对应一条配置(soulSkill + spirit1 + spirit2)
|
||||
/// _database — FormSkillDatabaseSO(与玩家 SkillManager 同一资产)
|
||||
/// _formTitleText — 显示当前形态标签名的文本
|
||||
/// _prevFormBtn / _nextFormBtn — 翻页按钮
|
||||
/// _activeFormIndicator — 高亮"当前形态"的标志(可选,如小圆点)
|
||||
@@ -26,19 +27,6 @@ namespace BaseGames.UI
|
||||
{
|
||||
// ── 子数据结构 ────────────────────────────────────────────────────────
|
||||
|
||||
[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
|
||||
{
|
||||
@@ -58,9 +46,8 @@ namespace BaseGames.UI
|
||||
|
||||
// ── Inspector 字段 ────────────────────────────────────────────────────
|
||||
|
||||
[Header("形态技能配置")]
|
||||
[Tooltip("每个元素对应一种形态的三项技能,顺序应与 FormType 枚举序号一致。")]
|
||||
[SerializeField] private FormSkillConfig[] _formSkillSets;
|
||||
[Header("形态技能数据库(权威源,与玩家 SkillManager 共享同一资产)")]
|
||||
[SerializeField] private FormSkillDatabaseSO _database;
|
||||
|
||||
[Header("UI 导航")]
|
||||
[SerializeField] private TMP_Text _formTitleText;
|
||||
@@ -87,6 +74,8 @@ namespace BaseGames.UI
|
||||
private int _activeIndex = 0; // 玩家当前实际使用的形态
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private FormSkillDatabaseSO.Entry[] Entries => _database != null ? _database.Entries : null;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
@@ -122,15 +111,17 @@ namespace BaseGames.UI
|
||||
|
||||
private void PrevForm()
|
||||
{
|
||||
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
|
||||
_viewIndex = (_viewIndex - 1 + _formSkillSets.Length) % _formSkillSets.Length;
|
||||
var entries = Entries;
|
||||
if (entries == null || entries.Length == 0) return;
|
||||
_viewIndex = (_viewIndex - 1 + entries.Length) % entries.Length;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void NextForm()
|
||||
{
|
||||
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
|
||||
_viewIndex = (_viewIndex + 1) % _formSkillSets.Length;
|
||||
var entries = Entries;
|
||||
if (entries == null || entries.Length == 0) return;
|
||||
_viewIndex = (_viewIndex + 1) % entries.Length;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -138,10 +129,11 @@ namespace BaseGames.UI
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
|
||||
_viewIndex = Mathf.Clamp(_viewIndex, 0, _formSkillSets.Length - 1);
|
||||
var entries = Entries;
|
||||
if (entries == null || entries.Length == 0) return;
|
||||
_viewIndex = Mathf.Clamp(_viewIndex, 0, entries.Length - 1);
|
||||
|
||||
var cfg = _formSkillSets[_viewIndex];
|
||||
var cfg = entries[_viewIndex];
|
||||
|
||||
// 标题(走本地化管道,fallback 为 key 本身)
|
||||
if (_formTitleText != null)
|
||||
@@ -159,7 +151,7 @@ namespace BaseGames.UI
|
||||
_activeFormIndicator.SetActive(_viewIndex == _activeIndex);
|
||||
|
||||
// 翻页按钮可见性(只有一种形态时隐藏)
|
||||
bool multiForm = _formSkillSets.Length > 1;
|
||||
bool multiForm = entries.Length > 1;
|
||||
if (_prevFormBtn != null) _prevFormBtn.gameObject.SetActive(multiForm);
|
||||
if (_nextFormBtn != null) _nextFormBtn.gameObject.SetActive(multiForm);
|
||||
|
||||
|
||||
@@ -118,6 +118,17 @@ namespace BaseGames.UI.Inventory
|
||||
/// <summary>切换到指定 Tab。无效索引或与当前相同则忽略。</summary>
|
||||
public void SelectTab(int index) => SelectTab(index, raise: true, animateEntry: false);
|
||||
|
||||
/// <summary>
|
||||
/// 按内容根名(如 "Content_Map" / "Content_Inventory" / "Content_Quests")选中对应 Tab;找不到则忽略。
|
||||
/// 供"快速直达键"用:与数字索引解耦,Tab 重排后仍正确。
|
||||
/// </summary>
|
||||
public void SelectTabByContentName(string contentName)
|
||||
{
|
||||
if (_tabs == null || string.IsNullOrEmpty(contentName)) return;
|
||||
for (int i = 0; i < _tabs.Length; i++)
|
||||
if (_tabs[i].content != null && _tabs[i].content.name == contentName) { SelectTab(i); return; }
|
||||
}
|
||||
|
||||
private void SelectTab(int index, bool raise, bool animateEntry)
|
||||
{
|
||||
if (_tabs == null || _tabs.Length == 0) return;
|
||||
|
||||
43
Assets/_Game/Scripts/UI/MenuPauseScope.cs
Normal file
43
Assets/_Game/Scripts/UI/MenuPauseScope.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 挂在"游戏内全屏菜单"根节点(如 InventoryHubRoot)上:
|
||||
/// 打开(OnEnable)时冻结游戏(Time.timeScale=0)并切到 UI 输入;关闭(OnDisable)时恢复时间并切回 Gameplay 输入。
|
||||
///
|
||||
/// 解决的 bug:游戏内菜单(统一背包屏)打开时本不改 GameState,导致 Gameplay 输入仍激活、
|
||||
/// 按 Esc 触发的是"暂停"而非"关闭菜单",且关闭后状态错乱使 Esc 不再暂停。
|
||||
/// 切到 UI 输入后:菜单内 Esc=关闭(UINavigator Cancel),关闭后 Esc=暂停(Gameplay)。
|
||||
/// 同时冻结时间符合"打开背包暂停游戏"的常规手感。
|
||||
///
|
||||
/// 仅作用于本菜单的开/关;与暂停菜单(PausedState)的真实暂停相互独立、互不冲突。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MenuPauseScope : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
[Tooltip("打开时是否冻结游戏时间(Time.timeScale=0)。菜单动画须用 unscaledDeltaTime。")]
|
||||
[SerializeField] private bool _freezeTime = true;
|
||||
|
||||
private float _prevTimeScale = 1f;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_freezeTime)
|
||||
{
|
||||
_prevTimeScale = Time.timeScale;
|
||||
Time.timeScale = 0f;
|
||||
}
|
||||
_inputReader?.EnableUIInput();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_freezeTime)
|
||||
Time.timeScale = _prevTimeScale; // 恢复打开前的时间倍率(若打开前已是 0 则维持暂停,正确)
|
||||
_inputReader?.EnableGameplayInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/MenuPauseScope.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/MenuPauseScope.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09d80c6e088f83c4f8a1edadbf5455cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -82,6 +82,9 @@ namespace BaseGames.UI
|
||||
case PauseMenuAction.OpenSettings:
|
||||
_uiManager?.OpenPanel(PanelId.Settings);
|
||||
break;
|
||||
case PauseMenuAction.OpenPanel:
|
||||
_uiManager?.OpenPanel(item.targetPanel);
|
||||
break;
|
||||
case PauseMenuAction.ReturnToMainMenu:
|
||||
_uiManager?.CloseTopPanel();
|
||||
_onSceneLoadRequest?.Raise(new SceneLoadRequest
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace BaseGames.UI
|
||||
{
|
||||
Resume, // 继续游戏(关闭暂停面板)
|
||||
OpenSettings, // 打开设置面板
|
||||
OpenPanel, // 打开任意已注册面板(用 targetPanel 指定,如 FormSkills/Abilities)
|
||||
ReturnToMainMenu, // 返回主菜单(场景加载)
|
||||
Quit, // 退出游戏
|
||||
RaiseEvent, // 触发 eventChannel(万能扩展)
|
||||
@@ -34,6 +35,9 @@ namespace BaseGames.UI
|
||||
[Tooltip("点击动作。")]
|
||||
public PauseMenuAction action;
|
||||
|
||||
[Tooltip("OpenPanel 动作要打开的面板 ID(如 FormSkills / Abilities / Map)。")]
|
||||
public PanelId targetPanel;
|
||||
|
||||
[Tooltip("ReturnToMainMenu 的目标场景 Addressable Key(留空用默认主菜单)。")]
|
||||
public string sceneKey;
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace BaseGames.UI
|
||||
CharmPanel,
|
||||
SpellSelect,
|
||||
Inventory,
|
||||
FormSkills, // 形态技能一览(FormSkillPanel)
|
||||
Abilities, // 能力解锁总览(DataDrivenAbilityPanel)
|
||||
}
|
||||
|
||||
[DefaultExecutionOrder(+50)]
|
||||
@@ -53,6 +55,8 @@ namespace BaseGames.UI
|
||||
[SerializeField] private VoidEventChannelSO _onSpellSelectOpen;
|
||||
[Tooltip("打开统一背包菜单(InventoryHub)。对应 EVT_InventoryOpen。")]
|
||||
[SerializeField] private VoidEventChannelSO _onInventoryOpen;
|
||||
[Tooltip("快速直达:打开统一屏并定位到地图 Tab。对应 EVT_QuickOpenMap。")]
|
||||
[SerializeField] private VoidEventChannelSO _onQuickOpenMap;
|
||||
|
||||
// ── 面板栈:委托给统一的 UINavigator(不再自管栈)─────────────────────
|
||||
private IUINavigator _navigator;
|
||||
@@ -119,6 +123,7 @@ namespace BaseGames.UI
|
||||
_onCharmPanelOpen?.Subscribe(OpenCharmPanel).AddTo(_subs);
|
||||
_onSpellSelectOpen?.Subscribe(OpenSpellSelect).AddTo(_subs);
|
||||
_onInventoryOpen?.Subscribe(OpenInventory).AddTo(_subs);
|
||||
_onQuickOpenMap?.Subscribe(() => OpenInventoryAt("Content_Map")).AddTo(_subs);
|
||||
// 取消键(ESC / 手柄 B)由 UINavigator 统一消费,UIManager 不再订阅。
|
||||
}
|
||||
|
||||
@@ -278,7 +283,32 @@ namespace BaseGames.UI
|
||||
private void OpenMap() => OpenPanel(PanelId.Map);
|
||||
private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel);
|
||||
private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect);
|
||||
private void OpenInventory() => OpenPanel(PanelId.Inventory);
|
||||
|
||||
/// <summary>背包键 toggle:若统一背包屏正位于栈顶则关闭,否则打开(同键开/关)。</summary>
|
||||
private void OpenInventory()
|
||||
{
|
||||
if (_panelRegistry.TryGetValue(PanelId.Inventory, out var inv)
|
||||
&& Navigator?.Top != null && Navigator.Top.gameObject == inv)
|
||||
Navigator.Pop();
|
||||
else
|
||||
OpenPanel(PanelId.Inventory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速直达:确保统一背包屏打开,并定位到指定内容根名对应的 Tab(如 "Content_Map")。
|
||||
/// 已打开则只切 Tab(不关闭),未打开则打开后切。按名定位,与 Tab 顺序解耦。
|
||||
/// </summary>
|
||||
private void OpenInventoryAt(string contentName)
|
||||
{
|
||||
if (!_panelRegistry.TryGetValue(PanelId.Inventory, out var inv) || inv == null)
|
||||
{
|
||||
Debug.LogWarning("[UIManager] 统一背包屏(PanelId.Inventory)未注册,快速直达失败。", this);
|
||||
return;
|
||||
}
|
||||
bool hubTop = Navigator?.Top != null && Navigator.Top.gameObject == inv;
|
||||
if (!hubTop) OpenPanel(PanelId.Inventory); // 激活时 OnEnable 同步运行
|
||||
inv.GetComponent<Inventory.InventoryHubPanel>()?.SelectTabByContentName(contentName);
|
||||
}
|
||||
|
||||
// ── 编辑器工具 ────────────────────────────────────────────────────────
|
||||
[ContextMenu("验证面板注册表")]
|
||||
|
||||
Reference in New Issue
Block a user