This commit is contained in:
Lucastaa
2026-05-02 20:33:34 +07:00
35 changed files with 1617 additions and 2635 deletions

View File

@@ -11,7 +11,19 @@ namespace Hallucinate.UI
{
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
public static BasicSpawner Instance { get; private set; }
private static BasicSpawner _instance;
public static BasicSpawner Instance
{
get
{
if (_instance == null)
{
_instance = UnityEngine.Object.FindFirstObjectByType<BasicSpawner>();
}
return _instance;
}
}
private NetworkRunner _runner;
public NetworkRunner Runner => _runner;
@@ -29,12 +41,15 @@ namespace Hallucinate.UI
private void Awake()
{
if (Instance != null && Instance != this)
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
_instance = this;
// Ensure this is a root object so DontDestroyOnLoad works correctly
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
@@ -57,8 +72,13 @@ namespace Hallucinate.UI
await _runner.Shutdown();
}
Debug.Log("[BasicSpawner] Destroying existing runner component.");
Destroy(_runner);
// Check if it still exists (Unity pseudo-null check)
if (_runner != null)
{
// Only log if it's actually a valid object to destroy
// If it's already marked for destruction, Unity == null will be true soon
Destroy(_runner);
}
_runner = null;
await Task.Yield();
@@ -69,6 +89,8 @@ namespace Hallucinate.UI
}
}
if (this == null) return; // BasicSpawner itself might be destroyed
Debug.Log("[BasicSpawner] Creating new NetworkRunner component.");
_runner = gameObject.AddComponent<NetworkRunner>();
_runner.ProvideInput = true;
@@ -103,7 +125,12 @@ namespace Hallucinate.UI
public async Task<bool> StartHost(string sessionName, string displayName, string password = null)
{
if (_isStarting) return false;
// Wait for any existing startup process (like StartLobby) to finish
while (_isStarting)
{
await Task.Yield();
}
_isStarting = true;
try
@@ -274,6 +301,17 @@ namespace Hallucinate.UI
_spawnedCharacters.Remove(player);
}
// Logic Reassign Leader (Logical)
if (runner.IsServer && PlayerDataManager.Instance != null && PlayerDataManager.Instance.Leader == player)
{
var nextLeader = runner.ActivePlayers.FirstOrDefault();
if (nextLeader != PlayerRef.None)
{
PlayerDataManager.Instance.Leader = nextLeader;
Debug.Log($"[BasicSpawner] Leader left. New logical leader: {nextLeader}");
}
}
if (runner.IsServer && player == runner.LocalPlayer)
{
runner.Shutdown();
@@ -292,6 +330,13 @@ namespace Hallucinate.UI
return;
}
// Nếu đang trong quá trình Host Migration, đừng quay về menu
if (shutdownReason == ShutdownReason.HostMigration)
{
Debug.Log("[BasicSpawner] Shutdown due to Host Migration. Waiting for recovery...");
return;
}
if (UIManager.Instance != null)
{
UIManager.Instance.OnBackToMenu();
@@ -327,7 +372,34 @@ namespace Hallucinate.UI
public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
public async void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)
{
Debug.Log("[BasicSpawner] OnHostMigration triggered!");
// 1. Shutdown existing runner properly
await runner.Shutdown(false);
// 2. Create new runner
await EnsureRunnerExists();
// 3. Restart as new Host/Server using the migration token
var result = await _runner.StartGame(new StartGameArgs()
{
HostMigrationToken = hostMigrationToken,
SceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
});
if (result.Ok)
{
Debug.Log("[BasicSpawner] Host Migration SUCCESSFUL");
}
else
{
Debug.LogError($"[BasicSpawner] Host Migration FAILED: {result.ShutdownReason}");
UIManager.Instance?.OnBackToMenu();
}
}
public void OnSceneLoadDone(NetworkRunner runner)
{
@@ -341,17 +413,10 @@ namespace Hallucinate.UI
_spawnedCharacters.Add(player, networkPlayerObject);
}
}
// Removed incorrect UI transition for Lobby/Menu scenes to allow LobbyController to manage its state.
// The original logic incorrectly called UIManager.OnBackToMenu() when entering the Lobby scene,
// causing the redirect to the Main Menu after creating a room.
// This block ensures that only the Main Scene triggers a specific UI transition (OnGameStarted).
// If other scenes like "Lobby" or "Menu" are loaded, no automatic transition is forced from here,
// letting scene-specific controllers (like LobbyController) manage their UI.
if (currentSceneName == "Main Scene")
{
UIManager.Instance?.OnGameStarted();
}
// Removed the problematic else-if block that would incorrectly call OnBackToMenu for "Lobby" or "Menu" scenes.
}

View File

