Update Setting

This commit is contained in:
2026-05-01 17:57:07 +07:00
parent bcb2c329c5
commit 9c784e77f8
26 changed files with 954 additions and 3233 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5,10 +5,28 @@
</component>
<component name="ChangeListManager">
<list default="true" id="f9183c68-daf0-43b8-be4c-fad79983f91b" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.gemini-workspace-history/active-context.md" beforeDir="false" afterPath="$PROJECT_DIR$/.gemini-workspace-history/active-context.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Resources/Localization/en.json" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Resources/Localization/en.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Resources/Localization/vi.json" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Resources/Localization/vi.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scenes/Lobby.unity" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scenes/Lobby.unity.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scove/UIScaleTest.unity" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scove/UIScaleTest.unity.meta" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Network/BasicSpawner.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Network/BasicSpawner.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Player Controller/PlayerDashState.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Player Controller/PlayerDashState.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/HUDController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/HUDController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/LobbyController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/LobbyController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/LocalizationManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/LocalizationManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/LoginController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/LoginController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/MainMenuController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/PerformanceOverlay.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/PerformanceOverlay.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/ProfileController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/ProfileController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Global.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Global.uss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Assets/UI/Settings.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Settings.uxml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ProjectSettings/EditorBuildSettings.asset" beforeDir="false" afterPath="$PROJECT_DIR$/ProjectSettings/EditorBuildSettings.asset" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -16,7 +34,7 @@
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="15" />
<option name="cachedIndexableFilesCount" value="16" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component>
<component name="Git.Settings">
@@ -151,7 +169,9 @@
<workItem from="1777443280908" duration="5223000" />
<workItem from="1777484328779" duration="32427000" />
<workItem from="1777568077522" duration="8613000" />
<workItem from="1777604072510" duration="12524000" />
<workItem from="1777604072510" duration="12724000" />
<workItem from="1777629255838" duration="2209000" />
<workItem from="1777631506531" duration="1317000" />
</task>
<servers />
</component>

View File

