2026-04-25 18:20:16 +07:00
using UnityEngine ;
using UnityEngine.UIElements ;
2026-04-30 20:58:59 +07:00
using UnityEngine.Audio ;
2026-05-01 02:25:25 +07:00
using UnityEngine.InputSystem ;
2026-04-29 01:04:28 +07:00
using System.Collections.Generic ;
2026-04-30 20:58:59 +07:00
using System.Linq ;
using System ;
2026-04-28 00:07:42 +07:00
using System.Threading.Tasks ;
2026-04-29 02:31:15 +07:00
using OnlyScove.Scripts ;
2026-04-30 20:58:59 +07:00
using Hallucinate.Audio ;
2026-04-30 21:46:37 +07:00
using PrimeTween ;
2026-04-25 18:20:16 +07:00
2026-04-28 00:07:42 +07:00
namespace Hallucinate.UI
2026-04-25 18:20:16 +07:00
{
2026-04-28 00:07:42 +07:00
public class SettingsController : BaseUIController
2026-04-25 18:20:16 +07:00
{
2026-04-28 00:07:42 +07:00
private VisualElement _sidebar ;
2026-05-01 16:51:08 +07:00
private VisualElement _tabsColumn ;
2026-04-28 00:07:42 +07:00
private Label _tabTitle ;
private ScrollView _content ;
2026-04-29 01:04:28 +07:00
private Dictionary < string , Button > _tabButtons = new Dictionary < string , Button > ( ) ;
private string _activeTab = "GENERAL" ;
2026-04-25 18:20:16 +07:00
2026-05-01 16:51:08 +07:00
private Tween _hoverTimer ;
private bool _isExpanded ;
2026-04-30 20:58:59 +07:00
// Advanced Mouse Metrics
private Label _mouseMetricsLabel ;
// FPS State
private bool _fpsVisible ;
// Hover Tracking for Arrow Key Slider Control
private Slider _hoveredSlider ;
2026-04-30 21:46:37 +07:00
private Action < float > _hoveredOnChanged ;
2026-04-30 20:58:59 +07:00
private float _sliderMin , _sliderMax ;
// Osu-style Volume Overlay
2026-05-01 01:25:02 +07:00
private VisualElement _volumeContainer ;
private VisualElement _masterRing ;
2026-04-30 20:58:59 +07:00
private Label _masterVolLabel ;
2026-05-01 01:25:02 +07:00
private Dictionary < string , ( VisualElement ring , Label label ) > _subRings = new Dictionary < string , ( VisualElement , Label ) > ( ) ;
private string _hoveredSubVolume = null ;
2026-04-30 20:58:59 +07:00
private float _masterVol = 80f ;
2026-05-01 01:25:02 +07:00
private int _overlayActiveCount = 0 ;
2026-04-30 20:58:59 +07:00
2026-04-28 00:07:42 +07:00
public override void Initialize ( VisualElement uxmlRoot , UIManager manager )
2026-04-25 18:20:16 +07:00
{
2026-04-28 00:07:42 +07:00
base . Initialize ( uxmlRoot , manager ) ;
2026-04-25 18:20:16 +07:00
2026-04-28 00:07:42 +07:00
_sidebar = root . Q < VisualElement > ( "Sidebar" ) ;
2026-05-01 16:51:08 +07:00
_tabsColumn = root . Q < VisualElement > ( "TabsColumn" ) ;
2026-04-28 00:07:42 +07:00
_tabTitle = root . Q < Label > ( "TabTitle" ) ;
_content = root . Q < ScrollView > ( "SettingsContent" ) ;
2026-04-25 18:20:16 +07:00
2026-05-01 16:51:08 +07:00
// Smart Sidebar Hover Logic
_tabsColumn . RegisterCallback < PointerEnterEvent > ( OnSidebarPointerEnter ) ;
_tabsColumn . RegisterCallback < PointerLeaveEvent > ( OnSidebarPointerLeave ) ;
// Global Volume Catch
2026-05-01 01:25:02 +07:00
uiManager . Root . RegisterCallback < WheelEvent > ( OnMouseWheel , TrickleDown . TrickleDown ) ;
SetupHierarchicalVolumeOverlay ( ) ;
2026-04-29 01:04:28 +07:00
2026-04-28 18:49:05 +07:00
root . RegisterCallback < PointerDownEvent > ( evt = > {
2026-04-30 20:58:59 +07:00
if ( evt . target = = root ) uiManager . ToggleSettings ( ) ;
2026-04-28 11:35:49 +07:00
} ) ;
2026-04-30 20:58:59 +07:00
root . RegisterCallback < KeyDownEvent > ( OnKeyDown ) ;
2026-04-29 01:04:28 +07:00
SetupTab ( "GeneralTab" , "GENERAL" ) ;
SetupTab ( "VideoTab" , "VIDEO" ) ;
SetupTab ( "SoundTab" , "SOUND" ) ;
SetupTab ( "ControlTab" , "CONTROL" ) ;
var closeBtn = root . Q < Button > ( "CloseSettingsBtn" ) ;
if ( closeBtn ! = null ) closeBtn . clicked + = ( ) = > uiManager . ToggleSettings ( ) ;
2026-04-30 20:58:59 +07:00
_masterVol = PlayerPrefs . GetFloat ( "MasterVolume" , 80f ) ;
2026-05-01 02:25:25 +07:00
ApplyVideoSettings ( ) ;
2026-04-29 01:04:28 +07:00
SwitchTab ( "GENERAL" ) ;
}
2026-05-01 16:51:08 +07:00
private void OnSidebarPointerEnter ( PointerEnterEvent evt )
{
_hoverTimer . Stop ( ) ;
// Bung ra ngay lập tức với animation cực mượt
ExpandSidebar ( ) ;
}
private void OnSidebarPointerLeave ( PointerLeaveEvent evt )
{
_hoverTimer . Stop ( ) ;
// Thu gọn ngay lập tức
CollapseSidebar ( ) ;
}
private void ExpandSidebar ( )
{
if ( _isExpanded ) return ;
_isExpanded = true ;
_tabsColumn . AddToClassList ( "sidebar-expanded" ) ;
// Animation 0.5s OutQuart cho cảm giác cao cấp
Tween . Custom ( _tabsColumn . resolvedStyle . width , 240f , duration : 0.5f , ease : Ease . OutQuart , onValueChange : val = > _tabsColumn . style . width = val ) ;
}
private void CollapseSidebar ( )
{
if ( ! _isExpanded ) return ;
_isExpanded = false ;
_tabsColumn . RemoveFromClassList ( "sidebar-expanded" ) ;
Tween . Custom ( _tabsColumn . resolvedStyle . width , 80f , duration : 0.45f , ease : Ease . OutQuart , onValueChange : val = > _tabsColumn . style . width = val ) ;
}
2026-05-01 02:25:25 +07:00
private void ApplyVideoSettings ( )
{
int frameLimitIdx = PlayerPrefs . GetInt ( "FrameLimiter" , 2 ) ;
ApplyFrameLimit ( frameLimitIdx ) ;
_fpsVisible = PlayerPrefs . GetInt ( "ShowFPS" , 0 ) = = 1 ;
PerformanceOverlay . SetVisible ( _fpsVisible ) ;
float dim = PlayerPrefs . GetFloat ( "BackgroundDim" , 50f ) ;
ApplyBackgroundDim ( dim ) ;
bool isFull = PlayerPrefs . GetInt ( "Fullscreen" , Screen . fullScreen ? 1 : 0 ) = = 1 ;
Screen . fullScreen = isFull ;
}
private void ApplyFrameLimit ( int index )
{
switch ( index )
{
case 0 : QualitySettings . vSyncCount = 1 ; Application . targetFrameRate = - 1 ; break ;
case 1 : QualitySettings . vSyncCount = 0 ; Application . targetFrameRate = 60 ; break ;
case 2 : QualitySettings . vSyncCount = 0 ; Application . targetFrameRate = 144 ; break ;
case 3 : QualitySettings . vSyncCount = 0 ; Application . targetFrameRate = 999 ; break ;
}
PlayerPrefs . SetInt ( "FrameLimiter" , index ) ;
}
private void ApplyBackgroundDim ( float value )
{
PlayerPrefs . SetFloat ( "BackgroundDim" , value ) ;
var dimOverlay = uiManager . Root . Q < VisualElement > ( "BackgroundDimOverlay" ) ;
2026-05-01 16:51:08 +07:00
if ( dimOverlay ! = null ) dimOverlay . style . backgroundColor = new Color ( 0 , 0 , 0 , value / 100f ) ;
2026-05-01 02:25:25 +07:00
}
2026-05-01 01:25:02 +07:00
private void SetupHierarchicalVolumeOverlay ( )
2026-04-30 20:58:59 +07:00
{
2026-05-01 16:51:08 +07:00
_volumeContainer = new VisualElement { name = "GlobalVolumeOverlay" } ;
2026-05-01 01:25:02 +07:00
_volumeContainer . style . position = Position . Absolute ;
2026-05-01 16:51:08 +07:00
_volumeContainer . style . right = 50 ; _volumeContainer . style . bottom = 50 ;
_volumeContainer . style . width = 300 ; _volumeContainer . style . height = 300 ;
2026-05-01 01:25:02 +07:00
_volumeContainer . style . display = DisplayStyle . None ;
_volumeContainer . pickingMode = PickingMode . Ignore ;
uiManager . Root . Add ( _volumeContainer ) ;
2026-05-01 16:51:08 +07:00
_masterRing = CreateRing ( "Master" , 120 , cyan : true ) ;
_masterRing . style . right = 0 ; _masterRing . style . bottom = 0 ;
2026-05-01 01:25:02 +07:00
_masterVolLabel = _masterRing . Q < Label > ( ) ;
_volumeContainer . Add ( _masterRing ) ;
string [ ] subs = { "MusicVolume" , "VFXVolume" , "PlayerVolume" , "UIVolume" } ;
string [ ] shortNames = { "MUS" , "VFX" , "PLY" , "UI" } ;
for ( int i = 0 ; i < subs . Length ; i + + )
{
var ring = CreateRing ( shortNames [ i ] , 70 , false ) ;
float angle = ( i * 30f ) * Mathf . Deg2Rad ;
float radius = 140f ;
ring . style . right = 25 + Mathf . Sin ( angle ) * radius ;
ring . style . bottom = 25 + Mathf . Cos ( angle ) * radius ;
string key = subs [ i ] ;
ring . RegisterCallback < PointerEnterEvent > ( evt = > _hoveredSubVolume = key ) ;
ring . RegisterCallback < PointerLeaveEvent > ( evt = > { if ( _hoveredSubVolume = = key ) _hoveredSubVolume = null ; } ) ;
2026-05-01 16:51:08 +07:00
ring . pickingMode = PickingMode . Position ;
2026-05-01 01:25:02 +07:00
_subRings [ key ] = ( ring , ring . Q < Label > ( ) ) ;
_volumeContainer . Add ( ring ) ;
}
}
private VisualElement CreateRing ( string text , float size , bool cyan )
{
var ring = new VisualElement ( ) ;
2026-05-01 16:51:08 +07:00
ring . style . width = size ; ring . style . height = size ;
2026-05-01 01:25:02 +07:00
ring . style . backgroundColor = new Color ( 0 , 0 , 0 , 0.85f ) ;
2026-05-01 16:51:08 +07:00
var radius = size / 2 ;
ring . style . borderTopLeftRadius = radius ; ring . style . borderTopRightRadius = radius ;
ring . style . borderBottomLeftRadius = radius ; ring . style . borderBottomRightRadius = radius ;
ring . style . borderTopWidth = 3 ; ring . style . borderBottomWidth = 3 ;
ring . style . borderLeftWidth = 3 ; ring . style . borderRightWidth = 3 ;
2026-05-01 01:25:02 +07:00
ring . style . borderTopColor = ring . style . borderBottomColor = ring . style . borderLeftColor = ring . style . borderRightColor = cyan ? Color . cyan : new Color ( 0.7f , 0.7f , 0.7f ) ;
2026-05-01 16:51:08 +07:00
ring . style . justifyContent = Justify . Center ; ring . style . alignItems = Align . Center ;
2026-05-01 01:25:02 +07:00
ring . style . position = Position . Absolute ;
var label = new Label ( "80%" ) ;
2026-05-01 16:51:08 +07:00
label . style . color = Color . white ; label . style . fontSize = size * 0.25f ;
2026-05-01 01:25:02 +07:00
label . style . unityFontStyleAndWeight = FontStyle . Bold ;
ring . Add ( label ) ;
var title = new Label ( text ) ;
2026-05-01 16:51:08 +07:00
title . style . color = Color . gray ; title . style . fontSize = size * 0.15f ;
title . style . position = Position . Absolute ; title . style . bottom = size * 0.15f ;
2026-05-01 01:25:02 +07:00
ring . Add ( title ) ;
return ring ;
2026-04-30 20:58:59 +07:00
}
private void OnMouseWheel ( WheelEvent evt )
{
2026-05-01 01:25:02 +07:00
var mainMenuRoot = uiManager . Root . Q < VisualElement > ( "MainMenuRoot" ) ;
bool isMainMenuVisible = mainMenuRoot ! = null & & mainMenuRoot . style . display = = DisplayStyle . Flex ;
2026-05-01 16:51:08 +07:00
if ( ! uiManager . IsSettingsOpen & & isMainMenuVisible ) return ;
2026-05-01 02:25:25 +07:00
VisualElement target = evt . target as VisualElement ;
bool isDirectUIInteraction = _hoveredSubVolume ! = null | | ( _hoveredSlider ! = null & & _activeTab = = "SOUND" ) ;
if ( ! isDirectUIInteraction & & target ! = null )
{
2026-05-01 16:51:08 +07:00
if ( target is ScrollView | | target . GetFirstAncestorOfType < ScrollView > ( ) ! = null ) return ;
2026-05-01 02:25:25 +07:00
}
2026-05-01 01:25:02 +07:00
_overlayActiveCount + + ;
ShowVolumeOverlay ( ) ;
2026-05-01 16:51:08 +07:00
if ( _hoveredSubVolume ! = null ) UpdateSubVolume ( _hoveredSubVolume , - evt . delta . y * 2f ) ;
2026-05-01 01:25:02 +07:00
else if ( _hoveredSlider ! = null & & _activeTab = = "SOUND" )
{
2026-04-30 21:46:37 +07:00
float step = ( _sliderMax - _sliderMin ) / 100f ;
2026-05-01 16:51:08 +07:00
float newVal = Mathf . Clamp ( _hoveredSlider . value - ( evt . delta . y * step * 5f ) , _sliderMin , _sliderMax ) ;
2026-04-30 21:46:37 +07:00
_hoveredSlider . value = newVal ;
_hoveredOnChanged ? . Invoke ( newVal ) ;
}
2026-05-01 16:51:08 +07:00
else UpdateMasterVolume ( - evt . delta . y * 2f ) ;
2026-05-01 01:25:02 +07:00
evt . StopPropagation ( ) ;
2026-04-30 20:58:59 +07:00
}
private void UpdateMasterVolume ( float delta )
{
_masterVol = Mathf . Clamp ( _masterVol + delta , 0f , 100f ) ;
PlayerPrefs . SetFloat ( "MasterVolume" , _masterVol ) ;
2026-05-01 01:25:02 +07:00
AudioManager . Instance ? . SetVolume ( "MasterVolume" , _masterVol ) ;
2026-04-30 20:58:59 +07:00
_masterVolLabel . text = $"{Mathf.RoundToInt(_masterVol)}%" ;
2026-05-01 01:25:02 +07:00
if ( _activeTab = = "SOUND" ) SwitchTab ( "SOUND" ) ;
}
private void UpdateSubVolume ( string key , float delta )
{
2026-05-01 16:51:08 +07:00
float newVal = Mathf . Clamp ( PlayerPrefs . GetFloat ( key , 80f ) + delta , 0f , 100f ) ;
2026-05-01 01:25:02 +07:00
PlayerPrefs . SetFloat ( key , newVal ) ;
AudioManager . Instance ? . SetVolume ( key , newVal ) ;
2026-05-01 16:51:08 +07:00
if ( _subRings . TryGetValue ( key , out var data ) ) data . label . text = $"{Mathf.RoundToInt(newVal)}%" ;
2026-05-01 01:25:02 +07:00
if ( _activeTab = = "SOUND" ) SwitchTab ( "SOUND" ) ;
2026-04-30 20:58:59 +07:00
}
private async void ShowVolumeOverlay ( )
{
2026-05-01 01:25:02 +07:00
_volumeContainer . BringToFront ( ) ;
uiManager . Root . Q < VisualElement > ( "CursorLayer" ) ? . BringToFront ( ) ;
_volumeContainer . style . display = DisplayStyle . Flex ;
_volumeContainer . style . opacity = 1f ;
2026-05-01 16:51:08 +07:00
foreach ( var kvp in _subRings ) kvp . Value . label . text = $"{Mathf.RoundToInt(PlayerPrefs.GetFloat(kvp.Key, 80f))}%" ;
2026-05-01 01:25:02 +07:00
_masterVolLabel . text = $"{Mathf.RoundToInt(_masterVol)}%" ;
int currentId = _overlayActiveCount ;
2026-05-01 16:51:08 +07:00
await Task . Delay ( 3000 ) ;
2026-05-01 01:25:02 +07:00
if ( currentId = = _overlayActiveCount & & _hoveredSubVolume = = null )
2026-04-30 20:58:59 +07:00
{
2026-05-01 01:25:02 +07:00
Tween . Custom ( 1f , 0f , duration : 0.5f , onValueChange : val = > _volumeContainer . style . opacity = val )
2026-05-01 16:51:08 +07:00
. OnComplete ( ( ) = > { if ( _volumeContainer . style . opacity = = 0f ) _volumeContainer . style . display = DisplayStyle . None ; } ) ;
2026-04-30 20:58:59 +07:00
}
}
2026-04-29 01:04:28 +07:00
private void SetupTab ( string btnName , string tabId )
{
var btn = root . Q < Button > ( btnName ) ;
2026-05-01 16:51:08 +07:00
if ( btn ! = null ) { _tabButtons [ tabId ] = btn ; btn . clicked + = ( ) = > SwitchTab ( tabId ) ; }
2026-04-25 18:20:16 +07:00
}
2026-04-29 01:04:28 +07:00
private void SwitchTab ( string tabId )
2026-04-25 18:20:16 +07:00
{
2026-05-01 16:51:08 +07:00
_activeTab = tabId ; _tabTitle . text = tabId ;
2026-04-29 01:04:28 +07:00
foreach ( var kvp in _tabButtons )
{
if ( kvp . Key = = tabId ) kvp . Value . AddToClassList ( "active-tab" ) ;
else kvp . Value . RemoveFromClassList ( "active-tab" ) ;
}
2026-05-01 16:51:08 +07:00
_content . Clear ( ) ; _hoveredSlider = null ;
2026-04-29 01:04:28 +07:00
switch ( tabId )
{
2026-04-30 20:58:59 +07:00
case "GENERAL" : RenderGeneralTab ( ) ; break ;
case "VIDEO" : RenderVideoTab ( ) ; break ;
case "SOUND" : RenderSoundTab ( ) ; break ;
case "CONTROL" : RenderControlTab ( ) ; break ;
2026-04-29 01:04:28 +07:00
}
}
2026-04-30 20:58:59 +07:00
private void RenderGeneralTab ( )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
_content . Add ( CreateSection ( "ACCOUNT" ) ) ;
var userRow = new VisualElement { style = { flexDirection = FlexDirection . Row , alignItems = Align . Center , marginBottom = 10 } } ;
2026-05-01 16:51:08 +07:00
var loggedInLabel = new Label ( "Logged in as: " ) ; loggedInLabel . AddToClassList ( "text-body" ) ;
2026-04-30 21:46:37 +07:00
userRow . Add ( loggedInLabel ) ;
2026-05-01 16:51:08 +07:00
userRow . Add ( new Label ( PlayerPrefs . GetString ( "Username" , "Guest" ) ) { style = { color = Color . cyan , marginLeft = 5 , unityFontStyleAndWeight = FontStyle . Bold } } ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( userRow ) ;
_content . Add ( CreateSection ( "LANGUAGE" ) ) ;
2026-05-01 16:51:08 +07:00
var langDropdown = new DropdownField ( new List < string > { "English" , "Tiếng Việt" } , LocalizationManager . Instance ? . CurrentLanguage = = "vi" ? 1 : 0 ) ;
2026-04-30 20:58:59 +07:00
langDropdown . AddToClassList ( "custom-dropdown" ) ;
2026-04-29 02:31:15 +07:00
langDropdown . RegisterValueChangedCallback ( evt = > {
2026-05-01 16:51:08 +07:00
LocalizationManager . Instance ? . LoadLanguage ( evt . newValue = = "Tiếng Việt" ? "vi" : "en" ) ; SwitchTab ( "GENERAL" ) ;
2026-04-29 02:31:15 +07:00
} ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( langDropdown ) ;
_content . Add ( CreateSection ( "UPDATES" ) ) ;
var versionBox = new VisualElement { style = { flexDirection = FlexDirection . Row , alignItems = Align . Center } } ;
2026-05-01 16:51:08 +07:00
var versionLabel = new Label ( $"Version: {Application.version}" ) ; versionLabel . AddToClassList ( "text-body" ) ;
2026-04-30 21:46:37 +07:00
versionBox . Add ( versionLabel ) ;
2026-05-01 16:51:08 +07:00
var checkBtn = new Button { text = "CHECK FOR UPDATES" } ; checkBtn . AddToClassList ( "button-spring" ) ;
2026-04-30 20:58:59 +07:00
checkBtn . clicked + = ( ) = > checkBtn . text = "UP TO DATE" ;
versionBox . Add ( checkBtn ) ;
_content . Add ( versionBox ) ;
_content . Add ( CreateSection ( "CURSOR & MOUSE" ) ) ;
2026-05-01 15:08:59 +07:00
_content . Add ( CreateSliderWithInput ( "Cursor Size" , 10 , 150 , PlayerPrefs . GetFloat ( "CursorSize" , 40 ) , val = > uiManager . SetCursorSize ( val ) ) ) ;
2026-04-30 20:58:59 +07:00
var trailToggle = new Toggle ( "Enable Cursor Trail" ) { value = PlayerPrefs . GetInt ( "CursorTrail" , 1 ) = = 1 } ;
2026-05-01 15:08:59 +07:00
trailToggle . RegisterValueChangedCallback ( evt = > uiManager . SetCursorTrail ( evt . newValue ) ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( trailToggle ) ;
var rippleToggle = new Toggle ( "Enable Ripple Effects" ) { value = PlayerPrefs . GetInt ( "CursorRipples" , 1 ) = = 1 } ;
2026-05-01 15:08:59 +07:00
rippleToggle . RegisterValueChangedCallback ( evt = > uiManager . SetCursorRipples ( evt . newValue ) ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( rippleToggle ) ;
2026-05-01 15:08:59 +07:00
_content . Add ( CreateSliderWithInput ( "Sensitivity" , 0.1f , 5.0f , PlayerPrefs . GetFloat ( "MouseSensitivity" , 1.0f ) , val = > uiManager . SetMouseSensitivity ( val ) ) ) ;
2026-05-01 16:51:08 +07:00
_content . Add ( new Toggle ( "Raw Input (Bypass Acceleration)" ) { value = true } ) ;
2026-04-30 20:58:59 +07:00
_mouseMetricsLabel = new Label ( "[(report: 0/sec latency: 0ms)]" ) { style = { fontSize = 11 , color = Color . gray , marginTop = 5 } } ;
_content . Add ( _mouseMetricsLabel ) ;
}
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
private void RenderVideoTab ( )
{
_content . Add ( CreateSection ( "RENDERER" ) ) ;
2026-05-01 16:51:08 +07:00
var frameLimit = new DropdownField ( "Frame Limiter" , new List < string > { "VSync" , "Power Saving" , "Optimal" , "Unlimited" } , PlayerPrefs . GetInt ( "FrameLimiter" , 2 ) ) ;
frameLimit . RegisterValueChangedCallback ( evt = > ApplyFrameLimit ( frameLimit . index ) ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( frameLimit ) ;
var fpsToggle = new Toggle ( "Show FPS Counter" ) { value = _fpsVisible } ;
2026-05-01 16:51:08 +07:00
fpsToggle . RegisterValueChangedCallback ( evt = > { _fpsVisible = evt . newValue ; PlayerPrefs . SetInt ( "ShowFPS" , _fpsVisible ? 1 : 0 ) ; PerformanceOverlay . SetVisible ( _fpsVisible ) ; } ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( fpsToggle ) ;
_content . Add ( CreateSection ( "LAYOUT" ) ) ;
Resolution native = Screen . currentResolution ;
var resList = Screen . resolutions . Select ( r = > $"{r.width}x{r.height}" ) . Distinct ( ) . Select ( s = > s = = $"{native.width}x{native.height}" ? s + " (native)" : s ) . ToList ( ) ;
2026-05-01 02:25:25 +07:00
string currentResStr = $"{Screen.width}x{Screen.height}" ;
int currentResIdx = resList . FindIndex ( s = > s . StartsWith ( currentResStr ) ) ;
if ( currentResIdx = = - 1 ) currentResIdx = resList . FindIndex ( s = > s . Contains ( "native" ) ) ;
var resDropdown = new DropdownField ( "Resolution" , resList , currentResIdx ) ;
2026-04-30 20:58:59 +07:00
resDropdown . RegisterValueChangedCallback ( evt = > {
string [ ] parts = evt . newValue . Split ( ' ' ) [ 0 ] . Split ( 'x' ) ;
2026-05-01 16:51:08 +07:00
int w = int . Parse ( parts [ 0 ] ) , h = int . Parse ( parts [ 1 ] ) ;
2026-05-01 02:25:25 +07:00
Screen . SetResolution ( w , h , Screen . fullScreen ) ;
2026-05-01 16:51:08 +07:00
PlayerPrefs . SetInt ( "ScreenWidth" , w ) ; PlayerPrefs . SetInt ( "ScreenHeight" , h ) ;
2026-04-29 02:31:15 +07:00
} ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( resDropdown ) ;
var fullToggle = new Toggle ( "Fullscreen Mode" ) { value = Screen . fullScreen } ;
2026-05-01 16:51:08 +07:00
fullToggle . RegisterValueChangedCallback ( evt = > { Screen . fullScreen = evt . newValue ; PlayerPrefs . SetInt ( "Fullscreen" , evt . newValue ? 1 : 0 ) ; } ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( fullToggle ) ;
2026-05-01 02:25:25 +07:00
_content . Add ( CreateSliderWithInput ( "Background Dim" , 0 , 100 , PlayerPrefs . GetFloat ( "BackgroundDim" , 50 ) , val = > ApplyBackgroundDim ( val ) ) ) ;
2026-04-30 20:58:59 +07:00
_content . Add ( CreateSliderWithInput ( "UI Scale" , 0.5f , 2.0f , PlayerPrefs . GetFloat ( "UIScale" , 1.0f ) , val = > uiManager . SetUIScale ( val ) ) ) ;
2026-04-29 02:31:15 +07:00
}
2026-04-30 20:58:59 +07:00
private void RenderSoundTab ( )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
_content . Add ( CreateSection ( "AUDIO VOLUMES" ) ) ;
_content . Add ( CreateAudioSlider ( "Master" , "MasterVolume" ) ) ;
_content . Add ( CreateAudioSlider ( "Music" , "MusicVolume" ) ) ;
_content . Add ( CreateAudioSlider ( "VFX" , "VFXVolume" ) ) ;
_content . Add ( CreateAudioSlider ( "Player" , "PlayerVolume" ) ) ;
_content . Add ( CreateAudioSlider ( "UI" , "UIVolume" ) ) ;
_content . Add ( new Label ( "Use Scroll Wheel to control volume." ) { style = { marginTop = 20 , color = Color . gray , fontSize = 12 } } ) ;
}
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
private VisualElement CreateAudioSlider ( string label , string prefKey )
{
2026-05-01 01:25:02 +07:00
var sliderRow = CreateSliderWithInput ( label , 0 , 100 , PlayerPrefs . GetFloat ( prefKey , 80 ) , val = > {
2026-05-01 16:51:08 +07:00
PlayerPrefs . SetFloat ( prefKey , val ) ; AudioManager . Instance ? . SetVolume ( prefKey , val ) ;
2026-05-01 01:25:02 +07:00
} ) ;
2026-04-30 20:58:59 +07:00
sliderRow . RegisterCallback < WheelEvent > ( evt = > {
2026-05-01 16:51:08 +07:00
float newVal = Mathf . Clamp ( PlayerPrefs . GetFloat ( prefKey , 80f ) - ( evt . delta . y * 2f ) , 0f , 100f ) ;
PlayerPrefs . SetFloat ( prefKey , newVal ) ; AudioManager . Instance ? . SetVolume ( prefKey , newVal ) ;
var slider = sliderRow . Q < Slider > ( ) ; if ( slider ! = null ) slider . value = newVal ;
2026-04-30 20:58:59 +07:00
} ) ;
return sliderRow ;
2026-04-29 01:04:28 +07:00
}
2026-04-30 20:58:59 +07:00
private void RenderControlTab ( )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
_content . Add ( CreateSection ( "KEY BINDINGS" ) ) ;
2026-05-01 16:51:08 +07:00
if ( uiManager . InputReader ? . InputActions = = null ) { _content . Add ( new Label ( "Input Actions not found." ) { style = { color = Color . white } } ) ; return ; }
2026-05-01 02:25:25 +07:00
foreach ( var map in uiManager . InputReader . InputActions . actionMaps )
{
2026-05-01 16:51:08 +07:00
var mapHeader = new Label ( map . name . ToUpper ( ) ) { style = { fontSize = 14 , unityFontStyleAndWeight = FontStyle . Bold , color = Color . cyan , marginTop = 15 , marginBottom = 5 } } ;
2026-05-01 02:25:25 +07:00
_content . Add ( mapHeader ) ;
foreach ( var action in map . actions )
{
2026-05-01 16:51:08 +07:00
if ( action . name = = "Look" | | action . name = = "Scroll" | | action . name = = "Navigate" | | action . name = = "Point" | | action . name = = "Click" ) continue ;
2026-05-01 02:25:25 +07:00
if ( action . bindings . Any ( b = > b . isComposite ) )
{
for ( int i = 0 ; i < action . bindings . Count ; i + + )
2026-05-01 16:51:08 +07:00
if ( action . bindings [ i ] . isPartOfComposite & & action . bindings [ i ] . groups . Contains ( "Keyboard&Mouse" ) )
_content . Add ( CreateRebindRow ( action , i , $"{action.name} {action.bindings[i].name}" . ToUpper ( ) ) ) ;
2026-05-01 02:25:25 +07:00
}
else
{
2026-05-01 16:51:08 +07:00
int idx = action . bindings . ToList ( ) . FindIndex ( b = > b . groups . Contains ( "Keyboard&Mouse" ) ) ;
if ( idx ! = - 1 ) _content . Add ( CreateRebindRow ( action , idx , action . name . ToUpper ( ) ) ) ;
2026-05-01 02:25:25 +07:00
}
}
}
2026-05-01 16:51:08 +07:00
var resetBtn = new Button { text = "RESET ALL TO DEFAULT" } ; resetBtn . AddToClassList ( "button-spring" ) ; resetBtn . style . marginTop = 30 ; resetBtn . style . alignSelf = Align . Center ;
resetBtn . clicked + = ( ) = > { uiManager . InputReader . ResetBindings ( ) ; SwitchTab ( "CONTROL" ) ; } ;
2026-05-01 02:25:25 +07:00
_content . Add ( resetBtn ) ;
}
2026-05-01 16:51:08 +07:00
2026-05-01 02:25:25 +07:00
private VisualElement CreateRebindRow ( UnityEngine . InputSystem . InputAction action , int bindingIndex , string labelText )
{
2026-05-01 16:51:08 +07:00
var row = new VisualElement ( ) ; row . AddToClassList ( "rebind-row" ) ; row . style . flexDirection = FlexDirection . Row ; row . style . justifyContent = Justify . SpaceBetween ; row . style . alignItems = Align . Center ; row . style . paddingTop = row . style . paddingBottom = 10 ; row . style . borderBottomWidth = 1 ; row . style . borderBottomColor = new Color ( 1 , 1 , 1 , 0.1f ) ;
var label = new Label ( labelText ) ; label . AddToClassList ( "rebind-label" ) ; label . style . color = Color . white ; row . Add ( label ) ;
var btn = new Button { text = action . GetBindingDisplayString ( bindingIndex ) . ToUpper ( ) } ; btn . AddToClassList ( "rebind-button" ) ; btn . style . width = 150 ;
btn . clicked + = ( ) = > StartRebinding ( action , bindingIndex , btn ) ; row . Add ( btn ) ;
2026-05-01 02:25:25 +07:00
return row ;
}
private void StartRebinding ( UnityEngine . InputSystem . InputAction action , int bindingIndex , Button btn )
{
2026-05-01 16:51:08 +07:00
btn . text = "> <" ; btn . style . color = Color . yellow ; action . actionMap . Disable ( ) ;
var op = action . PerformInteractiveRebinding ( bindingIndex ) . WithControlsExcluding ( "<Mouse>/position" ) . WithControlsExcluding ( "<Mouse>/delta" ) . WithControlsExcluding ( "<Keyboard>/escape" ) . OnMatchWaitForAnother ( 0.1f )
. OnComplete ( operation = > { btn . text = action . GetBindingDisplayString ( bindingIndex ) . ToUpper ( ) ; btn . style . color = Color . white ; operation . Dispose ( ) ; action . actionMap . Enable ( ) ; uiManager . InputReader . SaveBindings ( ) ; } )
. OnCancel ( operation = > { btn . text = action . GetBindingDisplayString ( bindingIndex ) . ToUpper ( ) ; btn . style . color = Color . white ; operation . Dispose ( ) ; action . actionMap . Enable ( ) ; } ) ;
op . Start ( ) ;
2026-04-29 01:04:28 +07:00
}
2026-05-01 16:51:08 +07:00
private VisualElement CreateSection ( string title ) { var label = new Label ( title ) ; label . AddToClassList ( "setting-section-header" ) ; label . style . marginTop = 20 ; return label ; }
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
private VisualElement CreateSliderWithInput ( string labelText , float min , float max , float startVal , Action < float > OnValueChanged )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
var row = new VisualElement { style = { flexDirection = FlexDirection . Row , alignItems = Align . Center , marginTop = 5 , marginBottom = 5 } } ;
2026-05-01 16:51:08 +07:00
var label = new Label ( labelText ) { style = { width = Length . Percent ( 35 ) } } ; label . AddToClassList ( "text-body" ) ;
2026-04-30 20:58:59 +07:00
var slider = new Slider ( min , max ) { value = startVal , style = { flexGrow = 1 } } ;
2026-05-01 16:51:08 +07:00
var input = new TextField { value = startVal . ToString ( "F1" ) , style = { width = 50 , marginLeft = 10 } } ; input . AddToClassList ( "input-field" ) ;
slider . RegisterCallback < PointerEnterEvent > ( evt = > { _hoveredSlider = slider ; _hoveredOnChanged = OnValueChanged ; _sliderMin = min ; _sliderMax = max ; } ) ;
slider . RegisterCallback < PointerLeaveEvent > ( evt = > { if ( _hoveredSlider = = slider ) { _hoveredSlider = null ; _hoveredOnChanged = null ; } } ) ;
slider . RegisterValueChangedCallback ( evt = > { float val = Mathf . Round ( evt . newValue * 10f ) / 10f ; if ( input . panel ? . focusController ? . focusedElement ! = input . ElementAt ( 0 ) ) input . value = val . ToString ( "F1" ) ; OnValueChanged ? . Invoke ( val ) ; } ) ;
input . RegisterValueChangedCallback ( evt = > { if ( float . TryParse ( evt . newValue , out float val ) ) { slider . value = Mathf . Clamp ( val , min , max ) ; OnValueChanged ? . Invoke ( slider . value ) ; } } ) ;
row . Add ( label ) ; row . Add ( slider ) ; row . Add ( input ) ; return row ;
2026-04-29 01:04:28 +07:00
}
2026-04-30 20:58:59 +07:00
private void OnKeyDown ( KeyDownEvent evt )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
if ( _hoveredSlider = = null ) return ;
float step = ( _sliderMax - _sliderMin ) / 100f ;
if ( evt . keyCode = = KeyCode . LeftArrow ) _hoveredSlider . value - = step ;
if ( evt . keyCode = = KeyCode . RightArrow ) _hoveredSlider . value + = step ;
2026-04-29 01:04:28 +07:00
}
2026-04-30 20:58:59 +07:00
public override void Update ( )
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
if ( _activeTab = = "GENERAL" & & _mouseMetricsLabel ! = null )
{
var ( polling , latency ) = MouseMetricsHelper . GetMetrics ( ) ;
_mouseMetricsLabel . text = $"[(report: {polling}/sec latency: {latency:F0}ms)]" ;
}
2026-04-28 00:07:42 +07:00
}
2026-04-26 05:02:49 +07:00
2026-04-28 00:07:42 +07:00
public override async Task PlayTransitionIn ( )
{
2026-04-30 20:58:59 +07:00
root . style . display = DisplayStyle . Flex ;
2026-04-28 00:07:42 +07:00
_sidebar . style . translate = new StyleTranslate ( new Translate ( Length . Percent ( - 100 ) , 0 ) ) ;
2026-04-30 21:46:37 +07:00
await Tween . Custom ( - 100f , 0f , duration : 0.4f , ease : Ease . OutQuad , onValueChange : val = > _sidebar . style . translate = new StyleTranslate ( new Translate ( Length . Percent ( val ) , 0 ) ) ) ;
2026-04-28 00:07:42 +07:00
}
2026-04-26 05:02:49 +07:00
2026-04-28 00:07:42 +07:00
public override async Task PlayTransitionOut ( )
{
2026-04-30 21:46:37 +07:00
await Tween . Custom ( 0f , - 100f , duration : 0.3f , ease : Ease . InQuad , onValueChange : val = > _sidebar . style . translate = new StyleTranslate ( new Translate ( Length . Percent ( val ) , 0 ) ) ) ;
2026-04-28 00:07:42 +07:00
Hide ( ) ;
2026-04-25 18:20:16 +07:00
}
}
}