using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using GeoSus.Client; /// /// Lives on create.unity, the post-creation lobby view. The scene already /// contains the art team's named UI: /// /// Canvas /// ├── Panel - full-screen background /// ├── max players - TMP "Max players: " label (we append the count) /// ├── lobby code - TMP "Lobby code: " label (we append the code) /// ├── smazatbutton - "delete/leave" button /// ├── lobby info - info button (currently no-op) /// ├── player list neglow - decorative glow behind the player list /// ├── player list - container for the player list (originally held /// │ a single placeholder text child) /// ├── tuff jmeno - hardcoded player slot 1 (placeholder) /// ├── netuff jmeno - hardcoded player slot 2 (placeholder) /// └── stvořit - "Start Game" button /// /// We DO NOT destroy any of these. Instead we resolve them by name, wire the /// buttons, update text labels live, hide the placeholder slots, and inject /// a ScrollRect inside the existing 'player list' container so any number of /// players can be displayed. /// /// GameManager_UI calls RefreshAll(LobbyState) when the lobby state changes; /// we cache the latest state and apply it on the next Update so all scene /// references stay on the main thread. /// public class LobbyDisplayUI : MonoBehaviour { // ── Static hub so GameManager_UI can push state updates ────────────────── private static readonly HashSet _all = new HashSet(); public static void RefreshAll(LobbyState state) { foreach (var ui in _all) ui._pending = state; } // ── Resolved scene refs ────────────────────────────────────────────────── private TMP_Text _codeLabel; // "lobby code" - prefix + JoinCode appended private TMP_Text _maxPlayersLabel; // "max players" - prefix + count appended private Button _leaveBtn; // "smazatbutton" private Button _infoBtn; // "lobby info" private Button _startBtn; // "stvořit" private RectTransform _playerListRT; // "player list" container - scroll lives inside private TMP_Text _waitingForHostText; // injected; visible to non-host only // ── Injected scroll list (added once at Start, populated on every refresh) ─ private RectTransform _scrollContent; private readonly List _rows = new List(); private LobbyState _pending; // ── Colour palette (forwarded from UITheme) ────────────────────────────── static readonly Color C_ROW_A = UITheme.RowA; static readonly Color C_ROW_B = UITheme.RowB; static readonly Color C_DIVIDER = UITheme.SurfaceAlt; static readonly Color C_ACCENT = UITheme.Accent; static readonly Color C_GOLD = UITheme.Caution; static readonly Color C_WHITE = UITheme.TextHi; static readonly Color C_SOFT = UITheme.TextMid; void OnEnable() => _all.Add(this); void OnDisable() => _all.Remove(this); void Start() { // Wire emoji fallback ASAP so player-list rows with emoji glyphs // (host crown, "you" badge, etc.) render correctly on the first // refresh. UITheme.EnsureEmojiFontFallback(); var canvasGO = GameObject.Find("Canvas"); if (canvasGO == null) { Debug.LogError("[LobbyDisplayUI] No Canvas found in create scene."); return; } var canvas = canvasGO.transform; // ── Bind existing scene elements ──────────────────────────────────── _codeLabel = FindTMP(canvas, "lobby code"); _maxPlayersLabel = FindTMP(canvas, "max players"); _leaveBtn = FindButton(canvas, "smazatbutton"); _infoBtn = FindButton(canvas, "lobby info"); _startBtn = FindButton(canvas, "stvořit"); var listGO = FindByName(canvas, "player list"); _playerListRT = listGO != null ? listGO as RectTransform : null; // ── Wire buttons (preserve existing AudioSource OnClick by appending) ── if (_leaveBtn != null) { // Don't clear; the art team's AudioSource.Play hook should still // fire alongside the leave action. _leaveBtn.onClick.AddListener(() => GameManager.Instance?.LeaveLobbyButton()); } else Debug.LogWarning("[LobbyDisplayUI] 'smazatbutton' not found."); if (_infoBtn != null) { // Tap-to-copy the lobby code to clipboard. Cheap useful action that // gives the existing info button real behavior. _infoBtn.onClick.AddListener(() => { var code = GameManager.Instance?.gameClient?.CurrentLobbyState?.JoinCode; if (!string.IsNullOrEmpty(code)) GUIUtility.systemCopyBuffer = code; }); } if (_startBtn != null) { _startBtn.onClick.AddListener(() => GameManager.Instance?.StartGameButton()); } else Debug.LogWarning("[LobbyDisplayUI] 'stvořit' not found."); // ── Hide the hardcoded placeholder slots ──────────────────────────── // These were kept in the scene as visual previews of what filled rows // would look like; with a working scrollable list they're redundant. var tuff = FindByName(canvas, "tuff jmeno"); var netuff = FindByName(canvas, "netuff jmeno"); if (tuff) tuff.gameObject.SetActive(false); if (netuff) netuff.gameObject.SetActive(false); // ── Override the player list's RectTransform to a sensible portrait // layout. The art team's anchored position + size were calibrated // for a different reference resolution and the element only filled // a third of the viewable area at 1080x1920. Stretch it to fill // the central portion of the screen with even insets. if (_playerListRT != null) { _playerListRT.anchorMin = new Vector2(0, 0); _playerListRT.anchorMax = new Vector2(1, 1); _playerListRT.pivot = new Vector2(0.5f, 0.5f); // Top inset = 320 (room for header / lobby code), bottom inset = // 380 (room for the start button or waiting message), 60px sides. _playerListRT.offsetMin = new Vector2(60, 380); _playerListRT.offsetMax = new Vector2(-60, -320); _playerListRT.anchoredPosition = Vector2.zero; BuildScrollList(_playerListRT); } else { Debug.LogWarning("[LobbyDisplayUI] 'player list' container not found - " + "falling back to no list rendering."); } // Also re-anchor the "player list neglow" decoration to track the new // player list region, so the glow doesn't float empty offscreen. var neglow = FindByName(canvas, "player list neglow") as RectTransform; if (neglow != null) { neglow.anchorMin = new Vector2(0, 0); neglow.anchorMax = new Vector2(1, 1); neglow.pivot = new Vector2(0.5f, 0.5f); neglow.offsetMin = new Vector2(40, 360); neglow.offsetMax = new Vector2(-40, -300); } // ── Inject "Waiting for host..." text for non-host players ────────── // Visible only when the local player isn't the lobby owner; sits in // the same vertical strip as the start button so the screen has a // single consistent action zone for both roles. var waitGO = MakeRT("WaitingForHost", canvas); Anchor(waitGO, new Vector2(0.05f, 0), new Vector2(0.95f, 0), new Vector2(0, 60), new Vector2(0, 240)); _waitingForHostText = waitGO.gameObject.AddComponent(); _waitingForHostText.text = "⌛ Waiting for host to start the game..."; _waitingForHostText.fontSize = 38; _waitingForHostText.color = new Color(0.73f, 0.80f, 0.88f); _waitingForHostText.fontStyle = FontStyles.Italic; _waitingForHostText.alignment = TextAlignmentOptions.Center; _waitingForHostText.gameObject.SetActive(false); // toggled per refresh } void Update() { var gm = GameManager.Instance; if (gm?.gameClient?.CurrentLobbyState != null) _pending = gm.gameClient.CurrentLobbyState; if (_pending != null) { Refresh(_pending); _pending = null; } } // ── Scroll list construction ───────────────────────────────────────────── void BuildScrollList(RectTransform listRoot) { // Strip any existing children of `player list` (typically one // placeholder TMP). Don't touch the listRoot itself - it has the art // team's anchoring + glow pairing we want to preserve. var kill = new List(); foreach (Transform child in listRoot) kill.Add(child.gameObject); foreach (var go in kill) DestroyImmediate(go); // ScrollRect on the list root var sr = listRoot.gameObject.GetComponent() ?? listRoot.gameObject.AddComponent(); sr.horizontal = false; sr.vertical = true; sr.movementType = ScrollRect.MovementType.Elastic; sr.elasticity = 0.1f; sr.scrollSensitivity = 80f; // Viewport (with mask for clipping) var viewport = MakeRT("Viewport", listRoot); Stretch(viewport); var vpImg = viewport.gameObject.AddComponent(); vpImg.color = new Color(0,0,0,0); viewport.gameObject.AddComponent(); // Content - populated from State.Players var content = MakeRT("Content", viewport); content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1); content.pivot = new Vector2(0.5f, 1); content.sizeDelta = Vector2.zero; content.anchoredPosition = Vector2.zero; var vlg = content.gameObject.AddComponent(); vlg.childControlWidth = true; vlg.childControlHeight = false; vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false; vlg.spacing = 4; var csf = content.gameObject.AddComponent(); csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; sr.viewport = viewport; sr.content = content; _scrollContent = content; } // ── State refresh ──────────────────────────────────────────────────────── void Refresh(LobbyState state) { // Lobby code label (preserve art team prefix) if (_codeLabel != null) { const string prefix = "Lobby code: "; _codeLabel.text = prefix + (state.JoinCode ?? "------"); } // Max players label - we use this for live player count too. The art // team's prefix "Max players: " gets appended with current/max. if (_maxPlayersLabel != null) { int n = state.Players != null ? state.Players.Count : 0; _maxPlayersLabel.text = $"Players: {n}"; } // Unified screen for both owner and joiner. Host gets the start // button + working settings; non-host gets a clear "waiting" message // in the same screen real estate so the layout stays consistent. bool isHost = GameManager.Instance?.gameClient?.IsOwner ?? false; if (_startBtn != null) _startBtn.gameObject.SetActive(isHost); if (_waitingForHostText != null) { _waitingForHostText.gameObject.SetActive(!isHost); _waitingForHostText.text = state.Phase == GamePhase.Loading ? "⏳ Downloading map data..." : "⌛ Waiting for host to start the game..."; } if (_scrollContent == null) return; // Clear and rebuild rows. Player count is small (<= 15 per server // config), so a full rebuild every refresh is cheap and avoids the // bookkeeping of incremental updates. foreach (var row in _rows) Destroy(row); _rows.Clear(); if (state.Players == null) return; string myId = GameManager.Instance?.gameClient?.ClientUuid ?? ""; for (int i = 0; i < state.Players.Count; i++) { var p = state.Players[i]; bool isMe = p.ClientUuid == myId; var row = BuildRow(p.DisplayName ?? "???", isMe, p.IsOwner, i % 2 == 0 ? C_ROW_A : C_ROW_B); row.transform.SetParent(_scrollContent, false); _rows.Add(row); } } GameObject BuildRow(string playerName, bool isMe, bool isHostPlayer, Color bg) { const float ROW_H = 130f; var go = new GameObject("PlayerRow"); var rt = go.AddComponent(); rt.sizeDelta = new Vector2(0f, ROW_H); var le = go.AddComponent(); le.minHeight = ROW_H; le.preferredHeight = ROW_H; var bgImg = go.AddComponent(); bgImg.color = bg; // Bottom divider line var divRT = MakeRT("Div", rt); divRT.anchorMin = new Vector2(0, 0); divRT.anchorMax = new Vector2(1, 0); divRT.pivot = new Vector2(0.5f, 0); divRT.offsetMin = new Vector2(20, 0); divRT.offsetMax = new Vector2(-20, 2); var divImg = divRT.gameObject.AddComponent(); divImg.color = C_DIVIDER; float nameLeft = 24f; if (isHostPlayer) { var crownRT = MakeRT("Crown", rt); crownRT.anchorMin = new Vector2(0, 0.5f); crownRT.anchorMax = new Vector2(0, 0.5f); crownRT.pivot = new Vector2(0, 0.5f); crownRT.sizeDelta = new Vector2(90, 90); crownRT.anchoredPosition = new Vector2(18, 0); var crownTmp = crownRT.gameObject.AddComponent(); crownTmp.text = "👑"; crownTmp.fontSize = 52; crownTmp.color = C_GOLD; crownTmp.alignment = TextAlignmentOptions.Center; nameLeft = 118f; } // Name label float nameMaxX = isMe ? 0.68f : 1f; var nameRT = MakeRT("Name", rt); nameRT.anchorMin = new Vector2(0, 0); nameRT.anchorMax = new Vector2(nameMaxX, 1); nameRT.offsetMin = new Vector2(nameLeft, 6); nameRT.offsetMax = new Vector2(-10, -6); var nt = nameRT.gameObject.AddComponent(); nt.text = playerName; nt.fontSize = 48; nt.color = isMe ? C_WHITE : C_SOFT; nt.alignment = TextAlignmentOptions.MidlineLeft; nt.fontStyle = isMe ? FontStyles.Bold : FontStyles.Normal; nt.overflowMode = TextOverflowModes.Ellipsis; // "YOU" badge if (isMe) { var badgeRT = MakeRT("YouBadge", rt); badgeRT.anchorMin = new Vector2(0.68f, 0.22f); badgeRT.anchorMax = new Vector2(1f, 0.78f); badgeRT.offsetMin = Vector2.zero; badgeRT.offsetMax = new Vector2(-20f, 0); var badgeImg = badgeRT.gameObject.AddComponent(); badgeImg.color = C_ACCENT; var badgeTxtRT = MakeRT("Txt", badgeRT); Stretch(badgeTxtRT); var badgeTmp = badgeTxtRT.gameObject.AddComponent(); badgeTmp.text = "YOU"; badgeTmp.fontSize = 30; badgeTmp.color = C_WHITE; badgeTmp.fontStyle = FontStyles.Bold; badgeTmp.alignment = TextAlignmentOptions.Center; } return go; } // ── Scene-binding helpers ──────────────────────────────────────────────── static Button FindButton(Transform root, string name) { var t = FindByName(root, name); return t != null ? t.GetComponent