地图系统
This commit is contained in:
121
Assets/_Game/Scripts/UI/Menus/ConfirmDialogController.cs
Normal file
121
Assets/_Game/Scripts/UI/Menus/ConfirmDialogController.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用模态确认对话框(是/否):用于删除 / 覆盖 / 退出等确认场景。
|
||||
///
|
||||
/// 设计:
|
||||
/// · 自包含、场景无关——通过本地 SetActive 显隐 + 回调 API 工作,不依赖 UIManager 面板栈,
|
||||
/// 因此既能用于主菜单场景(不走 UIManager),也能在游戏内复用。
|
||||
/// · 标题 / 正文 / 按钮文案均走本地化键(LocalizationManager.Get);传 null 则保留 Inspector 原文。
|
||||
/// · 默认焦点置于"取消"按钮,防止手柄连按误触确认(破坏性操作安全默认)。
|
||||
///
|
||||
/// 用法:
|
||||
/// _confirmDialog.Show("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY",
|
||||
/// onConfirm: () => DoDelete(),
|
||||
/// onCancel: () => {});
|
||||
/// </summary>
|
||||
public class ConfirmDialogController : MonoBehaviour
|
||||
{
|
||||
[Header("根节点(显隐用,留空则用本 GameObject)")]
|
||||
[SerializeField] private GameObject _root;
|
||||
|
||||
[Header("文本")]
|
||||
[SerializeField] private TMP_Text _titleText;
|
||||
[SerializeField] private TMP_Text _bodyText;
|
||||
[Tooltip("确认按钮标签(可选,传 confirmKey 时覆盖)")]
|
||||
[SerializeField] private TMP_Text _confirmLabel;
|
||||
[Tooltip("取消按钮标签(可选,传 cancelKey 时覆盖)")]
|
||||
[SerializeField] private TMP_Text _cancelLabel;
|
||||
|
||||
[Header("按钮")]
|
||||
[SerializeField] private Button _btnConfirm;
|
||||
[SerializeField] private Button _btnCancel;
|
||||
|
||||
private Action _onConfirm;
|
||||
private Action _onCancel;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_btnConfirm?.onClick.AddListener(HandleConfirm);
|
||||
_btnCancel? .onClick.AddListener(HandleCancel);
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出确认框。
|
||||
/// </summary>
|
||||
/// <param name="titleKey">标题本地化键;null 保留原文。</param>
|
||||
/// <param name="bodyKey">正文本地化键;null 保留原文。</param>
|
||||
/// <param name="onConfirm">点击确认后回调(确认框已自动关闭)。</param>
|
||||
/// <param name="onCancel">点击取消后回调(可选)。</param>
|
||||
/// <param name="confirmKey">确认按钮文案本地化键(可选)。</param>
|
||||
/// <param name="cancelKey">取消按钮文案本地化键(可选)。</param>
|
||||
public void Show(string titleKey, string bodyKey, Action onConfirm, Action onCancel = null,
|
||||
string confirmKey = null, string cancelKey = null)
|
||||
{
|
||||
_onConfirm = onConfirm;
|
||||
_onCancel = onCancel;
|
||||
|
||||
if (_titleText != null && titleKey != null) _titleText.text = Loc(titleKey);
|
||||
if (_bodyText != null && bodyKey != null) _bodyText.text = Loc(bodyKey);
|
||||
if (_confirmLabel != null && confirmKey != null) _confirmLabel.text = Loc(confirmKey);
|
||||
if (_cancelLabel != null && cancelKey != null) _cancelLabel.text = Loc(cancelKey);
|
||||
|
||||
SetVisible(true);
|
||||
|
||||
// 安全默认:焦点置于取消,避免手柄/键盘连按直接确认破坏性操作
|
||||
EventSystem.current?.SetSelectedGameObject(_btnCancel != null
|
||||
? _btnCancel.gameObject
|
||||
: _btnConfirm?.gameObject);
|
||||
}
|
||||
|
||||
/// <summary>外部强制关闭(如父面板被关闭时)。不触发任何回调。</summary>
|
||||
public void Close()
|
||||
{
|
||||
_onConfirm = null;
|
||||
_onCancel = null;
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
// ── 按钮回调 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void HandleConfirm()
|
||||
{
|
||||
var cb = _onConfirm;
|
||||
SetVisible(false);
|
||||
_onConfirm = null;
|
||||
_onCancel = null;
|
||||
cb?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleCancel()
|
||||
{
|
||||
var cb = _onCancel;
|
||||
SetVisible(false);
|
||||
_onConfirm = null;
|
||||
_onCancel = null;
|
||||
cb?.Invoke();
|
||||
}
|
||||
|
||||
// ── 工具 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void SetVisible(bool visible)
|
||||
{
|
||||
var go = _root != null ? _root : gameObject;
|
||||
go.SetActive(visible);
|
||||
}
|
||||
|
||||
private static string Loc(string key)
|
||||
{
|
||||
string s = LocalizationManager.Get(key, LocalizationTable.UI);
|
||||
return string.IsNullOrEmpty(s) ? key : s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d33b9d77d704f12438e54e4a74ff00c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// 地图传送确认控制器(UI 侧桥接)。
|
||||
/// <para>
|
||||
/// 订阅 <see cref="MapPanel.OnTeleportStationSelected"/>:玩家在全屏地图点击一个可传送站点时,
|
||||
/// 弹出 <see cref="ConfirmDialogController"/> 二次确认,确认后调用
|
||||
/// <see cref="ITeleportService.RequestTeleport"/> 发起传送,并按需关闭地图面板。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 放在 UI 程序集(已引用 BaseGames.World.Map)——MapPanel 自身不反向依赖 UI,避免循环引用。
|
||||
/// 这是"补全传送"闭环的最后一环:地图选点 → 确认 → 传送。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class MapTeleportConfirmController : MonoBehaviour
|
||||
{
|
||||
[Header("引用")]
|
||||
[Tooltip("全屏地图面板(订阅其 OnTeleportStationSelected)。")]
|
||||
[SerializeField] private MapPanel _mapPanel;
|
||||
|
||||
[Tooltip("通用确认框;留空则点击站点后直接传送(无二次确认)。")]
|
||||
[SerializeField] private ConfirmDialogController _confirmDialog;
|
||||
|
||||
[Header("文案(本地化键)")]
|
||||
[SerializeField] private string _confirmTitleKey = "TELEPORT_CONFIRM_TITLE";
|
||||
[Tooltip("确认正文前缀本地化键;后面拼接目的地房间显示名。")]
|
||||
[SerializeField] private string _confirmBodyPrefixKey = "TELEPORT_CONFIRM_BODY";
|
||||
[SerializeField] private string _confirmYesKey = "CONFIRM_YES";
|
||||
[SerializeField] private string _confirmNoKey = "CONFIRM_NO";
|
||||
|
||||
[Header("行为")]
|
||||
[Tooltip("确认传送后是否关闭地图面板(CloseTopPanel)。")]
|
||||
[SerializeField] private bool _closeMapOnConfirm = true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_mapPanel != null)
|
||||
_mapPanel.OnTeleportStationSelected += OnStationSelected;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_mapPanel != null)
|
||||
_mapPanel.OnTeleportStationSelected -= OnStationSelected;
|
||||
}
|
||||
|
||||
private void OnStationSelected(string roomId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(roomId)) return;
|
||||
|
||||
// 无确认框:直接传送
|
||||
if (_confirmDialog == null)
|
||||
{
|
||||
DoTeleport(roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
string destName = ResolveDestName(roomId);
|
||||
string prefix = LocalizationManager.Get(_confirmBodyPrefixKey, LocalizationTable.UI);
|
||||
if (string.IsNullOrEmpty(prefix) || prefix == _confirmBodyPrefixKey)
|
||||
prefix = "传送到:"; // 本地化键缺失时的兜底前缀
|
||||
string body = prefix + destName;
|
||||
|
||||
// ConfirmDialog 对 body 走 Loc 查找,未命中则原样显示——拼接串可直接呈现;
|
||||
// 确认/取消按钮文案也走本地化键,随语言切换。
|
||||
_confirmDialog.Show(_confirmTitleKey, body, onConfirm: () => DoTeleport(roomId),
|
||||
onCancel: null, confirmKey: _confirmYesKey, cancelKey: _confirmNoKey);
|
||||
}
|
||||
|
||||
private void DoTeleport(string roomId)
|
||||
{
|
||||
var teleportSvc = ServiceLocator.GetOrDefault<ITeleportService>();
|
||||
if (teleportSvc == null)
|
||||
{
|
||||
Debug.LogWarning("[MapTeleportConfirmController] ITeleportService 未注册,无法传送。");
|
||||
return;
|
||||
}
|
||||
teleportSvc.RequestTeleport(roomId);
|
||||
|
||||
if (_closeMapOnConfirm)
|
||||
ServiceLocator.GetOrDefault<IUIManager>()?.CloseTopPanel();
|
||||
}
|
||||
|
||||
/// <summary>解析目的地房间的玩家可读名(DisplayName 走本地化;无则回退 RoomId)。</summary>
|
||||
private static string ResolveDestName(string roomId)
|
||||
{
|
||||
var room = ServiceLocator.GetOrDefault<IMapService>()?.Database?.GetRoom(roomId);
|
||||
if (room == null || string.IsNullOrEmpty(room.DisplayName)) return roomId;
|
||||
// DisplayName 为本地化 Key 时解析为译文;为普通名称时原样返回(向后兼容)。
|
||||
return LocalizationManager.Get(room.DisplayName, LocalizationTable.UI);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f5d16c5b692621459b02ab74a1aaf91
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -9,19 +9,49 @@ using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.World.Map;
|
||||
using BaseGames.UI.MainMenu;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
{
|
||||
/// <summary>主菜单存档槽面板的打开语境。</summary>
|
||||
public enum SaveSlotPanelMode
|
||||
{
|
||||
/// <summary>继续游戏:仅有档槽可选,点击即读档进入。</summary>
|
||||
Continue,
|
||||
/// <summary>新游戏:空槽 → 模式选择 → 建档;有档槽 → 覆盖确认 → 模式选择 → 建档。</summary>
|
||||
NewGame,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。
|
||||
///
|
||||
/// 前端选档流程:
|
||||
/// · 新游戏开档前先选模式(普通 / 钢铁之魂),占用槽位需覆盖确认。
|
||||
/// · 删除强制走通用确认对话框(无静默删除旁路)。
|
||||
/// · 卡片展示游玩时长 / 区域 / 货币 / 生命 / 钢魂徽章。
|
||||
///
|
||||
/// 模式由 MainMenuController 在打开面板前通过 <see cref="SetMode"/> 指定。
|
||||
/// ConfirmDialog 与 NewGameMode 面板经 Inspector 注入(同处 MainMenu 场景)。
|
||||
/// </summary>
|
||||
public class SaveSlotController : MonoBehaviour
|
||||
public class SaveSlotController : MonoBehaviour, IFocusable
|
||||
{
|
||||
[SerializeField] private SaveSlotUI[] _slotUIs;
|
||||
[SerializeField] private SaveSlotUI[] _slotUIs;
|
||||
|
||||
[Header("子面板(Inspector 注入,同处 MainMenu 场景)")]
|
||||
[Tooltip("通用确认对话框,用于覆盖 / 删除确认。为空时:覆盖退化为直接建档,删除被忽略(绝不静默删除)。")]
|
||||
[SerializeField] private ConfirmDialogController _confirmDialog;
|
||||
[Tooltip("新游戏模式选择面板。为空时新游戏退化为普通模式。")]
|
||||
[SerializeField] private NewGameModeController _modeSelect;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private IntEventChannelSO _onSlotConfirmed;
|
||||
|
||||
[Header("焦点")]
|
||||
[Tooltip("面板恢复为栈顶时自动聚焦的默认按钮,通常为第一个存档槽的选择按钮。")]
|
||||
[SerializeField] private Button _defaultFocusButton;
|
||||
|
||||
private SaveSlotPanelMode _mode = SaveSlotPanelMode.NewGame;
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
private void Awake()
|
||||
@@ -30,6 +60,19 @@ namespace BaseGames.UI.Menus
|
||||
if (_slotUIs[i] != null) _slotUIs[i].Init(i, this);
|
||||
}
|
||||
|
||||
/// <summary>由 MainMenuController 在 SetActive(true) 之前调用,决定本次打开语境。</summary>
|
||||
public void SetMode(SaveSlotPanelMode mode) => _mode = mode;
|
||||
|
||||
// ── IFocusable ────────────────────────────────────────────────────────
|
||||
public void OnFocusRestored() => StartCoroutine(RestoreFocusNextFrame());
|
||||
|
||||
private System.Collections.IEnumerator RestoreFocusNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
if (UnityEngine.EventSystems.EventSystem.current != null && _defaultFocusButton != null)
|
||||
UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(_defaultFocusButton.gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
@@ -44,6 +87,10 @@ namespace BaseGames.UI.Menus
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 关闭子对话框,避免下次打开残留
|
||||
_confirmDialog?.Close();
|
||||
_modeSelect?.Close();
|
||||
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
@@ -64,35 +111,96 @@ namespace BaseGames.UI.Menus
|
||||
for (int i = 0; i < _slotUIs.Length; i++)
|
||||
{
|
||||
if (_slotUIs[i] == null) continue;
|
||||
_slotUIs[i].Refresh(summaries[i]);
|
||||
_slotUIs[i].Refresh(summaries[i], _mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>选中指定槽位(新局或继续)。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
// ── 选槽 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>选中指定槽位。行为取决于当前模式与槽位是否有档。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
public void OnSlotSelected(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return;
|
||||
_ = SelectSlotAsync(slotIndex);
|
||||
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc == null) return;
|
||||
|
||||
bool hasData = svc.HasSave(slotIndex);
|
||||
|
||||
if (_mode == SaveSlotPanelMode.Continue)
|
||||
{
|
||||
if (!hasData) return; // 继续模式:空槽不可选
|
||||
_ = ContinueSlotAsync(slotIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 新游戏模式 ──
|
||||
if (hasData)
|
||||
{
|
||||
// 占用槽位:先覆盖确认
|
||||
if (_confirmDialog != null)
|
||||
_confirmDialog.Show("CONFIRM_OVERWRITE_TITLE", "CONFIRM_OVERWRITE_BODY",
|
||||
onConfirm: () => BeginNewGameFlow(slotIndex));
|
||||
else
|
||||
BeginNewGameFlow(slotIndex); // 无对话框时退化为直接建档
|
||||
}
|
||||
else
|
||||
{
|
||||
BeginNewGameFlow(slotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectSlotAsync(int slotIndex)
|
||||
/// <summary>开新档流程:选模式 → 建档。</summary>
|
||||
private void BeginNewGameFlow(int slotIndex)
|
||||
{
|
||||
if (_modeSelect != null)
|
||||
_modeSelect.Show(level => _ = StartNewGameAsync(slotIndex, level));
|
||||
else
|
||||
_ = StartNewGameAsync(slotIndex, DifficultyLevel.Normal); // 无模式面板时退化为普通
|
||||
}
|
||||
|
||||
private async Task StartNewGameAsync(int slotIndex, DifficultyLevel level)
|
||||
{
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc == null) return;
|
||||
|
||||
// 覆盖既有档:先删盘,确保旧数据不残留(建档仅初始化内存,写盘发生在首次存档)
|
||||
if (svc.HasSave(slotIndex))
|
||||
await svc.LoadAsync(slotIndex);
|
||||
else
|
||||
svc.CreateSlot(slotIndex);
|
||||
await svc.DeleteSlotAsync(slotIndex);
|
||||
|
||||
bool steel = level == DifficultyLevel.SteelSoul;
|
||||
svc.CreateSlot(slotIndex, steel);
|
||||
ServiceLocator.GetOrDefault<IDifficultyService>()?.BeginNewGame(level);
|
||||
|
||||
_onSlotConfirmed?.Raise(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>删除指定槽位存档并刷新 UI。由 SaveSlotUI 内部按钮调用。</summary>
|
||||
private async Task ContinueSlotAsync(int slotIndex)
|
||||
{
|
||||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc == null) return;
|
||||
|
||||
bool ok = await svc.LoadAsync(slotIndex);
|
||||
if (!ok) return; // 损坏 / 不存在:不前进(后续可接错误提示)
|
||||
|
||||
_onSlotConfirmed?.Raise(slotIndex);
|
||||
}
|
||||
|
||||
// ── 删除(强制确认)────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>请求删除指定槽位。强制经通用确认对话框;无对话框时忽略,绝不静默删除。</summary>
|
||||
public void OnSlotDeleteRequested(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return;
|
||||
_ = DeleteAndRefreshAsync(slotIndex);
|
||||
|
||||
if (_confirmDialog == null)
|
||||
{
|
||||
Debug.LogWarning("[SaveSlotController] 未配置 ConfirmDialog,删除请求被忽略(防止静默删除)。");
|
||||
return;
|
||||
}
|
||||
|
||||
_confirmDialog.Show("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY",
|
||||
onConfirm: () => _ = DeleteAndRefreshAsync(slotIndex));
|
||||
}
|
||||
|
||||
private async Task DeleteAndRefreshAsync(int slotIndex)
|
||||
@@ -103,143 +211,4 @@ namespace BaseGames.UI.Menus
|
||||
await RefreshAsync(_cts?.Token ?? CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个存档槽卡片组件,负责显示存档摘要或空槽提示。
|
||||
/// </summary>
|
||||
public class SaveSlotUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text _playtimeText;
|
||||
[SerializeField] private TMP_Text _regionText;
|
||||
[SerializeField] private TMP_Text _lastSavedText;
|
||||
[SerializeField] private Image _formIcon;
|
||||
[SerializeField] private GameObject _emptyIndicator; // 空槽时显示的"新游戏"提示
|
||||
[SerializeField] private GameObject _dataIndicator; // 有数据时显示的内容根
|
||||
[SerializeField] private Button _selectButton;
|
||||
[SerializeField] private Button _deleteButton;
|
||||
|
||||
[Header("删除确认(可选)")]
|
||||
[Tooltip("删除确认对话框根节点,为 null 时删除按钮直接执行。")]
|
||||
[SerializeField] private GameObject _deleteConfirmRoot;
|
||||
[Tooltip("确认删除按钮。")]
|
||||
[SerializeField] private Button _btnConfirmDelete;
|
||||
[Tooltip("取消删除按钮。")]
|
||||
[SerializeField] private Button _btnCancelDelete;
|
||||
|
||||
private int _slotIndex;
|
||||
private SaveSlotController _controller;
|
||||
|
||||
/// <summary>由 SaveSlotController 在 Awake 或初始化时调用以完成按钮绑定。</summary>
|
||||
public void Init(int slotIndex, SaveSlotController controller)
|
||||
{
|
||||
_slotIndex = slotIndex;
|
||||
_controller = controller;
|
||||
|
||||
if (_selectButton != null)
|
||||
{
|
||||
_selectButton.onClick.RemoveAllListeners();
|
||||
_selectButton.onClick.AddListener(() => _controller.OnSlotSelected(_slotIndex));
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.onClick.RemoveAllListeners();
|
||||
// 配置了确认对话框时先弹出确认,未配置时直接删除
|
||||
_deleteButton.onClick.AddListener(ShowDeleteConfirm);
|
||||
}
|
||||
|
||||
if (_btnConfirmDelete != null)
|
||||
{
|
||||
_btnConfirmDelete.onClick.RemoveAllListeners();
|
||||
_btnConfirmDelete.onClick.AddListener(() =>
|
||||
{
|
||||
HideDeleteConfirm();
|
||||
_controller.OnSlotDeleteRequested(_slotIndex);
|
||||
});
|
||||
}
|
||||
|
||||
if (_btnCancelDelete != null)
|
||||
{
|
||||
_btnCancelDelete.onClick.RemoveAllListeners();
|
||||
_btnCancelDelete.onClick.AddListener(HideDeleteConfirm);
|
||||
}
|
||||
|
||||
HideDeleteConfirm();
|
||||
}
|
||||
|
||||
/// <summary>用摘要数据刷新显示;summary 为 null 表示空槽。</summary>
|
||||
public void Refresh(SlotSummary summary)
|
||||
{
|
||||
bool hasData = summary != null;
|
||||
|
||||
if (_emptyIndicator != null) _emptyIndicator.SetActive(!hasData);
|
||||
if (_dataIndicator != null) _dataIndicator.SetActive(hasData);
|
||||
if (_deleteButton != null) _deleteButton.gameObject.SetActive(hasData);
|
||||
|
||||
// 刷新时重置确认对话框状态
|
||||
HideDeleteConfirm();
|
||||
|
||||
if (!hasData) return;
|
||||
|
||||
if (_playtimeText != null) _playtimeText.text = FormatPlaytime(summary.Playtime);
|
||||
|
||||
if (_regionText != null)
|
||||
{
|
||||
string key = summary.SceneName ?? string.Empty;
|
||||
string loc = !string.IsNullOrEmpty(key)
|
||||
? LocalizationManager.Get(key, LocalizationTable.UI)
|
||||
: null;
|
||||
_regionText.text = !string.IsNullOrEmpty(loc) && loc != key ? loc : key;
|
||||
}
|
||||
|
||||
if (_lastSavedText != null) _lastSavedText.text = FormatDateTime(summary.LastSaved);
|
||||
}
|
||||
|
||||
// ── 删除确认 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 显示删除确认对话框。
|
||||
/// 未配置 _deleteConfirmRoot 时直接执行删除(向下兼容旧 Prefab)。
|
||||
/// </summary>
|
||||
private void ShowDeleteConfirm()
|
||||
{
|
||||
if (_deleteConfirmRoot == null)
|
||||
{
|
||||
// 旧 Prefab 未添加确认根节点,直接删除
|
||||
_controller.OnSlotDeleteRequested(_slotIndex);
|
||||
return;
|
||||
}
|
||||
_deleteConfirmRoot.SetActive(true);
|
||||
// 手柄导航:将焦点移至"确认"按钮,防止误触选择按钮
|
||||
EventSystem.current?.SetSelectedGameObject(_btnConfirmDelete?.gameObject);
|
||||
}
|
||||
|
||||
private void HideDeleteConfirm()
|
||||
{
|
||||
if (_deleteConfirmRoot != null) _deleteConfirmRoot.SetActive(false);
|
||||
}
|
||||
|
||||
// ── 格式化工具 ────────────────────────────────────────────────────────
|
||||
|
||||
private static string FormatPlaytime(float seconds)
|
||||
{
|
||||
int h = (int)(seconds / 3600);
|
||||
int m = (int)((seconds % 3600) / 60);
|
||||
int s = (int)(seconds % 60);
|
||||
return $"{h:D2}:{m:D2}:{s:D2}";
|
||||
}
|
||||
|
||||
private static string FormatDateTime(string iso8601)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iso8601)) return string.Empty;
|
||||
if (DateTime.TryParse(iso8601,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.RoundtripKind,
|
||||
out DateTime dt))
|
||||
{
|
||||
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
return iso8601;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
Assets/_Game/Scripts/UI/Menus/SaveSlotUI.cs
Normal file
143
Assets/_Game/Scripts/UI/Menus/SaveSlotUI.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.UI.Menus
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个存档槽卡片组件,负责显示存档摘要或空槽提示。
|
||||
///
|
||||
/// ⚠ 必须独立成文件:Unity 仅为与文件名同名的类生成 MonoScript 资产,
|
||||
/// 若与 SaveSlotController 合并在同一文件,AddComponent 后脚本引用会丢失(Missing Script)。
|
||||
/// </summary>
|
||||
public class SaveSlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("基本信息")]
|
||||
[SerializeField] private TMP_Text _playtimeText;
|
||||
[SerializeField] private TMP_Text _regionText;
|
||||
[SerializeField] private TMP_Text _lastSavedText;
|
||||
[SerializeField] private Image _formIcon;
|
||||
|
||||
[Header("扩展信息(可选)")]
|
||||
[Tooltip("货币(灵珠)持有量文本。")]
|
||||
[SerializeField] private TMP_Text _lingZhuText;
|
||||
[Tooltip("生命(面具)上限文本。")]
|
||||
[SerializeField] private TMP_Text _hpText;
|
||||
[Tooltip("钢铁之魂徽章根节点,仅一命模式存档显示。")]
|
||||
[SerializeField] private GameObject _steelSoulBadge;
|
||||
|
||||
[Header("区域背景图")]
|
||||
[Tooltip("RegionRegistry.asset,用于根据 SceneName 查找区域背景图。")]
|
||||
[SerializeField] private RegionRegistrySO _regionRegistry;
|
||||
[Tooltip("存档点不在任何已注册区域时显示的默认背景图。")]
|
||||
[SerializeField] private Sprite _fallbackBackground;
|
||||
[Tooltip("显示区域背景图的 Image 组件。")]
|
||||
[SerializeField] private Image _backgroundImage;
|
||||
|
||||
[Header("槽位状态")]
|
||||
[SerializeField] private GameObject _emptyIndicator; // 空槽时显示的"新游戏"提示
|
||||
[SerializeField] private GameObject _dataIndicator; // 有数据时显示的内容根
|
||||
[SerializeField] private Button _selectButton;
|
||||
[SerializeField] private Button _deleteButton;
|
||||
|
||||
private int _slotIndex;
|
||||
private SaveSlotController _controller;
|
||||
|
||||
/// <summary>由 SaveSlotController 在 Awake 时调用以完成按钮绑定。</summary>
|
||||
public void Init(int slotIndex, SaveSlotController controller)
|
||||
{
|
||||
_slotIndex = slotIndex;
|
||||
_controller = controller;
|
||||
|
||||
if (_selectButton != null)
|
||||
{
|
||||
_selectButton.onClick.RemoveAllListeners();
|
||||
_selectButton.onClick.AddListener(() => _controller.OnSlotSelected(_slotIndex));
|
||||
}
|
||||
|
||||
if (_deleteButton != null)
|
||||
{
|
||||
_deleteButton.onClick.RemoveAllListeners();
|
||||
// 删除统一走 Controller → 通用确认对话框(不再使用每卡内联确认)
|
||||
_deleteButton.onClick.AddListener(() => _controller.OnSlotDeleteRequested(_slotIndex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>用摘要数据刷新显示;summary 为 null 表示空槽。</summary>
|
||||
/// <param name="summary">槽位摘要,null 为空槽。</param>
|
||||
/// <param name="mode">当前面板模式:继续模式下空槽不可选。</param>
|
||||
public void Refresh(SlotSummary summary, SaveSlotPanelMode mode)
|
||||
{
|
||||
bool hasData = summary != null;
|
||||
|
||||
if (_emptyIndicator != null) _emptyIndicator.SetActive(!hasData);
|
||||
if (_dataIndicator != null) _dataIndicator.SetActive(hasData);
|
||||
if (_deleteButton != null) _deleteButton.gameObject.SetActive(hasData);
|
||||
|
||||
// 继续模式下空槽不可选;新游戏模式下空槽可选(建新档)
|
||||
if (_selectButton != null)
|
||||
_selectButton.interactable = hasData || mode == SaveSlotPanelMode.NewGame;
|
||||
|
||||
if (_steelSoulBadge != null)
|
||||
_steelSoulBadge.SetActive(hasData && summary.IsSteelSoul);
|
||||
|
||||
if (!hasData) return;
|
||||
|
||||
if (_playtimeText != null) _playtimeText.text = FormatPlaytime(summary.Playtime);
|
||||
|
||||
if (_regionText != null)
|
||||
{
|
||||
string key = summary.SceneName ?? string.Empty;
|
||||
string loc = !string.IsNullOrEmpty(key)
|
||||
? LocalizationManager.Get(key, LocalizationTable.UI)
|
||||
: null;
|
||||
_regionText.text = !string.IsNullOrEmpty(loc) && loc != key ? loc : key;
|
||||
}
|
||||
|
||||
if (_lastSavedText != null) _lastSavedText.text = FormatDateTime(summary.LastSaved);
|
||||
if (_lingZhuText != null) _lingZhuText.text = summary.CurrentLingZhu.ToString();
|
||||
if (_hpText != null) _hpText.text = summary.MaxHP.ToString();
|
||||
|
||||
RefreshBackground(summary);
|
||||
}
|
||||
|
||||
private void RefreshBackground(SlotSummary summary)
|
||||
{
|
||||
if (_backgroundImage == null) return;
|
||||
|
||||
Sprite bg = null;
|
||||
if (summary != null && !string.IsNullOrEmpty(summary.SceneName))
|
||||
bg = _regionRegistry?.FindBySceneName(summary.SceneName)?.saveSlotBackground;
|
||||
|
||||
_backgroundImage.sprite = bg != null ? bg : _fallbackBackground;
|
||||
_backgroundImage.enabled = _backgroundImage.sprite != null;
|
||||
}
|
||||
|
||||
// ── 格式化工具 ────────────────────────────────────────────────────────
|
||||
|
||||
private static string FormatPlaytime(float seconds)
|
||||
{
|
||||
int h = (int)(seconds / 3600);
|
||||
int m = (int)((seconds % 3600) / 60);
|
||||
int s = (int)(seconds % 60);
|
||||
return $"{h:D2}:{m:D2}:{s:D2}";
|
||||
}
|
||||
|
||||
private static string FormatDateTime(string iso8601)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iso8601)) return string.Empty;
|
||||
if (DateTime.TryParse(iso8601,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.RoundtripKind,
|
||||
out DateTime dt))
|
||||
{
|
||||
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
return iso8601;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Menus/SaveSlotUI.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Menus/SaveSlotUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f6853b6f549e314fa93ef199614d96c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user