@@ -16,23 +16,46 @@ public class PlayerDataManager : NetworkBehaviour
[Networked]
public NetworkDictionary<PlayerRef, _PlayerMetaData> Players => default;
[Networked]
public PlayerRef Leader { get; set; }
public event Action<PlayerRef, string> OnChatMessageReceived;
public override void Spawned()
{
Instance = this;
if (Object.HasStateAuthority)
{
Leader = Runner.LocalPlayer;
}
}
public override void Despawned(NetworkRunner runner, bool hasState)
{
if (Instance == this) Instance = null;
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_TransferLeader(PlayerRef newLeader)
{
if (Players.ContainsKey(newLeader))
{
Leader = newLeader;
}
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_UpdatePlayerMetaData(PlayerRef playerRef, _PlayerMetaData metaData)
{
if (Object == null || !Object.IsValid) return;
Players.Set(playerRef, metaData);
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_SetReady(PlayerRef playerRef, bool ready)
{
if (Object == null || !Object.IsValid) return;
if (Players.TryGet(playerRef, out var data))
{
data.IsReady = ready;
@@ -48,6 +71,10 @@ public class PlayerDataManager : NetworkBehaviour
public bool TryGetPlayerMetaData(PlayerRef playerRef, out _PlayerMetaData metaData)
{
metaData = default;
// Kiểm tra xem object đã được Spawned chưa trước khi truy cập networked property
if (Object == null || !Object.IsValid) return false;
return Players.TryGet(playerRef, out metaData);
}
}

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -12,118 +12,89 @@ namespace Hallucinate.UI
private VisualTreeAsset _roomItemTemplate;
private PlayerDataManager _playerDataManager;
// Containers
private VisualElement _joinContainer, _createContainer, _loungeContainer, _passOverlay;
// Create Room Fields
private TextField _roomIDInput, _roomNameInput, _roomPassInput;
private Toggle _passToggle;
// Join Room Fields
private Label _createErrorLabel;
private Button _confirmCreateBtn;
private ScrollView _roomList;
private TextField _joinPassInput;
private Label _joinPassError;
private SessionInfo _selectedSession;
// Lounge Elements
private Label _loungeRoomName;
private Button _readyBtn, _startBtn;
// Host Slot
private Label _hostNameLabel, _hostStatusLabel;
private VisualElement _hostChatBox;
private Label _hostChatMessage;
// Guest Slot
private Label _guestNameLabel, _guestStatusLabel;
private VisualElement _guestChatBox;
private Label _guestChatMessage;
// Chat Input
private TextField _chatInput;
private VisualElement _hostAvatar, _guestAvatar;
private VisualElement _transferHostOverlay;
private Button _confirmTransferBtn, _closeTransferBtn;
private PlayerRef _pendingTransferPlayer;
private bool _isBusy = false;
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
{
base.Initialize(uxmlRoot, manager);
// Query Containers
_joinContainer = root.Q<VisualElement>("JoinContainer");
_createContainer = root.Q<VisualElement>("CreateContainer");
_loungeContainer = root.Q<VisualElement>("LoungeContainer");
_passOverlay = root.Q<VisualElement>("PasswordOverlay");
// Create Room Fields
_roomIDInput = root.Q<TextField>("RoomIDInput");
_roomNameInput = root.Q<TextField>("RoomNameInput");
_roomPassInput = root.Q<TextField>("RoomPassInput");
_passToggle = root.Q<Toggle>("PassToggle");
// Join Room Fields
_createErrorLabel = root.Q<Label>("CreateErrorLabel");
_roomList = root.Q<ScrollView>("RoomList");
_joinPassInput = root.Q<TextField>("JoinPassInput");
_joinPassError = root.Q<Label>("JoinPassError");
// Lounge Elements
_loungeRoomName = root.Q<Label>("LoungeRoomName");
_readyBtn = root.Q<Button>("ReadyBtn");
_startBtn = root.Q<Button>("StartBtn");
// Host Slot
if (_startBtn != null) _startBtn.style.display = DisplayStyle.None; // Default to hidden
_hostNameLabel = root.Q<Label>("HostName");
_hostStatusLabel = root.Q<Label>("HostReadyStatus");
_hostChatBox = root.Q<VisualElement>("HostChatBox");
_hostChatMessage = root.Q<Label>("HostChatMessage");
// Guest Slot
_guestNameLabel = root.Q<Label>("GuestName");
_guestStatusLabel = root.Q<Label>("GuestReadyStatus");
_guestChatBox = root.Q<VisualElement>("GuestChatBox");
_guestChatMessage = root.Q<Label>("GuestChatMessage");
// Chat Input
_chatInput = root.Q<TextField>("ChatInput");
// Event Bindings
_hostAvatar = root.Q<VisualElement>("HostAvatar");
_guestAvatar = root.Q<VisualElement>("GuestAvatar");
_transferHostOverlay = root.Q<VisualElement>("TransferHostOverlay");
_confirmTransferBtn = root.Q<Button>("ConfirmTransferBtn");
_closeTransferBtn = root.Q<Button>("CloseTransferBtn");
root.Q<Button>("GoToCreateBtn").clicked += ShowCreate;
root.Q<Button>("CancelCreateBtn").clicked += ShowJoin;
root.Q<Button>("BackToMenuBtn").clicked += async () => await uiManager.Pop();
root.Q<Button>("ConfirmCreateBtn").clicked += OnCreateRoomClicked;
root.Q<Button>("BackToMenuBtn").clicked += async () => { if (_isBusy) return; await uiManager.Pop(); };
_confirmCreateBtn = root.Q<Button>("ConfirmCreateBtn");
if (_confirmCreateBtn != null) _confirmCreateBtn.clicked += OnCreateRoomClicked;
root.Q<Button>("ConfirmJoinBtn").clicked += OnConfirmPasswordClicked;
root.Q<Button>("ClosePassBtn").clicked += () => { if(_passOverlay != null) _passOverlay.style.display = DisplayStyle.None; };
root.Q<Button>("LeaveLoungeBtn").clicked += OnLeaveLoungeClicked;
if (_readyBtn != null) _readyBtn.clicked += OnReadyClicked;
if (_startBtn != null) _startBtn.clicked += OnStartClicked;
if (_passToggle != null) _passToggle.RegisterValueChangedCallback(evt => { if (_roomPassInput != null) _roomPassInput.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; });
if (_chatInput != null) _chatInput.RegisterCallback<KeyDownEvent>(OnChatKeyDown, TrickleDown.TrickleDown);
if (_hostAvatar != null) _hostAvatar.RegisterCallback<ClickEvent>(evt => OnAvatarClicked(true));
if (_guestAvatar != null) _guestAvatar.RegisterCallback<ClickEvent>(evt => OnAvatarClicked(false));
if (_confirmTransferBtn != null) _confirmTransferBtn.clicked += OnConfirmTransferHost;
if (_closeTransferBtn != null) _closeTransferBtn.clicked += () => { if(_transferHostOverlay != null) _transferHostOverlay.style.display = DisplayStyle.None; };
if (_passToggle != null)
{
_passToggle.RegisterValueChangedCallback(evt =>
{
if (_roomPassInput != null)
_roomPassInput.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None;
});
}
if (_chatInput != null)
{
_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();
}
else
{
Invoke(nameof(RegisterSpawnerEvents), 0.1f);
}
if (LocalizationManager.Instance != null) { LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization; ApplyLocalization(); }
if (BasicSpawner.Instance != null) RegisterSpawnerEvents();
else Invoke(nameof(RegisterSpawnerEvents), 0.1f);
}
private void RegisterSpawnerEvents()
@@ -131,7 +102,6 @@ namespace Hallucinate.UI
if (BasicSpawner.Instance == null) return;
BasicSpawner.Instance.OnSessionListUpdatedEvent += UpdateRoomList;
BasicSpawner.Instance.OnJoinFailedEvent += () => { if(_joinPassError != null) _joinPassError.style.display = DisplayStyle.Flex; };
BasicSpawner.Instance.OnJoinStartedEvent += () => { };
}
private void OnChatKeyDown(KeyDownEvent evt)
@@ -140,38 +110,23 @@ namespace Hallucinate.UI
{
evt.StopImmediatePropagation();
evt.PreventDefault();
string msg = _chatInput.value.Trim();
if (!string.IsNullOrEmpty(msg) && PlayerDataManager.Instance != null)
{
var runner = Object.FindFirstObjectByType<NetworkRunner>();
if (runner != null)
{
PlayerDataManager.Instance.RPC_SendChatMessage(runner.LocalPlayer, msg);
_chatInput.value = "";
// Re-focus after clearing
_chatInput.Focus();
}
var runner = BasicSpawner.Instance.Runner;
if (runner != null) { PlayerDataManager.Instance.RPC_SendChatMessage(runner.LocalPlayer, msg); _chatInput.value = ""; _chatInput.Focus(); }
}
}
}
private void OnChatMessageReceived(PlayerRef sender, string message)
{
var runner = Object.FindFirstObjectByType<NetworkRunner>();
var runner = BasicSpawner.Instance.Runner;
if (runner == null) return;
// Kiểm tra sender là Host hay Guest
bool isHost = sender.PlayerId == 1; // Trong Host Mode, người tạo phòng luôn có ID 1
if (isHost)
{
ShowChatBubble(_hostChatBox, _hostChatMessage, message);
}
else
{
ShowChatBubble(_guestChatBox, _guestChatMessage, message);
}
var sortedPlayers = runner.ActivePlayers.OrderBy(p => p.PlayerId).ToList();
bool isHost = sortedPlayers.Count > 0 && sender == sortedPlayers[0];
if (isHost) ShowChatBubble(_hostChatBox, _hostChatMessage, message);
else ShowChatBubble(_guestChatBox, _guestChatMessage, message);
}
private async void ShowChatBubble(VisualElement box, Label label, string msg)
@@ -180,106 +135,18 @@ namespace Hallucinate.UI
label.text = msg;
box.style.display = DisplayStyle.Flex;
await Task.Delay(4000);
if (label.text == msg) // Chỉ ẩn nếu chưa có tin nhắn mới đè lên
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");
if (label.text == msg) box.style.display = DisplayStyle.None;
}
private void ApplyLocalization() { if (LocalizationManager.Instance == null) return; }
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()
{
await base.PlayTransitionIn();
ShowJoin();
public override async Task PlayTransitionIn()
{
_isBusy = false;
await base.PlayTransitionIn();
ShowJoin();
}
public void ShowJoin()
@@ -287,19 +154,17 @@ 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;
// 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();
}
var spawner = BasicSpawner.Instance;
if (spawner != null && (spawner.Runner == null || !spawner.Runner.IsRunning)) _ = spawner.StartLobby();
}
public void ShowCreate()
{
if (_joinContainer != null) _joinContainer.style.display = DisplayStyle.None;
if (_createContainer != null) _createContainer.style.display = DisplayStyle.Flex;
if (_createErrorLabel != null) _createErrorLabel.style.display = DisplayStyle.None;
if (_confirmCreateBtn != null) _confirmCreateBtn.SetEnabled(true);
if (_roomIDInput != null) _roomIDInput.value = "ROOM_" + Random.Range(1000, 9999).ToString();
}
private void ShowLounge(string roomName)
@@ -308,43 +173,57 @@ namespace Hallucinate.UI
if (_createContainer != null) _createContainer.style.display = DisplayStyle.None;
if (_loungeContainer != null) _loungeContainer.style.display = DisplayStyle.Flex;
if (_loungeRoomName != null) _loungeRoomName.text = roomName.ToUpper();
_playerDataManager = Object.FindFirstObjectByType<PlayerDataManager>();
if (_playerDataManager != null)
var spawner = BasicSpawner.Instance;
if (spawner != null && spawner.Runner != null && spawner.Runner.SessionInfo != null)
{
_playerDataManager.OnChatMessageReceived += OnChatMessageReceived;
var loungeIdLabel = root.Q<Label>("LoungeID");
if (loungeIdLabel != null) loungeIdLabel.text = GetT("LOBBY_ID_PREFIX") + spawner.Runner.SessionInfo.Name;
}
_playerDataManager = PlayerDataManager.Instance;
if (_playerDataManager != null) _playerDataManager.OnChatMessageReceived += OnChatMessageReceived;
}
private async void OnCreateRoomClicked()
{
Debug.Log("[LobbyController] Create Room Clicked");
if (_isBusy) return;
_isBusy = true;
if (_confirmCreateBtn != null) _confirmCreateBtn.SetEnabled(false);
if (_createErrorLabel != null) _createErrorLabel.style.display = DisplayStyle.None;
var spawner = BasicSpawner.Instance;
if (spawner == null)
{
Debug.LogError("[LobbyController] Spawner Instance is NULL!");
return;
{
ShowCreateError("System Error: Spawner missing. Please re-enter the Lobby.");
_isBusy = false;
return;
}
string id = _roomIDInput != null && !string.IsNullOrEmpty(_roomIDInput.value)
? _roomIDInput.value.Trim()
: Random.Range(1000, 9999).ToString();
if (_roomIDInput != null) _roomIDInput.value = id;
string name = _roomNameInput != null && !string.IsNullOrEmpty(_roomNameInput.value)
? _roomNameInput.value
: $"Room {id}";
string pass = (_passToggle != null && _passToggle.value && _roomPassInput != null)
? _roomPassInput.value
: null;
bool success = await spawner.StartHost(id, name, pass);
if (success)
string id = (_roomIDInput != null && !string.IsNullOrEmpty(_roomIDInput.value)) ? _roomIDInput.value.Trim() : "ROOM_" + Random.Range(1000, 9999).ToString();
string name = (_roomNameInput != null && !string.IsNullOrEmpty(_roomNameInput.value)) ? _roomNameInput.value.Trim() : id;
string pass = (_passToggle != null && _passToggle.value && _roomPassInput != null) ? _roomPassInput.value : null;
try
{
ShowLounge(name);
bool success = await spawner.StartHost(id, name, pass);
if (success) ShowLounge(name);
else ShowCreateError("Failed to create room. ID might be taken.");
}
catch (System.Exception ex)
{
ShowCreateError("Network Error: " + ex.Message);
}
finally
{
_isBusy = false;
if (_confirmCreateBtn != null) _confirmCreateBtn.SetEnabled(true);
}
}
private void ShowCreateError(string message)
{
if (_createErrorLabel != null) { _createErrorLabel.text = message; _createErrorLabel.style.display = DisplayStyle.Flex; }
if (_confirmCreateBtn != null) _confirmCreateBtn.SetEnabled(true);
}
private void UpdateRoomList(List<SessionInfo> sessions)
@@ -355,37 +234,25 @@ namespace Hallucinate.UI
{
if (_roomItemTemplate == null) continue;
var item = _roomItemTemplate.Instantiate();
// Hiển thị tên phòng thân thiện nếu có
string displayName = session.Name;
if (session.Properties.TryGetValue("rn", out var rnProp))
{
displayName = rnProp;
}
if (session.Properties.TryGetValue("rn", out var rnProp)) displayName = rnProp;
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.text = GetT("ROOM_JOIN_BTN");
joinBtn.clicked += () => OnRoomItemClicked(session);
}
if (joinBtn != null) { joinBtn.text = GetT("ROOM_JOIN_BTN"); joinBtn.clicked += () => OnRoomItemClicked(session); }
_roomList.Add(item);
}
}
private async void OnRoomItemClicked(SessionInfo session)
{
if (_isBusy) return;
bool needsPass = session.Properties.ContainsKey("pw");
if (needsPass)
{
@@ -394,180 +261,228 @@ namespace Hallucinate.UI
if (_joinPassError != null) _joinPassError.style.display = DisplayStyle.None;
if (_joinPassInput != null) _joinPassInput.value = "";
}
else
{
await JoinRoom(session.Name, null);
}
else await JoinRoom(session.Name, null);
}
private async void OnConfirmPasswordClicked()
{
if (_selectedSession == null) return;
if (_isBusy || _selectedSession == null) return;
_isBusy = true;
string pass = _joinPassInput != null ? _joinPassInput.value : "";
if (_passOverlay != null) _passOverlay.style.display = DisplayStyle.None;
await JoinRoom(_selectedSession.Name, pass);
_isBusy = false;
}
private async Task JoinRoom(string sessionName, string password)
private async Task JoinRoom(string sessionName, string password)
{
if (BasicSpawner.Instance != null)
{
if (await BasicSpawner.Instance.StartClient(sessionName, password)) ShowLounge(sessionName);
}
}
private void OnReadyClicked()
{
if (_isBusy) return;
var runner = BasicSpawner.Instance.Runner;
if (runner != null && _playerDataManager != null && _playerDataManager.TryGetPlayerMetaData(runner.LocalPlayer, out var myData))
_playerDataManager.RPC_SetReady(runner.LocalPlayer, !myData.IsReady);
}
private void OnStartClicked()
{
if (_isBusy) return;
BasicSpawner.Instance?.StartGame();
}
private void OnAvatarClicked(bool isHostSlot)
{
if (BasicSpawner.Instance != null)
if (_playerDataManager == null || _playerDataManager.Leader != BasicSpawner.Instance.Runner.LocalPlayer) return;
var runner = BasicSpawner.Instance.Runner;
var sortedPlayers = runner.ActivePlayers.OrderBy(p => p.PlayerId).ToList();
PlayerRef clickedPlayer = PlayerRef.None;
if (isHostSlot && sortedPlayers.Count > 0) clickedPlayer = sortedPlayers[0];
else if (!isHostSlot && sortedPlayers.Count > 1) clickedPlayer = sortedPlayers[1];
if (clickedPlayer != PlayerRef.None && clickedPlayer != runner.LocalPlayer)
{
bool success = await BasicSpawner.Instance.StartClient(sessionName, password);
if (success) ShowLounge(sessionName);
_pendingTransferPlayer = clickedPlayer;
if (_transferHostOverlay != null) _transferHostOverlay.style.display = DisplayStyle.Flex;
}
}
private void OnReadyClicked()
private void OnConfirmTransferHost()
{
var runner = Object.FindFirstObjectByType<NetworkRunner>();
if (runner != null && _playerDataManager != null)
if (_pendingTransferPlayer != PlayerRef.None && _playerDataManager != null)
{
if (_playerDataManager.TryGetPlayerMetaData(runner.LocalPlayer, out var myData))
{
_playerDataManager.RPC_SetReady(runner.LocalPlayer, !myData.IsReady);
}
_playerDataManager.RPC_TransferLeader(_pendingTransferPlayer);
if (_transferHostOverlay != null) _transferHostOverlay.style.display = DisplayStyle.None;
}
}
private async void OnLeaveLoungeClicked()
{
if (_isBusy) return;
_isBusy = true;
var runner = BasicSpawner.Instance.Runner;
if (runner != null) await runner.Shutdown(false);
if (_playerDataManager != null) _playerDataManager.OnChatMessageReceived -= OnChatMessageReceived;
ShowJoin();
_isBusy = false;
}
private void OnStartClicked()
{
BasicSpawner.Instance?.StartGame();
}
private async void OnLeaveLoungeClicked()
{
var runner = Object.FindFirstObjectByType<NetworkRunner>();
if (runner != null)
{
await runner.Shutdown();
}
if (_playerDataManager != null)
{
_playerDataManager.OnChatMessageReceived -= OnChatMessageReceived;
}
ShowJoin();
}
public override void Update()
{
if (_loungeContainer != null && _loungeContainer.style.display == DisplayStyle.Flex)
{
UpdateLoungeUI();
}
}
public override void Update() { if (_loungeContainer != null && _loungeContainer.style.display == DisplayStyle.Flex) UpdateLoungeUI(); }
private void UpdateLoungeUI()
{
var runner = Object.FindFirstObjectByType<NetworkRunner>();
var spawner = BasicSpawner.Instance;
if (spawner == null) return;
var runner = spawner.Runner;
if (runner == null) return;
if (_playerDataManager == null)
// 1. PlayerDataManager Sync
if (_playerDataManager == null || _playerDataManager.Object == null || !_playerDataManager.Object.IsValid)
{
_playerDataManager = Object.FindFirstObjectByType<PlayerDataManager>();
if (_playerDataManager != null)
_playerDataManager = PlayerDataManager.Instance;
if (_playerDataManager != null)
{
_playerDataManager.OnChatMessageReceived += OnChatMessageReceived;
}
else return; // Still waiting for synchronization
}
if (_playerDataManager == null || _playerDataManager.Object == null || !_playerDataManager.Object.IsValid) return;
PlayerRef hostRef = PlayerRef.None;
PlayerRef guestRef = PlayerRef.None;
// Trong Host Mode, chủ phòng luôn là người có PlayerId = 1
// 2. Identify Players
var sortedPlayers = runner.ActivePlayers.OrderBy(p => p.PlayerId).ToList();
if (sortedPlayers.Count > 0) hostRef = sortedPlayers[0];
if (sortedPlayers.Count > 1) guestRef = sortedPlayers[1];
PlayerRef hostRef = sortedPlayers.Count > 0 ? sortedPlayers[0] : PlayerRef.None;
PlayerRef guestRef = sortedPlayers.Count > 1 ? sortedPlayers[1] : PlayerRef.None;
PlayerRef leaderRef = _playerDataManager.Leader;
// Update Room Name for Guest
if (runner.SessionInfo != null && runner.SessionInfo.Properties.TryGetValue("rn", out var rnProp))
// 3. Strict Visibility Check (Leader Only)
bool isLeader = runner.LocalPlayer == leaderRef;
if (_startBtn != null)
{
_loungeRoomName.text = rnProp.ToString().ToUpper();
_startBtn.style.display = isLeader ? DisplayStyle.Flex : DisplayStyle.None;
}
// Update Host UI
if (runner.SessionInfo != null && runner.SessionInfo.Properties.TryGetValue("rn", out var rnProp)) _loungeRoomName.text = rnProp.ToString().ToUpper();
// Host Display
if (hostRef != PlayerRef.None && _playerDataManager.TryGetPlayerMetaData(hostRef, out var hostData))
{
_hostNameLabel.text = hostData.Name.ToString().ToUpper();
_hostStatusLabel.text = hostData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
string readyStatus = hostData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
string roleLabel = (hostRef == leaderRef) ? GetT("LOBBY_HOST_LABEL") : GetT("LOBBY_PLAYER_LABEL");
_hostStatusLabel.text = $"{roleLabel} - {readyStatus}";
_hostStatusLabel.style.color = hostData.IsReady ? Color.green : Color.red;
// Highlight leader avatar
if (_hostAvatar != null)
{
float width = (hostRef == leaderRef) ? 2f : 0f;
_hostAvatar.style.borderTopWidth = width;
_hostAvatar.style.borderBottomWidth = width;
_hostAvatar.style.borderLeftWidth = width;
_hostAvatar.style.borderRightWidth = width;
}
}
else if (hostRef != PlayerRef.None)
{
_hostNameLabel.text = GetT("LOBBY_SYNCING");
_hostStatusLabel.text = "-";
}
else { _hostNameLabel.text = GetT("LOBBY_SYNCING"); _hostStatusLabel.text = "-"; }
// Update Guest UI
// Guest Display
if (guestRef != PlayerRef.None && _playerDataManager.TryGetPlayerMetaData(guestRef, out var guestData))
{
_guestNameLabel.text = guestData.Name.ToString().ToUpper();
_guestStatusLabel.text = guestData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
string readyStatus = guestData.IsReady ? GetT("LOBBY_READY") : GetT("LOBBY_NOT_READY");
string roleLabel = (guestRef == leaderRef) ? GetT("LOBBY_HOST_LABEL") : GetT("LOBBY_PLAYER_LABEL");
_guestStatusLabel.text = $"{roleLabel} - {readyStatus}";
_guestStatusLabel.style.color = guestData.IsReady ? Color.green : Color.red;
// Highlight leader avatar
if (_guestAvatar != null)
{
float width = (guestRef == leaderRef) ? 2f : 0f;
_guestAvatar.style.borderTopWidth = width;
_guestAvatar.style.borderBottomWidth = width;
_guestAvatar.style.borderLeftWidth = width;
_guestAvatar.style.borderRightWidth = width;
_guestAvatar.style.borderTopColor = Color.white;
_guestAvatar.style.borderBottomColor = Color.white;
_guestAvatar.style.borderLeftColor = Color.white;
_guestAvatar.style.borderRightColor = Color.white;
}
}
else if (runner.ActivePlayers.Count() >= 2)
{
_guestNameLabel.text = GetT("LOBBY_SYNCING");
_guestStatusLabel.text = "-";
}
else
{
_guestNameLabel.text = GetT("LOBBY_WAITING_LABEL");
_guestStatusLabel.text = "-";
_guestStatusLabel.style.color = Color.gray;
else if (runner.ActivePlayers.Count() >= 2) { _guestNameLabel.text = GetT("LOBBY_SYNCING"); _guestStatusLabel.text = "-"; }
else
{
_guestNameLabel.text = GetT("LOBBY_WAITING_LABEL");
_guestStatusLabel.text = "-";
_guestStatusLabel.style.color = Color.gray;
if (_guestAvatar != null)
{
_guestAvatar.style.borderTopWidth = 0;
_guestAvatar.style.borderBottomWidth = 0;
_guestAvatar.style.borderLeftWidth = 0;
_guestAvatar.style.borderRightWidth = 0;
}
}
// Start Button visibility logic
bool allReady = true;
int playerCount = 0;
foreach (var p in runner.ActivePlayers)
// 4. Start Button Logic (Leader Only)
if (_startBtn != null && isLeader)
{
playerCount++;
if (_playerDataManager.TryGetPlayerMetaData(p, out var data))
bool allReady = true;
int playerCount = 0;
foreach (var p in runner.ActivePlayers)
{
playerCount++;
if (_playerDataManager.TryGetPlayerMetaData(p, out var data))
{
if (!data.IsReady) allReady = false;
}
else allReady = false;
}
bool canStart = allReady && playerCount >= 2;
_startBtn.SetEnabled(canStart);
_startBtn.text = GetT("LOBBY_START_BTN");
if (canStart)
{
if (!data.IsReady) allReady = false;
_startBtn.style.backgroundColor = new StyleColor(Color.green);
_startBtn.style.color = new StyleColor(Color.black);
}
else
{
allReady = false;
_startBtn.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.8f));
_startBtn.style.color = new StyleColor(new Color(1f, 1f, 1f, 0.5f));
}
}
bool isHost = runner.LocalPlayer == hostRef;
if (_startBtn != null)
{
_startBtn.text = GetT("LOBBY_START_BTN");
_startBtn.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
_startBtn.SetEnabled(allReady && playerCount >= 2);
}
// Ready Button Logic (Everyone)
if (_readyBtn != null)
{
if (_playerDataManager.TryGetPlayerMetaData(runner.LocalPlayer, out var myData))
{
// Style for Ready Button
if (myData.IsReady)
{
_readyBtn.text = GetT("LOBBY_UNREADY_BTN");
_readyBtn.style.backgroundColor = new StyleColor(Color.green);
_readyBtn.style.color = new StyleColor(Color.black);
if (myData.IsReady)
{
_readyBtn.text = GetT("LOBBY_UNREADY_BTN");
_readyBtn.style.backgroundColor = new StyleColor(Color.green);
_readyBtn.style.color = new StyleColor(Color.black);
}
else
{
_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);
else
{
_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);
}
}
}
}
private async void Invoke(string methodName, float delay)
{
await Task.Delay((int)(delay * 1000));
if (methodName == nameof(RegisterSpawnerEvents)) RegisterSpawnerEvents();
}
private async void Invoke(string methodName, float delay) { await Task.Delay((int)(delay * 1000)); if (methodName == nameof(RegisterSpawnerEvents)) RegisterSpawnerEvents(); }
}
}

