412 lines
18 KiB
C#
412 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
using GeoSus.Client;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
{
|
|
// ── Static hub so GameManager_UI can push state updates ──────────────────
|
|
private static readonly HashSet<LobbyDisplayUI> _all = new HashSet<LobbyDisplayUI>();
|
|
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<GameObject> _rows = new List<GameObject>();
|
|
|
|
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<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()
|
|
{
|
|
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<GameObject>();
|
|
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<ScrollRect>()
|
|
?? listRoot.gameObject.AddComponent<ScrollRect>();
|
|
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<Image>();
|
|
vpImg.color = new Color(0,0,0,0);
|
|
viewport.gameObject.AddComponent<RectMask2D>();
|
|
|
|
// 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.childForceExpandHeight = false;
|
|
vlg.spacing = 4;
|
|
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
|
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<RectTransform>();
|
|
rt.sizeDelta = new Vector2(0f, ROW_H);
|
|
var le = go.AddComponent<LayoutElement>();
|
|
le.minHeight = ROW_H;
|
|
le.preferredHeight = ROW_H;
|
|
|
|
var bgImg = go.AddComponent<Image>();
|
|
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<Image>();
|
|
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<TextMeshProUGUI>();
|
|
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<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.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<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;
|
|
}
|
|
|
|
// ── 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>();
|
|
rt.SetParent(parent, false);
|
|
rt.localScale = Vector3.one;
|
|
return rt;
|
|
}
|
|
|
|
static void Stretch(RectTransform rt)
|
|
{
|
|
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
|
}
|
|
|
|
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
|
|
Vector2 offMin, Vector2 offMax)
|
|
{
|
|
rt.anchorMin = aMin; rt.anchorMax = aMax;
|
|
rt.offsetMin = offMin; rt.offsetMax = offMax;
|
|
}
|
|
}
|