Passion & UI
This commit is contained in:
8
Assets/Scripts/MainGame/UI/Common.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05cbb92ceab464d40add2eb0539ccd57
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/Scripts/MainGame/UI/Common/CieButton.cs
Normal file
26
Assets/Scripts/MainGame/UI/Common/CieButton.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using SLSUtilities.UI;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
public class CieButton : UIElementBase
|
||||
{
|
||||
public Button button;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
button ??= GetComponent<Button>();
|
||||
var trigger = button.gameObject.AddComponent<EventTrigger>();
|
||||
//悬停和按下时播放声音
|
||||
var pointerEnter = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter };
|
||||
pointerEnter.callback.AddListener(_ => AudioManager.Post(AK.EVENTS.UI_HOVER, button.gameObject));
|
||||
trigger.triggers.Add(pointerEnter);
|
||||
var pointerDown = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
|
||||
pointerDown.callback.AddListener(_ => AudioManager.Post(AK.EVENTS.UI_CLICK, button.gameObject));
|
||||
trigger.triggers.Add(pointerDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/UI/Common/CieButton.cs.meta
Normal file
2
Assets/Scripts/MainGame/UI/Common/CieButton.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7979f5a4eaa18c24997479b9744fe8c7
|
||||
8
Assets/Scripts/MainGame/UI/Common/ConfirmPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/ConfirmPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a03a9ea48aa335d4296ffed48fec6213
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 确认页面的单个按钮配置。
|
||||
/// </summary>
|
||||
public class ConfirmButtonConfig
|
||||
{
|
||||
/// <summary>按钮显示文本(传入时应已完成本地化)。</summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 点击回调。为 null 表示仅关闭确认页面,不执行额外操作。
|
||||
/// 回调在页面关闭动画结束后触发。
|
||||
/// </summary>
|
||||
public Action OnClick { get; }
|
||||
|
||||
public ConfirmButtonConfig(string label, Action onClick = null)
|
||||
{
|
||||
Label = label;
|
||||
OnClick = onClick;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认页面的完整配置数据,用于动态创建 <see cref="ConfirmUIPage"/>。
|
||||
/// </summary>
|
||||
public class ConfirmPageConfig
|
||||
{
|
||||
/// <summary>标题文本。</summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>描述文本。</summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 按钮列表,按从左到右的顺序排列。
|
||||
/// 至少需要一个按钮。
|
||||
/// </summary>
|
||||
public List<ConfirmButtonConfig> Buttons { get; set; } = new();
|
||||
|
||||
/// <summary>是否允许按 ESC 关闭(等同于取消),默认 true。</summary>
|
||||
public bool AllowEscClose { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d24d0a11550bba44b9c9dbff81a0d9eb
|
||||
196
Assets/Scripts/MainGame/UI/Common/ConfirmPage/ConfirmUIPage.cs
Normal file
196
Assets/Scripts/MainGame/UI/Common/ConfirmPage/ConfirmUIPage.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用确认 / 信息提示页面。
|
||||
/// <para>
|
||||
/// 作为动态 Prefab 实例化,使用完毕后自动销毁。
|
||||
/// 通过 <see cref="Show"/> 静态方法创建并显示。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 支持任意数量的按钮:
|
||||
/// - 两个按钮(确认 / 取消)用于需要用户确认的场景。
|
||||
/// - 单个按钮(确定)用于信息提示场景。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class ConfirmUIPage : UIPageBase
|
||||
{
|
||||
// ──────────────────── 常量 ────────────────────
|
||||
private const float FadeInDuration = 0.2f;
|
||||
private const float FadeOutDuration = 0.15f;
|
||||
|
||||
// ──────────────────── 序列化引用 ────────────────────
|
||||
[Header("Content")]
|
||||
[SerializeField] private TMP_Text titleText;
|
||||
[SerializeField] private TMP_Text descriptionText;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private Transform buttonContainer;
|
||||
[SerializeField] private Button buttonPrefab;
|
||||
|
||||
// ──────────────────── 运行时状态 ────────────────────
|
||||
private readonly List<Button> spawnedButtons = new();
|
||||
private Action pendingCallback;
|
||||
private bool allowEscClose = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CloseOnEsc => allowEscClose;
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// ──────────────────── 静态入口 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 创建并显示一个确认页面实例。
|
||||
/// </summary>
|
||||
/// <param name="config">页面配置,包含标题、描述和按钮。</param>
|
||||
/// <returns>已创建的页面实例。</returns>
|
||||
public static ConfirmUIPage Show(ConfirmPageConfig config)
|
||||
{
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var manager = UIPageManager.Instance;
|
||||
var go = manager.InstantiateDynamicPage(manager.ConfirmPagePrefab);
|
||||
var page = go.GetComponent<ConfirmUIPage>();
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
Debug.LogError("[ConfirmUIPage] Prefab is missing ConfirmUIPage component.");
|
||||
Destroy(go);
|
||||
return null;
|
||||
}
|
||||
|
||||
page.Initialize(config);
|
||||
page.Open();
|
||||
return page;
|
||||
}
|
||||
|
||||
// ──────────────────── 初始化 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置初始化页面内容和按钮。
|
||||
/// </summary>
|
||||
private void Initialize(ConfirmPageConfig config)
|
||||
{
|
||||
allowEscClose = config.AllowEscClose;
|
||||
|
||||
if (titleText != null)
|
||||
titleText.text = config.Title;
|
||||
|
||||
if (descriptionText != null)
|
||||
descriptionText.text = config.Description;
|
||||
|
||||
CreateButtons(config.Buttons);
|
||||
}
|
||||
|
||||
private void CreateButtons(List<ConfirmButtonConfig> buttonConfigs)
|
||||
{
|
||||
if (buttonPrefab == null)
|
||||
{
|
||||
Debug.LogError("[ConfirmUIPage] buttonPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏模板按钮
|
||||
buttonPrefab.gameObject.SetActive(false);
|
||||
|
||||
foreach (var btnConfig in buttonConfigs)
|
||||
{
|
||||
var button = Instantiate(buttonPrefab, buttonContainer);
|
||||
button.gameObject.SetActive(true);
|
||||
|
||||
var label = button.GetComponentInChildren<TMP_Text>();
|
||||
if (label != null)
|
||||
label.text = btnConfig.Label;
|
||||
|
||||
var callback = btnConfig.OnClick;
|
||||
button.onClick.AddListener(() => OnButtonClicked(callback));
|
||||
|
||||
spawnedButtons.Add(button);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
if (IsOpen) return;
|
||||
IsOpen = true;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
|
||||
UIPageManager.Instance.RegisterPage(this);
|
||||
|
||||
canvasGroup.DOFade(1f, FadeInDuration)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
OnPageOpened();
|
||||
}).Play();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
IsOpen = false;
|
||||
|
||||
UIPageManager.Instance.UnregisterPage(this);
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
|
||||
canvasGroup.DOFade(0f, FadeOutDuration)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
OnPageClosed();
|
||||
Destroy(gameObject);
|
||||
}).Play();
|
||||
}
|
||||
|
||||
protected override void OnPageClosed()
|
||||
{
|
||||
// 先触发基类事件
|
||||
base.OnPageClosed();
|
||||
|
||||
// 执行挂起的按钮回调
|
||||
var callback = pendingCallback;
|
||||
pendingCallback = null;
|
||||
callback?.Invoke();
|
||||
|
||||
// 清理按钮监听
|
||||
foreach (var btn in spawnedButtons)
|
||||
{
|
||||
if (btn != null)
|
||||
btn.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
spawnedButtons.Clear();
|
||||
}
|
||||
|
||||
// ──────────────────── 按钮处理 ────────────────────
|
||||
|
||||
private void OnButtonClicked(Action callback)
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
pendingCallback = callback;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56cd460c1559ee647aebac48a61fbc28
|
||||
8
Assets/Scripts/MainGame/UI/Common/InteractionPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/InteractionPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f006c806bdd62b04a965e96c18c13637
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互选项列表中单个条目的 UI 组件。
|
||||
/// 由 InteractionUIPage 生成并驱动。
|
||||
/// 支持三种视觉状态:普通、高亮(选中)、不可用(灰色)。
|
||||
/// </summary>
|
||||
public class InteractionChoiceItem : UIElementBase
|
||||
{
|
||||
[SerializeField] private TMP_Text labelText;
|
||||
|
||||
[Tooltip("普通状态下的背景(可选)。")]
|
||||
[SerializeField] private Image backgroundImage;
|
||||
|
||||
[Tooltip("高亮指示器,例如左侧箭头图标(当选中时显示)。")]
|
||||
[SerializeField] private GameObject highlightIndicator;
|
||||
|
||||
[Header("颜色配置")]
|
||||
[SerializeField] private Color normalColor = Color.white;
|
||||
[SerializeField] private Color highlightedColor = new Color(0.4f, 1f, 1f); // 青色
|
||||
[SerializeField] private Color disabledColor = new Color(0.45f, 0.45f, 0.45f); // 灰色
|
||||
|
||||
private bool _isInteractable = true;
|
||||
|
||||
// ====================================================================
|
||||
// 对外接口
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 初始化条目内容。由 InteractionUIPage 在 Show() 时调用。
|
||||
/// </summary>
|
||||
/// <param name="choiceName">选项名称。</param>
|
||||
/// <param name="isInteractable">false 时以灰色显示且不可高亮。</param>
|
||||
public void Setup(string choiceName, bool isInteractable)
|
||||
{
|
||||
_isInteractable = isInteractable;
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
labelText.text = choiceName;
|
||||
labelText.color = isInteractable ? normalColor : disabledColor;
|
||||
}
|
||||
|
||||
if (highlightIndicator != null)
|
||||
{
|
||||
highlightIndicator.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置高亮状态。由 InteractionUIPage.UpdateHighlight() 调用。
|
||||
/// 不可交互的选项不会显示高亮。
|
||||
/// </summary>
|
||||
public void SetHighlighted(bool highlighted)
|
||||
{
|
||||
bool showHighlight = highlighted && _isInteractable;
|
||||
|
||||
if (highlightIndicator != null)
|
||||
{
|
||||
highlightIndicator.SetActive(showHighlight);
|
||||
}
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
if (!_isInteractable)
|
||||
{
|
||||
labelText.color = disabledColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelText.color = showHighlight ? highlightedColor : normalColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 664206634872acf4ea30f2cc8be87424
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.Core.Interaction;
|
||||
using DG.Tweening;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 交互选项列表 UI 页面。
|
||||
/// 玩家进入可交互物体范围时由 PlayerInteractionSubcontroller 调用 Show(),离开时调用 Hide()。
|
||||
/// 当选项数量为 1 时,直接显示单个选项而不需要导航;
|
||||
/// 当选项数量大于 1 时,玩家可用鼠标滚轮或上/下键切换高亮选项,R 键执行。
|
||||
/// </summary>
|
||||
public class InteractionUIArea : UIElementBase
|
||||
{
|
||||
[Title("容器")]
|
||||
[Tooltip("单个选项条目的预制体,需包含 InteractionChoiceItem 组件。")]
|
||||
[SerializeField] private GameObject choiceItemPrefab;
|
||||
|
||||
[Tooltip("选项条目的父节点(Layout Group)。")]
|
||||
[SerializeField] private RectTransform choiceContainer;
|
||||
|
||||
private readonly List<InteractionChoiceItem> _items = new List<InteractionChoiceItem>();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 对外接口
|
||||
// ====================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 显示选项列表并高亮指定索引的选项。
|
||||
/// 由 PlayerInteractionSubcontroller.SetCurrentInteractable() 调用。
|
||||
/// </summary>
|
||||
public void Show(List<InteractionChoice> choices, int selectedIndex)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.DOFade(1f, 0.25f).From(0).OnComplete(() =>
|
||||
{
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
}).Play();
|
||||
rectTransform.DOLocalMoveY(-50f, 0.25f).From(-250f).Play();
|
||||
ClearItems();
|
||||
|
||||
if (choices == null || choices.Count == 0)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (InteractionChoice choice in choices)
|
||||
{
|
||||
InteractionChoiceItem item = CreateItem();
|
||||
if (item != null)
|
||||
{
|
||||
item.Setup(choice.choiceName, choice.isInteractable);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHighlight(selectedIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新高亮选项。由 PlayerInteractionSubcontroller.NavigateChoice() 调用。
|
||||
/// </summary>
|
||||
public void UpdateHighlight(int selectedIndex)
|
||||
{
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
_items[i].SetHighlighted(i == selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏选项列表。由 PlayerInteractionSubcontroller.RemoveCurrentInteractable() 调用。
|
||||
/// </summary>
|
||||
public override void Hide()
|
||||
{
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.DOFade(0f, 0.25f).OnComplete(() =>
|
||||
{
|
||||
ClearItems();
|
||||
gameObject.SetActive(false);
|
||||
}).Play();
|
||||
rectTransform.DOLocalMoveY(-250f, 0.25f).Play();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 内部工具
|
||||
// ====================================================================
|
||||
|
||||
private InteractionChoiceItem CreateItem()
|
||||
{
|
||||
if (choiceItemPrefab == null || choiceContainer == null)
|
||||
{
|
||||
Debug.LogError("[InteractionUIPage] choiceItemPrefab 或 choiceContainer 未在 Inspector 中配置。");
|
||||
return null;
|
||||
}
|
||||
|
||||
GameObject go = Instantiate(choiceItemPrefab, choiceContainer);
|
||||
InteractionChoiceItem item = go.GetComponent<InteractionChoiceItem>();
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError("[InteractionUIPage] choiceItemPrefab 缺少 InteractionChoiceItem 组件。");
|
||||
Destroy(go);
|
||||
return null;
|
||||
}
|
||||
|
||||
_items.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void ClearItems()
|
||||
{
|
||||
foreach (InteractionChoiceItem item in _items)
|
||||
{
|
||||
if (item != null) Destroy(item.gameObject);
|
||||
}
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c8943c86810b074d955a3585bc5e71e
|
||||
8
Assets/Scripts/MainGame/UI/Common/SettingsPage.meta
Normal file
8
Assets/Scripts/MainGame/UI/Common/SettingsPage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b5632578b757434d86d7140c9f6a154
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
462
Assets/Scripts/MainGame/UI/Common/SettingsPage/SettingsUIPage.cs
Normal file
462
Assets/Scripts/MainGame/UI/Common/SettingsPage/SettingsUIPage.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Cielonos.MainGame;
|
||||
using Cielonos.Settings;
|
||||
using Cielonos.Settings.UI;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置界面页面(单页滚动式 + 右侧面板)。
|
||||
/// <para>
|
||||
/// <b>左侧:</b>所有设置分类在同一页面中按顺序展示,每个分类前插入标题。
|
||||
/// 通过反射读取各设置类的公共字段,按类型自动选择 Entry Prefab 实例化。
|
||||
/// 同时支持手动添加的按钮条目(如"Key Bindings"按钮)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>右侧:</b>包含两种面板状态:
|
||||
/// <list type="number">
|
||||
/// <item>说明面板(<see cref="descriptionPanel"/>):鼠标悬停条目时显示说明文本,离开后隐藏。</item>
|
||||
/// <item>详情页面(如 <see cref="keyBindingPage"/>):点击按钮后打开的二级 UIPage,
|
||||
/// 打开期间悬停说明被抑制,需手动关闭。</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class SettingsUIPage : UIPageBase
|
||||
{
|
||||
// ──────────────────── Entry Prefabs ──────────────────
|
||||
|
||||
[Header("Entry Prefabs")]
|
||||
[Tooltip("bool 字段使用的 Toggle 条目预制体,需挂载 SettingsEntryToggle。")]
|
||||
[SerializeField] private GameObject toggleEntryPrefab;
|
||||
|
||||
[Tooltip("带 [Range] 的 int 字段使用的 Slider 条目预制体,需挂载 SettingsEntrySlider。")]
|
||||
[SerializeField] private GameObject sliderEntryPrefab;
|
||||
|
||||
[Tooltip("enum 字段使用的 Dropdown 条目预制体,需挂载 SettingsEntryDropdown。")]
|
||||
[SerializeField] private GameObject dropdownEntryPrefab;
|
||||
|
||||
[Tooltip("按钮条目预制体,需挂载 SettingsEntryButton。")]
|
||||
[SerializeField] private GameObject buttonEntryPrefab;
|
||||
|
||||
// ──────────────────── Section Header ──────────────────
|
||||
|
||||
[Header("Section Header")]
|
||||
[Tooltip("分类标题预制体,需包含 TMP_Text 组件。")]
|
||||
[SerializeField] private GameObject sectionHeaderPrefab;
|
||||
|
||||
// ──────────────────── Left Content ────────────────────
|
||||
|
||||
[Header("Left Content")]
|
||||
[Tooltip("条目的父容器,应挂载 VerticalLayoutGroup 和 ContentSizeFitter。")]
|
||||
[SerializeField] private RectTransform contentContainer;
|
||||
|
||||
// ──────────────────── Right Panel ─────────────────────
|
||||
|
||||
[Header("Right Panel - Description")]
|
||||
[Tooltip("右侧悬停说明面板,显示条目的标题和说明文本。")]
|
||||
[SerializeField] private SettingsDescriptionPanel descriptionPanel;
|
||||
|
||||
[Header("Right Panel - Detail Pages")]
|
||||
[Tooltip("键位绑定二级页面。")]
|
||||
[SerializeField] private KeyBindingPage keyBindingPage;
|
||||
|
||||
// ──────────────────── Input ───────────────────────────
|
||||
|
||||
[Header("Input")]
|
||||
[Tooltip("用于键位绑定页面的 InputActionAsset。为空时从 Player 获取。")]
|
||||
[SerializeField] private InputActionAsset inputActionAsset;
|
||||
|
||||
// ──────────────────── 底部按钮 ────────────────────
|
||||
|
||||
[Header("Bottom Buttons")]
|
||||
[SerializeField] private Button resetButton;
|
||||
[SerializeField] private Button backButton;
|
||||
|
||||
// ──────────────────── 运行时状态 ──────────────────
|
||||
|
||||
private readonly List<SettingsEntryBase> activeFieldEntries = new();
|
||||
private readonly List<SettingsEntryButton> activeButtonEntries = new();
|
||||
private readonly List<GameObject> spawnedObjects = new();
|
||||
|
||||
/// <summary>是否有详情页面处于打开状态(抑制悬停说明)。</summary>
|
||||
private bool isDetailPageOpen;
|
||||
|
||||
// ──────────────────── 分类定义 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 分类定义:显示名称、设置实例、Apply/Reset 回调、附加按钮列表。
|
||||
/// </summary>
|
||||
private struct SectionDefinition
|
||||
{
|
||||
public string displayName;
|
||||
public object settingsInstance;
|
||||
public Action applyAction;
|
||||
public Action resetAction;
|
||||
public List<ButtonDefinition> buttons;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动添加的按钮条目定义。
|
||||
/// </summary>
|
||||
private struct ButtonDefinition
|
||||
{
|
||||
public string label;
|
||||
public string description;
|
||||
public Action onClick;
|
||||
}
|
||||
|
||||
// ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
|
||||
resetButton?.onClick.AddListener(OnResetClicked);
|
||||
backButton?.onClick.AddListener(OnBackClicked);
|
||||
|
||||
// 监听详情页面的开关状态
|
||||
if (keyBindingPage != null)
|
||||
{
|
||||
keyBindingPage.PageOpened += OnDetailPageOpened;
|
||||
keyBindingPage.PageClosed += OnDetailPageClosed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPageOpened()
|
||||
{
|
||||
BuildAllSections();
|
||||
}
|
||||
|
||||
protected override void OnPageClosed()
|
||||
{
|
||||
// 先关闭可能打开的详情页面
|
||||
if (isDetailPageOpen && keyBindingPage != null && keyBindingPage.IsOpen)
|
||||
keyBindingPage.Close();
|
||||
|
||||
ClearAll();
|
||||
descriptionPanel?.Hide();
|
||||
GameSettingsManager.Instance?.Save();
|
||||
}
|
||||
|
||||
// ──────────────────── 详情页面状态 ────────────────
|
||||
|
||||
private void OnDetailPageOpened()
|
||||
{
|
||||
isDetailPageOpen = true;
|
||||
descriptionPanel?.Hide();
|
||||
}
|
||||
|
||||
private void OnDetailPageClosed()
|
||||
{
|
||||
isDetailPageOpen = false;
|
||||
}
|
||||
|
||||
// ──────────────────── 悬停回调 ────────────────────
|
||||
|
||||
private void HandleHoverEnter(string title, string description)
|
||||
{
|
||||
if (isDetailPageOpen) return;
|
||||
descriptionPanel?.Show(title, description);
|
||||
}
|
||||
|
||||
private void HandleHoverExit()
|
||||
{
|
||||
if (isDetailPageOpen) return;
|
||||
descriptionPanel?.Hide();
|
||||
}
|
||||
|
||||
// ──────────────────── 分类定义构建 ────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 构建所有分类定义。扩展时在此处追加。
|
||||
/// </summary>
|
||||
private List<SectionDefinition> BuildSectionDefinitions()
|
||||
{
|
||||
var manager = GameSettingsManager.Instance;
|
||||
if (manager == null) return null;
|
||||
|
||||
return new List<SectionDefinition>
|
||||
{
|
||||
new()
|
||||
{
|
||||
displayName = "Gameplay",
|
||||
settingsInstance = manager.gameplay,
|
||||
applyAction = manager.ApplyGameplay,
|
||||
resetAction = manager.ResetGameplayToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Graphics",
|
||||
settingsInstance = manager.graphics,
|
||||
applyAction = manager.ApplyGraphics,
|
||||
resetAction = manager.ResetGraphicsToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Sound",
|
||||
settingsInstance = manager.sound,
|
||||
applyAction = manager.ApplySound,
|
||||
resetAction = manager.ResetSoundToDefault
|
||||
},
|
||||
new()
|
||||
{
|
||||
displayName = "Controls",
|
||||
settingsInstance = manager.controls,
|
||||
applyAction = manager.ApplyControls,
|
||||
resetAction = manager.ResetControlsToDefault,
|
||||
buttons = new List<ButtonDefinition>
|
||||
{
|
||||
new()
|
||||
{
|
||||
label = "Key Bindings",
|
||||
description = "Customize keyboard and mouse bindings for all game actions.",
|
||||
onClick = OpenKeyBindingPage
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ──────────────────── 条目生成 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 构建所有分类的标题、字段条目和按钮条目。
|
||||
/// </summary>
|
||||
private void BuildAllSections()
|
||||
{
|
||||
ClearAll();
|
||||
|
||||
var sections = BuildSectionDefinitions();
|
||||
if (sections == null)
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] GameSettingsManager not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
BuildSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建单个分类的标题、字段条目和按钮条目。
|
||||
/// </summary>
|
||||
private void BuildSection(SectionDefinition section)
|
||||
{
|
||||
FieldInfo[] fields = section.settingsInstance.GetType()
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// 预扫描:确认该分类至少有一个可显示的字段或按钮
|
||||
bool hasVisibleContent = section.buttons is { Count: > 0 };
|
||||
|
||||
if (!hasVisibleContent)
|
||||
{
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
if (field.GetCustomAttribute<SettingsIgnoreAttribute>() == null
|
||||
&& ResolvePrefab(field) != null)
|
||||
{
|
||||
hasVisibleContent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasVisibleContent) return;
|
||||
|
||||
// 插入分类标题
|
||||
SpawnSectionHeader(section.displayName);
|
||||
|
||||
// 生成字段条目
|
||||
Action applyAction = section.applyAction;
|
||||
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
if (field.GetCustomAttribute<SettingsIgnoreAttribute>() != null)
|
||||
continue;
|
||||
|
||||
GameObject prefab = ResolvePrefab(field);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[SettingsUIPage] No matching prefab for field '{field.Name}' " +
|
||||
$"(type: {field.FieldType.Name}). Skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject entryObj = Instantiate(prefab, contentContainer);
|
||||
var entry = entryObj.GetComponent<SettingsEntryBase>();
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[SettingsUIPage] Prefab for field '{field.Name}' " +
|
||||
"is missing a SettingsEntryBase component.");
|
||||
Destroy(entryObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Initialize(section.settingsInstance, field, () => applyAction?.Invoke());
|
||||
RegisterFieldEntryHover(entry);
|
||||
activeFieldEntries.Add(entry);
|
||||
spawnedObjects.Add(entryObj);
|
||||
}
|
||||
|
||||
// 生成按钮条目
|
||||
if (section.buttons != null)
|
||||
{
|
||||
foreach (var btnDef in section.buttons)
|
||||
{
|
||||
SpawnButtonEntry(btnDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为字段条目注册悬停回调。
|
||||
/// </summary>
|
||||
private void RegisterFieldEntryHover(SettingsEntryBase entry)
|
||||
{
|
||||
entry.OnHoverEnter = HandleHoverEnter;
|
||||
entry.OnHoverExit = HandleHoverExit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化按钮条目。
|
||||
/// </summary>
|
||||
private void SpawnButtonEntry(ButtonDefinition def)
|
||||
{
|
||||
if (buttonEntryPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] buttonEntryPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject entryObj = Instantiate(buttonEntryPrefab, contentContainer);
|
||||
var buttonEntry = entryObj.GetComponent<SettingsEntryButton>();
|
||||
if (buttonEntry == null)
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] buttonEntryPrefab is missing SettingsEntryButton.");
|
||||
Destroy(entryObj);
|
||||
return;
|
||||
}
|
||||
|
||||
buttonEntry.Initialize(def.label, def.description, def.onClick);
|
||||
buttonEntry.OnHoverEnter = HandleHoverEnter;
|
||||
buttonEntry.OnHoverExit = HandleHoverExit;
|
||||
activeButtonEntries.Add(buttonEntry);
|
||||
spawnedObjects.Add(entryObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化分类标题预制体。
|
||||
/// </summary>
|
||||
private void SpawnSectionHeader(string title)
|
||||
{
|
||||
if (sectionHeaderPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] sectionHeaderPrefab is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject headerObj = Instantiate(sectionHeaderPrefab, contentContainer);
|
||||
var headerText = headerObj.GetComponentInChildren<TMP_Text>();
|
||||
if (headerText != null)
|
||||
headerText.text = title;
|
||||
|
||||
spawnedObjects.Add(headerObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据字段类型选择对应的预制体。
|
||||
/// </summary>
|
||||
private GameObject ResolvePrefab(FieldInfo field)
|
||||
{
|
||||
Type fieldType = field.FieldType;
|
||||
|
||||
if (fieldType == typeof(bool))
|
||||
return toggleEntryPrefab;
|
||||
|
||||
if (fieldType == typeof(int))
|
||||
return sliderEntryPrefab;
|
||||
|
||||
if (fieldType.IsEnum)
|
||||
return dropdownEntryPrefab;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ──────────────────── 详情页面入口 ────────────────
|
||||
|
||||
private void OpenKeyBindingPage()
|
||||
{
|
||||
if (keyBindingPage == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIPage] keyBindingPage is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先使用配置的 InputActionAsset,否则尝试从 Player 获取
|
||||
InputActionAsset asset = inputActionAsset;
|
||||
if (asset == null)
|
||||
{
|
||||
var player = MainGameManager.Player;
|
||||
if (player != null)
|
||||
asset = player.inputSc.inputActions.asset;
|
||||
}
|
||||
|
||||
if (asset != null)
|
||||
{
|
||||
keyBindingPage.Open(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SettingsUIPage] No InputActionAsset available for KeyBindingPage.");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 底部按钮回调 ────────────────
|
||||
|
||||
private void OnResetClicked()
|
||||
{
|
||||
GameSettingsManager.Instance?.ResetAllToDefault();
|
||||
BuildAllSections();
|
||||
}
|
||||
|
||||
private void OnBackClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
// ──────────────────── 清理 ────────────────────────
|
||||
|
||||
private void ClearAll()
|
||||
{
|
||||
foreach (var obj in spawnedObjects)
|
||||
{
|
||||
if (obj != null)
|
||||
Destroy(obj);
|
||||
}
|
||||
|
||||
activeFieldEntries.Clear();
|
||||
activeButtonEntries.Clear();
|
||||
spawnedObjects.Clear();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (keyBindingPage != null)
|
||||
{
|
||||
keyBindingPage.PageOpened -= OnDetailPageOpened;
|
||||
keyBindingPage.PageClosed -= OnDetailPageClosed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06a515f761ecf184399f3506f8cbf989
|
||||
142
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs
Normal file
142
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用展示文本解析器。
|
||||
/// 负责将文本中的 {key} 命名占位符替换为运行时动态值,
|
||||
/// 并提供各类游戏数据的展示值计算方法。
|
||||
/// </summary>
|
||||
public static class DisplayTextResolver
|
||||
{
|
||||
private static readonly Regex PlaceholderPattern = new Regex(@"\{(\w+)\}", RegexOptions.Compiled);
|
||||
|
||||
// ================================================================
|
||||
// 核心:占位符替换
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 将文本中的 {key} 占位符替换为 args 字典中对应的值。
|
||||
/// 未匹配到的占位符保留原样。
|
||||
/// </summary>
|
||||
public static string Resolve(string text, Dictionary<string, string> args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || args == null || args.Count == 0)
|
||||
return text;
|
||||
|
||||
return PlaceholderPattern.Replace(text, match =>
|
||||
{
|
||||
string key = match.Groups[1].Value;
|
||||
return args.TryGetValue(key, out string value) ? value : match.Value;
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 伤害相关
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示伤害值。
|
||||
/// 仅包含攻击者侧倍率,不含随机偏差和受击者侧减伤。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayDamage(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float damage = unit.startDamage;
|
||||
if (attacker == null) return damage;
|
||||
|
||||
damage *= attacker.attributeSm[CharacterAttribute.AttackDamageMultiplier];
|
||||
string typeKey = unit.type.AttackTypeToString() + "DamageDealtMultiplier";
|
||||
damage *= attacker.attributeSm[typeKey];
|
||||
damage *= attacker.attributeSm[CharacterAttribute.FinalDamageDealtMultiplier];
|
||||
return damage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示暴击率(含玩家属性加成),结果范围 [0, 1]。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayCriticalChance(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float chance = unit.criticalChance;
|
||||
if (attacker != null)
|
||||
chance += attacker.attributeSm[CharacterAttribute.CriticalAttackProbability];
|
||||
return Mathf.Clamp01(chance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 AttackUnit 的面板展示暴击伤害倍率(含玩家属性加成)。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayCriticalMultiplier(AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return 0f;
|
||||
|
||||
float multiplier = unit.criticalMultiplier;
|
||||
if (attacker != null)
|
||||
multiplier += attacker.attributeSm[CharacterAttribute.CriticalAttackDamageAmplifier];
|
||||
return multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量填充指定 AttackUnit 的伤害、暴击率、暴击倍率到 args 字典中。
|
||||
/// 生成的 key 格式:{prefix}_damage, {prefix}_crit_chance, {prefix}_crit_multiplier。
|
||||
/// </summary>
|
||||
public static void PopulateAttackArgs(
|
||||
Dictionary<string, string> args, string prefix,
|
||||
AttackUnit unit, CharacterBase attacker)
|
||||
{
|
||||
if (unit == null || unit.isInvalidAttack) return;
|
||||
|
||||
args[$"{prefix}_damage"] = FormatInt(ComputeDisplayDamage(unit, attacker));
|
||||
args[$"{prefix}_crit_chance"] = FormatPercent(ComputeDisplayCriticalChance(unit, attacker));
|
||||
args[$"{prefix}_crit_multiplier"] = FormatFloat(ComputeDisplayCriticalMultiplier(unit, attacker));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 资源花费相关
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 计算经过能量花费减少属性调整后的展示能量花费。
|
||||
/// </summary>
|
||||
public static float ComputeDisplayEnergyCost(float baseCost, CharacterBase character)
|
||||
{
|
||||
if (character == null) return baseCost;
|
||||
float reduction = character.attributeSm[CharacterAttribute.EnergyCostReduction];
|
||||
return Mathf.Max(0f, baseCost * (1f - reduction));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 格式化工具
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为整数字符串(四舍五入)。
|
||||
/// </summary>
|
||||
public static string FormatInt(float value)
|
||||
{
|
||||
return Mathf.RoundToInt(value).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为百分比字符串(如 0.15 → "15%")。
|
||||
/// </summary>
|
||||
public static string FormatPercent(float value)
|
||||
{
|
||||
return $"{Mathf.RoundToInt(value * 100f)}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将浮点数格式化为保留指定小数位的字符串。
|
||||
/// </summary>
|
||||
public static string FormatFloat(float value, int decimals = 1)
|
||||
{
|
||||
return value.ToString($"F{decimals}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs.meta
Normal file
2
Assets/Scripts/MainGame/UI/DisplayTextResolver.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02361f67e62f0064cbeffcf2ccc79c23
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b651e1fc3b3065b40ae2c20a253de26b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单条物品描述条目 UI 组件。
|
||||
/// 渲染管线:Localize → DisplayTextResolver.Resolve → InputGlyphParser.Parse → TMP_Text。
|
||||
/// </summary>
|
||||
public class ItemDescriptionEntry : MonoBehaviour
|
||||
{
|
||||
private const string LocalizationTable = "Items";
|
||||
|
||||
[Tooltip("描述文本,显示 descriptionKey 的本地化内容(含动态数值替换和按键图标解析)。")]
|
||||
public TMP_Text descriptionText;
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="ItemDescription"/> 的数据填充描述条目。
|
||||
/// 完整渲染管线:本地化 → {key} 占位符替换 → [Token] 按键图标解析。
|
||||
/// </summary>
|
||||
/// <param name="description">描述数据。</param>
|
||||
/// <param name="descriptionArgs">可选的动态值字典,用于替换本地化文本中的 {key} 占位符。</param>
|
||||
public void SetDescription(ItemDescription description, Dictionary<string, string> descriptionArgs = null)
|
||||
{
|
||||
if (description == null)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptionText == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(description.descriptionKey))
|
||||
{
|
||||
descriptionText.text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
string localizedText = description.descriptionKey.Localize(LocalizationTable);
|
||||
localizedText = DisplayTextResolver.Resolve(localizedText, descriptionArgs);
|
||||
descriptionText.text = InputGlyphParser.Parse(localizedText);
|
||||
}
|
||||
|
||||
/// <summary>清空条目内容。</summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (descriptionText != null)
|
||||
{
|
||||
descriptionText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bcc59ecfb9425b4a85a1e3ad51c8739
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.UI;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用物品详情面板,显示物品图标、名称、类型、描述条目、标签等信息。
|
||||
/// 可被不同的 UIPage 复用(机械台、商店、背包检视等)。
|
||||
/// </summary>
|
||||
public class ItemDetailPanel : UIElementBase
|
||||
{
|
||||
[Title("Detail Panel References")]
|
||||
public TMP_Text selectionHintText;
|
||||
//public Image itemIcon;
|
||||
public TMP_Text nameText;
|
||||
public TMP_Text typeRarityText;
|
||||
public TMP_Text institutionText;
|
||||
public RectTransform tagContainer;
|
||||
public GameObject tagPrefab;
|
||||
|
||||
// ─────────────────── 描述条目 ───────────────────
|
||||
|
||||
[Title("Description Entries")]
|
||||
[Tooltip("描述条目的容器,应挂载 VerticalLayoutGroup 以实现垂直排列。")]
|
||||
public RectTransform descriptionContainer;
|
||||
|
||||
[Tooltip("描述条目预制体,需挂载 ItemDescriptionEntry 组件。")]
|
||||
public GameObject descriptionEntryPrefab;
|
||||
|
||||
// ─────────────────── 运行时数据 ───────────────────
|
||||
|
||||
private ItemBase currentItem;
|
||||
private readonly List<ItemDescriptionEntry> activeEntries = new List<ItemDescriptionEntry>();
|
||||
|
||||
/// <summary>当前显示的物品。</summary>
|
||||
public ItemBase CurrentItem => currentItem;
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定物品的数据填充详情面板并显示。
|
||||
/// </summary>
|
||||
public void SetItem(ItemBase item)
|
||||
{
|
||||
currentItem = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
ClearPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标
|
||||
/*Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}*/
|
||||
|
||||
// 名称(本地化)
|
||||
nameText.text = data.displayNameKey.Localize("Items");
|
||||
|
||||
// 类型
|
||||
typeRarityText.text = data.itemType.ToString() + " - " + data.itemRarity.ToString();
|
||||
|
||||
//机构
|
||||
institutionText.text = string.Empty;
|
||||
for (var index = 0; index < data.institutions.Count; index++)
|
||||
{
|
||||
var institution = data.institutions[index];
|
||||
string comma = LocalizationSettings.SelectedLocale.Identifier.Code.StartsWith("zh") ? "、" : ", ";
|
||||
if(index < data.institutions.Count - 1)
|
||||
institutionText.text += institution.Localize("Items") + comma;
|
||||
else
|
||||
institutionText.text += institution.Localize("Items");
|
||||
}
|
||||
|
||||
// 描述条目
|
||||
RefreshDescriptions(data);
|
||||
|
||||
// 标签
|
||||
RefreshTags(data);
|
||||
|
||||
selectionHintText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>清空面板内容并隐藏。</summary>
|
||||
public void ClearPanel()
|
||||
{
|
||||
currentItem = null;
|
||||
|
||||
//if (itemIcon != null) itemIcon.enabled = false;
|
||||
if (nameText != null) nameText.text = string.Empty;
|
||||
if (typeRarityText != null) typeRarityText.text = string.Empty;
|
||||
|
||||
ClearDescriptions();
|
||||
ClearTags();
|
||||
selectionHintText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 描述条目
|
||||
// ================================================================
|
||||
|
||||
private void RefreshDescriptions(ContentData data)
|
||||
{
|
||||
ClearDescriptions();
|
||||
|
||||
if (descriptionContainer == null || descriptionEntryPrefab == null) return;
|
||||
if (data.descriptions == null || data.descriptions.Count == 0) return;
|
||||
|
||||
Dictionary<string, string> descriptionArgs = currentItem?.GetDescriptionArgs();
|
||||
|
||||
foreach (ItemDescription desc in data.descriptions)
|
||||
{
|
||||
ItemDescriptionEntry entry = Instantiate(descriptionEntryPrefab, descriptionContainer).GetComponent<ItemDescriptionEntry>();
|
||||
entry.SetDescription(desc, descriptionArgs);
|
||||
activeEntries.Add(entry);
|
||||
}
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(descriptionContainer);
|
||||
}
|
||||
|
||||
private void ClearDescriptions()
|
||||
{
|
||||
foreach (ItemDescriptionEntry entry in activeEntries)
|
||||
{
|
||||
if (entry != null)
|
||||
{
|
||||
Destroy(entry.gameObject);
|
||||
}
|
||||
}
|
||||
activeEntries.Clear();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 标签
|
||||
// ================================================================
|
||||
|
||||
private void RefreshTags(ContentData data)
|
||||
{
|
||||
ClearTags();
|
||||
|
||||
if (tagContainer == null || tagPrefab == null || data.tags == null) return;
|
||||
|
||||
foreach (string tag in data.tags)
|
||||
{
|
||||
GameObject tagObj = Instantiate(tagPrefab, tagContainer);
|
||||
TMP_Text tagText = tagObj.GetComponentInChildren<TMP_Text>();
|
||||
if (tagText != null)
|
||||
{
|
||||
tagText.text = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTags()
|
||||
{
|
||||
if (tagContainer == null) return;
|
||||
|
||||
for (int i = tagContainer.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(tagContainer.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.Items;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单选类别筛选下拉框,基于 <see cref="TMP_Dropdown"/>。
|
||||
/// 每个选项对应一组 <see cref="ItemType"/>,选中后构建对应的 <see cref="ItemFilter"/>。
|
||||
/// 第一个选项通常配置为"全部"(types 留空即可)。
|
||||
/// <para>可复用于背包、商店等任何需要按类型筛选物品的 UI 页面。</para>
|
||||
/// </summary>
|
||||
public class ItemCategoryDropdown : UIElementBase
|
||||
{
|
||||
[Serializable]
|
||||
public struct CategoryEntry
|
||||
{
|
||||
[Tooltip("显示标签,可填入本地化 Key(如 'UI_Category_All'),会自动调用 Localize()。")]
|
||||
public string label;
|
||||
|
||||
[Tooltip("对应的物品类型。为空时表示显示所有类型(不筛选)。")]
|
||||
public ItemType[] types;
|
||||
}
|
||||
|
||||
[Header("Dropdown")]
|
||||
public TMP_Dropdown dropdown;
|
||||
|
||||
[Header("Categories")]
|
||||
[Tooltip("类别选项列表。第一个通常为'全部'(types 留空)。")]
|
||||
public List<CategoryEntry> categoryEntries = new List<CategoryEntry>
|
||||
{
|
||||
new CategoryEntry { label = "全部", types = new[] { ItemType.MainWeapon, ItemType.Support, ItemType.Passive, ItemType.Consumable } },
|
||||
new CategoryEntry { label = "主武器", types = new[] { ItemType.MainWeapon } },
|
||||
new CategoryEntry { label = "支援装备", types = new[] { ItemType.Support } },
|
||||
new CategoryEntry { label = "被动装备", types = new[] { ItemType.Passive } },
|
||||
new CategoryEntry { label = "消耗品", types = new[] { ItemType.Consumable } }
|
||||
};
|
||||
|
||||
/// <summary>当筛选条件变化时触发,参数为对应的 <see cref="ItemFilter"/>。</summary>
|
||||
public event Action<ItemFilter> OnFilterChanged;
|
||||
|
||||
/// <summary>当前生效的筛选条件。</summary>
|
||||
public ItemFilter CurrentFilter { get; private set; } = ItemFilter.None;
|
||||
|
||||
// ================================================================
|
||||
// 生命周期
|
||||
// ================================================================
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeOptions();
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="categoryEntries"/> 重新初始化下拉选项。
|
||||
/// 语言切换后可手动调用以刷新标签文本。
|
||||
/// </summary>
|
||||
public void InitializeOptions()
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
|
||||
dropdown.ClearOptions();
|
||||
List<string> labels = new List<string>(categoryEntries.Count);
|
||||
|
||||
foreach (CategoryEntry entry in categoryEntries)
|
||||
{
|
||||
labels.Add(entry.label);
|
||||
}
|
||||
|
||||
dropdown.AddOptions(labels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前选中索引构建 <see cref="ItemFilter"/>。
|
||||
/// types 为空的选项返回 <see cref="ItemFilter.None"/>(即显示所有)。
|
||||
/// </summary>
|
||||
public ItemFilter BuildFilter()
|
||||
{
|
||||
return CurrentFilter;
|
||||
}
|
||||
|
||||
/// <summary>重置为第一个选项(通常为"全部"),不触发事件。</summary>
|
||||
public void ResetToAll()
|
||||
{
|
||||
CurrentFilter = ItemFilter.None;
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.SetValueWithoutNotify(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnValueChanged(int index)
|
||||
{
|
||||
if (index < 0 || index >= categoryEntries.Count) return;
|
||||
|
||||
CategoryEntry entry = categoryEntries[index];
|
||||
CurrentFilter = (entry.types == null || entry.types.Length == 0)
|
||||
? ItemFilter.None
|
||||
: ItemFilter.ByType(entry.types);
|
||||
|
||||
OnFilterChanged?.Invoke(CurrentFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4d9f5140e87ab84f9f53b7fd7839ea3
|
||||
@@ -1,118 +0,0 @@
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用物品详情面板,显示物品图标、名称、稀有度、描述、标签等信息。
|
||||
/// 可被不同的 UIPage 复用(机械台、商店、背包检视等)。
|
||||
/// </summary>
|
||||
public class ItemDetailPanel : UIElementBase
|
||||
{
|
||||
[Title("Detail Panel References")]
|
||||
public Image itemIcon;
|
||||
public Image rarityFrame;
|
||||
public TMP_Text itemNameText;
|
||||
public TMP_Text itemTypeText;
|
||||
public TMP_Text itemDescriptionText;
|
||||
public RectTransform tagContainer;
|
||||
public GameObject tagPrefab;
|
||||
|
||||
private ItemBase currentItem;
|
||||
|
||||
/// <summary>当前显示的物品。</summary>
|
||||
public ItemBase CurrentItem => currentItem;
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定物品的数据填充详情面板并显示。
|
||||
/// </summary>
|
||||
public void SetItem(ItemBase item)
|
||||
{
|
||||
currentItem = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
ClearPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标:主武器使用 rectIcon,其他使用 squareIcon
|
||||
Sprite icon = data.itemType == ItemType.MainWeapon ? data.rectIcon : data.squareIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}
|
||||
|
||||
// 名称(本地化)
|
||||
if (itemNameText != null)
|
||||
{
|
||||
itemNameText.text = data.displayNameKey.Localize();
|
||||
}
|
||||
|
||||
// 类型
|
||||
if (itemTypeText != null)
|
||||
{
|
||||
itemTypeText.text = data.itemType.ToString();
|
||||
}
|
||||
|
||||
// 描述(本地化)
|
||||
if (itemDescriptionText != null)
|
||||
{
|
||||
itemDescriptionText.text = data.descriptionKey.Localize();
|
||||
}
|
||||
|
||||
// 标签
|
||||
RefreshTags(data);
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>清空面板内容并隐藏。</summary>
|
||||
public void ClearPanel()
|
||||
{
|
||||
currentItem = null;
|
||||
|
||||
if (itemIcon != null) itemIcon.enabled = false;
|
||||
if (itemNameText != null) itemNameText.text = string.Empty;
|
||||
if (itemTypeText != null) itemTypeText.text = string.Empty;
|
||||
if (itemDescriptionText != null) itemDescriptionText.text = string.Empty;
|
||||
|
||||
ClearTags();
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void RefreshTags(ContentData data)
|
||||
{
|
||||
ClearTags();
|
||||
|
||||
if (tagContainer == null || tagPrefab == null || data.tags == null) return;
|
||||
|
||||
foreach (string tag in data.tags)
|
||||
{
|
||||
GameObject tagObj = Instantiate(tagPrefab, tagContainer);
|
||||
TMP_Text tagText = tagObj.GetComponentInChildren<TMP_Text>();
|
||||
if (tagText != null)
|
||||
{
|
||||
tagText.text = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTags()
|
||||
{
|
||||
if (tagContainer == null) return;
|
||||
|
||||
for (int i = tagContainer.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(tagContainer.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Items;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 单选排序模式下拉框,基于 <see cref="TMP_Dropdown"/>。
|
||||
/// 提供多种 <see cref="ItemSortMode"/>,切换后通知订阅者。
|
||||
/// <para>可复用于背包、商店等任何需要物品排序的 UI 页面。</para>
|
||||
/// </summary>
|
||||
public class ItemSortDropdown : UIElementBase
|
||||
{
|
||||
[Header("Dropdown")]
|
||||
public TMP_Dropdown dropdown;
|
||||
|
||||
[Header("Labels")]
|
||||
[Tooltip("排序模式的显示标签,按顺序对应 Default / ByRarity / ByName。\n" +
|
||||
"可填入本地化 Key(如 'UI_Sort_Default'),会自动调用 Localize()。\n" +
|
||||
"若无对应翻译则直接显示原文。")]
|
||||
public List<string> sortModeLabels = new List<string>
|
||||
{
|
||||
"默认排序",
|
||||
"按稀有度",
|
||||
"按名称"
|
||||
};
|
||||
|
||||
/// <summary>当排序模式变化时触发。</summary>
|
||||
public event Action<ItemSortMode> OnSortChanged;
|
||||
|
||||
/// <summary>当前选中的排序模式。</summary>
|
||||
public ItemSortMode CurrentMode { get; private set; } = ItemSortMode.Default;
|
||||
|
||||
// ================================================================
|
||||
// 生命周期
|
||||
// ================================================================
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeOptions();
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <see cref="sortModeLabels"/> 重新初始化下拉选项。
|
||||
/// 语言切换后可手动调用以刷新标签文本。
|
||||
/// </summary>
|
||||
public void InitializeOptions()
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
|
||||
dropdown.ClearOptions();
|
||||
dropdown.AddOptions(sortModeLabels/*.ConvertAll(label => label.Localize("Items"))*/);
|
||||
}
|
||||
|
||||
/// <summary>重置为默认排序模式(不触发事件)。</summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
CurrentMode = ItemSortMode.Default;
|
||||
|
||||
if (dropdown != null)
|
||||
{
|
||||
dropdown.SetValueWithoutNotify(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnValueChanged(int index)
|
||||
{
|
||||
int modeCount = Enum.GetValues(typeof(ItemSortMode)).Length;
|
||||
|
||||
if (index >= 0 && index < modeCount)
|
||||
{
|
||||
CurrentMode = (ItemSortMode)index;
|
||||
OnSortChanged?.Invoke(CurrentMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9aaaaa3c6c02ad246b7a25a59005f425
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96289eeb61566e54081a0137954e19a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,136 @@
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 背包网格中的单个物品槽位。
|
||||
/// 显示物品图标、名称,消耗品额外显示堆叠数量。
|
||||
/// 点击后通知 InventoryUIPage 选中该物品并更新详情面板。
|
||||
/// </summary>
|
||||
public class InventoryItemSelector : UIElementBase
|
||||
{
|
||||
private InventoryUIPage Page => PlayerCanvas.MainGamePages.inventoryPage;
|
||||
|
||||
// ─────────────────── 数据 ───────────────────
|
||||
|
||||
/// <summary>此槽位绑定的物品实例。</summary>
|
||||
public ItemBase Item { get; private set; }
|
||||
|
||||
// ─────────────────── UI 引用 ───────────────────
|
||||
|
||||
[Header("UI References")]
|
||||
public Button button;
|
||||
public Image background;
|
||||
public Image itemIcon;
|
||||
public Image rarityFrame;
|
||||
//public Image selectorHint;
|
||||
//public TMP_Text itemName;
|
||||
|
||||
[Tooltip("消耗品堆叠数量显示,非消耗品时隐藏。")]
|
||||
public TMP_Text stackText;
|
||||
|
||||
// ─────────────────── 视觉设置 ───────────────────
|
||||
|
||||
[Header("Visual Settings")]
|
||||
public Color normalColor = new Color(0.2f, 0.2f, 0.2f, 0.8f);
|
||||
public Color selectedColor = new Color(0.4f, 0.6f, 0.9f, 0.9f);
|
||||
|
||||
private bool isSelected;
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 用给定的物品数据配置此槽位的显示内容。
|
||||
/// </summary>
|
||||
public void Setup(ItemBase item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item == null || item.contentData == null)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentData data = item.contentData;
|
||||
|
||||
Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
itemIcon.enabled = icon != null;
|
||||
}
|
||||
|
||||
// 名称(本地化)
|
||||
/*if (itemName != null)
|
||||
{
|
||||
itemName.text = data.displayNameKey.Localize();
|
||||
}*/
|
||||
|
||||
// 堆叠数量(仅消耗品)
|
||||
RefreshStackDisplay();
|
||||
|
||||
// 默认取消选中
|
||||
SetSelected(false);
|
||||
|
||||
// 绑定按钮
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(OnClicked);
|
||||
}
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置选中/取消选中的视觉状态。
|
||||
/// </summary>
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
isSelected = selected;
|
||||
|
||||
if (background != null)
|
||||
{
|
||||
background.color = isSelected ? selectedColor : normalColor;
|
||||
}
|
||||
|
||||
/*if (selectorHint != null)
|
||||
{
|
||||
selectorHint.enabled = isSelected;
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新消耗品堆叠数量显示。非消耗品时隐藏堆叠文本。
|
||||
/// </summary>
|
||||
public void RefreshStackDisplay()
|
||||
{
|
||||
if (stackText == null) return;
|
||||
|
||||
if (Item is ConsumableBase consumable)
|
||||
{
|
||||
stackText.gameObject.SetActive(true);
|
||||
stackText.text = consumable.stackAmount.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
stackText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 内部 ───────────────────
|
||||
|
||||
private void OnClicked()
|
||||
{
|
||||
Page?.SelectItem(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2abecad67abab4e4692a134234d722c8
|
||||
@@ -0,0 +1,284 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Inventory;
|
||||
using Cielonos.MainGame.Items;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 背包/装备界面 UI 页面。
|
||||
/// 左侧为背包物品网格(GridLayoutGroup),支持按类型多选筛选和多种排序模式;
|
||||
/// 右侧为物品详情面板(ItemDetailPanel),选中物品后显示详细信息。
|
||||
/// </summary>
|
||||
public class InventoryUIPage : UIPageBase
|
||||
{
|
||||
// ─────────────────── 选择器 ───────────────────
|
||||
|
||||
[Title("Selectors")]
|
||||
[Tooltip("背包网格中每个物品槽位的预制体,需挂载 InventoryItemSelector 组件。")]
|
||||
public GameObject selectorPrefab;
|
||||
|
||||
[Tooltip("物品槽位的容器,应挂载 GridLayoutGroup 以实现网格布局。")]
|
||||
public RectTransform selectorContainer;
|
||||
|
||||
[ReadOnly]
|
||||
public List<InventoryItemSelector> selectors = new List<InventoryItemSelector>();
|
||||
|
||||
// ─────────────────── 详情面板 ───────────────────
|
||||
|
||||
[Title("Detail Panel")]
|
||||
public ItemDetailPanel itemDetailPanel;
|
||||
|
||||
// ─────────────────── 筛选与排序 ───────────────────
|
||||
|
||||
[Title("Filter & Sort")]
|
||||
[Tooltip("多选类别筛选下拉框,按 ItemType 过滤可见物品。")]
|
||||
public ItemCategoryDropdown categoryDropdown;
|
||||
|
||||
[Tooltip("单选排序模式下拉框,决定物品排列顺序。")]
|
||||
public ItemSortDropdown sortDropdown;
|
||||
|
||||
// ─────────────────── 按钮 ───────────────────
|
||||
|
||||
[Title("Buttons")]
|
||||
public Button closeButton;
|
||||
|
||||
// ─────────────────── 运行时数据 ───────────────────
|
||||
|
||||
private InventoryItemSelector currentSelected;
|
||||
|
||||
/// <summary>当前选中的物品,null 表示未选中。</summary>
|
||||
public ItemBase SelectedItem => currentSelected != null ? currentSelected.Item : null;
|
||||
|
||||
// ================================================================
|
||||
// 生命周期
|
||||
// ================================================================
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
|
||||
if (closeButton != null)
|
||||
{
|
||||
closeButton.onClick.AddListener(OnCloseClicked);
|
||||
}
|
||||
|
||||
if (categoryDropdown != null)
|
||||
{
|
||||
categoryDropdown.OnFilterChanged += OnFilterChanged;
|
||||
}
|
||||
|
||||
if (sortDropdown != null)
|
||||
{
|
||||
sortDropdown.OnSortChanged += OnSortChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPageOpened()
|
||||
{
|
||||
RefreshInventory();
|
||||
}
|
||||
|
||||
protected override void OnPageClosed()
|
||||
{
|
||||
currentSelected = null;
|
||||
ClearSelectors();
|
||||
|
||||
categoryDropdown?.ResetToAll();
|
||||
sortDropdown?.ResetToDefault();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 公开接口
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 从玩家背包读取所有物品,重新生成网格选择器并应用排序与筛选。
|
||||
/// </summary>
|
||||
public void RefreshInventory()
|
||||
{
|
||||
ClearSelectors();
|
||||
|
||||
PlayerInventorySubcontroller.BackpackSubmodule backpack =
|
||||
MainGameManager.Player.inventorySc.backpackSm;
|
||||
|
||||
PopulateFromList(backpack.mainWeapons);
|
||||
PopulateFromList(backpack.supportEquipments);
|
||||
PopulateFromList(backpack.passiveEquipments);
|
||||
PopulateFromList(backpack.consumables);
|
||||
|
||||
currentSelected = null;
|
||||
ApplySortAndFilter();
|
||||
|
||||
if (itemDetailPanel != null)
|
||||
{
|
||||
itemDetailPanel.ClearPanel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由 InventoryItemSelector 调用,选中指定的物品槽位。
|
||||
/// </summary>
|
||||
public void SelectItem(InventoryItemSelector selector)
|
||||
{
|
||||
if (selector == null || !selectors.Contains(selector)) return;
|
||||
|
||||
if (currentSelected != null)
|
||||
{
|
||||
currentSelected.SetSelected(false);
|
||||
}
|
||||
|
||||
currentSelected = selector;
|
||||
currentSelected.SetSelected(true);
|
||||
|
||||
if (itemDetailPanel != null)
|
||||
{
|
||||
itemDetailPanel.SetItem(currentSelected.Item);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 筛选与排序
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 对选择器列表执行排序,然后按当前筛选条件显示/隐藏。
|
||||
/// 当任一 Dropdown 变化或背包刷新时调用。
|
||||
/// </summary>
|
||||
private void ApplySortAndFilter()
|
||||
{
|
||||
// ── 排序 ──
|
||||
ItemSortMode sortMode = sortDropdown != null ? sortDropdown.CurrentMode : ItemSortMode.Default;
|
||||
List<ItemBase> items = selectors.ConvertAll(s => s.Item);
|
||||
ItemSorter.Sort(items, sortMode);
|
||||
|
||||
// 按排序结果重排选择器列表
|
||||
Dictionary<ItemBase, InventoryItemSelector> itemToSelector =
|
||||
new Dictionary<ItemBase, InventoryItemSelector>(selectors.Count);
|
||||
|
||||
foreach (InventoryItemSelector sel in selectors)
|
||||
{
|
||||
itemToSelector.TryAdd(sel.Item, sel);
|
||||
}
|
||||
|
||||
List<InventoryItemSelector> sorted = new List<InventoryItemSelector>(selectors.Count);
|
||||
foreach (ItemBase item in items)
|
||||
{
|
||||
if (itemToSelector.TryGetValue(item, out InventoryItemSelector sel))
|
||||
{
|
||||
sorted.Add(sel);
|
||||
}
|
||||
}
|
||||
|
||||
selectors.Clear();
|
||||
selectors.AddRange(sorted);
|
||||
|
||||
// 更新 UI 层级顺序
|
||||
for (int i = 0; i < selectors.Count; i++)
|
||||
{
|
||||
selectors[i].transform.SetSiblingIndex(i);
|
||||
}
|
||||
|
||||
// ── 筛选 ──
|
||||
ItemFilter filter = categoryDropdown != null ? categoryDropdown.BuildFilter() : ItemFilter.None;
|
||||
bool selectedHidden = false;
|
||||
|
||||
foreach (InventoryItemSelector selector in selectors)
|
||||
{
|
||||
if (selector == null || selector.Item == null) continue;
|
||||
|
||||
bool visible = filter.Match(selector.Item);
|
||||
selector.gameObject.SetActive(visible);
|
||||
|
||||
if (!visible && selector == currentSelected)
|
||||
{
|
||||
selectedHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedHidden)
|
||||
{
|
||||
currentSelected.SetSelected(false);
|
||||
currentSelected = null;
|
||||
itemDetailPanel?.ClearPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── Dropdown 回调 ───────────────────
|
||||
|
||||
private void OnFilterChanged(ItemFilter filter)
|
||||
{
|
||||
ApplySortAndFilter();
|
||||
}
|
||||
|
||||
private void OnSortChanged(ItemSortMode mode)
|
||||
{
|
||||
ApplySortAndFilter();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 选择器管理
|
||||
// ================================================================
|
||||
|
||||
private void PopulateFromList<T>(List<T> items) where T : ItemBase
|
||||
{
|
||||
if (items == null) return;
|
||||
|
||||
foreach (T item in items)
|
||||
{
|
||||
if (item == null || item.contentData == null) continue;
|
||||
|
||||
InventoryItemSelector selector = CreateSelector();
|
||||
if (selector != null)
|
||||
{
|
||||
selector.Setup(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryItemSelector CreateSelector()
|
||||
{
|
||||
if (selectorPrefab == null || selectorContainer == null)
|
||||
{
|
||||
Debug.LogError("[InventoryUIPage] selectorPrefab 或 selectorContainer 未配置。");
|
||||
return null;
|
||||
}
|
||||
|
||||
GameObject obj = Instantiate(selectorPrefab, selectorContainer);
|
||||
InventoryItemSelector selector = obj.GetComponent<InventoryItemSelector>();
|
||||
if (selector == null)
|
||||
{
|
||||
Debug.LogError("[InventoryUIPage] selectorPrefab 缺少 InventoryItemSelector 组件。");
|
||||
Destroy(obj);
|
||||
return null;
|
||||
}
|
||||
|
||||
selectors.Add(selector);
|
||||
return selector;
|
||||
}
|
||||
|
||||
private void ClearSelectors()
|
||||
{
|
||||
foreach (InventoryItemSelector selector in selectors)
|
||||
{
|
||||
if (selector != null)
|
||||
{
|
||||
Destroy(selector.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
selectors.Clear();
|
||||
}
|
||||
|
||||
// ─────────────────── 按钮回调 ───────────────────
|
||||
|
||||
private void OnCloseClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8ea321de103c2e4ab8833abc7897452
|
||||
@@ -57,7 +57,7 @@ namespace Cielonos.MainGame.UI
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标
|
||||
Sprite icon = data.itemType == ItemType.MainWeapon ? data.rectIcon : data.squareIcon;
|
||||
Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
@@ -67,7 +67,7 @@ namespace Cielonos.MainGame.UI
|
||||
// 名称(本地化)
|
||||
if (itemName != null)
|
||||
{
|
||||
itemName.text = data.displayNameKey.Localize();
|
||||
itemName.text = data.displayNameKey.Localize("Items");
|
||||
}
|
||||
|
||||
// 价格
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Cielonos.MainGame.UI
|
||||
{
|
||||
public MechanicalTableUIPage mechanicalTablePage;
|
||||
public LogisticsCenterUIPage logisticsCenterPage;
|
||||
public InventoryUIPage inventoryPage;
|
||||
public MapUIPage mapPage;
|
||||
public SettlementUIPage settlementPage;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Cielonos.MainGame.UI
|
||||
ContentData data = item.contentData;
|
||||
|
||||
// 图标
|
||||
Sprite icon = data.itemType == ItemType.MainWeapon ? data.rectIcon : data.squareIcon;
|
||||
Sprite icon = data.itemIcon;
|
||||
if (itemIcon != null)
|
||||
{
|
||||
itemIcon.sprite = icon;
|
||||
@@ -56,7 +56,7 @@ namespace Cielonos.MainGame.UI
|
||||
// 名称(本地化)
|
||||
if (itemName != null)
|
||||
{
|
||||
itemName.text = data.displayNameKey.Localize();
|
||||
itemName.text = data.displayNameKey.Localize("Items");
|
||||
}
|
||||
|
||||
// 堆叠数量
|
||||
|
||||
@@ -41,14 +41,20 @@ namespace Cielonos.MainGame.UI
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
RunManager.Instance.OnPhaseChanged += OnPhaseChanged;
|
||||
restartButton.onClick.AddListener(OnRestartClicked);
|
||||
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
||||
if (MainGameManager.Instance.sceneSm.IsCityArena)
|
||||
{
|
||||
RunManager.Instance.OnPhaseChanged += OnPhaseChanged;
|
||||
restartButton.onClick.AddListener(OnRestartClicked);
|
||||
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RunManager.Instance.OnPhaseChanged -= OnPhaseChanged;
|
||||
if (MainGameManager.Instance.sceneSm.IsCityArena)
|
||||
{
|
||||
RunManager.Instance.OnPhaseChanged -= OnPhaseChanged;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
8
Assets/Scripts/MainGame/UI/PlayerUI/PausePage.meta
Normal file
8
Assets/Scripts/MainGame/UI/PlayerUI/PausePage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8afdf8c1f5c61d34283e8f2025435c24
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
121
Assets/Scripts/MainGame/UI/PlayerUI/PausePage/PauseUIPage.cs
Normal file
121
Assets/Scripts/MainGame/UI/PlayerUI/PausePage/PauseUIPage.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.Core.SceneManagement;
|
||||
using DG.Tweening;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Cielonos.MainGame.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 暂停界面页面。
|
||||
/// <para>
|
||||
/// 打开时暂停游戏(<c>Time.timeScale = 0</c>),
|
||||
/// 关闭时恢复游戏。
|
||||
/// 提供三个按钮:继续游戏、打开设置、返回标题界面。
|
||||
/// 返回标题界面时会弹出 <see cref="ConfirmUIPage"/> 二次确认。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class PauseUIPage : UIPageBase
|
||||
{
|
||||
// ──────────────────── 按钮引用 ────────────────────
|
||||
|
||||
[FormerlySerializedAs("continueButton")]
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private Button resumeButton;
|
||||
[SerializeField] private Button settingsButton;
|
||||
[SerializeField] private Button returnToMenuButton;
|
||||
|
||||
// ──────────────────── 配置 ────────────────────────
|
||||
|
||||
[Header("Scene")]
|
||||
[Tooltip("标题界面的场景名称,通过 SceneBus 加载。")]
|
||||
[SerializeField] private string titleSceneName = "Menu";
|
||||
|
||||
// ──────────────────── 生命周期 ────────────────────
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
resumeButton.onClick.AddListener(OnContinueClicked);
|
||||
settingsButton.onClick.AddListener(OnSettingsClicked);
|
||||
returnToMenuButton.onClick.AddListener(OnReturnToTitleClicked);
|
||||
}
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
if (IsOpen) return;
|
||||
IsOpen = true;
|
||||
Time.timeScale = 0f;
|
||||
UIPageManager.Instance.RegisterPage(this);
|
||||
EventSystem.current?.SetSelectedGameObject(null);
|
||||
gameObject.SetActive(true);
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.DOFade(1f, 0.3f).From(0f).SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
OnPageOpened();
|
||||
}).Play();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
IsOpen = false;
|
||||
UIPageManager.Instance.UnregisterPage(this);
|
||||
canvasGroup.DOFade(0f, 0.3f).From(1f).SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
OnPageClosed();
|
||||
Time.timeScale = 1f;
|
||||
}).Play();
|
||||
}
|
||||
|
||||
// ──────────────────── 按钮回调 ────────────────────
|
||||
|
||||
private void OnContinueClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnSettingsClicked()
|
||||
{
|
||||
PlayerCanvas.SettingsUIPage.Open();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 点击"返回主菜单"按钮时,弹出确认弹窗而非直接切换场景。
|
||||
/// </summary>
|
||||
private void OnReturnToTitleClicked()
|
||||
{
|
||||
ConfirmUIPage.Show(new ConfirmPageConfig
|
||||
{
|
||||
Title = "ConfirmReturnToMenu_Title".Localize("UI"),
|
||||
Description = "ConfirmReturnToMenu_Desc".Localize("UI"),
|
||||
Buttons = new List<ConfirmButtonConfig>
|
||||
{
|
||||
new("Confirm".Localize("UI"), ReturnToMenu),
|
||||
new("Cancel".Localize("UI"))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户确认后执行的返回主菜单逻辑。
|
||||
/// </summary>
|
||||
private void ReturnToMenu()
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
AudioManager.Instance.backgroundMusicManager.TransitionToNextScene();
|
||||
UIPageManager.Instance.CloseAllPages();
|
||||
SceneBus.LoadScene(titleSceneName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b190c38fdc79d24b8c7cd550c838030
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Cielonos.UI;
|
||||
using SLSUtilities.General;
|
||||
using SLSUtilities.UI;
|
||||
using TMPro;
|
||||
@@ -24,6 +25,12 @@ namespace Cielonos.MainGame.UI
|
||||
private CombatSystemsUIArea combatSystemsUIArea;
|
||||
[SerializeField]
|
||||
private MainGamePages mainGamePages;
|
||||
[SerializeField]
|
||||
private PauseUIPage pauseUIPage;
|
||||
[SerializeField]
|
||||
private SettingsUIPage settingsUIPage;
|
||||
[SerializeField]
|
||||
private InteractionUIArea interactionUIArea;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
@@ -41,5 +48,8 @@ namespace Cielonos.MainGame.UI
|
||||
public static EnemyInfoUIArea EnemyInfoUIArea => Instance.enemyInfoUIArea;
|
||||
public static CombatSystemsUIArea CombatSystemsUIArea => Instance.combatSystemsUIArea;
|
||||
public static MainGamePages MainGamePages => Instance.mainGamePages;
|
||||
public static PauseUIPage PauseUIPage => Instance.pauseUIPage;
|
||||
public static SettingsUIPage SettingsUIPage => Instance.settingsUIPage;
|
||||
public static InteractionUIArea InteractionUIArea => Instance?.interactionUIArea;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace Cielonos.MainGame.UI
|
||||
else
|
||||
{
|
||||
this.supportEquipment = supportEquipment;
|
||||
iconImage.sprite = supportEquipment.contentData.squareIcon;
|
||||
iconImage.sprite = supportEquipment.contentData.itemIcon;
|
||||
|
||||
if (supportEquipment.functionSm != null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user