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 { /// /// 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 /// 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 { 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 { startValue = 0f, endValue = 1f, settings = new TweenSettings { duration = _introTextFadeInDuration, ease = Ease.OutQuad, useUnscaledTime = true } })); } if (_introTextCanvasGroup != null) { fadeInSequence.Group(Tween.Alpha(_introTextCanvasGroup, new TweenSettings { 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 { 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 { 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 { 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 { 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 { startValue = _mainMenuIntroScreenCanvasGroup.alpha, endValue = 0f, settings = new TweenSettings { duration = _introToMenuCrossfadeDuration, ease = Ease.OutQuad, useUnscaledTime = true } })) .Group(Tween.Alpha(_mainMenuWindowCanvasGroup, new TweenSettings { 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 { 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 { startValue = _selectSaveWindowCanvasGroup.alpha, endValue = 1f, settings = _selectSaveTweenSettings })) .Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings { 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 { startValue = _selectSaveWindowCanvasGroup.alpha, endValue = 0f, settings = _selectSaveTweenSettings })) .Group(Tween.Scale(_selectSaveWindow.transform, new TweenSettings { 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; } } }