using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using BaseGames.UI;
using BaseGames.UI.MainMenu;
namespace BaseGames.UI.Menus
{
/// 主菜单存档槽面板的打开语境。
public enum SaveSlotPanelMode
{
/// 继续游戏:仅有档槽可选,点击即读档进入。
Continue,
/// 新游戏:空槽 → 模式选择 → 建档;有档槽 → 覆盖确认 → 模式选择 → 建档。
NewGame,
}
///
/// 驱动主菜单存档槽选择面板(新游戏 / 继续 / 删除)。
///
/// 本面板经 压栈(由主菜单控制器 Push)。覆盖确认 / 模式选择
/// 这两个子对话框走导航器结果面板( /
/// )——线性 await,无回调嵌套,ESC 逐层回退。
///
/// 前端选档流程:
/// · 新游戏开档前先选模式(普通 / 钢铁之魂),占用槽位需覆盖确认。
/// · 删除强制走通用确认对话框(无静默删除旁路)。
/// · 卡片展示游玩时长 / 区域 / 货币 / 生命 / 钢魂徽章。
/// 模式由主菜单控制器在压栈前经 指定。
///
public class SaveSlotController : UIPanelBase
{
[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);
}
/// 由主菜单控制器在压栈前调用,决定本次打开语境。
public void SetMode(SaveSlotPanelMode mode) => _mode = mode;
/// 默认 / 恢复焦点回到首个存档槽按钮。
protected override GameObject ResolveFirstSelected()
=> _defaultFocusButton != null ? _defaultFocusButton.gameObject : null;
// ── 生命周期(UIPanelBase 驱动)──────────────────────────────────────
protected override void OnPanelOpen()
{
_cts = new CancellationTokenSource();
RunGuarded(RefreshAsync(_cts.Token));
}
protected override void OnPanelClose()
{
// 注意:不再级联 Close 子对话框(旧实现于此强关 confirm/mode,是"ESC 一次关多层"的根因)。
// 子对话框生命周期完全交给导航器;若本面板随场景卸载,其 await 由结果面板兜底收口。
_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;
RunGuarded(HandleSlotSelectedAsync(slotIndex));
}
private async Task HandleSlotSelectedAsync(int slotIndex)
{
var svc = ServiceLocator.GetOrDefault();
if (svc == null) return;
var ct = _cts?.Token ?? CancellationToken.None;
if (_mode == SaveSlotPanelMode.Continue)
{
if (!svc.HasSave(slotIndex)) return; // 继续模式:空槽不可选
await ContinueSlotAsync(slotIndex);
return;
}
// ── 新游戏模式 ──
if (svc.HasSave(slotIndex) && _confirmDialog != null)
{
bool ok = await _confirmDialog.ShowAsync("CONFIRM_OVERWRITE_TITLE", "CONFIRM_OVERWRITE_BODY", ct);
if (!ok) return; // 取消覆盖
}
DifficultyLevel level = DifficultyLevel.Normal;
if (_modeSelect != null)
{
DifficultyLevel? chosen = await _modeSelect.ShowAsync(ct);
if (chosen == null) return; // 取消模式选择
level = chosen.Value;
}
await StartNewGameAsync(slotIndex, level);
}
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;
}
RunGuarded(DeleteFlowAsync(slotIndex));
}
private async Task DeleteFlowAsync(int slotIndex)
{
var ct = _cts?.Token ?? CancellationToken.None;
bool ok = await _confirmDialog.ShowAsync("CONFIRM_DELETE_TITLE", "CONFIRM_DELETE_BODY", ct);
if (!ok) return;
await 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);
}
// ── 工具 ──────────────────────────────────────────────────────────────
/// fire-and-forget Task 的统一异常护栏(吞掉取消,记录其余)。
private void RunGuarded(Task task)
{
task.ContinueWith(t =>
{
if (t.IsFaulted && !(t.Exception?.InnerException is OperationCanceledException))
Debug.LogException(t.Exception?.InnerException ?? t.Exception, this);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
}