using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using PrimeTween; using OnlyScove.Scripts; #if UNITY_EDITOR using UnityEditor; #endif namespace Hallucinate.UI { [RequireComponent(typeof(UIDocument))] public class UIManager : MonoBehaviour { public static UIManager Instance { get; private set; } private UIDocument _uiDocument; private VisualElement _rootElement; public VisualElement Root => _rootElement; private VisualElement _cursorLayer; private VisualElement _mainCursor; private readonly Dictionary _controllers = new Dictionary(); private readonly Stack _history = new Stack(); [Header("References")] [SerializeField] private InputReader inputReader; public InputReader InputReader => inputReader; [Header("Game Metadata")] [SerializeField] private Texture2D gameIcon; [Header("Cursor & Effects Settings")] [SerializeField] private Sprite cursorSprite; [SerializeField] private Sprite cursorTrailSprite; [SerializeField, Range(10f, 150f)] private float cursorSize = 40f; [SerializeField, Range(2, 50)] private float trailDistanceThreshold = 10f; [SerializeField] private bool enableRipples = true; [SerializeField] private Color rippleColor = new Color(0, 1, 0.8f, 0.4f); [Header("UI Templates & Global Styles")] [SerializeField] private VisualTreeAsset loginTemplate; [SerializeField] private VisualTreeAsset mainMenuTemplate; [SerializeField] private VisualTreeAsset lobbyTemplate; [SerializeField] private VisualTreeAsset roomItemTemplate; [SerializeField] private VisualTreeAsset profileTemplate; [SerializeField] private VisualTreeAsset settingsTemplate; [SerializeField] private VisualTreeAsset hudTemplate; [SerializeField] private VisualTreeAsset pauseMenuTemplate; [SerializeField] private StyleSheet globalStyleSheet; private LoginController _loginController; private MainMenuController _mainMenuController; private LobbyController _lobbyController; private SettingsController _settingsController; private PauseMenuController _pauseMenuController; // Osu Trail Pooling private const int MAX_TRAIL_PARTICLES = 60; private readonly List _trailPool = new List(); private int _currentTrailIndex = 0; private Vector2 _lastTrailSpawnPos; private bool _isSettingsOpen = false; public bool IsSettingsOpen => _isSettingsOpen; private bool _isPauseMenuOpen = false; public bool IsPauseMenuOpen => _isPauseMenuOpen; private const string UI_SCALE_KEY = "UIScale"; #if UNITY_EDITOR private void OnValidate() { if (gameIcon == null) { var icons = PlayerSettings.GetIcons(UnityEditor.Build.NamedBuildTarget.Unknown, IconKind.Any); if (icons != null && icons.Length > 0) { gameIcon = icons[0]; UnityEditor.EditorUtility.SetDirty(this); } } } #endif private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); _uiDocument = GetComponent(); UnityEngine.Cursor.visible = false; LoadGeneralSettings(); ApplySavedUIScale(); } private void LoadGeneralSettings() { cursorSize = PlayerPrefs.GetFloat("CursorSize", 40f); enableRipples = PlayerPrefs.GetInt("CursorRipples", 1) == 1; } public void SetCursorSize(float size) { cursorSize = size; PlayerPrefs.SetFloat("CursorSize", size); if (_mainCursor != null) { _mainCursor.style.width = cursorSize; _mainCursor.style.height = cursorSize; } foreach(var p in _trailPool) { p.style.width = cursorSize; p.style.height = cursorSize; } } public void SetCursorTrail(bool enabled) { PlayerPrefs.SetInt("CursorTrail", enabled ? 1 : 0); } public void SetCursorRipples(bool enabled) { enableRipples = enabled; PlayerPrefs.SetInt("CursorRipples", enabled ? 1 : 0); } public void SetMouseSensitivity(float sensitivity) { PlayerPrefs.SetFloat("MouseSensitivity", sensitivity); if (OnlyScove.Scripts.SettingsManager.Instance != null) { OnlyScove.Scripts.SettingsManager.Instance.SetSensitivity(sensitivity); } } public void OnGameStarted() { _ = Push(); } public void OnBackToMenu() { _ = Push(); } public void SetUIScale(float scale) { if (_uiDocument == null || _uiDocument.panelSettings == null) return; _uiDocument.panelSettings.scale = scale * 1.3f; PlayerPrefs.SetFloat(UI_SCALE_KEY, scale); PlayerPrefs.Save(); } private void ApplySavedUIScale() { float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f); SetUIScale(savedScale); } private void Start() { if (_uiDocument == null) _uiDocument = GetComponent(); _rootElement = _uiDocument.rootVisualElement; if (globalStyleSheet != null) _rootElement.panel.visualTree.styleSheets.Add(globalStyleSheet); var dimOverlay = new VisualElement { name = "BackgroundDimOverlay" }; dimOverlay.style.position = Position.Absolute; dimOverlay.style.width = Length.Percent(100); dimOverlay.style.height = Length.Percent(100); dimOverlay.pickingMode = PickingMode.Ignore; float savedDim = PlayerPrefs.GetFloat("BackgroundDim", 50f); dimOverlay.style.backgroundColor = new Color(0, 0, 0, savedDim / 100f); _rootElement.Add(dimOverlay); _cursorLayer = new VisualElement { name = "CursorLayer" }; _cursorLayer.style.position = Position.Absolute; _cursorLayer.style.width = Length.Percent(100); _cursorLayer.style.height = Length.Percent(100); _cursorLayer.pickingMode = PickingMode.Ignore; _rootElement.Add(_cursorLayer); InitializeTrailPool(); _mainCursor = new VisualElement { name = "MainCursor" }; _mainCursor.style.position = Position.Absolute; _mainCursor.style.width = cursorSize; _mainCursor.style.height = cursorSize; _mainCursor.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorSprite)); // Căn giữa sprite hình tròn bằng translate _mainCursor.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50))); _mainCursor.pickingMode = PickingMode.Ignore; _cursorLayer.Add(_mainCursor); _rootElement.RegisterCallback(OnGlobalClick, TrickleDown.TrickleDown); if (inputReader != null) { inputReader.OnToggleSettingsEvent += ToggleSettings; inputReader.OnCancelEvent += HandleCancel; } InitializeControllers(); CheckLoginStatus(); } private void InitializeTrailPool() { for (int i = 0; i < MAX_TRAIL_PARTICLES; i++) { var particle = new VisualElement(); particle.style.position = Position.Absolute; particle.style.width = cursorSize; particle.style.height = cursorSize; particle.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorTrailSprite)); particle.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50))); particle.style.opacity = 0; particle.style.display = DisplayStyle.None; particle.pickingMode = PickingMode.Ignore; _cursorLayer.Add(particle); _trailPool.Add(particle); } } private void CheckLoginStatus() { string savedName = PlayerPrefs.GetString("Username", ""); if (string.IsNullOrEmpty(savedName)) _ = Push(); else _ = Push(); } public void OnLoginSuccess() => _ = Push(); private void OnDestroy() { if (inputReader != null) { inputReader.OnToggleSettingsEvent -= ToggleSettings; inputReader.OnCancelEvent -= HandleCancel; } } private void HandleCancel() { if (_isSettingsOpen) ToggleSettings(); else if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "Main Scene") { TogglePauseMenu(); } } public async void TogglePauseMenu() { if (_pauseMenuController == null) return; if (!_isPauseMenuOpen) { _isPauseMenuOpen = true; _pauseMenuController.Root.BringToFront(); if (_cursorLayer != null) _cursorLayer.BringToFront(); // Unlock cursor when menu is open UnityEngine.Cursor.lockState = CursorLockMode.None; UnityEngine.Cursor.visible = false; await _pauseMenuController.PlayTransitionIn(); } else { _isPauseMenuOpen = false; // Re-lock cursor when menu is closed if (!_isSettingsOpen) { UnityEngine.Cursor.lockState = CursorLockMode.Locked; } await _pauseMenuController.PlayTransitionOut(); } } public async void ToggleSettings() { if (_settingsController == null) return; if (!_isSettingsOpen) { _isSettingsOpen = true; _settingsController.Root.BringToFront(); if (_cursorLayer != null) _cursorLayer.BringToFront(); await _settingsController.PlayTransitionIn(); } else { _isSettingsOpen = false; await _settingsController.PlayTransitionOut(); } } private void Update() { if (_history.Count > 0) _history.Peek().Update(); UpdateCursorInput(); } private void UpdateCursorInput() { if (!Application.isFocused || _cursorLayer == null) return; // Dùng cách tính tọa độ thủ công để tránh offset khi pivot ở giữa Vector2 mousePos = Input.mousePosition; float scale = GetCurrentScale(); Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale); if (_mainCursor != null) { _mainCursor.style.left = uiPos.x; _mainCursor.style.top = uiPos.y; } bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1; if (trailEnabled && cursorTrailSprite != null) { float dist = Vector2.Distance(uiPos, _lastTrailSpawnPos); if (dist > trailDistanceThreshold) { SpawnPooledTrail(uiPos); _lastTrailSpawnPos = uiPos; } } } private void SpawnPooledTrail(Vector2 pos) { var particle = _trailPool[_currentTrailIndex]; _currentTrailIndex = (_currentTrailIndex + 1) % MAX_TRAIL_PARTICLES; Tween.StopAll(particle); particle.style.display = DisplayStyle.Flex; particle.style.left = pos.x; particle.style.top = pos.y; particle.style.opacity = 0f; particle.style.scale = Vector3.one; Sequence.Create() .Group(Tween.Custom(0f, 0.6f, duration: 0.05f, onValueChange: val => particle.style.opacity = val)) .Chain(Tween.Custom(0.6f, 0f, duration: 0.35f, onValueChange: val => particle.style.opacity = val)) .Group(Tween.Custom(1f, 0.2f, duration: 0.4f, onValueChange: val => { particle.style.scale = new StyleScale(new Scale(new Vector3(val, val, 1f))); })) .OnComplete(() => particle.style.display = DisplayStyle.None); } private float GetCurrentScale() => (_uiDocument != null && _uiDocument.panelSettings != null) ? _uiDocument.panelSettings.scale : 1.0f; private void OnGlobalClick(PointerDownEvent evt) { if (!enableRipples || _cursorLayer == null) return; var ripple = new VisualElement(); ripple.style.position = Position.Absolute; ripple.style.width = cursorSize; ripple.style.height = cursorSize; ripple.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50))); ripple.style.left = evt.localPosition.x; ripple.style.top = evt.localPosition.y; var radius = new StyleLength(new Length(50, LengthUnit.Percent)); ripple.style.borderTopLeftRadius = radius; ripple.style.borderTopRightRadius = radius; ripple.style.borderBottomLeftRadius = radius; ripple.style.borderBottomRightRadius = radius; ripple.style.borderTopColor = rippleColor; ripple.style.borderBottomColor = rippleColor; ripple.style.borderLeftColor = rippleColor; ripple.style.borderRightColor = rippleColor; ripple.style.borderTopWidth = 2; ripple.style.borderBottomWidth = 2; ripple.pickingMode = PickingMode.Ignore; _cursorLayer.Add(ripple); Tween.Custom(Vector3.one, Vector3.one * 2.5f, duration: 0.4f, onValueChange: val => ripple.style.scale = new StyleScale(new Scale(val)), ease: Ease.OutQuad); Tween.Custom(1f, 0f, duration: 0.4f, onValueChange: val => ripple.style.opacity = val).OnComplete(() => ripple.RemoveFromHierarchy()); } private void InitializeControllers() { try { _mainMenuController = RegisterController(mainMenuTemplate); if (_mainMenuController != null && gameIcon != null) _mainMenuController.SetGameIcon(gameIcon); _lobbyController = RegisterController(lobbyTemplate); if (_lobbyController != null) _lobbyController.SetRoomTemplate(roomItemTemplate); RegisterController(profileTemplate); _settingsController = RegisterController(settingsTemplate); RegisterController(hudTemplate); _pauseMenuController = RegisterController(pauseMenuTemplate); _loginController = RegisterController(loginTemplate); } catch (Exception e) { Debug.LogError($"[UIManager] Failed to initialize controllers: {e}"); } } private T RegisterController(VisualTreeAsset template) where T : BaseUIController { if (template == null || _rootElement == null) return null; VisualElement instance = template.Instantiate(); if (instance == null) return null; instance.style.flexGrow = 1; instance.style.position = Position.Absolute; instance.style.width = Length.Percent(100); instance.style.height = Length.Percent(100); instance.style.display = DisplayStyle.None; _rootElement.Add(instance); if (_cursorLayer != null) _cursorLayer.BringToFront(); var controller = ScriptableObject.CreateInstance(); controller.Initialize(instance, this); _controllers[typeof(T)] = controller; return controller; } public async Task Push() where T : BaseUIController { if (!_controllers.TryGetValue(typeof(T), out var newScreen)) return; if (_history.Count > 0 && _history.Peek() == newScreen) return; if (_history.Count > 0) await _history.Peek().PlayTransitionOut(); _history.Push(newScreen); await newScreen.PlayTransitionIn(); if (_cursorLayer != null) _cursorLayer.BringToFront(); } public async Task Pop() { if (_history.Count <= 1) return; await _history.Pop().PlayTransitionOut(); if (_history.Count > 0) await _history.Peek().PlayTransitionIn(); if (_cursorLayer != null) _cursorLayer.BringToFront(); } } }