Files
BABA_YAGA/Assets/Scripts/UI/SettingsController.cs

403 lines
18 KiB
C#
Raw Normal View History

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-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;
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-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
private VisualElement _volumeOverlay;
private Label _masterVolLabel;
private float _masterVol = 80f;
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");
_tabTitle = root.Q<Label>("TabTitle");
_content = root.Q<ScrollView>("SettingsContent");
2026-04-25 18:20:16 +07:00
2026-04-30 20:58:59 +07:00
// Osu Volume Logic - Registering on Root for Global Wheel Catch
root.RegisterCallback<WheelEvent>(OnMouseWheel);
SetupVolumeOverlay();
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
// Keyboard navigation for sliders
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-04-29 01:04:28 +07:00
SwitchTab("GENERAL");
}
2026-04-30 20:58:59 +07:00
private void SetupVolumeOverlay()
{
_volumeOverlay = new VisualElement();
_volumeOverlay.style.position = Position.Absolute;
_volumeOverlay.style.right = 40;
_volumeOverlay.style.top = Length.Percent(40);
_volumeOverlay.style.width = 120;
_volumeOverlay.style.height = 120;
_volumeOverlay.style.backgroundColor = new Color(0, 0, 0, 0.8f);
_volumeOverlay.style.borderTopLeftRadius = 60;
_volumeOverlay.style.borderTopRightRadius = 60;
_volumeOverlay.style.borderBottomLeftRadius = 60;
_volumeOverlay.style.borderBottomRightRadius = 60;
_volumeOverlay.style.borderTopWidth = 4;
_volumeOverlay.style.borderBottomWidth = 4;
_volumeOverlay.style.borderLeftWidth = 4;
_volumeOverlay.style.borderRightWidth = 4;
_volumeOverlay.style.borderTopColor = Color.cyan;
_volumeOverlay.style.borderBottomColor = Color.cyan;
_volumeOverlay.style.borderLeftColor = Color.cyan;
_volumeOverlay.style.borderRightColor = Color.cyan;
_volumeOverlay.style.justifyContent = Justify.Center;
_volumeOverlay.style.alignItems = Align.Center;
_volumeOverlay.style.display = DisplayStyle.None;
_volumeOverlay.pickingMode = PickingMode.Ignore;
_masterVolLabel = new Label("80%");
_masterVolLabel.style.color = Color.white;
_masterVolLabel.style.fontSize = 24;
_masterVolLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_volumeOverlay.Add(_masterVolLabel);
root.Add(_volumeOverlay);
}
private void OnMouseWheel(WheelEvent evt)
{
// Osu style: Volume control with scroll wheel
2026-04-30 21:46:37 +07:00
// Only apply if in the SOUND tab
if (_activeTab != "SOUND") return;
2026-04-30 20:58:59 +07:00
2026-04-30 21:46:37 +07:00
if (_hoveredSlider != null)
{
// Adjust the hovered slider's value
float currentVal = _hoveredSlider.value;
// Determine step size: default to 1% of range, adjusted for 0-100 range.
float step = (_sliderMax - _sliderMin) / 100f;
float newVal = Mathf.Clamp(currentVal - (evt.delta.y * step * 5f), _sliderMin, _sliderMax); // Multiply by a factor to make scroll smoother
_hoveredSlider.value = newVal;
// Trigger the associated OnValueChanged callback to save PlayerPrefs etc.
_hoveredOnChanged?.Invoke(newVal);
evt.StopPropagation(); // Consume the event so it doesn't affect other elements
}
else
{
// If not hovering a specific slider, control Master Volume
UpdateMasterVolume(-evt.delta.y * 2f);
evt.StopPropagation(); // Consume the event
}
2026-04-30 20:58:59 +07:00
}
private void UpdateMasterVolume(float delta)
{
_masterVol = Mathf.Clamp(_masterVol + delta, 0f, 100f);
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
ShowVolumeOverlay();
}
private async void ShowVolumeOverlay()
{
_volumeOverlay.style.display = DisplayStyle.Flex;
_volumeOverlay.style.opacity = 1f;
await Task.Delay(1500);
if (_volumeOverlay.style.opacity == 1f)
{
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeOverlay.style.opacity = val)
.OnComplete(() => _volumeOverlay.style.display = DisplayStyle.None);
}
}
2026-04-29 01:04:28 +07:00
private void SetupTab(string btnName, string tabId)
{
var btn = root.Q<Button>(btnName);
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-04-29 01:04:28 +07:00
_activeTab = tabId;
_tabTitle.text = tabId;
foreach (var kvp in _tabButtons)
{
if (kvp.Key == tabId) kvp.Value.AddToClassList("active-tab");
else kvp.Value.RemoveFromClassList("active-tab");
}
2026-04-28 00:07:42 +07:00
_content.Clear();
2026-04-30 20:58:59 +07:00
_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
#region GENERAL TAB
private void RenderGeneralTab()
2026-04-29 01:04:28 +07:00
{
2026-04-30 20:58:59 +07:00
_content.Add(CreateSection("ACCOUNT"));
string username = PlayerPrefs.GetString("Username", "Guest");
var userRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 10 } };
2026-04-30 21:46:37 +07:00
var loggedInLabel = new Label("Logged in as: ");
loggedInLabel.AddToClassList("text-body");
userRow.Add(loggedInLabel);
2026-04-30 20:58:59 +07:00
userRow.Add(new Label(username) { style = { color = Color.cyan, marginLeft = 5, unityFontStyleAndWeight = FontStyle.Bold } });
_content.Add(userRow);
_content.Add(CreateSection("LANGUAGE"));
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" },
LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
langDropdown.AddToClassList("custom-dropdown");
2026-04-29 02:31:15 +07:00
langDropdown.RegisterValueChangedCallback(evt => {
2026-04-30 20:58:59 +07:00
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en");
2026-04-29 02:31:15 +07:00
SwitchTab("GENERAL");
});
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-04-30 21:46:37 +07:00
var versionLabel = new Label($"Version: {Application.version}");
versionLabel.AddToClassList("text-body");
versionBox.Add(versionLabel);
2026-04-30 20:58:59 +07:00
var checkBtn = new Button { text = "CHECK FOR UPDATES" };
checkBtn.AddToClassList("button-spring");
checkBtn.clicked += () => checkBtn.text = "UP TO DATE";
versionBox.Add(checkBtn);
_content.Add(versionBox);
_content.Add(CreateSection("CURSOR & MOUSE"));
_content.Add(CreateSliderWithInput("Cursor Size", 10, 150, PlayerPrefs.GetFloat("CursorSize", 40), val => PlayerPrefs.SetFloat("CursorSize", val)));
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
var trailToggle = new Toggle("Enable Cursor Trail") { value = PlayerPrefs.GetInt("CursorTrail", 1) == 1 };
trailToggle.RegisterValueChangedCallback(evt => PlayerPrefs.SetInt("CursorTrail", evt.newValue ? 1 : 0));
_content.Add(trailToggle);
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
var rippleToggle = new Toggle("Enable Ripple Effects") { value = PlayerPrefs.GetInt("CursorRipples", 1) == 1 };
rippleToggle.RegisterValueChangedCallback(evt => PlayerPrefs.SetInt("CursorRipples", evt.newValue ? 1 : 0));
_content.Add(rippleToggle);
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
_content.Add(CreateSliderWithInput("Sensitivity", 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => PlayerPrefs.SetFloat("MouseSensitivity", val)));
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
var rawInputToggle = new Toggle("Raw Input (Bypass Acceleration)") { value = true };
_content.Add(rawInputToggle);
2026-04-29 02:31:15 +07:00
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);
}
#endregion
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
#region VIDEO TAB
private void RenderVideoTab()
{
_content.Add(CreateSection("RENDERER"));
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, 2);
frameLimit.RegisterValueChangedCallback(evt => {
switch (evt.newValue) {
case "VSync": QualitySettings.vSyncCount = 1; Application.targetFrameRate = -1; break;
case "Power Saving": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 60; break;
case "Optimal": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 144; break;
case "Unlimited": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 999; break;
2026-04-29 02:31:15 +07:00
}
});
2026-04-30 20:58:59 +07:00
_content.Add(frameLimit);
var fpsToggle = new Toggle("Show FPS Counter") { value = _fpsVisible };
fpsToggle.RegisterValueChangedCallback(evt => { _fpsVisible = evt.newValue; PerformanceOverlay.SetVisible(_fpsVisible); });
_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();
var resDropdown = new DropdownField("Resolution", resList, resList.FindIndex(s => s.Contains("native")));
resDropdown.RegisterValueChangedCallback(evt => {
string[] parts = evt.newValue.Split(' ')[0].Split('x');
Screen.SetResolution(int.Parse(parts[0]), int.Parse(parts[1]), Screen.fullScreen);
2026-04-29 02:31:15 +07:00
});
2026-04-30 20:58:59 +07:00
_content.Add(resDropdown);
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
var fullToggle = new Toggle("Fullscreen Mode") { value = Screen.fullScreen };
fullToggle.RegisterValueChangedCallback(evt => Screen.fullScreen = evt.newValue);
_content.Add(fullToggle);
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => PlayerPrefs.SetFloat("BackgroundDim", val)));
_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
#endregion
2026-04-29 02:31:15 +07:00
2026-04-30 20:58:59 +07:00
#region SOUND TAB
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)
{
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => PlayerPrefs.SetFloat(prefKey, val));
// Register wheel specifically on this row
sliderRow.RegisterCallback<WheelEvent>(evt => {
float current = PlayerPrefs.GetFloat(prefKey, 80f);
float newVal = Mathf.Clamp(current - (evt.delta.y * 2f), 0f, 100f);
PlayerPrefs.SetFloat(prefKey, newVal);
// Visual update only (to avoid heavy re-render of whole list)
var slider = sliderRow.Q<Slider>();
if (slider != null) slider.value = newVal;
});
2026-04-29 01:04:28 +07:00
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
#endregion
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
#region CONTROL TAB
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-04-30 21:46:37 +07:00
var pendingLabel = new Label("Controls Implementation Pending context.");
pendingLabel.AddToClassList("text-body");
_content.Add(pendingLabel);
2026-04-29 01:04:28 +07:00
}
2026-04-30 20:58:59 +07:00
#endregion
2026-04-29 01:04:28 +07:00
private VisualElement CreateSection(string title)
{
2026-04-30 20:58:59 +07:00
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 } };
var label = new Label(labelText) { style = { width = Length.Percent(35) } };
label.AddToClassList("text-body");
var slider = new Slider(min, max) { value = startVal, style = { flexGrow = 1 } };
var input = new TextField { value = startVal.ToString("F1"), style = { width = 50, marginLeft = 10 } };
input.AddToClassList("input-field");
2026-04-29 01:04:28 +07:00
2026-04-30 21:46:37 +07:00
slider.RegisterCallback<PointerEnterEvent>(evt => {
_hoveredSlider = slider;
_hoveredOnChanged = OnValueChanged;
_sliderMin = min;
_sliderMax = max;
});
slider.RegisterCallback<PointerLeaveEvent>(evt => {
if (_hoveredSlider == slider) {
_hoveredSlider = null;
_hoveredOnChanged = null;
}
});
2026-04-30 20:58:59 +07:00
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);
});
2026-04-29 01:04:28 +07:00
2026-04-30 20:58:59 +07:00
input.RegisterValueChangedCallback(evt => {
if (float.TryParse(evt.newValue, out float val)) {
slider.value = Mathf.Clamp(val, min, max);
OnValueChanged?.Invoke(slider.value);
}
});
2026-04-29 01:04:28 +07:00
row.Add(label);
2026-04-30 20:58:59 +07:00
row.Add(slider);
row.Add(input);
2026-04-29 01:04:28 +07:00
return row;
}
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
}
}
}