657 lines
22 KiB
C#
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;
|
|
}
|
|
}
|
|
} |