View File

@@ -22,6 +22,8 @@ namespace Hallucinate.UI
private Tween _rotationTween;
private Texture2D _currentIcon;
private bool _isBusy = false;
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
{
base.Initialize(uxmlRoot, manager);
@@ -42,11 +44,11 @@ namespace Hallucinate.UI
_logo.RegisterCallback<ClickEvent>(OnLogoClicked);
var settingsBtn = root.Q<Button>("SettingsBtn");
if (settingsBtn != null) settingsBtn.clicked += () => uiManager.ToggleSettings();
if (settingsBtn != null) settingsBtn.clicked += () => { if (_isBusy) return; uiManager.ToggleSettings(); };
root.Q<Button>("JoinBtn").clicked += async () => await uiManager.Push<LobbyController>();
root.Q<Button>("CreateBtn").clicked += async () => await uiManager.Push<LobbyController>();
root.Q<Button>("ProfileBtn").clicked += async () => await uiManager.Push<ProfileController>();
root.Q<Button>("JoinBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<LobbyController>(); };
root.Q<Button>("CreateBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<LobbyController>(); };
root.Q<Button>("ProfileBtn").clicked += async () => { if (_isBusy) return; _isBusy = true; await uiManager.Push<ProfileController>(); };
root.Q<Button>("ExitBtn").clicked += () => Application.Quit();
// Đăng ký Localization
@@ -142,6 +144,7 @@ namespace Hallucinate.UI
public override async Task PlayTransitionIn()
{
_isBusy = false;
_lastInteractionTime = Time.time;
_currentState = MenuState.Idle;
ResetLogoPosition();
@@ -167,9 +170,14 @@ namespace Hallucinate.UI
private async void OnLogoClicked(ClickEvent evt)
{
if (_isBusy) return;
_lastInteractionTime = Time.time;
if (_currentState == MenuState.Idle) TransitionToRibbon();
else await uiManager.Push<LobbyController>();
else
{
_isBusy = true;
await uiManager.Push<LobbyController>();
}
}
private void TransitionToRibbon()

View File

@@ -0,0 +1,76 @@
using UnityEngine.UIElements;
using System.Threading.Tasks;
using OnlyScove.Scripts;
using UnityEngine;
namespace Hallucinate.UI
{
public class PauseMenuController : BaseUIController
{
private Button _resumeBtn;
private Button _quitBtn;
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
{
base.Initialize(uxmlRoot, manager);
_resumeBtn = root.Q<Button>("ResumeBtn");
_quitBtn = root.Q<Button>("QuitBtn");
if (_resumeBtn != null) _resumeBtn.clicked += OnResumeClicked;
if (_quitBtn != null) _quitBtn.clicked += OnQuitClicked;
ApplyLocalization();
if (LocalizationManager.Instance != null)
LocalizationManager.Instance.OnLanguageChanged += ApplyLocalization;
}
private void OnDestroy()
{
if (LocalizationManager.Instance != null)
LocalizationManager.Instance.OnLanguageChanged -= ApplyLocalization;
}
private void ApplyLocalization()
{
if (_resumeBtn != null) _resumeBtn.text = GetLoc("PAUSE_RESUME");
if (_quitBtn != null) _quitBtn.text = GetLoc("PAUSE_QUIT");
var title = root.Q<Label>("PauseTitle");
if (title != null) title.text = GetLoc("PAUSE_TITLE");
}
private void OnResumeClicked()
{
uiManager.TogglePauseMenu();
}
private void OnQuitClicked()
{
Debug.Log("[PauseMenu] Quit clicked - shutting down runner.");
if (BasicSpawner.Instance != null && BasicSpawner.Instance.Runner != null)
{
BasicSpawner.Instance.Runner.Shutdown();
}
else
{
uiManager.OnBackToMenu();
}
}
public override async Task PlayTransitionIn()
{
Show();
root.style.opacity = 0;
PrimeTween.Tween.Custom(0f, 1f, duration: 0.2f, onValueChange: val => root.style.opacity = val);
await Task.Delay(200);
}
public override async Task PlayTransitionOut()
{
PrimeTween.Tween.Custom(1f, 0f, duration: 0.2f, onValueChange: val => root.style.opacity = val);
await Task.Delay(200);
Hide();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a8be766526476e44ea7c5e19567f2568

View File

@@ -39,6 +39,9 @@ namespace Hallucinate.UI
private Action<float> _hoveredOnChanged;
private float _sliderMin, _sliderMax;
// Audio Slider Tracking for Sync
private readonly Dictionary<string, (Slider slider, TextField input)> _audioSliders = new Dictionary<string, (Slider slider, TextField input)>();
// Osu-style Volume Overlay
private VisualElement _volumeContainer;
private VisualElement _masterRing;
@@ -303,6 +306,7 @@ namespace Hallucinate.UI
_sectionHeaders["SOUND"] = header;
_content.Add(header);
_audioSliders.Clear();
_content.Add(CreateSubSection("AUDIO_VOLUMES"));
_content.Add(CreateAudioSlider(GetT("MASTER"), "MasterVolume"));
_content.Add(CreateAudioSlider(GetT("MUSIC"), "MusicVolume"));
@@ -456,20 +460,31 @@ namespace Hallucinate.UI
evt.StopPropagation();
}
private void UpdateMasterVolume(float delta)
{
_masterVol = Mathf.Clamp(_masterVol + delta, 0f, 100f);
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
AudioManager.Instance?.SetVolume("MasterVolume", _masterVol);
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
}
private void UpdateMasterVolume(float delta) => UpdateVolume("MasterVolume", _masterVol + delta);
private void UpdateSubVolume(string key, float delta)
private void UpdateSubVolume(string key, float delta) => UpdateVolume(key, PlayerPrefs.GetFloat(key, 80f) + delta);
private void UpdateVolume(string key, float volume, bool updateSlider = true)
{
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)}%";
volume = Mathf.Clamp(volume, 0f, 100f);
PlayerPrefs.SetFloat(key, volume);
AudioManager.Instance?.SetVolume(key, volume);
if (key == "MasterVolume")
{
_masterVol = volume;
if (_masterVolLabel != null) _masterVolLabel.text = $"{Mathf.RoundToInt(volume)}%";
}
else
{
if (_subRings.TryGetValue(key, out var data)) data.label.text = $"{Mathf.RoundToInt(volume)}%";
}
if (updateSlider && _audioSliders.TryGetValue(key, out var sliderData))
{
sliderData.slider.SetValueWithoutNotify(volume);
sliderData.input.value = volume.ToString("F1");
}
}
private async void ShowVolumeOverlay()
@@ -508,12 +523,15 @@ namespace Hallucinate.UI
return label;
}
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged, string audioKey = null)
{
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");
if (audioKey != null) _audioSliders[audioKey] = (slider, input);
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); });
@@ -524,12 +542,10 @@ namespace Hallucinate.UI
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);
});
UpdateVolume(prefKey, val, false);
}, prefKey);
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;
UpdateVolume(prefKey, PlayerPrefs.GetFloat(prefKey, 80f) - (evt.delta.y * 2f));
});
return sliderRow;
}