@@ -1,15 +1,98 @@
{
"menu_create": "CREATE ROOM",
"menu_join": "JOIN ROOM",
"menu_settings": "SETTINGS",
"menu_exit": "EXIT",
"settings_title": "SETTINGS",
"settings_general": "GENERAL",
"settings_graphics": "GRAPHICS",
"settings_audio": "AUDIO",
"settings_controls": "CONTROLS",
"settings_back": "BACK",
"label_fov": "Field of View",
"label_sensitivity": "Sensitivity",
"label_language": "Language"
"GENERAL": "GENERAL",
"VIDEO": "VIDEO",
"SOUND": "SOUND",
"CONTROL": "CONTROL",
"ACCOUNT": "ACCOUNT",
"LOGGED_IN_AS": "Logged in as: ",
"LANGUAGE": "LANGUAGE",
"UPDATES": "UPDATES",
"VERSION": "Version: ",
"CHECK_FOR_UPDATES": "CHECK FOR UPDATES",
"UP_TO_DATE": "UP TO DATE",
"CURSOR_MOUSE": "CURSOR & MOUSE",
"CURSOR_SIZE": "Cursor Size",
"ENABLE_TRAIL": "Enable Cursor Trail",
"ENABLE_RIPPLES": "Enable Ripple Effects",
"SENSITIVITY": "Sensitivity",
"MOUSE_LATENCY": "Mouse metrics: ",
"RENDERER": "RENDERER",
"FRAME_LIMITER": "Frame Limiter",
"SHOW_FPS": "Show FPS Counter",
"LAYOUT": "LAYOUT",
"RESOLUTION": "Resolution",
"FULLSCREEN": "Fullscreen Mode",
"BACKGROUND_DIM": "Background Dim",
"UI_SCALE": "UI Scale",
"AUDIO_VOLUMES": "AUDIO VOLUMES",
"MASTER": "Master",
"MUSIC": "Music",
"VFX": "VFX",
"PLAYER": "Player",
"UI": "UI",
"KEY_BINDINGS": "KEY BINDINGS",
"RESET_ALL": "RESET ALL TO DEFAULT",
"SCROLL_HINT": "Use Scroll Wheel to control volume.",
"MENU_PLAY": "PLAY",
"MENU_JOIN": "JOIN",
"MENU_CREATE": "CREATE",
"MENU_SETTINGS": "SETTINGS",
"MENU_PROFILE": "PROFILE",
"MENU_EXIT": "EXIT",
"LOBBY_TITLE": "LOBBY",
"LOBBY_FIND_SESSIONS": "FIND SESSIONS",
"LOBBY_SEARCH_PLACEHOLDER": "Search room by name...",
"LOBBY_BACK": "BACK",
"LOBBY_CREATE_NEW": "CREATE NEW",
"LOBBY_CREATE_HEADER": "CREATE SESSION",
"LOBBY_ROOM_ID_LABEL": "ROOM ID (Required)",
"LOBBY_ROOM_ID_PLACEHOLDER": "e.g. ROOM_123",
"LOBBY_ROOM_NAME_LABEL": "ROOM NAME (Optional)",
"LOBBY_ROOM_NAME_PLACEHOLDER": "e.g. Pro Match Only",
"LOBBY_REQUIRE_PASS": "REQUIRE PASSWORD",
"LOBBY_PASS_PLACEHOLDER": "Password...",
"LOBBY_CANCEL": "CANCEL",
"LOBBY_CREATE_BTN": "CREATE",
"LOBBY_SESSION_NAME_DEFAULT": "SESSION NAME",
"LOBBY_ID_PREFIX": "ID: ",
"LOBBY_HOST_LABEL": "HOST",
"LOBBY_READY": "READY",
"LOBBY_NOT_READY": "NOT READY",
"LOBBY_VS": "VS",
"LOBBY_SYNCING": "SYNCING...",
"LOBBY_WAITING_LABEL": "WAITING...",
"LOBBY_CHAT_PLACEHOLDER": "Press Enter to chat...",
"LOBBY_READY_BTN": "READY UP",
"LOBBY_UNREADY_BTN": "UNREADY",
"LOBBY_START_BTN": "START GAME",
"LOBBY_LEAVE_BTN": "LEAVE ROOM",
"LOBBY_PROTECTED_TITLE": "PROTECTED SESSION",
"LOBBY_PROTECTED_DESC": "This room requires a password",
"LOBBY_JOIN_PASS_PLACEHOLDER": "Enter password...",
"LOBBY_JOIN_PASS_ERROR": "Incorrect password!",
"LOBBY_JOIN_BTN": "JOIN",
"ROOM_STATUS_WAITING": "WAITING",
"ROOM_JOIN_BTN": "JOIN",
"HUD_HEALTH": "HEALTH",
"HUD_STAMINA": "STAMINA",
"HUD_MINIMAP": "MINIMAP",
"HUD_PING_PREFIX": "PING: ",
"HUD_FPS_PREFIX": "FPS: ",
"PROFILE_WIN_RATE": "WIN RATE",
"PROFILE_INVENTORY": "INVENTORY",
"PROFILE_BACK": "BACK",
"PROFILE_LOGOUT": "LOGOUT",
"LOGIN_TITLE": "AUTHENTICATION",
"LOGIN_USER": "USERNAME",
"LOGIN_PASS": "PASSWORD",
"LOGIN_BTN": "LOGIN",
"LOGIN_GUEST": "PLAY AS GUEST",
"LOGIN_STATUS_CONNECTING": "Connecting to server...",
"LOGIN_STATUS_FAILED": "Login failed. Check credentials."
}

View File

