diff --git a/.idea/.idea.HALLUCINATE/.idea/workspace.xml b/.idea/.idea.HALLUCINATE/.idea/workspace.xml index b2192ac9..82314ef0 100644 --- a/.idea/.idea.HALLUCINATE/.idea/workspace.xml +++ b/.idea/.idea.HALLUCINATE/.idea/workspace.xml @@ -5,62 +5,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets/Scripts/UI/UIManager.cs b/Assets/Scripts/UI/UIManager.cs index 95894ec9..ac6c37dc 100644 --- a/Assets/Scripts/UI/UIManager.cs +++ b/Assets/Scripts/UI/UIManager.cs @@ -37,12 +37,11 @@ namespace Hallucinate.UI [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, Range(2, 50)] private float trailDistanceThreshold = 10f; [SerializeField] private bool enableRipples = true; - [SerializeField] private Color rippleColor = new Color(1, 1, 1, 0.4f); + [SerializeField] private Color rippleColor = new Color(0, 1, 0.8f, 0.4f); - [Header("UI Templates")] + [Header("UI Templates & Global Styles")] [SerializeField] private VisualTreeAsset loginTemplate; [SerializeField] private VisualTreeAsset mainMenuTemplate; [SerializeField] private VisualTreeAsset lobbyTemplate; @@ -50,24 +49,24 @@ namespace Hallucinate.UI [SerializeField] private VisualTreeAsset profileTemplate; [SerializeField] private VisualTreeAsset settingsTemplate; [SerializeField] private VisualTreeAsset hudTemplate; + [SerializeField] private StyleSheet globalStyleSheet; 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; + // 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 const string UI_SCALE_KEY = "UIScale"; - [Header("Development Settings")] - [SerializeField] private bool allowMultipleInstances = true; - #if UNITY_EDITOR private void OnValidate() { @@ -85,11 +84,7 @@ namespace Hallucinate.UI private void Awake() { - if (Instance != null && Instance != this) - { - Destroy(gameObject); - return; - } + if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); @@ -104,21 +99,23 @@ namespace Hallucinate.UI { cursorSize = PlayerPrefs.GetFloat("CursorSize", 40f); enableRipples = PlayerPrefs.GetInt("CursorRipples", 1) == 1; - bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1; - if (!trailEnabled) trailLength = 0; } public void SetCursorSize(float size) { cursorSize = size; PlayerPrefs.SetFloat("CursorSize", size); - SetupVirtualCursor(); + 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); - SetupVirtualCursor(); } public void SetCursorRipples(bool enabled) @@ -155,20 +152,34 @@ namespace Hallucinate.UI float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f); SetUIScale(savedScale); } + private void Start() { if (_uiDocument == null) _uiDocument = GetComponent(); _rootElement = _uiDocument.rootVisualElement; - _cursorLayer = new VisualElement(); - _cursorLayer.name = "CursorLayer"; + if (globalStyleSheet != null) + _rootElement.panel.visualTree.styleSheets.Add(globalStyleSheet); + + _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); - SetupVirtualCursor(); + 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); @@ -182,6 +193,24 @@ namespace Hallucinate.UI 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", ""); @@ -209,7 +238,7 @@ namespace Hallucinate.UI { _isSettingsOpen = true; _settingsController.Root.BringToFront(); - _cursorLayer.BringToFront(); + if (_cursorLayer != null) _cursorLayer.BringToFront(); await _settingsController.PlayTransitionIn(); } else @@ -222,47 +251,55 @@ namespace Hallucinate.UI private void Update() { if (_history.Count > 0) _history.Peek().Update(); - UpdateCursorAndTrail(); + UpdateCursorInput(); } - private void SetupVirtualCursor() + private void UpdateCursorInput() { - if (_cursorLayer == null) return; - _cursorLayer.Clear(); - _trailSegments.Clear(); - _posHistory.Clear(); + if (!Application.isFocused || _cursorLayer == null) return; - if (cursorTrailSprite != null) + // 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) { - 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.style.left = uiPos.x; + _mainCursor.style.top = uiPos.y; } - _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); + bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1; + if (trailEnabled && cursorTrailSprite != null) + { + float dist = Vector2.Distance(uiPos, _lastTrailSpawnPos); + if (dist > trailDistanceThreshold) + { + SpawnPooledTrail(uiPos); + _lastTrailSpawnPos = uiPos; + } + } + } - 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 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; @@ -270,57 +307,29 @@ namespace Hallucinate.UI 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.style.left = evt.localPosition.x; ripple.style.top = evt.localPosition.y; 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 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; - 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 @@ -346,7 +355,7 @@ namespace Hallucinate.UI 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(); // Giữ cursor layer trên cùng của root + if (_cursorLayer != null) _cursorLayer.BringToFront(); var controller = ScriptableObject.CreateInstance(); controller.Initialize(instance, this); _controllers[typeof(T)] = controller; diff --git a/Assets/UI/Global.uss b/Assets/UI/Global.uss index 4f86708b..7c55308b 100644 --- a/Assets/UI/Global.uss +++ b/Assets/UI/Global.uss @@ -1,7 +1,7 @@ /* Global Styles for Hallucinate UI */ /* ============================================================ - DESIGN TOKENS + DESIGN TOKENS & UNITY THEME OVERRIDES ============================================================ */ :root { --primary-color: #ffffff; @@ -14,6 +14,12 @@ --radius-md: 12px; --radius-lg: 24px; --radius-pill: 999px; + + /* ÉP UNITY DÙNG MÀU CỦA GAME CHO CÁC THÀNH PHẦN NỘI BỘ (DROPDOWN POPUP) */ + --unity-colors-surface-background: #0a0a0a; + --unity-colors-surface-border: #00ffcc; + --unity-colors-surface-text: #ffffff; + --unity-colors-highlight-background: rgba(0, 255, 204, 0.2); } /* ============================================================ @@ -143,18 +149,19 @@ /* ============================================================ DROPDOWN & FIELDS (THE BIG FIX) ============================================================ */ -.unity-dropdown-field { +DropdownField, .unity-dropdown-field { margin-bottom: 12px; + transition: 200ms; } -.unity-dropdown-field__label { +DropdownField .unity-dropdown-field__label { width: 35%; color: #aaaaaa; font-size: 14px; -unity-font-style: bold; } -.unity-dropdown-field__input { +DropdownField .unity-base-field__input { background-color: rgba(255, 255, 255, 0.05); border-radius: 10px; border-width: 1px; @@ -165,50 +172,150 @@ transition: 200ms; } -.unity-dropdown-field__input:hover { - background-color: rgba(255, 255, 255, 0.1); +DropdownField:hover .unity-base-field__input { + background-color: rgba(255, 255, 255, 0.08); border-color: #00ffcc; } /* NHẮM VÀO CÁI MENU XỔ XUỐNG (POPUP) */ -/* Cần selector cực kỳ mạnh để đè style mặc định của Unity */ +/* Cần selector cực kỳ mạnh và ghi đè toàn bộ phân cấp */ + +/* Container chính của Popup - Lớp phủ toàn màn hình */ .unity-base-dropdown { + background-color: rgba(0, 0, 0, 0) !important; /* Phải để trong suốt để không tràn màn hình */ + border-width: 0 !important; +} + +/* Phần nền nội bộ của cái menu box thực tế */ +.unity-base-dropdown__container-inner { background-color: #0a0a0a !important; border-width: 2px !important; border-color: #00ffcc !important; border-radius: 12px !important; margin-top: 10px !important; + padding: 5px !important; } -/* Đè phân cấp nội bộ của Popup */ -.unity-base-dropdown__container-inner { - background-color: #0a0a0a !important; - border-width: 0 !important; -} - +/* Từng dòng Item */ .unity-base-dropdown__item { padding: 12px 15px !important; background-color: transparent !important; color: #eeeeee !important; } +/* Chữ trong Item */ +.unity-base-dropdown__label { + color: #eeeeee !important; +} + /* Đè hiệu ứng hover mặc định */ .unity-base-dropdown__item:hover { background-color: rgba(0, 255, 204, 0.15) !important; color: #00ffcc !important; } +.unity-base-dropdown__item:hover .unity-base-dropdown__label { + color: #00ffcc !important; +} + /* Item được chọn */ .unity-base-dropdown__item--selected { background-color: rgba(0, 255, 204, 0.1) !important; color: #00ffcc !important; - -unity-font-style: bold; } .unity-base-dropdown__checkmark { -unity-background-image-tint-color: #00ffcc !important; } +/* ============================================================ + SMART SIDEBAR (OSU STYLE OVERLAY) + ============================================================ */ +.sidebar-tabs-container { + background-color: rgba(5, 5, 5, 0.95); + padding-top: 60px; + flex-shrink: 0; + border-right-width: 1px; + border-right-color: rgba(0, 255, 204, 0.1); + transition: width 0.3s ease-out-quad; + overflow: hidden; + height: 100%; +} + +.sidebar-collapsed { + width: 80px; +} + +.sidebar-expanded { + width: 240px; + position: absolute; + z-index: 100; + border-right-color: #00ffcc; + border-right-width: 2px; +} + +.sidebar-tab { + height: 60px; + background-color: transparent; + border-width: 0; + padding: 0; + flex-direction: row; + align-items: center; + border-left-width: 4px; + border-left-color: transparent; + margin: 2px 0; +} + +.tab-icon-box { + width: 80px; + height: 60px; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.tab-icon { + width: 32px; + height: 32px; + background-color: #ffffff; /* Placeholder color */ + -unity-background-image-tint-color: #ffffff; +} + +.active-tab .tab-icon { + -unity-background-image-tint-color: #00ffcc; + background-color: #00ffcc; +} + +.tab-label { + font-size: 16px; + -unity-font-style: bold; + color: #888888; + margin-left: 10px; + white-space: nowrap; + opacity: 0; + transition: opacity 0.2s; +} + +.sidebar-expanded .tab-label { + opacity: 1; + color: #ffffff; +} + +/* ICON-ONLY BACK BUTTON */ +.btn-icon-only { + width: 44px; + height: 44px; + border-radius: 22px; + padding: 0; + justify-content: center; + align-items: center; +} + +.btn-icon-only .tab-icon { + width: 24px; + height: 24px; +} + /* ============================================================ PANELS ============================================================ */