Files

657 lines
22 KiB
C#

using System;
using System.Threading;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Framework.Managers.UI.Events;
using BriarQueen.Framework.Services.Game;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.UI.Menus
{
/// <summary>
/// Main Menu flow:
/// - Starts on intro screen
/// - Intro light pulses
/// - Intro text/title fade in after delay
/// - Submit fades intro text/title out, pushes light to full alpha, then fades main menu in
/// - Start Game => opens SelectSaveWindow
/// - Settings => opens settings menu
/// - Quit => quits app
/// </summary>
public class MainMenuWindow : MonoBehaviour
{
[Header("Main Menu Window")]
[SerializeField]
private CanvasGroup _mainMenuIntroScreenCanvasGroup;
[SerializeField]
private CanvasGroup _mainMenuWindowCanvasGroup;
[Header("Intro Screen")]
[SerializeField]
private Image _introScreenLightImage;
[SerializeField]
private CanvasGroup _introTextCanvasGroup;
[SerializeField]
private TextMeshProUGUI _introTextText;
[SerializeField]
private CanvasGroup _introTitleCanvasGroup;
[Header("Buttons")]
[SerializeField]
private Button _startGameButton;
[SerializeField]
private Button _settingsButton;
[SerializeField]
private Button _quitButton;
[Header("Select Save Window")]
[SerializeField]
private SelectSaveWindow _selectSaveWindow;
[SerializeField]
private CanvasGroup _selectSaveWindowCanvasGroup;
[Header("Tween Settings")]
[SerializeField]
private TweenSettings _selectSaveTweenSettings = new()
{
duration = 0.25f,
ease = Ease.OutQuad,
useUnscaledTime = true
};
[Header("Intro Timing")]
[SerializeField]
private float _introLightPulseDuration = 2f;
[SerializeField]
private float _introTextDelaySeconds = 1.5f;
[SerializeField]
private float _introTextFadeInDuration = 0.8f;
[SerializeField]
private float _introTextPulseDuration = 1.4f;
[SerializeField]
private float _introSubmitTextFadeOutDuration = 0.25f;
[SerializeField]
private float _introSubmitLightToFullDuration = 0.75f;
[SerializeField]
private float _introToMenuCrossfadeDuration = 0.6f;
private EventCoordinator _eventCoordinator;
private GameService _gameService;
private InputManager _inputManager;
private CancellationTokenSource _introCts;
private CancellationTokenSource _selectSaveCts;
private Sequence _introLightPulseSequence;
private Sequence _introTextPulseSequence;
private Sequence _introTransitionSequence;
private Sequence _selectSaveSequence;
private bool _introFinished;
private bool _introTransitioning;
private DeviceInputType _lastDeviceInputType;
[Inject]
public void Construct(GameService gameService, EventCoordinator eventCoordinator, InputManager inputManager)
{
_gameService = gameService;
_eventCoordinator = eventCoordinator;
_inputManager = inputManager;
}
private void Awake()
{
ApplyInitialVisualState();
if (_selectSaveWindow != null)
{
_selectSaveWindow.OnCloseWindow += CloseSelectSaveWindow;
_selectSaveWindow.gameObject.SetActive(false);
}
UpdateSubmitText(force: true);
}
private void OnEnable()
{
BindButtons();
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(false));
_inputManager?.BindSubmitForStart(OnIntroSubmit);
StartIntroScreen().Forget();
}
private void OnDisable()
{
UnbindButtons();
_inputManager?.ResetSubmitBind(OnIntroSubmit);
StopIntroTweens();
StopSelectSaveTween();
}
private void OnDestroy()
{
if (_selectSaveWindow != null)
_selectSaveWindow.OnCloseWindow -= CloseSelectSaveWindow;
StopIntroTweens();
StopSelectSaveTween();
}
private void LateUpdate()
{
UpdateSubmitText();
}
private void BindButtons()
{
if (_startGameButton != null)
_startGameButton.onClick.AddListener(OnStartClicked);
if (_settingsButton != null)
_settingsButton.onClick.AddListener(OnSettingsClicked);
if (_quitButton != null)
_quitButton.onClick.AddListener(OnQuitClicked);
}
private void UnbindButtons()
{
if (_startGameButton != null)
_startGameButton.onClick.RemoveListener(OnStartClicked);
if (_settingsButton != null)
_settingsButton.onClick.RemoveListener(OnSettingsClicked);
if (_quitButton != null)
_quitButton.onClick.RemoveListener(OnQuitClicked);
}
private void ApplyInitialVisualState()
{
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 1f, true);
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
if (_introScreenLightImage != null)
{
var color = _introScreenLightImage.color;
color.a = 0f;
_introScreenLightImage.color = color;
}
}
private void UpdateSubmitText(bool force = false)
{
if (_introTextText == null || _inputManager == null)
return;
var currentDevice = _inputManager.DeviceInputType;
if (!force && currentDevice == _lastDeviceInputType)
return;
_lastDeviceInputType = currentDevice;
var isKeyboardMouse = currentDevice == DeviceInputType.KeyboardAndMouse;
_introTextText.text = isKeyboardMouse
? "Press Enter to begin."
: "Press Start to begin.";
}
private async UniTaskVoid StartIntroScreen()
{
_introFinished = false;
_introTransitioning = false;
ResetIntroCtsAndCancelRunning();
ApplyInitialVisualState();
UpdateSubmitText(force: true);
try
{
StartIntroLightPulse(_introCts.Token);
await StartIntroTextFlow(_introCts.Token);
}
catch (OperationCanceledException)
{
}
}
private void StartIntroLightPulse(CancellationToken token)
{
if (_introScreenLightImage == null)
return;
_introLightPulseSequence = Sequence.Create(
useUnscaledTime: true,
cycleMode: Sequence.SequenceCycleMode.Yoyo,
cycles: -1)
.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
{
startValue = _introScreenLightImage.color.a,
endValue = 1f,
settings = new TweenSettings
{
duration = _introLightPulseDuration,
ease = Ease.InOutSine,
useUnscaledTime = true
}
}));
_introLightPulseSequence.ToUniTask(cancellationToken: token).Forget();
}
private async UniTask StartIntroTextFlow(CancellationToken token)
{
await UniTask.Delay(TimeSpan.FromSeconds(_introTextDelaySeconds), cancellationToken: token);
var fadeInSequence = Sequence.Create(useUnscaledTime: true);
if (_introTitleCanvasGroup != null)
{
fadeInSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = new TweenSettings
{
duration = _introTextFadeInDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
}
if (_introTextCanvasGroup != null)
{
fadeInSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = new TweenSettings
{
duration = _introTextFadeInDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
}
await fadeInSequence.ToUniTask(cancellationToken: token);
if (_introTextCanvasGroup == null)
return;
_introTextPulseSequence = Sequence.Create(
useUnscaledTime: true,
cycleMode: Sequence.SequenceCycleMode.Yoyo,
cycles: -1)
.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
{
startValue = _introTextCanvasGroup.alpha,
endValue = 0.1f,
settings = new TweenSettings
{
duration = _introTextPulseDuration,
ease = Ease.InOutSine,
useUnscaledTime = true
}
}));
await _introTextPulseSequence.ToUniTask(cancellationToken: token);
}
private void OnIntroSubmit(InputAction.CallbackContext ctx)
{
if (_introFinished || _introTransitioning)
return;
if (!ctx.performed)
return;
TransitionFromIntroToMainMenu().Forget();
}
private async UniTaskVoid TransitionFromIntroToMainMenu()
{
if (_introFinished || _introTransitioning)
return;
_introTransitioning = true;
ResetIntroCtsAndCancelRunning();
try
{
// Phase 1: fade intro title + text fully out together, while pushing light to full.
var introElementsSequence = Sequence.Create(useUnscaledTime: true);
if (_introTitleCanvasGroup != null)
{
introElementsSequence.Group(Tween.Alpha(_introTitleCanvasGroup, new TweenSettings<float>
{
startValue = _introTitleCanvasGroup.alpha,
endValue = 0f,
settings = new TweenSettings
{
duration = _introSubmitTextFadeOutDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
}
if (_introTextCanvasGroup != null)
{
introElementsSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings<float>
{
startValue = _introTextCanvasGroup.alpha,
endValue = 0f,
settings = new TweenSettings
{
duration = _introSubmitTextFadeOutDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
}
if (_introScreenLightImage != null)
{
introElementsSequence.Group(Tween.Alpha(_introScreenLightImage, new TweenSettings<float>
{
startValue = _introScreenLightImage.color.a,
endValue = 1f,
settings = new TweenSettings
{
duration = _introSubmitLightToFullDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
}
await introElementsSequence.ToUniTask(cancellationToken: _introCts.Token);
// Ensure intro text/title are fully gone before menu begins fading in.
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 0f, false);
// Phase 2: only after intro text/title have finished fading, crossfade to main menu.
if (_mainMenuIntroScreenCanvasGroup != null && _mainMenuWindowCanvasGroup != null)
{
_introTransitionSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_mainMenuIntroScreenCanvasGroup, new TweenSettings<float>
{
startValue = _mainMenuIntroScreenCanvasGroup.alpha,
endValue = 0f,
settings = new TweenSettings
{
duration = _introToMenuCrossfadeDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}))
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = new TweenSettings
{
duration = _introToMenuCrossfadeDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
await _introTransitionSequence.ToUniTask(cancellationToken: _introCts.Token);
}
else if (_mainMenuWindowCanvasGroup != null)
{
var menuFade = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = new TweenSettings
{
duration = _introToMenuCrossfadeDuration,
ease = Ease.OutQuad,
useUnscaledTime = true
}
}));
await menuFade.ToUniTask(cancellationToken: _introCts.Token);
}
}
catch (OperationCanceledException)
{
_introTransitioning = false;
return;
}
finally
{
_introTransitionSequence = default;
}
SetCanvasGroupState(_introTextCanvasGroup, 0f, false);
SetCanvasGroupState(_introTitleCanvasGroup, 0f, false);
SetCanvasGroupState(_mainMenuIntroScreenCanvasGroup, 0f, false);
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
if (_introScreenLightImage != null)
{
var color = _introScreenLightImage.color;
color.a = 1f;
_introScreenLightImage.color = color;
}
_introFinished = true;
_introTransitioning = false;
}
private void OnStartClicked()
{
Debug.Log("[MainMenuWindow] Starting game");
ShowSelectSaveWindow().Forget();
}
private void OnSettingsClicked()
{
_eventCoordinator?.PublishImmediate(new UIToggleSettingsWindow(true));
}
private void OnQuitClicked()
{
_gameService?.QuitGame();
}
private async UniTask ShowSelectSaveWindow()
{
Debug.Log("[MainMenuWindow] Showing select save window");
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
{
Debug.Log("[MainMenuWindow] SelectSaveWindow references not set.");
return;
}
ResetSelectSaveCtsAndCancelRunning();
_selectSaveWindow.gameObject.SetActive(true);
_selectSaveWindowCanvasGroup.alpha = 0f;
_selectSaveWindow.transform.localScale = Vector3.zero;
_selectSaveWindow.Refresh();
SetCanvasGroupState(_mainMenuWindowCanvasGroup, _mainMenuWindowCanvasGroup != null ? _mainMenuWindowCanvasGroup.alpha : 0f, false);
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 0f, false);
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
{
startValue = _selectSaveWindowCanvasGroup.alpha,
endValue = 1f,
settings = _selectSaveTweenSettings
}))
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
{
startValue = _selectSaveWindow.transform.localScale,
endValue = Vector3.one,
settings = _selectSaveTweenSettings
}));
try
{
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
_selectSaveSequence = default;
}
SetCanvasGroupState(_selectSaveWindowCanvasGroup, 1f, true);
}
private void CloseSelectSaveWindow()
{
CloseSelectSaveWindowInternal().Forget();
}
private async UniTask CloseSelectSaveWindowInternal()
{
if (_selectSaveWindow == null || _selectSaveWindowCanvasGroup == null)
return;
ResetSelectSaveCtsAndCancelRunning();
SetCanvasGroupState(_selectSaveWindowCanvasGroup, _selectSaveWindowCanvasGroup.alpha, false);
_selectSaveSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_selectSaveWindowCanvasGroup, new TweenSettings<float>
{
startValue = _selectSaveWindowCanvasGroup.alpha,
endValue = 0f,
settings = _selectSaveTweenSettings
}))
.Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings<Vector3>
{
startValue = _selectSaveWindow.transform.localScale,
endValue = Vector3.zero,
settings = _selectSaveTweenSettings
}));
try
{
await _selectSaveSequence.ToUniTask(cancellationToken: _selectSaveCts.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
_selectSaveSequence = default;
}
_selectSaveWindowCanvasGroup.alpha = 0f;
_selectSaveWindow.gameObject.SetActive(false);
SetCanvasGroupState(_mainMenuWindowCanvasGroup, 1f, true);
}
private void ResetIntroCtsAndCancelRunning()
{
StopSequence(ref _introLightPulseSequence);
StopSequence(ref _introTextPulseSequence);
StopSequence(ref _introTransitionSequence);
CancelAndDispose(ref _introCts);
_introCts = new CancellationTokenSource();
}
private void StopIntroTweens()
{
StopSequence(ref _introLightPulseSequence);
StopSequence(ref _introTextPulseSequence);
StopSequence(ref _introTransitionSequence);
CancelAndDispose(ref _introCts);
}
private void ResetSelectSaveCtsAndCancelRunning()
{
StopSequence(ref _selectSaveSequence);
CancelAndDispose(ref _selectSaveCts);
_selectSaveCts = new CancellationTokenSource();
}
private void StopSelectSaveTween()
{
StopSequence(ref _selectSaveSequence);
CancelAndDispose(ref _selectSaveCts);
}
private static void StopSequence(ref Sequence sequence)
{
if (sequence.isAlive)
sequence.Stop();
sequence = default;
}
private static void CancelAndDispose(ref CancellationTokenSource cts)
{
if (cts == null)
return;
try
{
cts.Cancel();
}
catch
{
}
cts.Dispose();
cts = null;
}
private static void SetCanvasGroupState(CanvasGroup group, float alpha, bool inputEnabled)
{
if (group == null)
return;
group.alpha = alpha;
group.interactable = inputEnabled;
group.blocksRaycasts = inputEnabled;
}
}
}