@@ -1,15 +1,98 @@
{
"menu_create": "TẠO PHÒNG",
"menu_join": "VÀO PHÒNG",
"menu_settings": "CÀI ĐẶT",
"menu_exit": "THOÁT",
"settings_title": "CÀI ĐẶT",
"settings_general": "CHUNG",
"settings_graphics": "ĐỒ HỌA",
"settings_audio": "ÂM THANH",
"settings_controls": "ĐIỀU KHIỂN",
"settings_back": "QUAY LẠI",
"label_fov": "Tầm nhìn (FOV)",
"label_sensitivity": "Độ nhạy chuột",
"label_language": "Ngôn ngữ"
"GENERAL": "CHUNG",
"VIDEO": "HÌNH ẢNH",
"SOUND": "ÂM THANH",
"CONTROL": "ĐIỀU KHIỂN",
"ACCOUNT": "TÀI KHOẢN",
"LOGGED_IN_AS": "Đã đăng nhập: ",
"LANGUAGE": "NGÔN NGỮ",
"UPDATES": "CẬP NHẬT",
"VERSION": "Phiên bản: ",
"CHECK_FOR_UPDATES": "KIỂM TRA CẬP NHẬT",
"UP_TO_DATE": "BẢN MỚI NHẤT",
"CURSOR_MOUSE": "CON TRỎ & CHUỘT",
"CURSOR_SIZE": "Kích thước con trỏ",
"ENABLE_TRAIL": "Hiệu ứng vệt dài",
"ENABLE_RIPPLES": "Hiệu ứng gợn sóng",
"SENSITIVITY": "Độ nhạy chuột",
"MOUSE_LATENCY": "Tốc độ phản hồi: ",
"RENDERER": "HIỂN THỊ",
"FRAME_LIMITER": "Giới hạn khung hình",
"SHOW_FPS": "Hiện chỉ số FPS",
"LAYOUT": "BỐ CỤC",
"RESOLUTION": "Độ phân giải",
"FULLSCREEN": "Toàn màn hình",
"BACKGROUND_DIM": "Làm tối nền",
"UI_SCALE": "Tỉ lệ giao diện",
"AUDIO_VOLUMES": "ÂM LƯỢNG",
"MASTER": "Tổng",
"MUSIC": "Nhạc nền",
"VFX": "Hiệu ứng",
"PLAYER": "Người chơi",
"UI": "Giao diện",
"KEY_BINDINGS": "PHÍM ĐIỀU KHIỂN",
"RESET_ALL": "ĐẶT LẠI TẤT CẢ",
"SCROLL_HINT": "Sử dụng con lăn chuột để điều chỉnh nhanh âm lượng.",
"MENU_PLAY": "VÀO GAME",
"MENU_JOIN": "VÀO PHÒNG",
"MENU_CREATE": "TẠO PHÒNG",
"MENU_SETTINGS": "CÀI ĐẶT",
"MENU_PROFILE": "HỒ SƠ",
"MENU_EXIT": "THOÁT",
"LOBBY_TITLE": "PHÒNG CHỜ",
"LOBBY_FIND_SESSIONS": "TÌM TRẬN",
"LOBBY_SEARCH_PLACEHOLDER": "Tìm phòng theo tên...",
"LOBBY_BACK": "QUAY LẠI",
"LOBBY_CREATE_NEW": "TẠO PHÒNG MỚI",
"LOBBY_CREATE_HEADER": "TẠO TRẬN ĐẤU",
"LOBBY_ROOM_ID_LABEL": "ID PHÒNG (Bắt buộc)",
"LOBBY_ROOM_ID_PLACEHOLDER": "VD: ROOM_123",
"LOBBY_ROOM_NAME_LABEL": "TÊN PHÒNG (Tùy chọn)",
"LOBBY_ROOM_NAME_PLACEHOLDER": "VD: Chỉ dành cho Pro",
"LOBBY_REQUIRE_PASS": "YÊU CẦU MẬT KHẨU",
"LOBBY_PASS_PLACEHOLDER": "Mật khẩu...",
"LOBBY_CANCEL": "HỦY BỎ",
"LOBBY_CREATE_BTN": "TẠO NGAY",
"LOBBY_SESSION_NAME_DEFAULT": "TÊN PHÒNG",
"LOBBY_ID_PREFIX": "ID: ",
"LOBBY_HOST_LABEL": "CHỦ PHÒNG",
"LOBBY_READY": "SẴN SÀNG",
"LOBBY_NOT_READY": "CHƯA SẴN SÀNG",
"LOBBY_VS": "VS",
"LOBBY_SYNCING": "ĐANG ĐỒNG BỘ...",
"LOBBY_WAITING_LABEL": "ĐANG ĐỢI...",
"LOBBY_CHAT_PLACEHOLDER": "Nhấn Enter để chat...",
"LOBBY_READY_BTN": "SẴN SÀNG",
"LOBBY_UNREADY_BTN": "HỦY SẴN SÀNG",
"LOBBY_START_BTN": "BẮT ĐẦU",
"LOBBY_LEAVE_BTN": "RỜI PHÒNG",
"LOBBY_PROTECTED_TITLE": "PHÒNG CÓ MẬT KHẨU",
"LOBBY_PROTECTED_DESC": "Phòng này yêu cầu mật khẩu để vào",
"LOBBY_JOIN_PASS_PLACEHOLDER": "Nhập mật khẩu...",
"LOBBY_JOIN_PASS_ERROR": "Mật khẩu không chính xác!",
"LOBBY_JOIN_BTN": "VÀO PHÒNG",
"ROOM_STATUS_WAITING": "ĐANG ĐỢI",
"ROOM_JOIN_BTN": "VÀO",
"HUD_HEALTH": "MÁU",
"HUD_STAMINA": "THỂ LỰC",
"HUD_MINIMAP": "BẢN ĐỒ",
"HUD_PING_PREFIX": "PING: ",
"HUD_FPS_PREFIX": "FPS: ",
"PROFILE_WIN_RATE": "TỈ LỆ THẮNG",
"PROFILE_INVENTORY": "KHO ĐỒ",
"PROFILE_BACK": "QUAY LẠI",
"PROFILE_LOGOUT": "ĐĂNG XUẤT",
"LOGIN_TITLE": "XÁC THỰC",
"LOGIN_USER": "TÀI KHOẢN",
"LOGIN_PASS": "MẬT KHẨU",
"LOGIN_BTN": "ĐĂNG NHẬP",
"LOGIN_GUEST": "CHƠI KHÁCH",
"LOGIN_STATUS_CONNECTING": "Đang kết nối server...",
"LOGIN_STATUS_FAILED": "Đăng nhập thất bại. Kiểm tra lại thông tin."
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: e1bb3a5dcccfeee4f948b63b991fd8e6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -225,6 +225,7 @@ GameObject:
- component: {fileID: 458228301}
- component: {fileID: 458228300}
- component: {fileID: 458228299}
- component: {fileID: 458228302}
m_Layer: 0
m_Name: UIManager
m_TagString: Untagged
@@ -298,6 +299,18 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &458228302
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 458228298}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ff7ac0ce8c8c98445b895ac53a4618f1, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Hallucinate.UI.LocalizationManager
--- !u!1 &626355268
GameObject:
m_ObjectHideFlags: 0

