355 lines
10 KiB
C#
355 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using BriarQueen.Framework.Managers.IO;
|
||
using BriarQueen.Framework.Services.Game;
|
||
using BriarQueen.UI.Menus.Components;
|
||
using Cysharp.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
using VContainer;
|
||
|
||
namespace BriarQueen.UI.Menus
|
||
{
|
||
/// <summary>
|
||
/// Select Save Window:
|
||
/// - Shows EXACTLY 3 slots (filled first, then empty).
|
||
/// - Clicking a slot:
|
||
/// - Filled slot => loads that save, then StartGame
|
||
/// - Empty slot => opens NewSaveWindow (create + load), then StartGame
|
||
/// - Delete is still supported (per-slot delete button + confirm modal).
|
||
/// - Back closes this window (handled by MainMenuWindow).
|
||
/// </summary>
|
||
public class SelectSaveWindow : MonoBehaviour
|
||
{
|
||
private const int MAX_SLOTS = 3;
|
||
|
||
[Header("UI")]
|
||
[SerializeField]
|
||
private RectTransform _listContentParent;
|
||
|
||
[SerializeField]
|
||
private SaveSlotUI _saveSlotPrefab;
|
||
|
||
[Header("Buttons")]
|
||
[SerializeField]
|
||
private Button _backButton;
|
||
|
||
[Header("New Save Window")]
|
||
[SerializeField]
|
||
private NewSaveWindow _newSaveWindow;
|
||
|
||
[Header("Confirm Delete Window (optional but recommended)")]
|
||
[SerializeField]
|
||
private ConfirmDeleteWindow _confirmDeleteWindow;
|
||
|
||
private readonly List<SaveSlotUI> _instantiatedSlots = new();
|
||
|
||
private int _currentSelectionIndex;
|
||
private GameService _gameService;
|
||
private bool _isBusy;
|
||
|
||
private SaveManager _saveManager;
|
||
|
||
private void Awake()
|
||
{
|
||
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
|
||
|
||
if (_newSaveWindow != null)
|
||
{
|
||
_newSaveWindow.OnCloseWindow += HandleNewSaveClosed;
|
||
_newSaveWindow.OnSaveCreated += HandleSaveCreatedAndStartGame;
|
||
}
|
||
|
||
if (_confirmDeleteWindow != null)
|
||
{
|
||
_confirmDeleteWindow.OnConfirmDelete += HandleConfirmDelete;
|
||
_confirmDeleteWindow.OnCancel += HandleCancelDelete;
|
||
_confirmDeleteWindow.Close();
|
||
}
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
|
||
|
||
if (_newSaveWindow != null)
|
||
{
|
||
_newSaveWindow.OnCloseWindow -= HandleNewSaveClosed;
|
||
_newSaveWindow.OnSaveCreated -= HandleSaveCreatedAndStartGame;
|
||
}
|
||
|
||
if (_confirmDeleteWindow != null)
|
||
{
|
||
_confirmDeleteWindow.OnConfirmDelete -= HandleConfirmDelete;
|
||
_confirmDeleteWindow.OnCancel -= HandleCancelDelete;
|
||
}
|
||
|
||
ClearSlots();
|
||
}
|
||
|
||
public event Action OnCloseWindow;
|
||
|
||
[Inject]
|
||
public void Construct(SaveManager saveManager, GameService gameService)
|
||
{
|
||
_saveManager = saveManager;
|
||
_gameService = gameService;
|
||
}
|
||
|
||
/// <summary>Called by MainMenuWindow after enabling this GO.</summary>
|
||
public void Refresh()
|
||
{
|
||
if (_newSaveWindow != null)
|
||
_newSaveWindow.CloseImmediate();
|
||
|
||
if (_confirmDeleteWindow != null)
|
||
_confirmDeleteWindow.Close();
|
||
|
||
EnsureThreeSlotsExist();
|
||
RefreshSlotsData();
|
||
SetBusy(false);
|
||
}
|
||
|
||
private void OnBackClicked()
|
||
{
|
||
if (_isBusy)
|
||
return;
|
||
|
||
// Check if Confirm Delete or New Save Window is open.
|
||
|
||
OnCloseWindow?.Invoke();
|
||
}
|
||
|
||
private void HandleNewSaveClosed()
|
||
{
|
||
// NewSaveWindow was closed (cancel/back) => return control to the list.
|
||
SetBusy(false);
|
||
RestoreSelection();
|
||
RefreshSlotsData();
|
||
}
|
||
|
||
private void HandleSaveCreatedAndStartGame(string _)
|
||
{
|
||
// NewSaveWindow already created + loaded the save.
|
||
OnCloseWindow?.Invoke();
|
||
_gameService?.StartGame().Forget();
|
||
}
|
||
|
||
private void SetBusy(bool busy)
|
||
{
|
||
Debug.Log("[SelectSaveWindow] SetBusy: " + busy);
|
||
_isBusy = busy;
|
||
|
||
if (_backButton != null)
|
||
_backButton.interactable = !busy;
|
||
|
||
foreach (var slot in _instantiatedSlots)
|
||
if (slot != null)
|
||
slot.SetInteractable(!busy);
|
||
|
||
Debug.Log($"[SelectSaveWindow] Finished set busy: {busy}");
|
||
}
|
||
|
||
private void EnsureThreeSlotsExist()
|
||
{
|
||
if (_listContentParent == null || _saveSlotPrefab == null)
|
||
return;
|
||
|
||
if (_instantiatedSlots.Count == MAX_SLOTS)
|
||
return;
|
||
|
||
ClearSlots();
|
||
|
||
for (var i = 0; i < MAX_SLOTS; i++)
|
||
{
|
||
var slot = Instantiate(_saveSlotPrefab, _listContentParent);
|
||
_instantiatedSlots.Add(slot);
|
||
}
|
||
}
|
||
|
||
private void RefreshSlotsData()
|
||
{
|
||
// Always show 3 slots; if save system is missing, they’ll all appear empty/disabled.
|
||
if (_saveManager == null)
|
||
{
|
||
for (var i = 0; i < _instantiatedSlots.Count; i++)
|
||
_instantiatedSlots[i]?.SetEmpty(OnEmptySlotClicked);
|
||
|
||
SelectBackButton();
|
||
return;
|
||
}
|
||
|
||
// Newest first, cap at 3
|
||
var saveFiles = _saveManager.GetAvailableSaves();
|
||
var infos = (saveFiles ?? new List<(string, DateTime)>())
|
||
.Select(sf => new SaveFileInfo(sf.FileName, sf.LastModified))
|
||
.OrderByDescending(i => i.LastModified)
|
||
.Take(MAX_SLOTS)
|
||
.ToArray();
|
||
|
||
for (var i = 0; i < MAX_SLOTS; i++)
|
||
{
|
||
var slot = _instantiatedSlots.ElementAtOrDefault(i);
|
||
if (slot == null) continue;
|
||
|
||
if (i < infos.Length)
|
||
slot.SetFilled(infos[i], OnFilledSlotClicked, OnSlotDeleteClicked);
|
||
else
|
||
slot.SetEmpty(OnEmptySlotClicked);
|
||
}
|
||
|
||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||
SelectSlot(_currentSelectionIndex);
|
||
}
|
||
|
||
private void ClearSlots()
|
||
{
|
||
foreach (var slot in _instantiatedSlots)
|
||
if (slot != null)
|
||
Destroy(slot.gameObject);
|
||
|
||
_instantiatedSlots.Clear();
|
||
_currentSelectionIndex = 0;
|
||
}
|
||
|
||
private void OnFilledSlotClicked(SaveFileInfo saveInfo)
|
||
{
|
||
if (_isBusy) return;
|
||
if (_saveManager == null) return;
|
||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||
|
||
if (!_saveManager.DoesSaveExist(saveInfo.FileName))
|
||
{
|
||
RefreshSlotsData();
|
||
return;
|
||
}
|
||
|
||
LoadAndStartGame(saveInfo.FileName).Forget();
|
||
}
|
||
|
||
private void OnEmptySlotClicked()
|
||
{
|
||
Debug.Log("[SelectSaveWindow] Empty slot clicked.");
|
||
if (_isBusy)
|
||
return;
|
||
|
||
if (_newSaveWindow == null)
|
||
{
|
||
Debug.LogWarning("[SelectSaveWindow] NewSaveWindow reference not set.");
|
||
return;
|
||
}
|
||
|
||
SetBusy(true);
|
||
_newSaveWindow.Open();
|
||
}
|
||
|
||
private async UniTask LoadAndStartGame(string profileName)
|
||
{
|
||
SetBusy(true);
|
||
|
||
try
|
||
{
|
||
await _saveManager.LoadGameData(profileName);
|
||
OnCloseWindow?.Invoke();
|
||
_gameService?.StartGame().Forget();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogError($"[SelectSaveWindow] Failed to load profile '{profileName}': {ex}");
|
||
SetBusy(false);
|
||
RefreshSlotsData();
|
||
RestoreSelection();
|
||
}
|
||
}
|
||
|
||
private void OnSlotDeleteClicked(SaveFileInfo saveInfo)
|
||
{
|
||
if (_isBusy) return;
|
||
if (_saveManager == null) return;
|
||
if (string.IsNullOrWhiteSpace(saveInfo.FileName)) return;
|
||
|
||
// No confirm window wired? Do a direct delete.
|
||
if (_confirmDeleteWindow == null)
|
||
{
|
||
TryDeleteAndRefresh(saveInfo.FileName);
|
||
return;
|
||
}
|
||
|
||
SetBusy(true);
|
||
_confirmDeleteWindow.Open(saveInfo);
|
||
}
|
||
|
||
private void HandleConfirmDelete(SaveFileInfo saveInfo)
|
||
{
|
||
try
|
||
{
|
||
TryDeleteAndRefresh(saveInfo.FileName);
|
||
}
|
||
finally
|
||
{
|
||
SetBusy(false);
|
||
RestoreSelection();
|
||
}
|
||
}
|
||
|
||
private void HandleCancelDelete()
|
||
{
|
||
SetBusy(false);
|
||
RestoreSelection();
|
||
}
|
||
|
||
private void TryDeleteAndRefresh(string fileName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
return;
|
||
|
||
if (!_saveManager.DoesSaveExist(fileName))
|
||
{
|
||
RefreshSlotsData();
|
||
return;
|
||
}
|
||
|
||
var deleted = _saveManager.Delete(fileName);
|
||
if (!deleted)
|
||
Debug.LogWarning($"[SelectSaveWindow] Failed to delete save '{fileName}'.");
|
||
|
||
RefreshSlotsData();
|
||
}
|
||
|
||
private void RestoreSelection()
|
||
{
|
||
_currentSelectionIndex = Mathf.Clamp(_currentSelectionIndex, 0, MAX_SLOTS - 1);
|
||
SelectSlot(_currentSelectionIndex);
|
||
}
|
||
|
||
private void SelectBackButton()
|
||
{
|
||
if (_backButton == null) return;
|
||
|
||
if (EventSystem.current != null)
|
||
EventSystem.current.SetSelectedGameObject(_backButton.gameObject);
|
||
else
|
||
_backButton.Select();
|
||
}
|
||
|
||
private void SelectSlot(int index)
|
||
{
|
||
if (_instantiatedSlots.Count == 0)
|
||
{
|
||
SelectBackButton();
|
||
return;
|
||
}
|
||
|
||
index = Mathf.Clamp(index, 0, _instantiatedSlots.Count - 1);
|
||
_currentSelectionIndex = index;
|
||
|
||
var go = _instantiatedSlots[index]?.GetSelectableGameObject();
|
||
if (go == null) return;
|
||
|
||
if (EventSystem.current != null)
|
||
EventSystem.current.SetSelectedGameObject(go);
|
||
}
|
||
}
|
||
} |