This commit is contained in:
Bandwidth
2026-04-26 20:49:32 +02:00
parent e0b808faed
commit d886f97e14
66 changed files with 8327 additions and 933 deletions

View File

@@ -5,9 +5,30 @@ using TMPro;
using GeoSus.Client;
/// <summary>
/// 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.
/// 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.
/// </summary>
public class LobbyDisplayUI : MonoBehaviour
{
@@ -18,54 +39,141 @@ public class LobbyDisplayUI : MonoBehaviour
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;
// ── 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<GameObject> _rows = new List<GameObject>();
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);
// ── 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);
// ── Lifecycle ─────────────────────────────────────────────────────────────
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 scene!");
Debug.LogError("[LobbyDisplayUI] No Canvas found in create scene.");
return;
}
var canvas = canvasGO.transform;
// Remove all placeholder Art children immediately (before we build)
var kill = new List<GameObject>();
foreach (Transform child in canvasGO.transform)
kill.Add(child.gameObject);
foreach (var go in kill)
DestroyImmediate(go);
// ── 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;
Build(canvasGO.transform);
// ── 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<TextMeshProUGUI>();
_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()
@@ -74,177 +182,110 @@ public class LobbyDisplayUI : MonoBehaviour
if (gm?.gameClient?.CurrentLobbyState != null)
_pending = gm.gameClient.CurrentLobbyState;
if (_pending != null && _listContent != null)
if (_pending != null)
{
Refresh(_pending);
_pending = null;
}
}
// ── Full UI construction ──────────────────────────────────────────────────
void Build(Transform canvasRoot)
// ── Scroll list construction ─────────────────────────────────────────────
void BuildScrollList(RectTransform listRoot)
{
const float HDR_H = 250f;
const float SUB_H = 88f;
const float FOOT_H = 180f;
const float BTN_W = 200f;
// 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<GameObject>();
foreach (Transform child in listRoot) kill.Add(child.gameObject);
foreach (var go in kill) DestroyImmediate(go);
// Fullscreen dark overlay
var root = RT("Root", canvasRoot);
Stretch(root);
Img(root, C_BG);
// ScrollRect on the list root
var sr = listRoot.gameObject.GetComponent<ScrollRect>()
?? listRoot.gameObject.AddComponent<ScrollRect>();
sr.horizontal = false; sr.vertical = true;
sr.movementType = ScrollRect.MovementType.Elastic;
sr.elasticity = 0.1f;
sr.scrollSensitivity = 80f;
// ─── 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<RectTransform>();
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<RectTransform>();
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<ScrollRect>();
var viewport = RT("Viewport", rt);
// Viewport (with mask for clipping)
var viewport = MakeRT("Viewport", listRoot);
Stretch(viewport);
var vpImg = viewport.gameObject.AddComponent<Image>();
vpImg.color = new Color(0,0,0,0);
viewport.gameObject.AddComponent<RectMask2D>();
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 - 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<VerticalLayoutGroup>();
vlg.childControlWidth = true;
vlg.childControlHeight = false;
vlg.childForceExpandWidth = true;
vlg.childControlWidth = true;
vlg.childControlHeight = false;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.spacing = 2f;
vlg.padding = new RectOffset(0, 0, 0, 0);
vlg.spacing = 4;
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
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;
sr.viewport = viewport;
sr.content = content;
_listContent = content;
_scrollContent = content;
}
// ── State refresh ────────────────────────────────────────────────────────
// ── State refresh ────────────────────────────────────────────────────────
void Refresh(LobbyState state)
{
if (_codeText != null) _codeText.text = state.JoinCode ?? "------";
// Lobby code label (preserve art team prefix)
if (_codeLabel != null)
{
const string prefix = "Lobby code: ";
_codeLabel.text = prefix + (state.JoinCode ?? "------");
}
int n = state.Players.Count;
if (_countText != null)
_countText.text = $"{n} player{(n == 1 ? "" : "s")} in lobby";
// 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}";
}
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
// 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...";
: "⌛ Waiting for host to start the game...";
}
if (_listContent == null) return;
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 me = p.ClientUuid == myId;
var row = BuildRow(p.DisplayName ?? "???", me, p.IsOwner,
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(_listContent, false);
row.transform.SetParent(_scrollContent, false);
_rows.Add(row);
}
}
@@ -255,69 +296,98 @@ public class LobbyDisplayUI : MonoBehaviour
var go = new GameObject("PlayerRow");
var rt = go.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(0f, ROW_H);
var le = go.AddComponent<LayoutElement>();
le.minHeight = ROW_H;
le.minHeight = ROW_H;
le.preferredHeight = ROW_H;
Img(rt, bg);
var bgImg = go.AddComponent<Image>();
bgImg.color = 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);
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<Image>();
divImg.color = 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);
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<TextMeshProUGUI>();
crownTmp.text = "👑"; crownTmp.fontSize = 52;
crownTmp.color = C_GOLD; crownTmp.alignment = TextAlignmentOptions.Center;
nameLeft = 118f;
}
// Player name
// Name label
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 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<TextMeshProUGUI>();
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.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);
var badgeRT = MakeRT("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);
badgeRT.offsetMin = Vector2.zero;
badgeRT.offsetMax = new Vector2(-20f, 0);
var badgeImg = badgeRT.gameObject.AddComponent<Image>();
badgeImg.color = C_ACCENT;
var badgeTxtRT = MakeRT("Txt", badgeRT);
Stretch(badgeTxtRT);
var badgeTmp = badgeTxtRT.gameObject.AddComponent<TextMeshProUGUI>();
badgeTmp.text = "YOU"; badgeTmp.fontSize = 30;
badgeTmp.color = C_WHITE; badgeTmp.fontStyle = FontStyles.Bold;
badgeTmp.alignment = TextAlignmentOptions.Center;
}
return go;
}
// ── Layout helpers ────────────────────────────────────────────────────────
RectTransform RT(string name, Transform parent)
// ── Scene-binding helpers ────────────────────────────────────────────────
static Button FindButton(Transform root, string name)
{
var t = FindByName(root, name);
return t != null ? t.GetComponent<Button>() : null;
}
static TMP_Text FindTMP(Transform root, string name)
{
var t = FindByName(root, name);
if (t == null) return null;
return t.GetComponent<TMP_Text>() ?? t.GetComponentInChildren<TMP_Text>();
}
static Transform FindByName(Transform root, string name)
{
if (root == null) return null;
if (root.name == name) return root;
foreach (Transform child in root)
{
var found = FindByName(child, name);
if (found != null) return found;
}
return null;
}
// ── Layout helpers ───────────────────────────────────────────────────────
static RectTransform MakeRT(string name, Transform parent)
{
var go = new GameObject(name);
var rt = go.AddComponent<RectTransform>();
@@ -326,92 +396,16 @@ public class LobbyDisplayUI : MonoBehaviour
return rt;
}
void Stretch(RectTransform rt)
static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
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)
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
Vector2 offMin, Vector2 offMax)
{
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<Image>();
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<TextMeshProUGUI>();
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<Button>();
btn.targetGraphic = rt.gameObject.GetComponent<Image>();
btn.onClick.AddListener(() => onClick());
var cols = btn.colors;
cols.normalColor = normal;
cols.highlightedColor = Color.Lerp(normal, Color.white, 0.3f);
cols.pressedColor = Color.Lerp(normal, Color.black, 0.3f);
cols.selectedColor = normal;
btn.colors = cols;
rt.anchorMin = aMin; rt.anchorMax = aMax;
rt.offsetMin = offMin; rt.offsetMax = offMax;
}
}