View File

@@ -13,6 +13,9 @@ namespace Hallucinate.UI
{
public static BasicSpawner Instance { get; private set; }
private NetworkRunner _runner;
public NetworkRunner Runner => _runner;
private bool _isStarting = false;
public event Action<List<SessionInfo>> OnSessionListUpdatedEvent;
public event Action<string> OnShutdownEvent;
@@ -42,93 +45,125 @@ 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>();
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;
// Đợi 1 frame để đảm bảo component đã bị hủy thực sự
await Task.Yield();
}
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;
}
}
@@ -137,13 +172,17 @@ namespace Hallucinate.UI
OnJoinStartedEvent?.Invoke();
await EnsureRunnerExists();
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 = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
SceneManager = sceneManager
});
if (result.Ok)
{
return true;

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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()
@@ -218,8 +312,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()
@@ -264,12 +363,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 +487,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 +500,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 +535,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 +547,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);
}

View File

@@ -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;

View File

@@ -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()

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -24,6 +24,10 @@ namespace Hallucinate.UI
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;
@@ -57,6 +61,15 @@ namespace Hallucinate.UI
_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();
@@ -76,21 +89,48 @@ namespace Hallucinate.UI
if (closeBtn != null) closeBtn.clicked += () => uiManager.ToggleSettings();
_masterVol = PlayerPrefs.GetFloat("MasterVolume", 80f);
// 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();
// Bung ra ngay lập tức với animation cực mượt
ExpandSidebar();
}
private void OnSidebarPointerLeave(PointerLeaveEvent evt)
{
_hoverTimer.Stop();
// Thu gọn ngay lập tức
CollapseSidebar();
}
@@ -99,7 +139,6 @@ namespace Hallucinate.UI
if (_isExpanded) return;
_isExpanded = true;
_tabsColumn.AddToClassList("sidebar-expanded");
// Animation 0.5s OutQuart cho cảm giác cao cấp
Tween.Custom(_tabsColumn.resolvedStyle.width, 240f, duration: 0.5f, ease: Ease.OutQuart, onValueChange: val => _tabsColumn.style.width = val);
}
@@ -111,6 +150,201 @@ namespace Hallucinate.UI
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()
{
int frameLimitIdx = PlayerPrefs.GetInt("FrameLimiter", 2);
@@ -119,8 +353,6 @@ namespace Hallucinate.UI
PerformanceOverlay.SetVisible(_fpsVisible);
float dim = PlayerPrefs.GetFloat("BackgroundDim", 50f);
ApplyBackgroundDim(dim);
bool isFull = PlayerPrefs.GetInt("Fullscreen", Screen.fullScreen ? 1 : 0) == 1;
Screen.fullScreen = isFull;
}
private void ApplyFrameLimit(int index)
@@ -230,7 +462,6 @@ namespace Hallucinate.UI
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
AudioManager.Instance?.SetVolume("MasterVolume", _masterVol);
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
if (_activeTab == "SOUND") SwitchTab("SOUND");
}
private void UpdateSubVolume(string key, float delta)
@@ -239,7 +470,6 @@ namespace Hallucinate.UI
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");
}
private async void ShowVolumeOverlay()
@@ -259,109 +489,36 @@ namespace Hallucinate.UI
}
}
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;
}
private VisualElement CreateSubSection(string title)
{
var label = new Label(GetT(title));
label.AddToClassList("setting-section-header");
label.style.marginTop = 20;
return label;
}
private void RenderGeneralTab()
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
{
_content.Add(CreateSection("ACCOUNT"));
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(PlayerPrefs.GetString("Username", "Guest")) { 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 => uiManager.SetCursorSize(val)));
var trailToggle = new Toggle("Enable Cursor Trail") { value = PlayerPrefs.GetInt("CursorTrail", 1) == 1 };
trailToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorTrail(evt.newValue));
_content.Add(trailToggle);
var rippleToggle = new Toggle("Enable Ripple Effects") { value = PlayerPrefs.GetInt("CursorRipples", 1) == 1 };
rippleToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorRipples(evt.newValue));
_content.Add(rippleToggle);
_content.Add(CreateSliderWithInput("Sensitivity", 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => uiManager.SetMouseSensitivity(val)));
_content.Add(new Toggle("Raw Input (Bypass Acceleration)") { value = true });
_mouseMetricsLabel = new Label("[(report: 0/sec latency: 0ms)]") { style = { fontSize = 11, color = Color.gray, marginTop = 5 } };
_content.Add(_mouseMetricsLabel);
}
private void RenderVideoTab()
{
_content.Add(CreateSection("RENDERER"));
var frameLimit = new DropdownField("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("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();
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]), 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)));
}
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 } });
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 5, marginBottom = 5 } };
var label = new Label(labelText) { style = { width = Length.Percent(35) } }; label.AddToClassList("text-body");
var slider = new Slider(min, max) { value = startVal, style = { flexGrow = 1 } };
var input = new TextField { value = startVal.ToString("F1"), style = { width = 50, marginLeft = 10 } }; input.AddToClassList("input-field");
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;
}
private VisualElement CreateAudioSlider(string label, string prefKey)
@@ -377,35 +534,6 @@ namespace Hallucinate.UI
return sliderRow;
}
private void RenderControlTab()
{
_content.Add(CreateSection("KEY BINDINGS"));
if (uiManager.InputReader?.InputActions == null) { _content.Add(new Label("Input Actions not found.") { style = { color = Color.white } }); 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 = "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 = row.style.paddingBottom = 10; row.style.borderBottomWidth = 1; row.style.borderBottomColor = new Color(1, 1, 1, 0.1f);
@@ -424,21 +552,6 @@ namespace Hallucinate.UI
op.Start();
}
private VisualElement CreateSection(string title) { var label = new Label(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 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");
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;
}
private void OnKeyDown(KeyDownEvent evt)
{
if (_hoveredSlider == null) return;
@@ -449,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";
}
}
@@ -468,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;
}
}
}
}

