using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using TMPro; 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 { /// 主菜单存档槽面板的打开语境。 public enum SaveSlotPanelMode { /// 继续游戏:仅有档槽可选,点击即读档进入。 Continue, /// 新游戏:空槽 → 模式选择 → 建档;有档槽 → 覆盖确认 → 模式选择 → 建档。 NewGame, } /// /// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。 /// /// 前端选档流程: /// · 新游戏开档前先选模式(普通 / 钢铁之魂),占用槽位需覆盖确认。 /// · 删除强制走通用确认对话框(无静默删除旁路)。 /// · 卡片展示游玩时长 / 区域 / 货币 / 生命 / 钢魂徽章。 /// /// 模式由 MainMenuController 在打开面板前通过 指定。 /// ConfirmDialog 与 NewGameMode 面板经 Inspector 注入(同处 MainMenu 场景)。 /// public class SaveSlotController : MonoBehaviour, IFocusable { [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() { for (int i = 0; i < _slotUIs.Length; i++) if (_slotUIs[i] != null) _slotUIs[i].Init(i, this); } /// 由 MainMenuController 在 SetActive(true) 之前调用,决定本次打开语境。 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(); var ct = _cts.Token; var task = RefreshAsync(ct); task.ContinueWith(t => { if (t.IsFaulted && !(t.Exception?.InnerException is OperationCanceledException)) Debug.LogException(t.Exception?.InnerException ?? t.Exception, this); }, TaskScheduler.FromCurrentSynchronizationContext()); // 面板打开时设置初始焦点(键盘 / 手柄导航入口) StartCoroutine(RestoreFocusNextFrame()); } private void OnDisable() { // 关闭子对话框,避免下次打开残留 _confirmDialog?.Close(); _modeSelect?.Close(); _cts?.Cancel(); _cts?.Dispose(); _cts = null; } private async Task RefreshAsync(CancellationToken ct = default) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; var tasks = new Task[_slotUIs.Length]; for (int i = 0; i < _slotUIs.Length; i++) tasks[i] = svc.GetSlotSummaryAsync(i); var summaries = await Task.WhenAll(tasks); ct.ThrowIfCancellationRequested(); // 组件已失活时不写 UI for (int i = 0; i < _slotUIs.Length; i++) { if (_slotUIs[i] == null) continue; _slotUIs[i].Refresh(summaries[i], _mode); } } // ── 选槽 ──────────────────────────────────────────────────────────────── /// 选中指定槽位。行为取决于当前模式与槽位是否有档。由 SaveSlotUI 内部按钮调用。 public void OnSlotSelected(int slotIndex) { if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return; var svc = ServiceLocator.GetOrDefault(); 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 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(); if (svc == null) return; // 覆盖既有档:先删盘,确保旧数据不残留(建档仅初始化内存,写盘发生在首次存档) if (svc.HasSave(slotIndex)) await svc.DeleteSlotAsync(slotIndex); bool steel = level == DifficultyLevel.SteelSoul; svc.CreateSlot(slotIndex, steel); ServiceLocator.GetOrDefault()?.BeginNewGame(level); _onSlotConfirmed?.Raise(slotIndex); } private async Task ContinueSlotAsync(int slotIndex) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; bool ok = await svc.LoadAsync(slotIndex); if (!ok) return; // 损坏 / 不存在:不前进(后续可接错误提示) _onSlotConfirmed?.Raise(slotIndex); } // ── 删除(强制确认)──────────────────────────────────────────────────── /// 请求删除指定槽位。强制经通用确认对话框;无对话框时忽略,绝不静默删除。 public void OnSlotDeleteRequested(int slotIndex) { if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return; 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) { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; await svc.DeleteSlotAsync(slotIndex); await RefreshAsync(_cts?.Token ?? CancellationToken.None); } } }