Merge branch 'main' of https://scove-vault.duckdns.org/scove/HALLUCINATION
This commit is contained in:
@@ -13,6 +13,10 @@ namespace Hallucinate.UI
|
||||
{
|
||||
public static BasicSpawner Instance { get; private set; }
|
||||
private NetworkRunner _runner;
|
||||
public NetworkRunner Runner => _runner;
|
||||
|
||||
private bool _isStarting = false;
|
||||
private bool _isInternalShutdown = false;
|
||||
|
||||
public event Action<List<SessionInfo>> OnSessionListUpdatedEvent;
|
||||
public event Action<string> OnShutdownEvent;
|
||||
@@ -42,120 +46,173 @@ namespace Hallucinate.UI
|
||||
|
||||
private async Task EnsureRunnerExists()
|
||||
{
|
||||
if (_runner == null)
|
||||
if (_runner != null)
|
||||
{
|
||||
_runner = GetComponent<NetworkRunner>();
|
||||
}
|
||||
|
||||
if (_runner != null && _runner.IsRunning)
|
||||
{
|
||||
await _runner.Shutdown();
|
||||
}
|
||||
|
||||
if (_runner == null)
|
||||
{
|
||||
_runner = gameObject.AddComponent<NetworkRunner>();
|
||||
_isInternalShutdown = true;
|
||||
try
|
||||
{
|
||||
if (_runner.IsRunning)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Shutting down existing runner before recreation.");
|
||||
await _runner.Shutdown();
|
||||
}
|
||||
|
||||
Debug.Log("[BasicSpawner] Destroying existing runner component.");
|
||||
Destroy(_runner);
|
||||
_runner = null;
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isInternalShutdown = false;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("[BasicSpawner] Creating new NetworkRunner component.");
|
||||
_runner = gameObject.AddComponent<NetworkRunner>();
|
||||
_runner.ProvideInput = true;
|
||||
_runner.RemoveCallbacks(this);
|
||||
_runner.AddCallbacks(this);
|
||||
}
|
||||
|
||||
public async Task StartLobby()
|
||||
{
|
||||
await EnsureRunnerExists();
|
||||
if (_isStarting) return;
|
||||
|
||||
if (_runner.SessionInfo.IsValid) return;
|
||||
// Nếu đã ở trong lobby rồi thì không cần làm gì
|
||||
if (_runner != null && _runner.IsRunning && _runner.LobbyInfo.IsValid) return;
|
||||
|
||||
var result = await _runner.JoinSessionLobby(SessionLobby.ClientServer);
|
||||
if (!result.Ok)
|
||||
Debug.Log("[BasicSpawner] StartLobby called");
|
||||
_isStarting = true;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.LogWarning($"Join lobby result: {result.ShutdownReason}. This is often normal on first run if already connecting.");
|
||||
await EnsureRunnerExists();
|
||||
Debug.Log("[BasicSpawner] Joining Lobby...");
|
||||
var result = await _runner.JoinSessionLobby(SessionLobby.ClientServer);
|
||||
if (!result.Ok)
|
||||
{
|
||||
Debug.LogWarning($"Join lobby result: {result.ShutdownReason}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StartHost(string sessionName, string displayName, string password = null)
|
||||
{
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
if (_isStarting) return false;
|
||||
_isStarting = true;
|
||||
|
||||
bool sceneExists = false;
|
||||
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCountInBuildSettings; i++)
|
||||
try
|
||||
{
|
||||
if (UnityEngine.SceneManagement.SceneUtility.GetScenePathByBuildIndex(i).Contains("Main Scene"))
|
||||
Debug.Log($"[BasicSpawner] StartHost called: {sessionName} ({displayName})");
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
|
||||
bool sceneExists = false;
|
||||
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCountInBuildSettings; i++)
|
||||
{
|
||||
sceneExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sceneExists)
|
||||
{
|
||||
Debug.LogError("CRITICAL: 'Main Scene' is NOT in Build Settings!");
|
||||
return false;
|
||||
}
|
||||
|
||||
await EnsureRunnerExists();
|
||||
|
||||
var customProps = new Dictionary<string, SessionProperty>();
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
customProps.Add("pw", password);
|
||||
}
|
||||
customProps.Add("rn", displayName);
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Host,
|
||||
SessionName = sessionName,
|
||||
SessionProperties = customProps,
|
||||
PlayerCount = 2,
|
||||
SceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
if (_runner.IsServer && _playerDataManagerPrefab.IsValid)
|
||||
{
|
||||
if (FindFirstObjectByType<PlayerDataManager>() == null)
|
||||
if (UnityEngine.SceneManagement.SceneUtility.GetScenePathByBuildIndex(i).Contains("Main Scene"))
|
||||
{
|
||||
_runner.Spawn(_playerDataManagerPrefab, Vector3.zero, Quaternion.identity, null);
|
||||
sceneExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
if (!sceneExists)
|
||||
{
|
||||
Debug.LogError("CRITICAL: 'Main Scene' is NOT in Build Settings!");
|
||||
return false;
|
||||
}
|
||||
|
||||
await EnsureRunnerExists();
|
||||
|
||||
var customProps = new Dictionary<string, SessionProperty>();
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
customProps.Add("pw", password);
|
||||
}
|
||||
customProps.Add("rn", displayName);
|
||||
|
||||
// Re-create or find SceneManager to ensure it matches the new runner
|
||||
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
|
||||
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Host,
|
||||
SessionName = sessionName,
|
||||
SessionProperties = customProps,
|
||||
PlayerCount = 2,
|
||||
SceneManager = sceneManager
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] StartHost SUCCESS");
|
||||
if (_runner.IsServer && _playerDataManagerPrefab.IsValid)
|
||||
{
|
||||
if (FindFirstObjectByType<PlayerDataManager>() == null)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Spawning PlayerDataManager");
|
||||
_runner.Spawn(_playerDataManagerPrefab, Vector3.zero, Quaternion.identity, null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Fusion StartHost Failed: {result.ShutdownReason}.");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
Debug.LogError($"Fusion StartHost Failed: {result.ShutdownReason}.");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StartClient(string sessionName, string password = null)
|
||||
{
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
await EnsureRunnerExists();
|
||||
if (_isStarting) return false;
|
||||
_isStarting = true;
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
try
|
||||
{
|
||||
GameMode = GameMode.Client,
|
||||
SessionName = sessionName,
|
||||
SceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
|
||||
});
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
await EnsureRunnerExists();
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
return true;
|
||||
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
|
||||
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Client,
|
||||
SessionName = sessionName,
|
||||
SceneManager = sceneManager
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Fusion StartClient Failed: {result.ShutdownReason}");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
Debug.LogError($"Fusion StartClient Failed: {result.ShutdownReason}");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
|
||||
|
||||
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
|
||||
@@ -228,6 +285,13 @@ namespace Hallucinate.UI
|
||||
Debug.LogWarning($"[Fusion] Shutdown occurred. Reason: {shutdownReason}");
|
||||
OnShutdownEvent?.Invoke(shutdownReason.ToString());
|
||||
|
||||
// Nếu shutdown là do hệ thống chủ động hủy để tạo runner mới, KHÔNG quay về Menu
|
||||
if (_isInternalShutdown)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Internal shutdown detected, skipping Menu routing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (UIManager.Instance != null)
|
||||
{
|
||||
UIManager.Instance.OnBackToMenu();
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace OnlyScove.Scripts
|
||||
dashDirection = stateMachine.CameraRotation * dashDirection;
|
||||
dashDirection.y = 0;
|
||||
dashDirection.Normalize();
|
||||
|
||||
// Instantly snap rotation to face the dash direction
|
||||
stateMachine.transform.rotation = Quaternion.LookRotation(dashDirection);
|
||||
}
|
||||
|
||||
8
Assets/Scripts/Player.meta
Normal file
8
Assets/Scripts/Player.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5b780c3b8cfd464ead0e292cd8afd02
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Scripts/Player/PlayerCollision.cs
Normal file
16
Assets/Scripts/Player/PlayerCollision.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerCollision : MonoBehaviour
|
||||
{
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/PlayerCollision.cs.meta
Normal file
2
Assets/Scripts/Player/PlayerCollision.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: feba9d7d89c862142aa1614997a3227f
|
||||
@@ -26,9 +26,34 @@ namespace Hallucinate.UI
|
||||
_healthBar = root.Q<ProgressBar>("HealthBar");
|
||||
_staminaBar = root.Q<ProgressBar>("StaminaBar");
|
||||
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
||||
ApplyLocalization();
|
||||
}
|
||||
|
||||
_lastActionTime = Time.time;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
if (LocalizationManager.Instance == null) return;
|
||||
|
||||
root.Query<Label>().ForEach(l => {
|
||||
if (l.text == "HEALTH") l.text = LocalizationManager.Instance.GetLocalizedString("HUD_HEALTH");
|
||||
if (l.text == "STAMINA") l.text = LocalizationManager.Instance.GetLocalizedString("HUD_STAMINA");
|
||||
if (l.text == "MINIMAP") l.text = LocalizationManager.Instance.GetLocalizedString("HUD_MINIMAP");
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateHUD(float health, float stamina)
|
||||
{
|
||||
_healthBar.value = health;
|
||||
@@ -38,8 +63,11 @@ namespace Hallucinate.UI
|
||||
|
||||
public void UpdateStats(int ping, int fps)
|
||||
{
|
||||
root.Q<Label>("PingLabel").text = $"PING: {ping}ms";
|
||||
root.Q<Label>("FPSLabel").text = $"FPS: {fps}";
|
||||
string pingPrefix = LocalizationManager.Instance != null ? LocalizationManager.Instance.GetLocalizedString("HUD_PING_PREFIX") : "PING: ";
|
||||
string fpsPrefix = LocalizationManager.Instance != null ? LocalizationManager.Instance.GetLocalizedString("HUD_FPS_PREFIX") : "FPS: ";
|
||||
|
||||
root.Q<Label>("PingLabel").text = $"{pingPrefix}{ping}ms";
|
||||
root.Q<Label>("FPSLabel").text = $"{fpsPrefix}{fps}";
|
||||
}
|
||||
|
||||
public void WakeUpHUD()
|
||||
|
||||
@@ -109,11 +109,16 @@ namespace Hallucinate.UI
|
||||
_chatInput.RegisterCallback<KeyDownEvent>(OnChatKeyDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
||||
ApplyLocalization();
|
||||
}
|
||||
|
||||
// Đăng ký sự kiện từ Spawner
|
||||
if (BasicSpawner.Instance != null)
|
||||
{
|
||||
RegisterSpawnerEvents();
|
||||
_ = BasicSpawner.Instance.StartLobby();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -127,7 +132,6 @@ namespace Hallucinate.UI
|
||||
BasicSpawner.Instance.OnSessionListUpdatedEvent += UpdateRoomList;
|
||||
BasicSpawner.Instance.OnJoinFailedEvent += () => { if(_joinPassError != null) _joinPassError.style.display = DisplayStyle.Flex; };
|
||||
BasicSpawner.Instance.OnJoinStartedEvent += () => { };
|
||||
_ = BasicSpawner.Instance.StartLobby();
|
||||
}
|
||||
|
||||
private void OnChatKeyDown(KeyDownEvent evt)
|
||||
@@ -180,6 +184,96 @@ namespace Hallucinate.UI
|
||||
box.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
if (LocalizationManager.Instance == null) return;
|
||||
|
||||
// JOIN VIEW
|
||||
var joinHeading = root.Q<Label>(null, "text-heading"); // Header in JoinContainer
|
||||
if (joinHeading != null && _joinContainer.Contains(joinHeading)) joinHeading.text = GetT("LOBBY_FIND_SESSIONS");
|
||||
|
||||
var searchInput = root.Q<TextField>("SearchInput");
|
||||
if (searchInput != null) searchInput.textEdition.placeholder = GetT("LOBBY_SEARCH_PLACEHOLDER");
|
||||
|
||||
var backBtn = root.Q<Button>("BackToMenuBtn");
|
||||
if (backBtn != null) backBtn.text = GetT("LOBBY_BACK");
|
||||
|
||||
var goToCreateBtn = root.Q<Button>("GoToCreateBtn");
|
||||
if (goToCreateBtn != null) goToCreateBtn.text = GetT("LOBBY_CREATE_NEW");
|
||||
|
||||
// CREATE VIEW
|
||||
var createHeading = root.Q<Label>(null, "text-heading"); // Header in CreateContainer
|
||||
// Note: Querying by class might be ambiguous if multiple exist, better to find within container
|
||||
var createHeader = _createContainer?.Q<Label>(null, "text-heading");
|
||||
if (createHeader != null) createHeader.text = GetT("LOBBY_CREATE_HEADER");
|
||||
|
||||
var roomIdLabel = _createContainer?.Q<Label>(null, "text-label"); // First label is usually ID
|
||||
// Since they don't have unique names, we'll try to find them by order or text match
|
||||
_createContainer?.Query<Label>().ForEach(l => {
|
||||
if (l.text.Contains("ROOM ID")) l.text = GetT("LOBBY_ROOM_ID_LABEL");
|
||||
if (l.text.Contains("ROOM NAME")) l.text = GetT("LOBBY_ROOM_NAME_LABEL");
|
||||
});
|
||||
|
||||
if (_roomIDInput != null) _roomIDInput.textEdition.placeholder = GetT("LOBBY_ROOM_ID_PLACEHOLDER");
|
||||
if (_roomNameInput != null) _roomNameInput.textEdition.placeholder = GetT("LOBBY_ROOM_NAME_PLACEHOLDER");
|
||||
if (_passToggle != null) _passToggle.label = GetT("LOBBY_REQUIRE_PASS");
|
||||
if (_roomPassInput != null) _roomPassInput.textEdition.placeholder = GetT("LOBBY_PASS_PLACEHOLDER");
|
||||
|
||||
var cancelCreateBtn = root.Q<Button>("CancelCreateBtn");
|
||||
if (cancelCreateBtn != null) cancelCreateBtn.text = GetT("LOBBY_CANCEL");
|
||||
|
||||
var confirmCreateBtn = root.Q<Button>("ConfirmCreateBtn");
|
||||
if (confirmCreateBtn != null) confirmCreateBtn.text = GetT("LOBBY_CREATE_BTN");
|
||||
|
||||
// LOUNGE VIEW
|
||||
if (_loungeRoomName != null && _loungeRoomName.text == "SESSION NAME")
|
||||
_loungeRoomName.text = GetT("LOBBY_SESSION_NAME_DEFAULT");
|
||||
|
||||
var loungeIdLabel = root.Q<Label>("LoungeID");
|
||||
if (loungeIdLabel != null)
|
||||
{
|
||||
string currentId = loungeIdLabel.text.Replace("ID: ", "");
|
||||
loungeIdLabel.text = GetT("LOBBY_ID_PREFIX") + currentId;
|
||||
}
|
||||
|
||||
var vsLabel = _loungeContainer?.Q<Label>(null); // VS label doesn't have name
|
||||
_loungeContainer?.Query<Label>().ForEach(l => {
|
||||
if (l.text == "VS") l.text = GetT("LOBBY_VS");
|
||||
});
|
||||
|
||||
if (_chatInput != null) _chatInput.textEdition.placeholder = GetT("LOBBY_CHAT_PLACEHOLDER");
|
||||
|
||||
var leaveLoungeBtn = root.Q<Button>("LeaveLoungeBtn");
|
||||
if (leaveLoungeBtn != null) leaveLoungeBtn.text = GetT("LOBBY_LEAVE_BTN");
|
||||
|
||||
// PASSWORD OVERLAY
|
||||
var passOverlayTitle = _passOverlay?.Q<Label>(null, "text-subheading");
|
||||
if (passOverlayTitle != null) passOverlayTitle.text = GetT("LOBBY_PROTECTED_TITLE");
|
||||
|
||||
var passOverlayDesc = _passOverlay?.Q<Label>(null, "text-label");
|
||||
if (passOverlayDesc != null && passOverlayDesc.text.Contains("requires a password"))
|
||||
passOverlayDesc.text = GetT("LOBBY_PROTECTED_DESC");
|
||||
|
||||
if (_joinPassInput != null) _joinPassInput.textEdition.placeholder = GetT("LOBBY_JOIN_PASS_PLACEHOLDER");
|
||||
if (_joinPassError != null) _joinPassError.text = GetT("LOBBY_JOIN_PASS_ERROR");
|
||||
|
||||
var closePassBtn = root.Q<Button>("ClosePassBtn");
|
||||
if (closePassBtn != null) closePassBtn.text = GetT("LOBBY_CANCEL");
|
||||
|
||||
var confirmJoinBtn = root.Q<Button>("ConfirmJoinBtn");
|
||||
if (confirmJoinBtn != null) confirmJoinBtn.text = GetT("LOBBY_JOIN_BTN");
|
||||
}
|
||||
|
||||
private string GetT(string key) => LocalizationManager.Instance != null ? LocalizationManager.Instance.GetLocalizedString(key) : key;
|
||||
|
||||
public void SetRoomTemplate(VisualTreeAsset template) => _roomItemTemplate = template;
|
||||
|
||||
public override async Task PlayTransitionIn()
|
||||
@@ -193,7 +287,13 @@ namespace Hallucinate.UI
|
||||
if (_joinContainer != null) _joinContainer.style.display = DisplayStyle.Flex;
|
||||
if (_createContainer != null) _createContainer.style.display = DisplayStyle.None;
|
||||
if (_loungeContainer != null) _loungeContainer.style.display = DisplayStyle.None;
|
||||
_ = BasicSpawner.Instance?.StartLobby();
|
||||
|
||||
// Chỉ bắt đầu Lobby nếu chưa có session nào đang chạy
|
||||
var runner = BasicSpawner.Instance?.Runner;
|
||||
if (runner == null || !runner.IsRunning)
|
||||
{
|
||||
_ = BasicSpawner.Instance?.StartLobby();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowCreate()
|
||||
@@ -218,8 +318,13 @@ namespace Hallucinate.UI
|
||||
|
||||
private async void OnCreateRoomClicked()
|
||||
{
|
||||
Debug.Log("[LobbyController] Create Room Clicked");
|
||||
var spawner = BasicSpawner.Instance;
|
||||
if (spawner == null) return;
|
||||
if (spawner == null)
|
||||
{
|
||||
Debug.LogError("[LobbyController] Spawner Instance is NULL!");
|
||||
return;
|
||||
}
|
||||
|
||||
string id = _roomIDInput != null && !string.IsNullOrEmpty(_roomIDInput.value)
|
||||
? _roomIDInput.value.Trim()
|
||||
@@ -239,9 +344,6 @@ namespace Hallucinate.UI
|
||||
if (success)
|
||||
{
|
||||
ShowLounge(name);
|
||||
// Explicitly push the LobbyController to ensure it's the active UI screen.
|
||||
// This helps prevent unintended navigation away from the lounge.
|
||||
await uiManager.Push<LobbyController>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +366,19 @@ namespace Hallucinate.UI
|
||||
item.Q<Label>("RoomName").text = displayName;
|
||||
item.Q<Label>("PlayerCount").text = $"{session.PlayerCount}/{session.MaxPlayers}";
|
||||
|
||||
var statusBadge = item.Q<Label>("StatusBadge");
|
||||
if (statusBadge != null) statusBadge.text = GetT("ROOM_STATUS_WAITING");
|
||||
|
||||
bool needsPass = session.Properties.ContainsKey("pw");
|
||||
var lockIcon = item.Q<Label>("LockIcon");
|
||||
if (lockIcon != null) lockIcon.style.display = needsPass ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
var joinBtn = item.Q<Button>("JoinBtn");
|
||||
if (joinBtn != null) joinBtn.clicked += () => OnRoomItemClicked(session);
|
||||
if (joinBtn != null)
|
||||
{
|
||||
joinBtn.text = GetT("ROOM_JOIN_BTN");
|
||||
joinBtn.clicked += () => OnRoomItemClicked(session);
|
||||
}
|
||||
|
||||
_roomList.Add(item);
|
||||
}
|
||||
@@ -381,12 +490,12 @@ namespace Hallucinate.UI
|
||||
if (hostRef != PlayerRef.None && _playerDataManager.TryGetPlayerMetaData(hostRef, out var hostData))
|
||||
{
|
||||
_hostNameLabel.text = hostData.Name.ToString().ToUpper();
|
||||
_hostStatusLabel.text = hostData.IsReady ? "READY" : "NOT READY";
|
||||
_hostStatusLabel.text = hostData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
|
||||
_hostStatusLabel.style.color = hostData.IsReady ? Color.green : Color.red;
|
||||
}
|
||||
else if (hostRef != PlayerRef.None)
|
||||
{
|
||||
_hostNameLabel.text = "SYNCING...";
|
||||
_hostNameLabel.text = GetT("LOBBY_SYNCING");
|
||||
_hostStatusLabel.text = "-";
|
||||
}
|
||||
|
||||
@@ -394,17 +503,17 @@ namespace Hallucinate.UI
|
||||
if (guestRef != PlayerRef.None && _playerDataManager.TryGetPlayerMetaData(guestRef, out var guestData))
|
||||
{
|
||||
_guestNameLabel.text = guestData.Name.ToString().ToUpper();
|
||||
_guestStatusLabel.text = guestData.IsReady ? "READY" : "NOT READY";
|
||||
_guestStatusLabel.text = guestData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
|
||||
_guestStatusLabel.style.color = guestData.IsReady ? Color.green : Color.red;
|
||||
}
|
||||
else if (runner.ActivePlayers.Count() >= 2)
|
||||
{
|
||||
_guestNameLabel.text = "SYNCING...";
|
||||
_guestNameLabel.text = GetT("LOBBY_SYNCING");
|
||||
_guestStatusLabel.text = "-";
|
||||
}
|
||||
else
|
||||
{
|
||||
_guestNameLabel.text = "WAITING...";
|
||||
_guestNameLabel.text = GetT("LOBBY_WAITING_LABEL");
|
||||
_guestStatusLabel.text = "-";
|
||||
_guestStatusLabel.style.color = Color.gray;
|
||||
}
|
||||
@@ -429,6 +538,7 @@ namespace Hallucinate.UI
|
||||
|
||||
if (_startBtn != null)
|
||||
{
|
||||
_startBtn.text = GetT("LOBBY_START_BTN");
|
||||
_startBtn.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
_startBtn.SetEnabled(allReady && playerCount >= 2);
|
||||
}
|
||||
@@ -440,13 +550,13 @@ namespace Hallucinate.UI
|
||||
// Style for Ready Button
|
||||
if (myData.IsReady)
|
||||
{
|
||||
_readyBtn.text = "UNREADY";
|
||||
_readyBtn.text = GetT("LOBBY_UNREADY_BTN");
|
||||
_readyBtn.style.backgroundColor = new StyleColor(Color.green);
|
||||
_readyBtn.style.color = new StyleColor(Color.black);
|
||||
}
|
||||
else
|
||||
{
|
||||
_readyBtn.text = "READY UP";
|
||||
_readyBtn.text = GetT("LOBBY_READY_BTN");
|
||||
_readyBtn.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.8f));
|
||||
_readyBtn.style.color = new StyleColor(Color.white);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
@@ -13,26 +14,16 @@ namespace Hallucinate.UI
|
||||
|
||||
public event Action OnLanguageChanged;
|
||||
|
||||
[Serializable]
|
||||
private class LocalizationData
|
||||
{
|
||||
public List<LocalizationEntry> items;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class LocalizationEntry
|
||||
{
|
||||
public string key;
|
||||
public string value;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
LoadLanguage(PlayerPrefs.GetString("Language", "en"));
|
||||
|
||||
// Đọc ngôn ngữ đã lưu hoặc mặc định là tiếng Anh
|
||||
string savedLang = PlayerPrefs.GetString("Language", "en");
|
||||
LoadLanguage(savedLang);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -47,48 +38,47 @@ namespace Hallucinate.UI
|
||||
|
||||
if (jsonAsset != null)
|
||||
{
|
||||
// Vì JsonUtility không hỗ trợ Dictionary, chúng ta parse tay một chút nếu file là JSON object phẳng
|
||||
// Hoặc giả định file JSON có cấu trúc phù hợp.
|
||||
// Ở đây tôi sẽ dùng giải pháp parse đơn giản cho JSON phẳng { "key": "value" }
|
||||
ParseFlatJson(jsonAsset.text);
|
||||
ParseJsonRobust(jsonAsset.text);
|
||||
|
||||
PlayerPrefs.SetString("Language", langCode);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
// Thông báo cho các UI khác biết ngôn ngữ đã đổi
|
||||
OnLanguageChanged?.Invoke();
|
||||
Debug.Log($"[Localization] Loaded language: {langCode}");
|
||||
Debug.Log($"[Localization] Successfully loaded language: {langCode} ({_localizedText.Count} keys)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[Localization] Language file not found: Localization/{langCode}");
|
||||
Debug.LogError($"[Localization] Language file NOT FOUND in Resources/Localization/{langCode}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseFlatJson(string json)
|
||||
// Dùng Regex để bóc tách Key-Value từ JSON cực kỳ chính xác
|
||||
private void ParseJsonRobust(string json)
|
||||
{
|
||||
_localizedText.Clear();
|
||||
// Xóa các ký tự thừa
|
||||
json = json.Trim().Trim('{', '}');
|
||||
string[] pairs = json.Split(new[] { "\",", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var pair in pairs)
|
||||
// Regex này sẽ tìm tất cả các cặp "key" : "value" bất kể khoảng trắng hay xuống dòng
|
||||
MatchCollection matches = Regex.Matches(json, "\"([^\"]+)\"\\s*:\\s*\"([^\"]+)\"");
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
string[] kv = pair.Split(new[] { "\":\"" }, StringSplitOptions.None);
|
||||
if (kv.Length == 2)
|
||||
if (match.Groups.Count == 3)
|
||||
{
|
||||
string key = kv[0].Trim().Trim('"', ' ', '\t', '\r');
|
||||
string val = kv[1].Trim().Trim('"', ' ', '\t', '\r');
|
||||
_localizedText[key] = val;
|
||||
string key = match.Groups[1].Value;
|
||||
string value = match.Groups[2].Value;
|
||||
_localizedText[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetLocalizedString(string key)
|
||||
{
|
||||
if (_localizedText != null && _localizedText.TryGetValue(key, out string value))
|
||||
if (_localizedText.TryGetValue(key, out string value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return key;
|
||||
return key; // Trả về chính key nếu không tìm thấy dịch thuật
|
||||
}
|
||||
|
||||
public string CurrentLanguage => _currentLanguage;
|
||||
|
||||
@@ -21,6 +21,28 @@ namespace Hallucinate.UI
|
||||
|
||||
if (_confirmBtn != null)
|
||||
_confirmBtn.clicked += OnConfirmClicked;
|
||||
|
||||
// Đăng ký Localization
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
||||
}
|
||||
ApplyLocalization();
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
if (LocalizationManager.Instance == null) return;
|
||||
|
||||
var title = root.Q<Label>(className: "text-heading");
|
||||
if (title != null) title.text = LocalizationManager.Instance.GetLocalizedString("LOGIN_TITLE");
|
||||
|
||||
if (_nameInput != null) _nameInput.label = LocalizationManager.Instance.GetLocalizedString("LOGIN_USER");
|
||||
if (_confirmBtn != null) _confirmBtn.text = LocalizationManager.Instance.GetLocalizedString("LOGIN_BTN");
|
||||
|
||||
var guestBtn = root.Q<Button>("GuestBtn");
|
||||
if (guestBtn != null) guestBtn.text = LocalizationManager.Instance.GetLocalizedString("LOGIN_GUEST");
|
||||
|
||||
}
|
||||
|
||||
private async void OnConfirmClicked()
|
||||
|
||||
@@ -49,11 +49,41 @@ namespace Hallucinate.UI
|
||||
root.Q<Button>("ProfileBtn").clicked += async () => await uiManager.Push<ProfileController>();
|
||||
root.Q<Button>("ExitBtn").clicked += () => Application.Quit();
|
||||
|
||||
// Đăng ký Localization
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
||||
}
|
||||
ApplyLocalization();
|
||||
|
||||
ResetLogoPosition();
|
||||
StartPulse();
|
||||
_lastInteractionTime = Time.time;
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
if (LocalizationManager.Instance == null) return;
|
||||
|
||||
var joinBtn = root.Q<Button>("JoinBtn");
|
||||
if (joinBtn != null) joinBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_JOIN");
|
||||
|
||||
var createBtn = root.Q<Button>("CreateBtn");
|
||||
if (createBtn != null) createBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_CREATE");
|
||||
|
||||
var settingsBtn = root.Q<Button>("SettingsBtn");
|
||||
if (settingsBtn != null) settingsBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_SETTINGS");
|
||||
|
||||
var profileBtn = root.Q<Button>("ProfileBtn");
|
||||
if (profileBtn != null) profileBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_PROFILE");
|
||||
|
||||
var exitBtn = root.Q<Button>("ExitBtn");
|
||||
if (exitBtn != null) exitBtn.text = LocalizationManager.Instance.GetLocalizedString("MENU_EXIT");
|
||||
|
||||
var playLabel = _logo.Q<Label>();
|
||||
if (playLabel != null) playLabel.text = LocalizationManager.Instance.GetLocalizedString("MENU_PLAY");
|
||||
}
|
||||
|
||||
private void OnScreenResize(GeometryChangedEvent evt)
|
||||
{
|
||||
// Khi màn hình thay đổi kích thước, nếu đang ở Ribbon thì cập nhật theo LogoSpace
|
||||
@@ -252,5 +282,13 @@ namespace Hallucinate.UI
|
||||
cycleMode: CycleMode.Yoyo,
|
||||
ease: Ease.InOutSine);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Fusion;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
{
|
||||
@@ -35,18 +36,26 @@ namespace Hallucinate.UI
|
||||
private void Awake()
|
||||
{
|
||||
_uiDocument = gameObject.AddComponent<UIDocument>();
|
||||
// Use same panel settings as UIManager if possible, or default
|
||||
_uiDocument.panelSettings = Resources.Load<PanelSettings>("UI/PerformancePanelSettings");
|
||||
|
||||
// Đặt thứ tự hiển thị cực cao để luôn nằm trên cùng
|
||||
_uiDocument.sortingOrder = 999;
|
||||
|
||||
// Thử lấy PanelSettings từ UIManager để đồng bộ tỉ lệ scale
|
||||
if (UIManager.Instance != null && UIManager.Instance.GetComponent<UIDocument>() != null)
|
||||
{
|
||||
_uiDocument.panelSettings = UIManager.Instance.GetComponent<UIDocument>().panelSettings;
|
||||
}
|
||||
|
||||
_root = new VisualElement();
|
||||
_root.style.position = Position.Absolute;
|
||||
_root.style.bottom = 10;
|
||||
_root.style.left = 10;
|
||||
_root.style.bottom = 15;
|
||||
_root.style.right = 15;
|
||||
_root.pickingMode = PickingMode.Ignore;
|
||||
|
||||
_fpsLabel = new Label("0 FPS (0.0ms)");
|
||||
_fpsLabel = new Label("0 FPS | 0.0ms | PING: 0ms");
|
||||
_fpsLabel.pickingMode = PickingMode.Ignore;
|
||||
_fpsLabel.style.color = Color.white;
|
||||
_fpsLabel.style.fontSize = 14;
|
||||
_fpsLabel.style.fontSize = 12;
|
||||
_fpsLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
// Shadow effect
|
||||
_fpsLabel.style.textShadow = new TextShadow { offset = new Vector2(1, 1), blurRadius = 1, color = Color.black };
|
||||
@@ -65,7 +74,19 @@ namespace Hallucinate.UI
|
||||
float fps = 1.0f / _deltaTime;
|
||||
float ms = _deltaTime * 1000.0f;
|
||||
|
||||
_fpsLabel.text = $"{Mathf.Ceil(fps)} FPS ({ms:F1}ms)";
|
||||
int ping = 0;
|
||||
if (BasicSpawner.Instance != null && BasicSpawner.Instance.Runner != null && BasicSpawner.Instance.Runner.IsRunning)
|
||||
{
|
||||
var runner = BasicSpawner.Instance.Runner;
|
||||
if (runner.LocalPlayer != PlayerRef.None)
|
||||
{
|
||||
ping = (int)(runner.GetPlayerRtt(runner.LocalPlayer) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
var (_, deviceLatency) = MouseMetricsHelper.GetMetrics();
|
||||
|
||||
_fpsLabel.text = $"{Mathf.Ceil(fps)} FPS | {ms:F1}ms | LATENCY: {deviceLatency:F0}ms | PING: {ping}ms";
|
||||
|
||||
// Color coding based on performance
|
||||
if (fps < 30) _fpsLabel.style.color = Color.red;
|
||||
|
||||
@@ -33,9 +33,38 @@ namespace Hallucinate.UI
|
||||
_logoutBtn.clicked += Logout;
|
||||
}
|
||||
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
|
||||
ApplyLocalization();
|
||||
}
|
||||
|
||||
LoadProfileData();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
if (LocalizationManager.Instance == null) return;
|
||||
|
||||
root.Query<Label>().ForEach(l => {
|
||||
if (l.text == "WIN RATE") l.text = LocalizationManager.Instance.GetLocalizedString("PROFILE_WIN_RATE");
|
||||
if (l.text == "INVENTORY") l.text = LocalizationManager.Instance.GetLocalizedString("PROFILE_INVENTORY");
|
||||
});
|
||||
|
||||
var backBtn = root.Q<Button>("BackBtn");
|
||||
if (backBtn != null) backBtn.text = LocalizationManager.Instance.GetLocalizedString("PROFILE_BACK");
|
||||
|
||||
if (_logoutBtn != null) _logoutBtn.text = LocalizationManager.Instance.GetLocalizedString("PROFILE_LOGOUT");
|
||||
}
|
||||
|
||||
public override async Task PlayTransitionIn()
|
||||
{
|
||||
LoadProfileData(); // Refresh data every time we show the profile
|
||||
|
||||
@@ -15,11 +15,19 @@ namespace Hallucinate.UI
|
||||
public class SettingsController : BaseUIController
|
||||
{
|
||||
private VisualElement _sidebar;
|
||||
private VisualElement _tabsColumn;
|
||||
private Label _tabTitle;
|
||||
private ScrollView _content;
|
||||
private Dictionary<string, Button> _tabButtons = new Dictionary<string, Button>();
|
||||
private string _activeTab = "GENERAL";
|
||||
|
||||
private Tween _hoverTimer;
|
||||
private bool _isExpanded;
|
||||
|
||||
// Osu Style Scroll Tracking
|
||||
private readonly Dictionary<string, VisualElement> _sectionHeaders = new Dictionary<string, VisualElement>();
|
||||
private bool _isManualScrolling;
|
||||
|
||||
// Advanced Mouse Metrics
|
||||
private Label _mouseMetricsLabel;
|
||||
|
||||
@@ -45,10 +53,24 @@ namespace Hallucinate.UI
|
||||
base.Initialize(uxmlRoot, manager);
|
||||
|
||||
_sidebar = root.Q<VisualElement>("Sidebar");
|
||||
_tabsColumn = root.Q<VisualElement>("TabsColumn");
|
||||
_tabTitle = root.Q<Label>("TabTitle");
|
||||
_content = root.Q<ScrollView>("SettingsContent");
|
||||
|
||||
// Global Volume Catch - Use TrickleDown to catch events before they are consumed by children (like ScrollViews)
|
||||
// Smart Sidebar Hover Logic
|
||||
_tabsColumn.RegisterCallback<PointerEnterEvent>(OnSidebarPointerEnter);
|
||||
_tabsColumn.RegisterCallback<PointerLeaveEvent>(OnSidebarPointerLeave);
|
||||
|
||||
// Scroll Tracking cho Osu Style
|
||||
_content.verticalScroller.valueChanged += OnScrollValueChanged;
|
||||
|
||||
// Đăng ký sự kiện đổi ngôn ngữ
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
|
||||
}
|
||||
|
||||
// Global Volume Catch
|
||||
uiManager.Root.RegisterCallback<WheelEvent>(OnMouseWheel, TrickleDown.TrickleDown);
|
||||
SetupHierarchicalVolumeOverlay();
|
||||
|
||||
@@ -56,7 +78,6 @@ namespace Hallucinate.UI
|
||||
if (evt.target == root) uiManager.ToggleSettings();
|
||||
});
|
||||
|
||||
// Keyboard navigation for sliders
|
||||
root.RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
|
||||
SetupTab("GeneralTab", "GENERAL");
|
||||
@@ -68,30 +89,270 @@ namespace Hallucinate.UI
|
||||
if (closeBtn != null) closeBtn.clicked += () => uiManager.ToggleSettings();
|
||||
|
||||
_masterVol = PlayerPrefs.GetFloat("MasterVolume", 80f);
|
||||
|
||||
// Enforce Video Settings on start
|
||||
|
||||
// Render ban đầu
|
||||
RefreshUI();
|
||||
ApplyVideoSettings();
|
||||
}
|
||||
|
||||
SwitchTab("GENERAL");
|
||||
private void OnLanguageChanged()
|
||||
{
|
||||
// Lưu lại vị trí cuộn hiện tại
|
||||
float currentScroll = _content.scrollOffset.y;
|
||||
RefreshUI();
|
||||
// Khôi phục vị trí cuộn sau một frame để layout kịp cập nhật
|
||||
_content.schedule.Execute(() => _content.scrollOffset = new Vector2(0, currentScroll)).StartingIn(10);
|
||||
}
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
RenderAllSettings();
|
||||
UpdateTabLabels();
|
||||
HighlightTab(_activeTab);
|
||||
}
|
||||
|
||||
private void UpdateTabLabels()
|
||||
{
|
||||
foreach (var kvp in _tabButtons)
|
||||
{
|
||||
var label = kvp.Value.Q<Label>(className: "tab-label");
|
||||
if (label != null) label.text = GetT(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetT(string key) => LocalizationManager.Instance != null ? LocalizationManager.Instance.GetLocalizedString(key) : key;
|
||||
|
||||
private void OnSidebarPointerEnter(PointerEnterEvent evt)
|
||||
{
|
||||
_hoverTimer.Stop();
|
||||
ExpandSidebar();
|
||||
}
|
||||
|
||||
private void OnSidebarPointerLeave(PointerLeaveEvent evt)
|
||||
{
|
||||
_hoverTimer.Stop();
|
||||
CollapseSidebar();
|
||||
}
|
||||
|
||||
private void ExpandSidebar()
|
||||
{
|
||||
if (_isExpanded) return;
|
||||
_isExpanded = true;
|
||||
_tabsColumn.AddToClassList("sidebar-expanded");
|
||||
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);
|
||||
}
|
||||
|
||||
private void RenderAllSettings()
|
||||
{
|
||||
_content.Clear();
|
||||
_sectionHeaders.Clear();
|
||||
|
||||
RenderGeneralTab();
|
||||
RenderVideoTab();
|
||||
RenderSoundTab();
|
||||
RenderControlTab();
|
||||
|
||||
// Thêm khoảng trống cuối để cuộn thoải mái
|
||||
var spacer = new VisualElement { style = { height = 200 } };
|
||||
_content.Add(spacer);
|
||||
}
|
||||
|
||||
private void SetupTab(string btnName, string tabId)
|
||||
{
|
||||
var btn = root.Q<Button>(btnName);
|
||||
if (btn != null)
|
||||
{
|
||||
_tabButtons[tabId] = btn;
|
||||
btn.clicked += () => ScrollToSection(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToSection(string tabId)
|
||||
{
|
||||
if (!_sectionHeaders.TryGetValue(tabId, out var header)) return;
|
||||
|
||||
_isManualScrolling = true;
|
||||
HighlightTab(tabId);
|
||||
|
||||
float targetY = header.layout.y;
|
||||
Tween.Custom(_content.scrollOffset.y, targetY, duration: 0.5f, ease: Ease.OutQuart, onValueChange: val => {
|
||||
_content.scrollOffset = new Vector2(0, val);
|
||||
}).OnComplete(() => _isManualScrolling = false);
|
||||
}
|
||||
|
||||
private void OnScrollValueChanged(float val)
|
||||
{
|
||||
if (_isManualScrolling) return;
|
||||
|
||||
string currentActive = "GENERAL";
|
||||
float minDistance = float.MaxValue;
|
||||
|
||||
foreach (var kvp in _sectionHeaders)
|
||||
{
|
||||
float dist = Math.Abs(kvp.Value.worldBound.y - _content.worldBound.y);
|
||||
if (dist < minDistance)
|
||||
{
|
||||
minDistance = dist;
|
||||
currentActive = kvp.Key;
|
||||
}
|
||||
}
|
||||
|
||||
if (_activeTab != currentActive) HighlightTab(currentActive);
|
||||
}
|
||||
|
||||
private void HighlightTab(string tabId)
|
||||
{
|
||||
_activeTab = tabId;
|
||||
_tabTitle.text = GetT(tabId);
|
||||
foreach (var kvp in _tabButtons)
|
||||
{
|
||||
if (kvp.Key == tabId) kvp.Value.AddToClassList("active-tab");
|
||||
else kvp.Value.RemoveFromClassList("active-tab");
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderGeneralTab()
|
||||
{
|
||||
var header = CreateSection("GENERAL");
|
||||
_sectionHeaders["GENERAL"] = header;
|
||||
_content.Add(header);
|
||||
|
||||
_content.Add(CreateSubSection("ACCOUNT"));
|
||||
var userRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 10 } };
|
||||
var loggedInLabel = new Label(GetT("LOGGED_IN_AS")); loggedInLabel.AddToClassList("text-body");
|
||||
userRow.Add(loggedInLabel);
|
||||
userRow.Add(new Label(PlayerPrefs.GetString("Username", "Guest")) { style = { color = Color.cyan, marginLeft = 5, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
_content.Add(userRow);
|
||||
|
||||
_content.Add(CreateSubSection("LANGUAGE"));
|
||||
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" }, LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
|
||||
langDropdown.AddToClassList("custom-dropdown");
|
||||
langDropdown.RegisterValueChangedCallback(evt => {
|
||||
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en");
|
||||
});
|
||||
_content.Add(langDropdown);
|
||||
|
||||
_content.Add(CreateSubSection("UPDATES"));
|
||||
var versionBox = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||||
var versionLabel = new Label($"{GetT("VERSION")} {Application.version}"); versionLabel.AddToClassList("text-body");
|
||||
versionBox.Add(versionLabel);
|
||||
var checkBtn = new Button { text = GetT("CHECK_FOR_UPDATES") }; checkBtn.AddToClassList("button-spring");
|
||||
checkBtn.clicked += () => checkBtn.text = GetT("UP_TO_DATE");
|
||||
versionBox.Add(checkBtn);
|
||||
_content.Add(versionBox);
|
||||
|
||||
_content.Add(CreateSubSection("CURSOR_MOUSE"));
|
||||
_content.Add(CreateSliderWithInput(GetT("CURSOR_SIZE"), 10, 150, PlayerPrefs.GetFloat("CursorSize", 40), val => uiManager.SetCursorSize(val)));
|
||||
var trailToggle = new Toggle(GetT("ENABLE_TRAIL")) { value = PlayerPrefs.GetInt("CursorTrail", 1) == 1 };
|
||||
trailToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorTrail(evt.newValue));
|
||||
_content.Add(trailToggle);
|
||||
var rippleToggle = new Toggle(GetT("ENABLE_RIPPLES")) { value = PlayerPrefs.GetInt("CursorRipples", 1) == 1 };
|
||||
rippleToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorRipples(evt.newValue));
|
||||
_content.Add(rippleToggle);
|
||||
_content.Add(CreateSliderWithInput(GetT("SENSITIVITY"), 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => uiManager.SetMouseSensitivity(val)));
|
||||
_mouseMetricsLabel = new Label($"{GetT("MOUSE_LATENCY")} report: 0/sec latency: 0ms") { style = { fontSize = 11, color = Color.gray, marginTop = 5 } };
|
||||
_content.Add(_mouseMetricsLabel);
|
||||
}
|
||||
|
||||
private void RenderVideoTab()
|
||||
{
|
||||
var header = CreateSection("VIDEO");
|
||||
_sectionHeaders["VIDEO"] = header;
|
||||
_content.Add(header);
|
||||
|
||||
_content.Add(CreateSubSection("RENDERER"));
|
||||
var frameLimit = new DropdownField(GetT("FRAME_LIMITER"), new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, PlayerPrefs.GetInt("FrameLimiter", 2));
|
||||
frameLimit.RegisterValueChangedCallback(evt => ApplyFrameLimit(frameLimit.index));
|
||||
_content.Add(frameLimit);
|
||||
var fpsToggle = new Toggle(GetT("SHOW_FPS")) { value = _fpsVisible };
|
||||
fpsToggle.RegisterValueChangedCallback(evt => { _fpsVisible = evt.newValue; PlayerPrefs.SetInt("ShowFPS", _fpsVisible ? 1 : 0); PerformanceOverlay.SetVisible(_fpsVisible); });
|
||||
_content.Add(fpsToggle);
|
||||
|
||||
_content.Add(CreateSubSection("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();
|
||||
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(GetT("RESOLUTION"), resList, currentResIdx);
|
||||
resDropdown.RegisterValueChangedCallback(evt => {
|
||||
string[] parts = evt.newValue.Split(' ')[0].Split('x');
|
||||
int w = int.Parse(parts[0]), h = int.Parse(parts[1]);
|
||||
Screen.SetResolution(w, h, Screen.fullScreen);
|
||||
PlayerPrefs.SetInt("ScreenWidth", w); PlayerPrefs.SetInt("ScreenHeight", h);
|
||||
});
|
||||
_content.Add(resDropdown);
|
||||
var fullToggle = new Toggle(GetT("FULLSCREEN")) { value = Screen.fullScreen };
|
||||
fullToggle.RegisterValueChangedCallback(evt => { Screen.fullScreen = evt.newValue; PlayerPrefs.SetInt("Fullscreen", evt.newValue ? 1 : 0); });
|
||||
_content.Add(fullToggle);
|
||||
_content.Add(CreateSliderWithInput(GetT("BACKGROUND_DIM"), 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => ApplyBackgroundDim(val)));
|
||||
_content.Add(CreateSliderWithInput(GetT("UI_SCALE"), 0.5f, 2.0f, PlayerPrefs.GetFloat("UIScale", 1.0f), val => uiManager.SetUIScale(val)));
|
||||
}
|
||||
|
||||
private void RenderSoundTab()
|
||||
{
|
||||
var header = CreateSection("SOUND");
|
||||
_sectionHeaders["SOUND"] = header;
|
||||
_content.Add(header);
|
||||
|
||||
_content.Add(CreateSubSection("AUDIO_VOLUMES"));
|
||||
_content.Add(CreateAudioSlider(GetT("MASTER"), "MasterVolume"));
|
||||
_content.Add(CreateAudioSlider(GetT("MUSIC"), "MusicVolume"));
|
||||
_content.Add(CreateAudioSlider(GetT("VFX"), "VFXVolume"));
|
||||
_content.Add(CreateAudioSlider(GetT("PLAYER"), "PlayerVolume"));
|
||||
_content.Add(CreateAudioSlider(GetT("UI"), "UIVolume"));
|
||||
_content.Add(new Label(GetT("SCROLL_HINT")) { style = { marginTop = 20, color = Color.gray, fontSize = 12 } });
|
||||
}
|
||||
|
||||
private void RenderControlTab()
|
||||
{
|
||||
var header = CreateSection("CONTROL");
|
||||
_sectionHeaders["CONTROL"] = header;
|
||||
_content.Add(header);
|
||||
|
||||
_content.Add(CreateSubSection("KEY_BINDINGS"));
|
||||
if (uiManager.InputReader?.InputActions == null) return;
|
||||
foreach (var map in uiManager.InputReader.InputActions.actionMaps)
|
||||
{
|
||||
var mapHeader = new Label(map.name.ToUpper()) { style = { fontSize = 14, unityFontStyleAndWeight = FontStyle.Bold, color = Color.cyan, marginTop = 15, marginBottom = 5 } };
|
||||
_content.Add(mapHeader);
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
if (action.name == "Look" || action.name == "Scroll" || action.name == "Navigate" || action.name == "Point" || action.name == "Click") continue;
|
||||
if (action.bindings.Any(b => b.isComposite))
|
||||
{
|
||||
for (int i = 0; i < action.bindings.Count; i++)
|
||||
if (action.bindings[i].isPartOfComposite && action.bindings[i].groups.Contains("Keyboard&Mouse"))
|
||||
_content.Add(CreateRebindRow(action, i, $"{action.name} {action.bindings[i].name}".ToUpper()));
|
||||
}
|
||||
else
|
||||
{
|
||||
int idx = action.bindings.ToList().FindIndex(b => b.groups.Contains("Keyboard&Mouse"));
|
||||
if (idx != -1) _content.Add(CreateRebindRow(action, idx, action.name.ToUpper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
var resetBtn = new Button { text = GetT("RESET_ALL") }; resetBtn.AddToClassList("button-spring"); resetBtn.style.marginTop = 30; resetBtn.style.alignSelf = Align.Center;
|
||||
resetBtn.clicked += () => { uiManager.InputReader.ResetBindings(); RefreshUI(); };
|
||||
_content.Add(resetBtn);
|
||||
}
|
||||
|
||||
private void ApplyVideoSettings()
|
||||
{
|
||||
// Frame Limiter
|
||||
int frameLimitIdx = PlayerPrefs.GetInt("FrameLimiter", 2);
|
||||
ApplyFrameLimit(frameLimitIdx);
|
||||
|
||||
// FPS Counter
|
||||
_fpsVisible = PlayerPrefs.GetInt("ShowFPS", 0) == 1;
|
||||
PerformanceOverlay.SetVisible(_fpsVisible);
|
||||
|
||||
// Background Dim
|
||||
float dim = PlayerPrefs.GetFloat("BackgroundDim", 50f);
|
||||
ApplyBackgroundDim(dim);
|
||||
|
||||
// Fullscreen
|
||||
bool isFull = PlayerPrefs.GetInt("Fullscreen", Screen.fullScreen ? 1 : 0) == 1;
|
||||
Screen.fullScreen = isFull;
|
||||
}
|
||||
|
||||
private void ApplyFrameLimit(int index)
|
||||
@@ -109,59 +370,38 @@ namespace Hallucinate.UI
|
||||
private void ApplyBackgroundDim(float value)
|
||||
{
|
||||
PlayerPrefs.SetFloat("BackgroundDim", value);
|
||||
// Search for dim overlay in root
|
||||
var dimOverlay = uiManager.Root.Q<VisualElement>("BackgroundDimOverlay");
|
||||
if (dimOverlay != null)
|
||||
{
|
||||
dimOverlay.style.backgroundColor = new Color(0, 0, 0, value / 100f);
|
||||
}
|
||||
if (dimOverlay != null) dimOverlay.style.backgroundColor = new Color(0, 0, 0, value / 100f);
|
||||
}
|
||||
|
||||
private void SetupHierarchicalVolumeOverlay()
|
||||
{
|
||||
_volumeContainer = new VisualElement();
|
||||
_volumeContainer.name = "GlobalVolumeOverlay";
|
||||
_volumeContainer = new VisualElement { name = "GlobalVolumeOverlay" };
|
||||
_volumeContainer.style.position = Position.Absolute;
|
||||
_volumeContainer.style.right = 50;
|
||||
_volumeContainer.style.bottom = 50;
|
||||
_volumeContainer.style.width = 300;
|
||||
_volumeContainer.style.height = 300;
|
||||
_volumeContainer.style.right = 50; _volumeContainer.style.bottom = 50;
|
||||
_volumeContainer.style.width = 300; _volumeContainer.style.height = 300;
|
||||
_volumeContainer.style.display = DisplayStyle.None;
|
||||
_volumeContainer.pickingMode = PickingMode.Ignore;
|
||||
|
||||
// Add to UIManager root so it stays even when Settings is hidden
|
||||
uiManager.Root.Add(_volumeContainer);
|
||||
|
||||
// Master Ring (Bottom Right)
|
||||
_masterRing = CreateRing("Master", 120, cyan: true);
|
||||
_masterRing.style.right = 0;
|
||||
_masterRing.style.bottom = 0;
|
||||
_masterRing = CreateRing("Master", 120, cyan: true);
|
||||
_masterRing.style.right = 0; _masterRing.style.bottom = 0;
|
||||
_masterVolLabel = _masterRing.Q<Label>();
|
||||
_volumeContainer.Add(_masterRing);
|
||||
|
||||
// Sub Rings (Music, VFX, Player, UI)
|
||||
string[] subs = { "MusicVolume", "VFXVolume", "PlayerVolume", "UIVolume" };
|
||||
string[] shortNames = { "MUS", "VFX", "PLY", "UI" };
|
||||
|
||||
// Layout sub-rings in an arc around Master
|
||||
for (int i = 0; i < subs.Length; i++)
|
||||
{
|
||||
var ring = CreateRing(shortNames[i], 70, false);
|
||||
|
||||
// Angle 0 = Top, Angle 90 = Left
|
||||
float angle = (i * 30f) * Mathf.Deg2Rad;
|
||||
float radius = 140f;
|
||||
|
||||
// right moves element to the LEFT, bottom moves element UP
|
||||
// Starting from top (right=25, bottom=140+25) to left (right=140+25, bottom=25)
|
||||
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; });
|
||||
ring.pickingMode = PickingMode.Position; // Allow hover detection
|
||||
|
||||
ring.pickingMode = PickingMode.Position;
|
||||
_subRings[key] = (ring, ring.Q<Label>());
|
||||
_volumeContainer.Add(ring);
|
||||
}
|
||||
@@ -170,91 +410,49 @@ namespace Hallucinate.UI
|
||||
private VisualElement CreateRing(string text, float size, bool cyan)
|
||||
{
|
||||
var ring = new VisualElement();
|
||||
ring.style.width = size;
|
||||
ring.style.height = size;
|
||||
ring.style.width = size; ring.style.height = size;
|
||||
ring.style.backgroundColor = new Color(0, 0, 0, 0.85f);
|
||||
ring.style.borderTopLeftRadius = size / 2;
|
||||
ring.style.borderTopRightRadius = size / 2;
|
||||
ring.style.borderBottomLeftRadius = size / 2;
|
||||
ring.style.borderBottomRightRadius = size / 2;
|
||||
ring.style.borderTopWidth = 3;
|
||||
ring.style.borderBottomWidth = 3;
|
||||
ring.style.borderLeftWidth = 3;
|
||||
ring.style.borderRightWidth = 3;
|
||||
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;
|
||||
ring.style.borderTopColor = ring.style.borderBottomColor = ring.style.borderLeftColor = ring.style.borderRightColor = cyan ? Color.cyan : new Color(0.7f, 0.7f, 0.7f);
|
||||
ring.style.justifyContent = Justify.Center;
|
||||
ring.style.alignItems = Align.Center;
|
||||
ring.style.justifyContent = Justify.Center; ring.style.alignItems = Align.Center;
|
||||
ring.style.position = Position.Absolute;
|
||||
|
||||
var label = new Label("80%");
|
||||
label.style.color = Color.white;
|
||||
label.style.fontSize = size * 0.25f;
|
||||
label.style.color = Color.white; label.style.fontSize = size * 0.25f;
|
||||
label.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
ring.Add(label);
|
||||
|
||||
var title = new Label(text);
|
||||
title.style.color = Color.gray;
|
||||
title.style.fontSize = size * 0.15f;
|
||||
title.style.position = Position.Absolute;
|
||||
title.style.bottom = size * 0.15f;
|
||||
title.style.color = Color.gray; title.style.fontSize = size * 0.15f;
|
||||
title.style.position = Position.Absolute; title.style.bottom = size * 0.15f;
|
||||
ring.Add(title);
|
||||
|
||||
return ring;
|
||||
}
|
||||
|
||||
private void OnMouseWheel(WheelEvent evt)
|
||||
{
|
||||
// Debug Log to see if event is even reaching here
|
||||
Debug.Log($"[SettingsController] Mouse Wheel Detected. Settings Open: {uiManager.IsSettingsOpen}");
|
||||
|
||||
// 1. Do not control volume if we are in MainMenu (unless Settings is explicitly open)
|
||||
var mainMenuRoot = uiManager.Root.Q<VisualElement>("MainMenuRoot");
|
||||
bool isMainMenuVisible = mainMenuRoot != null && mainMenuRoot.style.display == DisplayStyle.Flex;
|
||||
|
||||
if (!uiManager.IsSettingsOpen && isMainMenuVisible)
|
||||
{
|
||||
Debug.Log("[SettingsController] Volume control suppressed: Currently at Main Menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Detect target and context
|
||||
if (!uiManager.IsSettingsOpen && isMainMenuVisible) return;
|
||||
VisualElement target = evt.target as VisualElement;
|
||||
bool isDirectUIInteraction = _hoveredSubVolume != null || (_hoveredSlider != null && _activeTab == "SOUND");
|
||||
|
||||
// 3. If NOT direct interaction, check for ScrollView to prevent accidental volume changes while scrolling lists
|
||||
if (!isDirectUIInteraction && target != null)
|
||||
{
|
||||
if (target is ScrollView || target.GetFirstAncestorOfType<ScrollView>() != null)
|
||||
{
|
||||
Debug.Log("[SettingsController] Ignoring volume scroll: Hovering a ScrollView.");
|
||||
return;
|
||||
}
|
||||
if (target is ScrollView || target.GetFirstAncestorOfType<ScrollView>() != null) return;
|
||||
}
|
||||
|
||||
// 4. Osu style volume control
|
||||
_overlayActiveCount++;
|
||||
ShowVolumeOverlay();
|
||||
|
||||
if (_hoveredSubVolume != null)
|
||||
{
|
||||
Debug.Log($"[SettingsController] Adjusting Sub Volume: {_hoveredSubVolume}");
|
||||
UpdateSubVolume(_hoveredSubVolume, -evt.delta.y * 2f);
|
||||
}
|
||||
if (_hoveredSubVolume != null) UpdateSubVolume(_hoveredSubVolume, -evt.delta.y * 2f);
|
||||
else if (_hoveredSlider != null && _activeTab == "SOUND")
|
||||
{
|
||||
// If hovering a slider in the Sound tab, adjust that
|
||||
float currentVal = _hoveredSlider.value;
|
||||
float step = (_sliderMax - _sliderMin) / 100f;
|
||||
float newVal = Mathf.Clamp(currentVal - (evt.delta.y * step * 5f), _sliderMin, _sliderMax);
|
||||
float newVal = Mathf.Clamp(_hoveredSlider.value - (evt.delta.y * step * 5f), _sliderMin, _sliderMax);
|
||||
_hoveredSlider.value = newVal;
|
||||
_hoveredOnChanged?.Invoke(newVal);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[SettingsController] Adjusting Master Volume.");
|
||||
UpdateMasterVolume(-evt.delta.y * 2f);
|
||||
}
|
||||
|
||||
else UpdateMasterVolume(-evt.delta.y * 2f);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
@@ -264,408 +462,96 @@ namespace Hallucinate.UI
|
||||
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
|
||||
AudioManager.Instance?.SetVolume("MasterVolume", _masterVol);
|
||||
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
|
||||
|
||||
// Refresh Sound Tab UI if visible
|
||||
if (_activeTab == "SOUND") SwitchTab("SOUND");
|
||||
}
|
||||
|
||||
private void UpdateSubVolume(string key, float delta)
|
||||
{
|
||||
float current = PlayerPrefs.GetFloat(key, 80f);
|
||||
float newVal = Mathf.Clamp(current + delta, 0f, 100f);
|
||||
float newVal = Mathf.Clamp(PlayerPrefs.GetFloat(key, 80f) + delta, 0f, 100f);
|
||||
PlayerPrefs.SetFloat(key, newVal);
|
||||
AudioManager.Instance?.SetVolume(key, newVal);
|
||||
|
||||
if (_subRings.TryGetValue(key, out var data))
|
||||
data.label.text = $"{Mathf.RoundToInt(newVal)}%";
|
||||
|
||||
if (_activeTab == "SOUND") SwitchTab("SOUND");
|
||||
if (_subRings.TryGetValue(key, out var data)) data.label.text = $"{Mathf.RoundToInt(newVal)}%";
|
||||
}
|
||||
|
||||
private async void ShowVolumeOverlay()
|
||||
{
|
||||
Debug.Log("[SettingsController] Showing Volume Overlay.");
|
||||
// Ensure overlay is on top of other UI screens
|
||||
_volumeContainer.BringToFront();
|
||||
|
||||
// CRITICAL: Ensure Virtual Cursor is ALWAYS on top of the volume rings
|
||||
uiManager.Root.Q<VisualElement>("CursorLayer")?.BringToFront();
|
||||
|
||||
_volumeContainer.style.display = DisplayStyle.Flex;
|
||||
_volumeContainer.style.opacity = 1f;
|
||||
|
||||
// Refresh all sub labels
|
||||
foreach (var kvp in _subRings)
|
||||
{
|
||||
float val = PlayerPrefs.GetFloat(kvp.Key, 80f);
|
||||
kvp.Value.label.text = $"{Mathf.RoundToInt(val)}%";
|
||||
}
|
||||
foreach (var kvp in _subRings) kvp.Value.label.text = $"{Mathf.RoundToInt(PlayerPrefs.GetFloat(kvp.Key, 80f))}%";
|
||||
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
|
||||
|
||||
int currentId = _overlayActiveCount;
|
||||
await Task.Delay(3000); // Wait 3s as requested
|
||||
|
||||
// Only fade out if no new scroll activity happened
|
||||
await Task.Delay(3000);
|
||||
if (currentId == _overlayActiveCount && _hoveredSubVolume == null)
|
||||
{
|
||||
Debug.Log("[SettingsController] Fading out Volume Overlay.");
|
||||
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeContainer.style.opacity = val)
|
||||
.OnComplete(() => {
|
||||
if (_volumeContainer.style.opacity == 0f)
|
||||
_volumeContainer.style.display = DisplayStyle.None;
|
||||
});
|
||||
.OnComplete(() => { if (_volumeContainer.style.opacity == 0f) _volumeContainer.style.display = DisplayStyle.None; });
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTab(string btnName, string tabId)
|
||||
{
|
||||
var btn = root.Q<Button>(btnName);
|
||||
if (btn != null)
|
||||
{
|
||||
_tabButtons[tabId] = btn;
|
||||
btn.clicked += () => SwitchTab(tabId);
|
||||
}
|
||||
private VisualElement CreateSection(string title)
|
||||
{
|
||||
var label = new Label(GetT(title));
|
||||
label.AddToClassList("text-heading");
|
||||
label.style.marginTop = 60;
|
||||
label.style.borderBottomWidth = 2;
|
||||
label.style.borderBottomColor = Color.cyan;
|
||||
label.style.paddingBottom = 10;
|
||||
return label;
|
||||
}
|
||||
|
||||
private void SwitchTab(string tabId)
|
||||
{
|
||||
_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");
|
||||
}
|
||||
|
||||
_content.Clear();
|
||||
_hoveredSlider = null;
|
||||
|
||||
switch (tabId)
|
||||
{
|
||||
case "GENERAL": RenderGeneralTab(); break;
|
||||
case "VIDEO": RenderVideoTab(); break;
|
||||
case "SOUND": RenderSoundTab(); break;
|
||||
case "CONTROL": RenderControlTab(); break;
|
||||
}
|
||||
}
|
||||
|
||||
#region GENERAL TAB
|
||||
private void RenderGeneralTab()
|
||||
{
|
||||
_content.Add(CreateSection("ACCOUNT"));
|
||||
string username = PlayerPrefs.GetString("Username", "Guest");
|
||||
var userRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 10 } };
|
||||
var loggedInLabel = new Label("Logged in as: ");
|
||||
loggedInLabel.AddToClassList("text-body");
|
||||
userRow.Add(loggedInLabel);
|
||||
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");
|
||||
langDropdown.RegisterValueChangedCallback(evt => {
|
||||
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en");
|
||||
SwitchTab("GENERAL");
|
||||
});
|
||||
_content.Add(langDropdown);
|
||||
|
||||
_content.Add(CreateSection("UPDATES"));
|
||||
var versionBox = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||||
var versionLabel = new Label($"Version: {Application.version}");
|
||||
versionLabel.AddToClassList("text-body");
|
||||
versionBox.Add(versionLabel);
|
||||
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)));
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Sensitivity", 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => PlayerPrefs.SetFloat("MouseSensitivity", val)));
|
||||
|
||||
var rawInputToggle = new Toggle("Raw Input (Bypass Acceleration)") { value = true };
|
||||
_content.Add(rawInputToggle);
|
||||
|
||||
_mouseMetricsLabel = new Label("[(report: 0/sec latency: 0ms)]") { style = { fontSize = 11, color = Color.gray, marginTop = 5 } };
|
||||
_content.Add(_mouseMetricsLabel);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VIDEO TAB
|
||||
private void RenderVideoTab()
|
||||
{
|
||||
_content.Add(CreateSection("RENDERER"));
|
||||
|
||||
int currentFrameLimit = PlayerPrefs.GetInt("FrameLimiter", 2);
|
||||
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, currentFrameLimit);
|
||||
frameLimit.RegisterValueChangedCallback(evt => {
|
||||
ApplyFrameLimit(frameLimit.index);
|
||||
});
|
||||
_content.Add(frameLimit);
|
||||
|
||||
var fpsToggle = new Toggle("Show FPS Counter") { value = _fpsVisible };
|
||||
fpsToggle.RegisterValueChangedCallback(evt => {
|
||||
_fpsVisible = evt.newValue;
|
||||
PlayerPrefs.SetInt("ShowFPS", _fpsVisible ? 1 : 0);
|
||||
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();
|
||||
|
||||
// Find current res in list
|
||||
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);
|
||||
resDropdown.RegisterValueChangedCallback(evt => {
|
||||
string[] parts = evt.newValue.Split(' ')[0].Split('x');
|
||||
int w = int.Parse(parts[0]);
|
||||
int h = int.Parse(parts[1]);
|
||||
Screen.SetResolution(w, h, Screen.fullScreen);
|
||||
PlayerPrefs.SetInt("ScreenWidth", w);
|
||||
PlayerPrefs.SetInt("ScreenHeight", h);
|
||||
});
|
||||
_content.Add(resDropdown);
|
||||
|
||||
var fullToggle = new Toggle("Fullscreen Mode") { value = Screen.fullScreen };
|
||||
fullToggle.RegisterValueChangedCallback(evt => {
|
||||
Screen.fullScreen = evt.newValue;
|
||||
PlayerPrefs.SetInt("Fullscreen", evt.newValue ? 1 : 0);
|
||||
});
|
||||
_content.Add(fullToggle);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => ApplyBackgroundDim(val)));
|
||||
_content.Add(CreateSliderWithInput("UI Scale", 0.5f, 2.0f, PlayerPrefs.GetFloat("UIScale", 1.0f), val => uiManager.SetUIScale(val)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SOUND TAB
|
||||
private void RenderSoundTab()
|
||||
{
|
||||
_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 } });
|
||||
}
|
||||
|
||||
private VisualElement CreateAudioSlider(string label, string prefKey)
|
||||
{
|
||||
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => {
|
||||
PlayerPrefs.SetFloat(prefKey, val);
|
||||
AudioManager.Instance?.SetVolume(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);
|
||||
AudioManager.Instance?.SetVolume(prefKey, newVal);
|
||||
|
||||
// Visual update only (to avoid heavy re-render of whole list)
|
||||
var slider = sliderRow.Q<Slider>();
|
||||
if (slider != null) slider.value = newVal;
|
||||
});
|
||||
|
||||
return sliderRow;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CONTROL TAB
|
||||
private void RenderControlTab()
|
||||
{
|
||||
_content.Add(CreateSection("KEY BINDINGS"));
|
||||
|
||||
if (uiManager.InputReader == null || uiManager.InputReader.InputActions == null)
|
||||
{
|
||||
var errorLabel = new Label("Input Actions not found.");
|
||||
errorLabel.AddToClassList("text-body");
|
||||
_content.Add(errorLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var map in uiManager.InputReader.InputActions.actionMaps)
|
||||
{
|
||||
// Add a sub-header for each map
|
||||
var mapHeader = new Label(map.name.ToUpper());
|
||||
mapHeader.style.fontSize = 14;
|
||||
mapHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
mapHeader.style.color = Color.cyan;
|
||||
mapHeader.style.marginTop = 15;
|
||||
mapHeader.style.marginBottom = 5;
|
||||
_content.Add(mapHeader);
|
||||
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
// Skip internal/complex actions that shouldn't be rebound manually
|
||||
if (action.name == "Look" || action.name == "Scroll" ||
|
||||
action.name == "Navigate" || action.name == "Point" || action.name == "Click") continue;
|
||||
|
||||
if (action.bindings.Any(b => b.isComposite))
|
||||
{
|
||||
for (int i = 0; i < action.bindings.Count; i++)
|
||||
{
|
||||
if (action.bindings[i].isPartOfComposite)
|
||||
{
|
||||
if (action.bindings[i].groups.Contains("Keyboard&Mouse"))
|
||||
{
|
||||
string label = $"{action.name} {action.bindings[i].name}".ToUpper();
|
||||
_content.Add(CreateRebindRow(action, i, label));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int bindingIndex = action.bindings.ToList().FindIndex(b => b.groups.Contains("Keyboard&Mouse"));
|
||||
if (bindingIndex != -1)
|
||||
{
|
||||
_content.Add(CreateRebindRow(action, bindingIndex, action.name.ToUpper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
};
|
||||
_content.Add(resetBtn);
|
||||
}
|
||||
private VisualElement CreateRebindRow(UnityEngine.InputSystem.InputAction action, int bindingIndex, string labelText)
|
||||
{
|
||||
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 = 10;
|
||||
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 bindingDisplay = action.GetBindingDisplayString(bindingIndex);
|
||||
var btn = new Button { text = bindingDisplay.ToUpper() };
|
||||
btn.AddToClassList("rebind-button");
|
||||
btn.style.width = 150;
|
||||
|
||||
btn.clicked += () => StartRebinding(action, bindingIndex, btn);
|
||||
|
||||
row.Add(btn);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void StartRebinding(UnityEngine.InputSystem.InputAction action, int bindingIndex, Button btn)
|
||||
{
|
||||
btn.text = "> <"; // Minecraft style "waiting"
|
||||
btn.style.color = Color.yellow;
|
||||
|
||||
// Disable input while rebinding to avoid side effects
|
||||
action.actionMap.Disable();
|
||||
|
||||
var rebindOperation = action.PerformInteractiveRebinding(bindingIndex)
|
||||
.WithControlsExcluding("<Mouse>/position")
|
||||
.WithControlsExcluding("<Mouse>/delta")
|
||||
.WithControlsExcluding("<Keyboard>/escape") // Keep ESC for cancel if needed
|
||||
.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();
|
||||
});
|
||||
|
||||
rebindOperation.Start();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private VisualElement CreateSection(string title)
|
||||
{
|
||||
var label = new Label(title);
|
||||
label.AddToClassList("setting-section-header");
|
||||
label.style.marginTop = 20;
|
||||
return label;
|
||||
private VisualElement CreateSubSection(string title)
|
||||
{
|
||||
var label = new Label(GetT(title));
|
||||
label.AddToClassList("setting-section-header");
|
||||
label.style.marginTop = 20;
|
||||
return label;
|
||||
}
|
||||
|
||||
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
|
||||
{
|
||||
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 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");
|
||||
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;
|
||||
}
|
||||
|
||||
slider.RegisterCallback<PointerEnterEvent>(evt => {
|
||||
_hoveredSlider = slider;
|
||||
_hoveredOnChanged = OnValueChanged;
|
||||
_sliderMin = min;
|
||||
_sliderMax = max;
|
||||
private VisualElement CreateAudioSlider(string label, string prefKey)
|
||||
{
|
||||
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => {
|
||||
PlayerPrefs.SetFloat(prefKey, val); AudioManager.Instance?.SetVolume(prefKey, val);
|
||||
});
|
||||
slider.RegisterCallback<PointerLeaveEvent>(evt => {
|
||||
if (_hoveredSlider == slider) {
|
||||
_hoveredSlider = null;
|
||||
_hoveredOnChanged = null;
|
||||
}
|
||||
sliderRow.RegisterCallback<WheelEvent>(evt => {
|
||||
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;
|
||||
});
|
||||
return sliderRow;
|
||||
}
|
||||
|
||||
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);
|
||||
private VisualElement CreateRebindRow(UnityEngine.InputSystem.InputAction action, int bindingIndex, string labelText)
|
||||
{
|
||||
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);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void StartRebinding(UnityEngine.InputSystem.InputAction action, int bindingIndex, Button btn)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (_hoveredSlider == null) return;
|
||||
@@ -676,10 +562,10 @@ namespace Hallucinate.UI
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (_activeTab == "GENERAL" && _mouseMetricsLabel != null)
|
||||
if (_mouseMetricsLabel != null && _mouseMetricsLabel.panel != null)
|
||||
{
|
||||
var (polling, latency) = MouseMetricsHelper.GetMetrics();
|
||||
_mouseMetricsLabel.text = $"[(report: {polling}/sec latency: {latency:F0}ms)]";
|
||||
_mouseMetricsLabel.text = $"{GetT("MOUSE_LATENCY")} report: {polling}/sec latency: {latency:F0}ms";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,5 +581,13 @@ namespace Hallucinate.UI
|
||||
await Tween.Custom(0f, -100f, duration: 0.3f, ease: Ease.InQuad, onValueChange: val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (LocalizationManager.Instance != null)
|
||||
{
|
||||
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using PrimeTween;
|
||||
using OnlyScove.Scripts;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
#endif
|
||||
|
||||
namespace Hallucinate.UI
|
||||
@@ -20,6 +19,7 @@ namespace Hallucinate.UI
|
||||
private UIDocument _uiDocument;
|
||||
private VisualElement _rootElement;
|
||||
public VisualElement Root => _rootElement;
|
||||
|
||||
private VisualElement _cursorLayer;
|
||||
private VisualElement _mainCursor;
|
||||
|
||||
@@ -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,53 +49,90 @@ 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<VisualElement> _trailSegments = new List<VisualElement>();
|
||||
private List<Vector2> _posHistory = new List<Vector2>();
|
||||
|
||||
private Vector2 _lastMousePos;
|
||||
private float _trailOpacity = 1f;
|
||||
// Osu Trail Pooling
|
||||
private const int MAX_TRAIL_PARTICLES = 60;
|
||||
private readonly List<VisualElement> _trailPool = new List<VisualElement>();
|
||||
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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
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<UIDocument>();
|
||||
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<HUDController>();
|
||||
@@ -110,9 +146,6 @@ namespace Hallucinate.UI
|
||||
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();
|
||||
@@ -123,31 +156,43 @@ namespace Hallucinate.UI
|
||||
float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f);
|
||||
SetUIScale(savedScale);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_uiDocument == null) _uiDocument = GetComponent<UIDocument>();
|
||||
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";
|
||||
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);
|
||||
|
||||
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<PointerDownEvent>(OnGlobalClick, TrickleDown.TrickleDown);
|
||||
|
||||
@@ -157,35 +202,36 @@ namespace Hallucinate.UI
|
||||
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 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<LoginController>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[UIManager] Welcome back, {savedName}!");
|
||||
_ = Push<MainMenuController>();
|
||||
}
|
||||
if (string.IsNullOrEmpty(savedName)) _ = Push<LoginController>();
|
||||
else _ = Push<MainMenuController>();
|
||||
}
|
||||
|
||||
public void OnLoginSuccess()
|
||||
{
|
||||
_ = Push<MainMenuController>();
|
||||
}
|
||||
public void OnLoginSuccess() => _ = Push<MainMenuController>();
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
@@ -196,20 +242,16 @@ namespace Hallucinate.UI
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCancel()
|
||||
{
|
||||
if (_isSettingsOpen) ToggleSettings();
|
||||
}
|
||||
private void HandleCancel() { if (_isSettingsOpen) ToggleSettings(); }
|
||||
|
||||
public async void ToggleSettings()
|
||||
{
|
||||
if (_settingsController == null) return;
|
||||
|
||||
if (!_isSettingsOpen)
|
||||
{
|
||||
_isSettingsOpen = true;
|
||||
_settingsController.Root.BringToFront();
|
||||
_cursorLayer.BringToFront();
|
||||
if (_cursorLayer != null) _cursorLayer.BringToFront();
|
||||
await _settingsController.PlayTransitionIn();
|
||||
}
|
||||
else
|
||||
@@ -222,206 +264,111 @@ 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;
|
||||
if (!Application.isFocused || _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
|
||||
// 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);
|
||||
|
||||
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++)
|
||||
bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1;
|
||||
if (trailEnabled && cursorTrailSprite != null)
|
||||
{
|
||||
int historyIndex = (i + 1) * trailSpacing;
|
||||
if (historyIndex < _posHistory.Count)
|
||||
float dist = Vector2.Distance(uiPos, _lastTrailSpawnPos);
|
||||
if (dist > trailDistanceThreshold)
|
||||
{
|
||||
_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;
|
||||
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<MainMenuController>(mainMenuTemplate);
|
||||
if (_mainMenuController != null && gameIcon != null) _mainMenuController.SetGameIcon(gameIcon);
|
||||
|
||||
_lobbyController = RegisterController<LobbyController>(lobbyTemplate);
|
||||
if (_lobbyController != null) _lobbyController.SetRoomTemplate(roomItemTemplate);
|
||||
|
||||
RegisterController<ProfileController>(profileTemplate);
|
||||
_settingsController = RegisterController<SettingsController>(settingsTemplate);
|
||||
RegisterController<HUDController>(hudTemplate);
|
||||
|
||||
_loginController = RegisterController<LoginController>(loginTemplate);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[UIManager] Failed to initialize controllers: {e}");
|
||||
}
|
||||
catch (Exception e) { Debug.LogError($"[UIManager] Failed to initialize controllers: {e}"); }
|
||||
}
|
||||
|
||||
private T RegisterController<T>(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 (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.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<T>();
|
||||
controller.Initialize(instance, this);
|
||||
_controllers[typeof(T)] = controller;
|
||||
@@ -433,7 +380,6 @@ namespace Hallucinate.UI
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user