using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using GeoSus.Client; /// /// Attach to any manager GameObject in create.unity or join loading.unity. /// On Start(), removes all placeholder Art elements from the Canvas and builds /// a proper mobile-portrait lobby screen entirely in code. /// 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; } // ── Built UI references ─────────────────────────────────────────────────── private TMP_Text _codeText; private TMP_Text _countText; private Transform _listContent; private TMP_Text _statusText; private GameObject _startFooter; private GameObject _waitFooter; private readonly List _rows = new List(); private LobbyState _pending; // ── Colour palette ──────────────────────────────────────────────────────── static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; } static readonly Color C_BG = H("#0D0F1A"); static readonly Color C_HDR = H("#141927"); static readonly Color C_SUBBG = H("#0F1221"); static readonly Color C_ROW_A = H("#1A2035"); static readonly Color C_ROW_B = H("#161C2E"); static readonly Color C_DIVIDER = H("#252A3F"); static readonly Color C_ACCENT = H("#3399FF"); static readonly Color C_GOLD = H("#FFB800"); static readonly Color C_GREEN = H("#2DB84B"); static readonly Color C_RED = H("#C43232"); static readonly Color C_MUTED = new Color(0.47f, 0.53f, 0.67f); static readonly Color C_WHITE = Color.white; static readonly Color C_SOFT = new Color(0.73f, 0.80f, 0.88f); void OnEnable() => _all.Add(this); void OnDisable() => _all.Remove(this); // ── Lifecycle ───────────────────────────────────────────────────────────── void Start() { var canvasGO = GameObject.Find("Canvas"); if (canvasGO == null) { Debug.LogError("[LobbyDisplayUI] No Canvas found in scene!"); return; } // Remove all placeholder Art children immediately (before we build) var kill = new List(); foreach (Transform child in canvasGO.transform) kill.Add(child.gameObject); foreach (var go in kill) DestroyImmediate(go); Build(canvasGO.transform); } void Update() { var gm = GameManager.Instance; if (gm?.gameClient?.CurrentLobbyState != null) _pending = gm.gameClient.CurrentLobbyState; if (_pending != null && _listContent != null) { Refresh(_pending); _pending = null; } } // ── Full UI construction ────────────────────────────────────────────────── void Build(Transform canvasRoot) { const float HDR_H = 250f; const float SUB_H = 88f; const float FOOT_H = 180f; const float BTN_W = 200f; // Fullscreen dark overlay var root = RT("Root", canvasRoot); Stretch(root); Img(root, C_BG); // ─── Header bar ─────────────────────────────────────────────────────── var header = RT("Header", root); PinTop(header, HDR_H); Img(header, C_HDR); // Back (✕) button — left side of header var backBtn = RT("BackBtn", header); backBtn.anchorMin = new Vector2(0f, 0f); backBtn.anchorMax = new Vector2(0f, 1f); backBtn.pivot = new Vector2(0f, 0.5f); backBtn.offsetMin = new Vector2(18f, 22f); backBtn.offsetMax = new Vector2(BTN_W + 18f, -22f); Img(backBtn, C_RED); Btn(backBtn, C_RED, () => GameManager.Instance?.LeaveLobbyButton()); TxtChild(backBtn, "✕", 72, C_WHITE, TextAlignmentOptions.Center, bold: true); // "LOBBY CODE" micro label — upper-center of header var codeLbl = RT("CodeLbl", header); codeLbl.anchorMin = new Vector2(0.14f, 0.52f); codeLbl.anchorMax = new Vector2(0.86f, 0.97f); codeLbl.offsetMin = codeLbl.offsetMax = Vector2.zero; TmpDirect(codeLbl, "LOBBY CODE", 28, C_MUTED, TextAlignmentOptions.Center, bold: true); // Large code value — lower-center of header var codeValRT = RT("CodeVal", header); codeValRT.anchorMin = new Vector2(0.14f, 0.05f); codeValRT.anchorMax = new Vector2(0.86f, 0.52f); codeValRT.offsetMin = codeValRT.offsetMax = Vector2.zero; _codeText = TmpDirect(codeValRT, "------", 76, C_ACCENT, TextAlignmentOptions.Center, bold: true); // Copy (⎘) button — right side of header var copyBtn = RT("CopyBtn", header); copyBtn.anchorMin = new Vector2(1f, 0f); copyBtn.anchorMax = new Vector2(1f, 1f); copyBtn.pivot = new Vector2(1f, 0.5f); copyBtn.offsetMin = new Vector2(-(BTN_W + 18f), 22f); copyBtn.offsetMax = new Vector2(-18f, -22f); Img(copyBtn, C_ACCENT); Btn(copyBtn, C_ACCENT, () => { if (_codeText != null) GUIUtility.systemCopyBuffer = _codeText.text; }); TxtChild(copyBtn, "⎘", 60, C_WHITE, TextAlignmentOptions.Center); // ─── Player count subtitle bar ───────────────────────────────────────── var subBar = RT("CountBar", root); PinBelowTop(subBar, HDR_H, SUB_H); Img(subBar, C_SUBBG); _countText = TxtChild(subBar, "0 players in lobby", 34, C_MUTED, TextAlignmentOptions.Center); // ─── Scrollable player list ──────────────────────────────────────────── var scrollArea = RT("PlayerScroll", root); Fill(scrollArea, HDR_H + SUB_H, FOOT_H); BuildScroll(scrollArea); // ─── Footer: START GAME (host) or waiting text (others) ─────────────── _startFooter = new GameObject("StartFooter"); var sfRT = _startFooter.AddComponent(); sfRT.SetParent(root, false); sfRT.localScale = Vector3.one; PinBottom(sfRT, FOOT_H); Img(sfRT, C_SUBBG); var startBtnRT = RT("StartBtn", sfRT); Fill(startBtnRT, 20f, 20f, 24f, 24f); Img(startBtnRT, C_GREEN); Btn(startBtnRT, C_GREEN, () => GameManager.Instance?.StartGameButton()); TxtChild(startBtnRT, "▶ START GAME", 54, C_WHITE, TextAlignmentOptions.Center, bold: true); _startFooter.SetActive(false); _waitFooter = new GameObject("WaitFooter"); var wfRT = _waitFooter.AddComponent(); wfRT.SetParent(root, false); wfRT.localScale = Vector3.one; PinBottom(wfRT, FOOT_H); Img(wfRT, C_SUBBG); _statusText = TxtChild(wfRT, "⌛ Waiting for host to start...", 38, C_MUTED, TextAlignmentOptions.Center, italic: true); _waitFooter.SetActive(true); } void BuildScroll(RectTransform rt) { var sr = rt.gameObject.AddComponent(); var viewport = RT("Viewport", rt); Stretch(viewport); viewport.gameObject.AddComponent(); var content = RT("Content", viewport); content.anchorMin = new Vector2(0f, 1f); content.anchorMax = new Vector2(1f, 1f); content.pivot = new Vector2(0.5f, 1f); content.sizeDelta = new Vector2(0f, 0f); content.anchoredPosition = Vector2.zero; var vlg = content.gameObject.AddComponent(); vlg.childControlWidth = true; vlg.childControlHeight = false; vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false; vlg.spacing = 2f; vlg.padding = new RectOffset(0, 0, 0, 0); var csf = content.gameObject.AddComponent(); csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; sr.viewport = viewport; sr.content = content; sr.horizontal = false; sr.vertical = true; sr.scrollSensitivity = 80f; sr.movementType = ScrollRect.MovementType.Elastic; sr.elasticity = 0.1f; _listContent = content; } // ── State refresh ───────────────────────────────────────────────────────── void Refresh(LobbyState state) { if (_codeText != null) _codeText.text = state.JoinCode ?? "------"; int n = state.Players.Count; if (_countText != null) _countText.text = $"{n} player{(n == 1 ? "" : "s")} in lobby"; var gm = GameManager.Instance; bool isHost = gm?.gameClient?.IsOwner ?? false; string myId = gm?.gameClient?.ClientUuid ?? ""; if (_startFooter != null) _startFooter.SetActive(isHost); if (_waitFooter != null) _waitFooter.SetActive(!isHost); if (_statusText != null) _statusText.text = state.Phase == GamePhase.Loading ? "⏳ Downloading map data..." : "⌛ Waiting for host to start..."; if (_listContent == null) return; foreach (var row in _rows) Destroy(row); _rows.Clear(); for (int i = 0; i < state.Players.Count; i++) { var p = state.Players[i]; bool me = p.ClientUuid == myId; var row = BuildRow(p.DisplayName ?? "???", me, p.IsOwner, i % 2 == 0 ? C_ROW_A : C_ROW_B); row.transform.SetParent(_listContent, 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; Img(rt, bg); // Bottom divider line var divRT = RT("Div", rt); divRT.anchorMin = new Vector2(0f, 0f); divRT.anchorMax = new Vector2(1f, 0f); divRT.pivot = new Vector2(0.5f, 0f); divRT.offsetMin = new Vector2(20f, 0f); divRT.offsetMax = new Vector2(-20f, 2f); Img(divRT, C_DIVIDER); float nameLeft = 24f; // Crown emoji for lobby host if (isHostPlayer) { var crownRT = RT("Crown", rt); crownRT.anchorMin = new Vector2(0f, 0.5f); crownRT.anchorMax = new Vector2(0f, 0.5f); crownRT.pivot = new Vector2(0f, 0.5f); crownRT.sizeDelta = new Vector2(90f, 90f); crownRT.anchoredPosition = new Vector2(18f, 0f); TmpDirect(crownRT, "👑", 52, C_GOLD, TextAlignmentOptions.Center); nameLeft = 118f; } // Player name float nameMaxX = isMe ? 0.68f : 1f; var nameRT = RT("Name", rt); nameRT.anchorMin = new Vector2(0f, 0f); nameRT.anchorMax = new Vector2(nameMaxX, 1f); nameRT.offsetMin = new Vector2(nameLeft, 6f); nameRT.offsetMax = new Vector2(-10f, -6f); 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 = RT("YouBadge", rt); badgeRT.anchorMin = new Vector2(0.68f, 0.22f); badgeRT.anchorMax = new Vector2(1f, 0.78f); badgeRT.offsetMin = new Vector2(0f, 0f); badgeRT.offsetMax = new Vector2(-20f, 0f); Img(badgeRT, C_ACCENT); TxtChild(badgeRT, "YOU", 30, C_WHITE, TextAlignmentOptions.Center, bold: true); } return go; } // ── Layout helpers ──────────────────────────────────────────────────────── RectTransform RT(string name, Transform parent) { var go = new GameObject(name); var rt = go.AddComponent(); rt.SetParent(parent, false); rt.localScale = Vector3.one; return rt; } void Stretch(RectTransform rt) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } void Fill(RectTransform rt, float top, float bottom, float left = 0f, float right = 0f) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = new Vector2(left, bottom); rt.offsetMax = new Vector2(-right, -top); } void PinTop(RectTransform rt, float h) { rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f); rt.pivot = new Vector2(0.5f, 1f); rt.offsetMin = new Vector2(0f, -h); rt.offsetMax = Vector2.zero; } void PinBelowTop(RectTransform rt, float fromTop, float h) { rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(1f, 1f); rt.pivot = new Vector2(0.5f, 1f); rt.offsetMin = new Vector2(0f, -(fromTop + h)); rt.offsetMax = new Vector2(0f, -fromTop); } void PinBottom(RectTransform rt, float h) { rt.anchorMin = Vector2.zero; rt.anchorMax = new Vector2(1f, 0f); rt.pivot = new Vector2(0.5f, 0f); rt.offsetMin = Vector2.zero; rt.offsetMax = new Vector2(0f, h); } // ── Graphic helpers ─────────────────────────────────────────────────────── /// Adds an Image directly to rt. Image Img(RectTransform rt, Color c) { var img = rt.gameObject.AddComponent(); img.color = c; return img; } /// Adds TMP directly to rt — only use when rt has NO Image component. TMP_Text TmpDirect(RectTransform rt, string text, float size, Color color, TextAlignmentOptions align, bool bold = false, bool italic = false) { var tmp = rt.gameObject.AddComponent(); tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align; if (bold) tmp.fontStyle |= FontStyles.Bold; if (italic) tmp.fontStyle |= FontStyles.Italic; return tmp; } /// Creates a stretch-fill child GO with TMP — safe when parent already has Image. TMP_Text TxtChild(RectTransform parent, string text, float size, Color color, TextAlignmentOptions align, bool bold = false, bool italic = false) { var childRT = RT("Txt", parent); Stretch(childRT); return TmpDirect(childRT, text, size, color, align, bold, italic); } void Btn(RectTransform rt, Color normal, System.Action onClick) { var btn = rt.gameObject.AddComponent