diff --git a/game/Assets/Scripts/Network/LobbyUI.cs b/game/Assets/Scripts/Network/LobbyUI.cs index 197e3be..198e9d2 100644 --- a/game/Assets/Scripts/Network/LobbyUI.cs +++ b/game/Assets/Scripts/Network/LobbyUI.cs @@ -1,107 +1,126 @@ +using System.Collections.Generic; using UnityEngine; /// -/// Lobby UI displayed at scene start. Player enters a name, picks a color, -/// and clicks "Rejoindre" to connect to the arena. -/// Manages the full pre-game → in-game transition: -/// - Hides the Player hierarchy until connected -/// - Activates a spectator camera while in lobby -/// - Teleports the player ball to the server spawn position on join -/// Uses Dear ImGui–style skin via ImGuiSkin. +/// Lobby UI: character setup + room list side by side. +/// - T to open/close chat, Tab for keybinds (handled elsewhere) +/// - Lists available rooms, lets the player create or join one /// public class LobbyUI : MonoBehaviour { [Header("Scene References")] - [Tooltip("The root 'Player' GameObject (contains PlayerSphere + cameras). Will be deactivated until connected.")] public GameObject playerRoot; - - [Tooltip("The spectator camera GameObject (SpectatorCamera component).")] public SpectatorCamera spectatorCamera; - // Preset colors for selection - private static readonly Color[] PresetColors = new Color[] + private static readonly Color[] PresetColors = { - new Color(1f, 0.35f, 0.2f), // Orange-red - new Color(0.2f, 0.6f, 1f), // Blue - new Color(0.3f, 1f, 0.4f), // Green - new Color(1f, 0.85f, 0.1f), // Yellow - new Color(0.8f, 0.3f, 1f), // Purple - new Color(1f, 0.5f, 0.7f), // Pink - }; - - private static readonly string[] ColorNames = new string[] - { - "Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose" + new Color(1f, 0.35f, 0.2f), + new Color(0.2f, 0.6f, 1f), + new Color(0.3f, 1f, 0.4f), + new Color(1f, 0.85f, 0.1f), + new Color(0.8f, 0.3f, 1f), + new Color(1f, 0.5f, 0.7f), }; + private static readonly string[] ColorNames = { "Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose" }; // UI state - private bool _lobbyActive = true; - private string _playerName = ""; - private int _selectedColorIndex = 0; + private bool _lobbyActive = true; + private string _playerName = ""; + private int _selectedColorIndex = 0; private string _statusMessage = ""; - private bool _isConnecting = false; - private bool _isReady = false; + private bool _isConnecting = false; + private bool _isReady = false; - // Cached color preview texture (avoid per-frame leak) + // Room list + private NetworkManager.RoomInfo[] _rooms = new NetworkManager.RoomInfo[0]; + private bool _roomsFetching = false; + private float _refreshTimer = 0f; + private const float REFRESH_INTERVAL = 4f; + private Vector2 _roomsScroll; + + // Color preview texture private Texture2D _colorPreviewTex; private int _lastPreviewColorIndex = -1; void Start() { - // Generate a default name - _playerName = "Joueur" + Random.Range(100, 999); + _playerName = PlayerPrefs.GetString("rolld_player_name", "Joueur" + Random.Range(100, 999)); - // --- Hide the player hierarchy until connected --- if (playerRoot != null) playerRoot.SetActive(false); - // --- Activate spectator camera --- if (spectatorCamera != null) { - // Wire the gameplay camera reference so spectator knows what to re-enable var gameplayCam = playerRoot?.GetComponentInChildren(true); if (gameplayCam != null) spectatorCamera.gameplayCamera = gameplayCam; - spectatorCamera.Activate(); } - // Subscribe to network events - if (NetworkManager.Instance != null) + var nm = NetworkManager.Instance; + if (nm != null) { - NetworkManager.Instance.OnConnected += OnConnected; - NetworkManager.Instance.OnDisconnected += OnDisconnected; + nm.OnConnected += OnConnected; + nm.OnDisconnected += OnDisconnected; + nm.OnRoomsRefreshed += OnRoomsRefreshed; } + + RefreshRooms(); } void OnDestroy() { - if (NetworkManager.Instance != null) + var nm = NetworkManager.Instance; + if (nm != null) { - NetworkManager.Instance.OnConnected -= OnConnected; - NetworkManager.Instance.OnDisconnected -= OnDisconnected; + nm.OnConnected -= OnConnected; + nm.OnDisconnected -= OnDisconnected; + nm.OnRoomsRefreshed -= OnRoomsRefreshed; } } + void Update() + { + if (!_lobbyActive || _isConnecting) return; + _refreshTimer += Time.deltaTime; + if (_refreshTimer >= REFRESH_INTERVAL) + { + _refreshTimer = 0f; + RefreshRooms(); + } + } + + private void RefreshRooms() + { + if (_roomsFetching) return; + _roomsFetching = true; + NetworkManager.Instance?.FetchRooms(); + } + + private void OnRoomsRefreshed(NetworkManager.RoomInfo[] rooms) + { + _rooms = rooms; + _roomsFetching = false; + } + + // ─── Network callbacks ──────────────────────────────────────────────── + private void OnConnected() { - _lobbyActive = false; + _lobbyActive = false; _isConnecting = false; _statusMessage = ""; - CancelInvoke(nameof(CheckConnectionTimeout)); + CancelInvoke(nameof(ConnectionTimeout)); - // --- Activate the player hierarchy --- if (playerRoot != null) playerRoot.SetActive(true); - // Teleport player ball to the server-assigned spawn position var nm = NetworkManager.Instance; if (nm != null && playerRoot != null) { var pc = playerRoot.GetComponentInChildren(true); if (pc != null) { - // Get spawn pos from the local player's state in the room var localState = nm.GetLocalPlayerState(); if (localState != null) { @@ -109,42 +128,36 @@ public class LobbyUI : MonoBehaviour var rb = pc.GetComponent(); if (rb != null) { - rb.linearVelocity = Vector3.zero; + rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; rb.position = spawnPos; } pc.transform.position = spawnPos; pc.SetSpawnPosition(spawnPos); - Debug.Log($"[Lobby] Player teleported to spawn: {spawnPos}"); } pc.enabled = true; - - // Setup local player visuals: 50% color tint + floating name label pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor); } } - // --- Switch from spectator to gameplay camera --- if (spectatorCamera != null) spectatorCamera.Deactivate(); - // Unlock cursor for gameplay Cursor.lockState = CursorLockMode.Locked; - Cursor.visible = false; + Cursor.visible = false; } private void OnDisconnected() { - _lobbyActive = true; - _isConnecting = false; - _isReady = false; + _lobbyActive = true; + _isConnecting = false; + _isReady = false; _statusMessage = "Déconnecté du serveur"; + _refreshTimer = REFRESH_INTERVAL; // force immediate refresh - // Show cursor for lobby Cursor.lockState = CursorLockMode.None; - Cursor.visible = true; + Cursor.visible = true; - // --- Deactivate the player hierarchy --- if (playerRoot != null) { var pc = playerRoot.GetComponentInChildren(true); @@ -152,188 +165,267 @@ public class LobbyUI : MonoBehaviour playerRoot.SetActive(false); } - // --- Re-enable spectator camera --- if (spectatorCamera != null) spectatorCamera.Activate(); } + // ─── OnGUI ──────────────────────────────────────────────────────────── + void OnGUI() { if (!_lobbyActive) return; ImGuiSkin.EnsureReady(); - if (Cursor.lockState != CursorLockMode.None) { Cursor.lockState = CursorLockMode.None; - Cursor.visible = true; + Cursor.visible = true; } - ImGuiSkin.DrawOverlay(); bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected; if (!isConnected) + DrawSetupAndRoomList(); + else + DrawWaitingRoom(); + } + + // ─── Setup + room list ──────────────────────────────────────────────── + + private void DrawSetupAndRoomList() + { + const float W = 620f, H = 520f; + float x = (Screen.width - W) * 0.5f; + float y = (Screen.height - H) * 0.5f; + + ImGuiSkin.BeginWindowAt(x, y, W, H, "ROLL'D"); + GUILayout.Label("Choisir une salle et configurer son personnage", ImGuiSkin.WindowSubtitle); + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + + // ── Left column : character setup ───────────────────────────── + GUILayout.BeginVertical(GUILayout.Width(240)); + ImGuiSkin.DrawSectionHeader("PERSONNAGE"); + GUILayout.Space(4); + _playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30)); + GUILayout.Space(10); + + ImGuiSkin.DrawSectionHeader("COULEUR"); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < PresetColors.Length; i++) { - // ── Pre-connect panel ──────────────────────────────────────── - float panelWidth = 420; - float panelHeight = 440; - ImGuiSkin.BeginWindow(panelWidth, panelHeight, "ROLL'D"); + Color c = PresetColors[i]; + bool selected = _selectedColorIndex == i; + Color prevBg = GUI.backgroundColor; + GUI.backgroundColor = selected ? c : c * 0.6f; + var btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall) + { fontStyle = selected ? FontStyle.Bold : FontStyle.Normal }; + if (selected) btnStyle.normal.textColor = Color.white; + if (GUILayout.Button(selected ? $"▸{ColorNames[i][0]}" : $"{ColorNames[i][0]}", + btnStyle, GUILayout.Height(30), GUILayout.Width(34))) + _selectedColorIndex = i; + GUI.backgroundColor = prevBg; + } + GUILayout.EndHorizontal(); - GUILayout.Label("Rejoindre l'arène multijoueur", ImGuiSkin.WindowSubtitle); - GUILayout.Space(16); - - ImGuiSkin.DrawSectionHeader("PSEUDO"); - GUILayout.Space(4); - _playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30)); - GUILayout.Space(12); - - ImGuiSkin.DrawSectionHeader("COULEUR"); - GUILayout.Space(6); - - GUILayout.BeginHorizontal(); - for (int i = 0; i < PresetColors.Length; i++) + GUILayout.Space(4); + // Color swatch + if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex) + { + if (_colorPreviewTex == null) { - Color c = PresetColors[i]; - bool selected = _selectedColorIndex == i; - Color prevBg = GUI.backgroundColor; - GUI.backgroundColor = selected ? c : c * 0.7f; - GUIStyle btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall) - { - fontStyle = selected ? FontStyle.Bold : FontStyle.Normal, - }; - if (selected) btnStyle.normal.textColor = Color.white; - string label = selected ? $"▸ {ColorNames[i]}" : ColorNames[i]; - if (GUILayout.Button(label, btnStyle, GUILayout.Height(32), GUILayout.Width(60))) - _selectedColorIndex = i; - GUI.backgroundColor = prevBg; + _colorPreviewTex = new Texture2D(1, 1, TextureFormat.RGBA32, false); + _colorPreviewTex.hideFlags = HideFlags.HideAndDontSave; } - GUILayout.EndHorizontal(); - GUILayout.Space(4); + _colorPreviewTex.SetPixel(0, 0, PresetColors[_selectedColorIndex]); + _colorPreviewTex.Apply(); + _lastPreviewColorIndex = _selectedColorIndex; + } + var swatchStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleLeft, fontSize = 11 }; + GUILayout.Label($"▌ {ColorNames[_selectedColorIndex]}", swatchStyle); - if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex) - { - if (_colorPreviewTex == null) - { - _colorPreviewTex = new Texture2D(1, 1, TextureFormat.RGBA32, false); - _colorPreviewTex.hideFlags = HideFlags.HideAndDontSave; - } - _colorPreviewTex.SetPixel(0, 0, PresetColors[_selectedColorIndex]); - _colorPreviewTex.Apply(); - _lastPreviewColorIndex = _selectedColorIndex; - } - GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + // Create room button + GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName); + if (GUILayout.Button("+ Créer une salle", ImGuiSkin.Button, GUILayout.Height(36))) + DoCreate(); + + GUILayout.Space(4); + + // Join any (join or create fallback) + if (GUILayout.Button("▶ Rejoindre n'importe", ImGuiSkin.ButtonAccent, GUILayout.Height(36))) + DoJoinAny(); + + GUI.enabled = true; + GUILayout.EndVertical(); + + GUILayout.Space(12); + + // ── Right column : room list ─────────────────────────────────── + GUILayout.BeginVertical(); + GUILayout.BeginHorizontal(); + ImGuiSkin.DrawSectionHeader("SALLES DISPONIBLES"); + GUILayout.FlexibleSpace(); + GUI.enabled = !_roomsFetching; + if (GUILayout.Button(_roomsFetching ? "…" : "↻", ImGuiSkin.ButtonSmall, + GUILayout.Width(28), GUILayout.Height(22))) + { + _refreshTimer = 0f; + RefreshRooms(); + } + GUI.enabled = true; + GUILayout.EndHorizontal(); + GUILayout.Space(4); + + float listH = H - 160f; + _roomsScroll = GUILayout.BeginScrollView(_roomsScroll, ImGuiSkin.ScrollView, + GUILayout.Height(listH)); + + if (_rooms.Length == 0) + { + var emptyStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter }; GUILayout.FlexibleSpace(); - GUILayout.Box(_colorPreviewTex, GUIStyle.none, GUILayout.Width(80), GUILayout.Height(16)); + GUILayout.Label(_roomsFetching ? "Chargement…" : "Aucune salle ouverte.", emptyStyle); GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.Space(16); - - GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName); - string buttonText = _isConnecting ? "Connexion..." : "▶ Rejoindre l'arène"; - if (GUILayout.Button(buttonText, ImGuiSkin.ButtonAccent, GUILayout.Height(44))) - JoinArena(); - GUI.enabled = true; - GUILayout.Space(8); - - if (!string.IsNullOrEmpty(_statusMessage)) - { - bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté"); - GUIStyle statusStyle = isError ? ImGuiSkin.StatusRed : new GUIStyle(ImGuiSkin.Hint); - if (!isError) statusStyle.normal.textColor = ImGuiSkin.ColYellow; - GUILayout.Label(_statusMessage, statusStyle); - } - - ImGuiSkin.EndWindow(); } else { - // ── Waiting room panel (connected, waiting for game to start) ── - float panelWidth = 380; - float panelHeight = 320; - ImGuiSkin.BeginWindow(panelWidth, panelHeight, "SALLE D'ATTENTE"); - - GUILayout.Label("En attente des joueurs...", ImGuiSkin.WindowSubtitle); - GUILayout.Space(12); - - // Player list - ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS"); - GUILayout.Space(4); - var nm = NetworkManager.Instance; - if (nm != null && nm.IsConnected) + foreach (var room in _rooms) { - // We can't directly iterate NetworkState.players from here easily, - // so show basic count - var style = new GUIStyle(GUI.skin.label) { fontSize = 13 }; - style.normal.textColor = new Color(0.75f, 0.75f, 0.85f); - GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", style); - } - GUILayout.Space(16); + string roomName = room.metadata?.name ?? ("Salle #" + room.roomId.Substring(0, 6)); + int clients = room.clients; + int maxCli = room.maxClients; - // Ready button - if (!_isReady) - { - if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44))) - { - _isReady = true; - NetworkManager.Instance?.SendReady(); - } - } - else - { - var readyStyle = new GUIStyle(GUI.skin.label) - { - alignment = TextAnchor.MiddleCenter, - fontSize = 16, - fontStyle = FontStyle.Bold, - }; - readyStyle.normal.textColor = new Color(0.3f, 1f, 0.5f); - GUILayout.Label("✔ Prêt ! En attente des autres...", readyStyle, GUILayout.Height(44)); - } + GUILayout.BeginHorizontal(); - GUILayout.Space(8); - var hintStyle = new GUIStyle(ImGuiSkin.Hint); - hintStyle.normal.textColor = new Color(0.5f, 0.5f, 0.6f); - GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", hintStyle); + var nameStyle = new GUIStyle(ImGuiSkin.LabelBold) { fontSize = 12 }; + GUILayout.Label(roomName, nameStyle, GUILayout.Width(140)); - ImGuiSkin.EndWindow(); + var countStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter, fontSize = 11 }; + GUILayout.Label($"{clients} / {maxCli}", countStyle, GUILayout.Width(48)); + + GUILayout.FlexibleSpace(); + + bool full = clients >= maxCli; + GUI.enabled = !_isConnecting && !full && !string.IsNullOrWhiteSpace(_playerName); + if (GUILayout.Button(full ? "Pleine" : "▶ Rejoindre", + ImGuiSkin.ButtonSmall, GUILayout.Width(90), GUILayout.Height(26))) + DoJoinRoom(room.roomId); + GUI.enabled = true; + + GUILayout.EndHorizontal(); + ImGuiSkin.Separator(); + GUILayout.Space(2); + } } + + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); // end columns + + // ── Status bar ──────────────────────────────────────────────── + GUILayout.Space(4); + if (!string.IsNullOrEmpty(_statusMessage)) + { + bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté"); + GUILayout.Label(_statusMessage, isError ? ImGuiSkin.StatusRed : ImGuiSkin.Hint); + } + + ImGuiSkin.EndWindow(); } - private void JoinArena() + // ─── Waiting room ───────────────────────────────────────────────────── + + private void DrawWaitingRoom() { - if (NetworkManager.Instance == null) + ImGuiSkin.BeginWindow(400f, 300f, "SALLE D'ATTENTE"); + + var nm = NetworkManager.Instance; + string roomDisplay = nm != null ? ("Salle #" + nm.RoomId.Substring(0, Mathf.Min(6, nm.RoomId.Length))) : "—"; + GUILayout.Label(roomDisplay, ImGuiSkin.WindowSubtitle); + GUILayout.Space(12); + + ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS"); + GUILayout.Space(4); + if (nm != null) { - _statusMessage = "Erreur : NetworkManager introuvable"; - return; + var s = new GUIStyle(GUI.skin.label) { fontSize = 13 }; + s.normal.textColor = new Color(0.75f, 0.75f, 0.85f); + GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", s); } + GUILayout.Space(16); - if (string.IsNullOrWhiteSpace(_playerName)) + if (!_isReady) { - _statusMessage = "Entrez un pseudo"; - return; - } - - _isConnecting = true; - _statusMessage = "Connexion au serveur..."; - - Color selectedColor = PresetColors[_selectedColorIndex]; - NetworkManager.Instance.JoinArena(_playerName.Trim(), selectedColor); - - // Monitor for errors after a delay - Invoke(nameof(CheckConnectionTimeout), 10f); - } - - private void CheckConnectionTimeout() - { - if (_isConnecting && !NetworkManager.Instance.IsConnected) - { - _isConnecting = false; - _statusMessage = "Erreur : Impossible de joindre rolld.io. Réessayez dans quelques instants."; - if (!string.IsNullOrEmpty(NetworkManager.Instance.LastError)) + if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44))) { - _statusMessage += $"\n{NetworkManager.Instance.LastError}"; + _isReady = true; + NetworkManager.Instance?.SendReady(); } } + else + { + var rs = new GUIStyle(GUI.skin.label) + { alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold }; + rs.normal.textColor = new Color(0.3f, 1f, 0.5f); + GUILayout.Label("✔ Prêt ! En attente des autres…", rs, GUILayout.Height(44)); + } + + GUILayout.Space(8); + GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", ImGuiSkin.Hint); + + ImGuiSkin.EndWindow(); + } + + // ─── Actions ────────────────────────────────────────────────────────── + + private string ValidateName() + { + string n = _playerName.Trim(); + if (string.IsNullOrEmpty(n)) { _statusMessage = "Entre un pseudo d'abord."; return null; } + return n; + } + + private void DoJoinRoom(string roomId) + { + string n = ValidateName(); if (n == null) return; + _isConnecting = true; + _statusMessage = "Connexion à la salle…"; + NetworkManager.Instance?.JoinByRoomId(roomId, n, PresetColors[_selectedColorIndex]); + Invoke(nameof(ConnectionTimeout), 10f); + } + + private void DoCreate() + { + string n = ValidateName(); if (n == null) return; + _isConnecting = true; + _statusMessage = "Création d'une salle…"; + NetworkManager.Instance?.CreateRoom(n, PresetColors[_selectedColorIndex]); + Invoke(nameof(ConnectionTimeout), 10f); + } + + private void DoJoinAny() + { + string n = ValidateName(); if (n == null) return; + _isConnecting = true; + _statusMessage = "Connexion…"; + NetworkManager.Instance?.JoinArena(n, PresetColors[_selectedColorIndex]); + Invoke(nameof(ConnectionTimeout), 10f); + } + + private void ConnectionTimeout() + { + if (!_isConnecting) return; + _isConnecting = false; + var nm = NetworkManager.Instance; + _statusMessage = "Impossible de se connecter. Réessaie."; + if (nm != null && !string.IsNullOrEmpty(nm.LastError)) + _statusMessage += $"\n{nm.LastError}"; } } diff --git a/game/Assets/Scripts/Network/NetworkManager.cs b/game/Assets/Scripts/Network/NetworkManager.cs index c245846..a9f8345 100644 --- a/game/Assets/Scripts/Network/NetworkManager.cs +++ b/game/Assets/Scripts/Network/NetworkManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using UnityEngine; +using UnityEngine.Networking; using Colyseus; using Colyseus.Schema; @@ -33,6 +35,24 @@ public class NetworkManager : MonoBehaviour public string LocalPlayerName { get; private set; } = ""; public Color LocalPlayerColor { get; private set; } = Color.white; + // --- Room listing --- + [System.Serializable] public class RoomMeta { public string name; } + [System.Serializable] public class RoomInfo { public string roomId; public int clients; public int maxClients; public RoomMeta metadata; } + [System.Serializable] private class RoomListWrapper { public List items; } + + public event Action OnRoomsRefreshed; + + public void FetchRooms() => StartCoroutine(DoFetchRooms()); + + private IEnumerator DoFetchRooms() + { + using var req = UnityWebRequest.Get($"{serverURL.Replace("wss://", "https://").Replace("ws://", "http://")}/rooms"); + yield return req.SendWebRequest(); + if (req.result != UnityWebRequest.Result.Success) { OnRoomsRefreshed?.Invoke(Array.Empty()); yield break; } + var wrapper = JsonUtility.FromJson($"{{\"items\":{req.downloadHandler.text}}}"); + OnRoomsRefreshed?.Invoke(wrapper?.items?.ToArray() ?? Array.Empty()); + } + // --- Events --- public event Action OnConnected; public event Action OnDisconnected; @@ -100,97 +120,101 @@ public class NetworkManager : MonoBehaviour // ─── Join / Leave ──────────────────────────────────────────────────── - public async void JoinArena(string playerName, Color color) - { - if (_isJoining || IsConnected) - { - Debug.LogWarning("[Network] Already connecting or connected."); - return; - } + // ─── Join helpers ───────────────────────────────────────────────────── + private Dictionary BuildJoinOptions(string playerName, Color color) => new() + { + { "name", playerName }, + { "colorR", color.r }, + { "colorG", color.g }, + { "colorB", color.b }, + }; + + private void PrepareJoin(string playerName, Color color) + { _isJoining = true; ConnectionStatus = "Connexion en cours..."; LastError = ""; LocalPlayerName = playerName; LocalPlayerColor = color; PlayerPrefs.SetString("rolld_player_name", playerName); + _client = new Client(serverURL); + } + private void FinishJoin() + { + LocalSessionId = _room.SessionId; + RoomId = _room.RoomId; + IsConnected = true; + ConnectionStatus = "Connecté"; + Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}"); + + _callbacks = Callbacks.Get(_room); + _callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player)); + _callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player)); + _callbacks.Listen(state => state.phase, (v, _) => _OnPhaseChanged(v)); + _callbacks.Listen(state => state.countdown, (v, _) => OnCountdownChanged?.Invoke(v)); + + _room.OnMessage("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); }); + _room.OnMessage ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); }); + _room.OnMessage("roundStart", msg => { OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); }); + _room.OnMessage ("roundEnd", msg => { OnRoundEnd?.Invoke(msg.round); }); + _room.OnMessage ("gameEnd", msg => { OnGameEnd?.Invoke(msg.winner); }); + _room.OnMessage("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); }); + _room.OnLeave += OnRoomLeave; + + OnConnected?.Invoke(); + } + + private void HandleJoinError(Exception e) + { + Debug.LogError($"[Network] Failed to join: {e.Message}"); + ConnectionStatus = "Erreur de connexion"; + LastError = e.Message; + IsConnected = false; + } + + // ─── Public join methods ────────────────────────────────────────────── + + public async void JoinArena(string playerName, Color color) + { + if (_isJoining || IsConnected) return; + PrepareJoin(playerName, color); try { - Debug.Log($"[Network] Connecting to {serverURL}..."); - _client = new Client(serverURL); - - var options = new Dictionary - { - { "name", playerName }, - { "colorR", color.r }, - { "colorG", color.g }, - { "colorB", color.b } - }; - - _room = await _client.JoinOrCreate("arena", options); - LocalSessionId = _room.SessionId; - RoomId = _room.RoomId; - IsConnected = true; - ConnectionStatus = "Connecté"; - - Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}"); - - _callbacks = Callbacks.Get(_room); - - // Players - _callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player)); - _callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player)); - - // Game state changes - _callbacks.Listen(state => state.phase, (newValue, prevValue) => _OnPhaseChanged(newValue)); - _callbacks.Listen(state => state.countdown, (newValue, prevValue) => OnCountdownChanged?.Invoke(newValue)); - - // Server messages - _room.OnMessage("eliminated", msg => - { - Debug.Log($"[Network] Eliminated: {msg.sessionId} ({msg.reason})"); - OnEliminated?.Invoke(msg.sessionId, msg.reason); - }); - _room.OnMessage("qualified", msg => - { - Debug.Log($"[Network] Qualified: {msg.sessionId}"); - OnQualified?.Invoke(msg.sessionId); - }); - _room.OnMessage("roundStart", msg => - { - Debug.Log($"[Network] Round {msg.round} started ({msg.mode})"); - OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); - }); - _room.OnMessage("roundEnd", msg => - { - Debug.Log($"[Network] Round {msg.round} ended"); - OnRoundEnd?.Invoke(msg.round); - }); - _room.OnMessage("gameEnd", msg => - { - Debug.Log($"[Network] Game over — Winner: {msg.winner}"); - OnGameEnd?.Invoke(msg.winner); - }); - _room.OnMessage("chat", msg => - { - ChatUI.Instance?.ReceiveChatMessage(msg); - }); - - _room.OnLeave += OnRoomLeave; - OnConnected?.Invoke(); + _room = await _client.JoinOrCreate("arena", BuildJoinOptions(playerName, color)); + FinishJoin(); } - catch (Exception e) + catch (Exception e) { HandleJoinError(e); } + finally { _isJoining = false; } + } + + public async void JoinByRoomId(string roomId, string playerName, Color color) + { + if (_isJoining || IsConnected) return; + PrepareJoin(playerName, color); + try { - Debug.LogError($"[Network] Failed to join: {e.Message}"); - ConnectionStatus = "Erreur de connexion"; - LastError = e.Message; - IsConnected = false; + _room = await _client.JoinById(roomId, BuildJoinOptions(playerName, color)); + FinishJoin(); } - finally + catch (Exception e) { HandleJoinError(e); } + finally { _isJoining = false; } + } + + public async void CreateRoom(string playerName, Color color, string roomName = null) + { + if (_isJoining || IsConnected) return; + PrepareJoin(playerName, color); + try { - _isJoining = false; + var opts = BuildJoinOptions(playerName, color); + if (roomName != null) opts["roomName"] = roomName; + _room = await _client.Create("arena", opts); + FinishJoin(); } + catch (Exception e) { HandleJoinError(e); } + finally { _isJoining = false; } } public async void LeaveRoom() diff --git a/rolld_backend/game/src/index.js b/rolld_backend/game/src/index.js index 90481a3..a72ec22 100644 --- a/rolld_backend/game/src/index.js +++ b/rolld_backend/game/src/index.js @@ -1,5 +1,5 @@ const cors = require('cors'); -const { Server } = require('@colyseus/core'); +const { Server, matchMaker } = require('@colyseus/core'); const { WebSocketTransport } = require('@colyseus/ws-transport'); const { ArenaRoom } = require('./rooms/ArenaRoom'); const Stats = require('./stats/StatsManager'); @@ -61,6 +61,21 @@ const gameServer = new Server({ res.json({ ok }); }); + // ── Rooms ──────────────────────────────────────────────────────────── + app.get('/rooms', async (_req, res) => { + try { + const rooms = await matchMaker.query({ name: 'arena' }); + res.json(rooms.map(r => ({ + roomId: r.roomId, + clients: r.clients, + maxClients: r.maxClients, + metadata: r.metadata || {}, + }))); + } catch (_) { + res.json([]); + } + }); + // ── Chat ───────────────────────────────────────────────────────────── app.get('/chat/history', (req, res) => { res.json(Chat.getHistory(req.query.since)); diff --git a/rolld_backend/game/src/rooms/ArenaRoom.js b/rolld_backend/game/src/rooms/ArenaRoom.js index da7d4c6..7c44fc3 100644 --- a/rolld_backend/game/src/rooms/ArenaRoom.js +++ b/rolld_backend/game/src/rooms/ArenaRoom.js @@ -14,6 +14,7 @@ class ArenaRoom extends Room { onCreate(options) { this.setState(new GameState()); this.setPatchRate(16); // ~62.5 Hz + this.setMetadata({ name: options?.roomName || ('Salle #' + this.roomId.substring(0, 6)) }); this._phaseTimer = null; this._lobbyTimer = null;