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; using UnityEditor.Build; #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(5, 50)] private int trailLength = 20; [SerializeField, Range(1, 10)] private int trailSpacing = 2; [SerializeField] private bool enableRipples = true; [SerializeField] private Color rippleColor = new Color(1, 1, 1, 0.4f); [Header("UI Templates")] [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; private LoginController _loginController; private MainMenuController _mainMenuController; private LobbyController _lobbyController; private SettingsController _settingsController; private List _trailSegments = new List(); private List _posHistory = new List(); private Vector2 _lastMousePos; private float _trailOpacity = 1f; private bool _isSettingsOpen = false; public bool IsSettingsOpen => _isSettingsOpen; private const string UI_SCALE_KEY = "UIScale"; [Header("Development Settings")] [SerializeField] private bool allowMultipleInstances = true; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // Single instance guard if (!Application.isEditor && !allowMultipleInstances) { var currentProcess = System.Diagnostics.Process.GetCurrentProcess(); var processes = System.Diagnostics.Process.GetProcessesByName(currentProcess.ProcessName); if (processes.Length > 1) { Debug.LogError("[UIManager] Another instance is already running. Quitting to prevent save conflict."); Application.Quit(); return; } } _uiDocument = GetComponent(); UnityEngine.Cursor.visible = false; ApplySavedUIScale(); } public void OnGameStarted() { _ = Push(); } public void OnBackToMenu() { _ = Push(); } public void SetUIScale(float scale) { if (_uiDocument == null || _uiDocument.panelSettings == null) return; // Unity UI Toolkit dùng panelSettings để điều khiển tỉ lệ // Chúng ta thay đổi scale multiplier. Mặc định 1.0 sẽ tương ứng với visual scale là 1.3 _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(); if (_uiDocument == null) { Debug.LogError("[UIManager] UIDocument component missing!"); return; } _rootElement = _uiDocument.rootVisualElement; if (_rootElement == null) { Debug.LogError("[UIManager] Root VisualElement is null!"); return; } _cursorLayer = new VisualElement(); _cursorLayer.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); SetupVirtualCursor(); _rootElement.RegisterCallback(OnGlobalClick, TrickleDown.TrickleDown); if (inputReader != null) { inputReader.OnToggleSettingsEvent += ToggleSettings; inputReader.OnCancelEvent += HandleCancel; } #if UNITY_EDITOR if (gameIcon == null) { var icons = PlayerSettings.GetIcons(NamedBuildTarget.Unknown, IconKind.Any); if (icons != null && icons.Length > 0) gameIcon = icons[0]; } #endif InitializeControllers(); CheckLoginStatus(); } private void CheckLoginStatus() { string savedName = PlayerPrefs.GetString("Username", ""); if (string.IsNullOrEmpty(savedName)) { _ = Push(); } else { Debug.Log($"[UIManager] Welcome back, {savedName}!"); _ = Push(); } } public void OnLoginSuccess() { _ = Push(); } private void OnDestroy() { if (inputReader != null) { inputReader.OnToggleSettingsEvent -= ToggleSettings; inputReader.OnCancelEvent -= HandleCancel; } } private void HandleCancel() { if (_isSettingsOpen) ToggleSettings(); } public async void ToggleSettings() { if (_settingsController == null) return; if (!_isSettingsOpen) { _isSettingsOpen = true; _settingsController.Root.BringToFront(); _cursorLayer.BringToFront(); await _settingsController.PlayTransitionIn(); } else { _isSettingsOpen = false; await _settingsController.PlayTransitionOut(); } } private void Update() { if (_history.Count > 0) _history.Peek().Update(); UpdateCursorAndTrail(); } private void SetupVirtualCursor() { if (_cursorLayer == null) return; _cursorLayer.Clear(); _trailSegments.Clear(); _posHistory.Clear(); if (cursorTrailSprite != null) { for (int i = 0; i < trailLength; i++) { var segment = new VisualElement(); segment.style.position = Position.Absolute; segment.style.width = cursorSize; segment.style.height = cursorSize; segment.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorTrailSprite)); float ratio = 1f - ((float)i / trailLength); segment.style.opacity = 0f; segment.style.scale = new StyleScale(new Scale(new Vector3(ratio, ratio, 1f))); segment.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50))); segment.pickingMode = PickingMode.Ignore; _cursorLayer.Add(segment); _trailSegments.Add(segment); } } _mainCursor = new VisualElement(); _mainCursor.style.position = Position.Absolute; _mainCursor.style.width = cursorSize; _mainCursor.style.height = cursorSize; _mainCursor.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorSprite != null ? cursorSprite : cursorTrailSprite)); _mainCursor.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50))); _mainCursor.pickingMode = PickingMode.Ignore; _cursorLayer.Add(_mainCursor); Vector2 startPos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y); _lastMousePos = startPos; for (int i = 0; i < trailLength * trailSpacing + 1; i++) _posHistory.Add(startPos); } private float GetCurrentScale() { if (_uiDocument != null && _uiDocument.panelSettings != null) return _uiDocument.panelSettings.scale; return 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))); 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.style.borderLeftWidth = 2; ripple.style.borderRightWidth = 2; // PointerDownEvent.localPosition đã được Unity tự động scale theo Panel ripple.style.left = evt.localPosition.x; ripple.style.top = evt.localPosition.y; ripple.pickingMode = PickingMode.Ignore; _cursorLayer.Add(ripple); // Correct Fluent API for PrimeTween 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 UpdateCursorAndTrail() { if (!Application.isFocused || _cursorLayer == null) { if (_cursorLayer != null) _cursorLayer.style.display = DisplayStyle.None; return; } Vector3 mousePos = Input.mousePosition; bool isMouseInWindow = mousePos.x >= 0 && mousePos.x <= Screen.width && mousePos.y >= 0 && mousePos.y <= Screen.height; if (!isMouseInWindow) { _cursorLayer.style.display = DisplayStyle.None; return; } _cursorLayer.style.display = DisplayStyle.Flex; // QUAN TRỌNG: Chia tọa độ pixel cho scale của UI để có tọa độ local chính xác float scale = GetCurrentScale(); Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale); float mouseSpeed = Vector2.Distance(uiPos, _lastMousePos); _lastMousePos = uiPos; if (mouseSpeed > 0.1f) _trailOpacity = Mathf.MoveTowards(_trailOpacity, 1f, Time.deltaTime * 5f); else _trailOpacity = Mathf.MoveTowards(_trailOpacity, 0f, Time.deltaTime * 3f); _posHistory.Insert(0, uiPos); if (_posHistory.Count > trailLength * trailSpacing + 1) _posHistory.RemoveAt(_posHistory.Count - 1); if (_mainCursor != null) { _mainCursor.style.left = uiPos.x; _mainCursor.style.top = uiPos.y; } for (int i = 0; i < _trailSegments.Count; i++) { int historyIndex = (i + 1) * trailSpacing; if (historyIndex < _posHistory.Count) { _trailSegments[i].style.left = _posHistory[historyIndex].x; _trailSegments[i].style.top = _posHistory[historyIndex].y; float baseRatio = 1f - ((float)i / trailLength); _trailSegments[i].style.opacity = baseRatio * 0.5f * _trailOpacity; } } } 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); _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) { Debug.LogWarning($"[UIManager] Template for {typeof(T).Name} is missing in Inspector."); return null; } if (_rootElement == null) return null; VisualElement instance = null; try { instance = template.Instantiate(); } catch (Exception e) { Debug.LogError($"[UIManager] Failed to instantiate template for {typeof(T).Name}: {e.Message}"); return null; } 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(); } } }