地图系统

This commit is contained in:
2026-06-05 18:41:33 +08:00
parent 613f2a4d13
commit fe4fd60083
234 changed files with 33090 additions and 4899 deletions

View File

@@ -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;
}
}
}