View File

@@ -127,6 +127,10 @@ namespace Hallucinate.UI
public void SetMouseSensitivity(float sensitivity)
{
PlayerPrefs.SetFloat("MouseSensitivity", sensitivity);
if (OnlyScove.Scripts.SettingsManager.Instance != null)
{
OnlyScove.Scripts.SettingsManager.Instance.SetSensitivity(sensitivity);
}
}
public void OnGameStarted()
@@ -161,6 +165,15 @@ namespace Hallucinate.UI
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);

View File

@@ -67,6 +67,7 @@
font-size: 18px;
color: #eeeeee;
-unity-font-style: normal;
/* Giúp chữ mượt hơn */
-unity-text-outline-width: 0.1px;
-unity-text-outline-color: rgba(255, 255, 255, 0.1);
}
@@ -221,16 +222,17 @@ DropdownField:hover .unity-base-field__input {
SMART SIDEBAR (STABLE OVERLAY SYSTEM)
============================================================ */
.sidebar-tabs-container {
background-color: rgba(15, 15, 15, 0.98);
background-color: rgb(10, 10, 10) !important; /* ĐEN ĐẶC TUYỆT ĐỐI */
opacity: 1 !important;
padding-top: 60px;
border-right-width: 2px;
border-right-color: rgba(0, 255, 204, 0.1);
overflow: hidden;
height: 100%;
position: absolute; /* Phải là absolute để lướt đè */
position: absolute;
left: 0;
top: 0;
z-index: 100;
z-index: 999; /* ƯU TIÊN HIỂN THỊ CAO NHẤT */
}
.sidebar-collapsed {
@@ -239,6 +241,7 @@ DropdownField:hover .unity-base-field__input {
.sidebar-expanded {
border-right-color: #00ffcc;
background-color: rgb(10, 10, 10) !important;
}
.sidebar-tab {
@@ -295,7 +298,7 @@ DropdownField:hover .unity-base-field__input {
width: 50px;
height: 50px;
border-radius: 25px;
margin: 15px auto;
margin: 15px 15px;
background-color: rgba(255, 255, 255, 0.1);
}

View File

@@ -1,8 +1,17 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<Style src="project:/Assets/UI/Global.uss" />
<ui:VisualElement name="SettingsRoot" class="screen-root" picking-mode="Position" style="background-color: rgba(0, 0, 0, 0.7); justify-content: flex-start; align-items: stretch;">
<ui:VisualElement name="Sidebar" picking-mode="Position" class="panel-glass border-accent" style="width: 40%; height: 100%; flex-direction: row; padding: 0; border-radius: 0 30px 30px 0; border-right-width: 3px; border-left-width: 0; border-top-width: 0; border-bottom-width: 0; min-width: 500px;">
<!-- Tabs Column (The Smart Sidebar) -->
<ui:VisualElement name="Sidebar" picking-mode="Position" class="panel-glass border-accent" style="width: 40%; height: 100%; flex-direction: row; padding: 0; border-radius: 0 30px 30px 0; border-right-width: 3px; border-left-width: 0; border-top-width: 0; border-bottom-width: 0; min-width: 500px; position: relative;">
<!-- Details Column (Rendered FIRST = BOTTOM LAYER) -->
<ui:VisualElement name="DetailsColumn" class="settings-details-column" style="min-width: 320px;">
<ui:Label name="TabTitle" text="GENERAL" class="text-heading" />
<ui:ScrollView name="SettingsContent" class="scroll-list" style="flex-grow: 1;">
<!-- Content will be injected here -->
</ui:ScrollView>
</ui:VisualElement>
<!-- Tabs Column (Rendered SECOND = TOP LAYER) -->
<ui:VisualElement name="TabsColumn" class="sidebar-tabs-container sidebar-collapsed">
<ui:Button name="GeneralTab" class="sidebar-tab active-tab">
<ui:VisualElement class="tab-icon-box"><ui:VisualElement class="tab-icon" /></ui:VisualElement>
@@ -22,18 +31,11 @@
</ui:Button>
<ui:VisualElement style="flex-grow: 1;" />
<ui:Button name="CloseSettingsBtn" class="button-spring btn-icon-only" style="margin: 20px;">
<ui:Button name="CloseSettingsBtn" class="button-spring btn-icon-only" style="margin-bottom: 20px;">
<ui:VisualElement class="tab-icon" style="background-image: none;" />
</ui:Button>
</ui:VisualElement>
<!-- Details Column -->
<ui:VisualElement name="DetailsColumn" class="settings-details-column" style="min-width: 320px;">
<ui:Label name="TabTitle" text="GENERAL" class="text-heading" />
<ui:ScrollView name="SettingsContent" class="scroll-list" style="flex-grow: 1;">
<!-- Content will be injected here -->
</ui:ScrollView>
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -5,17 +5,8 @@ EditorBuildSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Scenes:
- enabled: 0
path: Assets/Scove/DEMO FUSION.unity
guid: c3d4d808eec835545b53364265c55e97
- enabled: 0
path: Assets/Scove/Player Movement.unity
guid: 18e8e6bc4985a3b46a52116295088fce
- enabled: 0
path: Assets/Scenes/SampleScene.unity
guid: 99c9720ab356a0642a771bea13969a05
- enabled: 1
path: Assets/Scove/UIScaleTest.unity
path: Assets/Scenes/UI.unity
guid: 9feda3fec581ecb4aa311e4a937c625a
- enabled: 1
path: Assets/Scenes/Main Scene.unity