View File

@@ -49,12 +49,14 @@ namespace Hallucinate.UI
[SerializeField] private VisualTreeAsset profileTemplate;
[SerializeField] private VisualTreeAsset settingsTemplate;
[SerializeField] private VisualTreeAsset hudTemplate;
[SerializeField] private VisualTreeAsset pauseMenuTemplate;
[SerializeField] private StyleSheet globalStyleSheet;
private LoginController _loginController;
private MainMenuController _mainMenuController;
private LobbyController _lobbyController;
private SettingsController _settingsController;
private PauseMenuController _pauseMenuController;
// Osu Trail Pooling
private const int MAX_TRAIL_PARTICLES = 60;
@@ -65,6 +67,9 @@ namespace Hallucinate.UI
private bool _isSettingsOpen = false;
public bool IsSettingsOpen => _isSettingsOpen;
private bool _isPauseMenuOpen = false;
public bool IsPauseMenuOpen => _isPauseMenuOpen;
private const string UI_SCALE_KEY = "UIScale";
#if UNITY_EDITOR
@@ -242,7 +247,43 @@ namespace Hallucinate.UI
}
}
private void HandleCancel() { if (_isSettingsOpen) ToggleSettings(); }
private void HandleCancel()
{
if (_isSettingsOpen) ToggleSettings();
else if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "Main Scene")
{
TogglePauseMenu();
}
}
public async void TogglePauseMenu()
{
if (_pauseMenuController == null) return;
if (!_isPauseMenuOpen)
{
_isPauseMenuOpen = true;
_pauseMenuController.Root.BringToFront();
if (_cursorLayer != null) _cursorLayer.BringToFront();
// Unlock cursor when menu is open
UnityEngine.Cursor.lockState = CursorLockMode.None;
UnityEngine.Cursor.visible = false;
await _pauseMenuController.PlayTransitionIn();
}
else
{
_isPauseMenuOpen = false;
// Re-lock cursor when menu is closed
if (!_isSettingsOpen)
{
UnityEngine.Cursor.lockState = CursorLockMode.Locked;
}
await _pauseMenuController.PlayTransitionOut();
}
}
public async void ToggleSettings()
{
@@ -354,6 +395,7 @@ namespace Hallucinate.UI
RegisterController<ProfileController>(profileTemplate);
_settingsController = RegisterController<SettingsController>(settingsTemplate);
RegisterController<HUDController>(hudTemplate);
_pauseMenuController = RegisterController<PauseMenuController>(pauseMenuTemplate);
_loginController = RegisterController<LoginController>(loginTemplate);
}
catch (Exception e) { Debug.LogError($"[UIManager] Failed to initialize controllers: {e}"); }