/* ╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ UŽIVATELSKÉ ROZHRANÍ - UnityTestClient_UI.cs ║ ║ ║ ║ Tento soubor obsahuje veškeré UI pomocí Unity IMGUI: ║ ║ • Hlavní menu (připojení, vytvoření/vstup do lobby, statistiky) ║ ║ • Lobby obrazovka (nastavení, seznam hráčů, chat) ║ ║ • Loading obrazovka (progress bar načítání mapových dat) ║ ║ • Herní HUD (role, úkoly, minimap, sabotáž timer) ║ ║ • Meeting panel (hlasování, diskuze) ║ ║ • Konec hry (výsledky, statistiky) ║ ║ ║ ║ HERNÍ HUD OBSAHUJE: ║ ║ • Panel role (nahoře) - vaše role, jméno, stav ║ ║ • Panel úkolů (vpravo) - globální progress bar + seznam VAŠICH úkolů ║ ║ - Zelený checkmark = VY jste dokončili tento úkol ║ ║ - Progress bar = celkový pokrok VŠECH hráčů ║ ║ • Sabotáž timer (při aktivní sabotáži) - zbývající čas ║ ║ • Akční tlačítka (dole) - USE, KILL, SABOTAGE, REPORT ║ ║ ║ ║ POZNÁMKA PRO STUDENTY: ║ ║ IMGUI je vhodné pro prototypování a debug, ale pro produkční hru ║ ║ doporučujeme použít Unity UI (Canvas) nebo UI Toolkit, které nabízejí: ║ ║ • Lepší výkon (batching, atlasy) ║ ║ • WYSIWYG editor v Unity ║ ║ • Lepší podporu animací a přechodů ║ ║ • Snadnější lokalizaci a škálování ║ ║ • Lepší podporu dotykového ovládání ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ */ using UnityEngine; using System; using System.Collections.Generic; using GeoSus.Client; public partial class UnityTestClient { #region ═══════════════════════════════════════════════════════════════════ // UI PROMĚNNÉ // ════════════════════════════════════════════════════════════════════════ #endregion // ───────────────────────────────────────────────────────────────────────── // GUI Styly - inicializují se v InitializeUIStyles() // ───────────────────────────────────────────────────────────────────────── /// Styl pro velké nadpisy protected GUIStyle titleStyle; /// Styl pro podnadpisy protected GUIStyle subtitleStyle; /// Styl pro běžný text protected GUIStyle labelStyle; /// Styl pro tlačítka protected GUIStyle buttonStyle; /// Styl pro velká tlačítka protected GUIStyle bigButtonStyle; /// Styl pro textová pole protected GUIStyle textFieldStyle; /// Styl pro boxy/panely protected GUIStyle boxStyle; /// Styl pro rich text protected GUIStyle richTextStyle; /// Styl pro notifikace protected GUIStyle notificationStyle; // ───────────────────────────────────────────────────────────────────────── // Vstupy pro formuláře // ───────────────────────────────────────────────────────────────────────── /// Vstupní pole pro jméno hráče protected string inputPlayerName = ""; /// Vstupní pole pro kód lobby protected string inputJoinCode = ""; /// Vstupní pole pro heslo protected string inputPassword = ""; /// Vstupní pole pro zeměpisnou šířku protected string inputLatitude = "50.7735892"; /// Vstupní pole pro zeměpisnou délku protected string inputLongitude = "15.0721653"; /// Vstupní pole pro poloměr protected string inputRadius = "300"; /// Počet impostorů protected int inputImpostorCount = 1; /// Počet úkolů protected int inputTaskCount = 5; // ───────────────────────────────────────────────────────────────────────── // UI stav // ───────────────────────────────────────────────────────────────────────── /// Aktuální záložka v hlavním menu protected int mainMenuTab = 0; /// Scroll pozice pro seznam hráčů protected Vector2 playerListScroll; /// Scroll pozice pro statistiky protected Vector2 statsScroll; /// Škálovací faktor UI protected float uiScale = 1f; // ───────────────────────────────────────────────────────────────────────── // Loading stav // ───────────────────────────────────────────────────────────────────────── /// Zpráva během načítání protected string loadingMessage = "Načítání..."; /// Progress načítání (0-1) protected float loadingProgress = 0f; // ───────────────────────────────────────────────────────────────────────── // Meeting stav // ───────────────────────────────────────────────────────────────────────── /// Aktuální meeting data protected MeetingStartedPayload currentMeeting; /// Kdo už hlasoval protected Dictionary meetingVotes = new Dictionary(); /// Kdo dorazil na meeting protected HashSet arrivedAtMeeting = new HashSet(); /// Dorazili jsme na meeting? protected bool iArrivedAtMeeting = false; /// Náš hlas (UUID nebo null pro skip) protected string myVote; /// Můžeme hlasovat? protected bool canVote = false; /// Můžeme hlasovat v tomto meetingu (jsme naživu)? protected bool canVoteInMeeting = false; /// Výsledky hlasování protected VotingClosedPayload votingResults; /// Zobrazit výsledky hlasování? protected bool showVotingResults = false; /// Čas konce zobrazení výsledků protected float votingResultsEndTime; // ───────────────────────────────────────────────────────────────────────── // Herní HUD stav // ───────────────────────────────────────────────────────────────────────── /// Celkový počet dokončených úkolů (globálně pro všechny hráče) protected int totalTasksCompleted = 0; /// Celkový počet požadovaných úkolů (globálně) protected int totalTasksRequired = 0; /// ID mých splněných úkolů protected HashSet myCompletedTaskIds = new HashSet(); /// Zobrazit sabotage panel? protected bool showSabotagePanel = false; #region ═══════════════════════════════════════════════════════════════════ // INICIALIZACE STYLŮ // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Inicializace GUI stylů. /// DŮLEŽITÉ: Musí se volat v Start() nebo později, ne v Awake()! /// GUISkin není dostupný v Awake. /// protected void InitializeUIStyles() { // Styl pro velké nadpisy titleStyle = new GUIStyle(GUI.skin.label) { fontSize = 32, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter }; titleStyle.normal.textColor = Color.white; // Styl pro podnadpisy subtitleStyle = new GUIStyle(GUI.skin.label) { fontSize = 20, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter }; subtitleStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f); // Styl pro běžný text labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 16, alignment = TextAnchor.MiddleLeft }; labelStyle.normal.textColor = Color.white; // Styl pro tlačítka buttonStyle = new GUIStyle(GUI.skin.button) { fontSize = 16, fontStyle = FontStyle.Bold }; // Styl pro velká tlačítka bigButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 20, fontStyle = FontStyle.Bold, fixedHeight = 50 }; // Styl pro textová pole textFieldStyle = new GUIStyle(GUI.skin.textField) { fontSize = 16, fixedHeight = 30 }; // Styl pro boxy boxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 10, 10) }; // Styl pro rich text richTextStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }; // Styl pro notifikace notificationStyle = new GUIStyle(GUI.skin.box) { fontSize = 18, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter }; notificationStyle.normal.textColor = Color.white; // Načteme uložené jméno inputPlayerName = displayName; } /// /// Aplikace škálování UI pro různá rozlišení. /// Základní rozlišení je 1920x1080. /// protected void ApplyUIScaling() { // Výpočet škálovacího faktoru float baseWidth = 1920f; float baseHeight = 1080f; // Použijeme menší z obou faktorů pro zachování poměru stran float scaleX = Screen.width / baseWidth; float scaleY = Screen.height / baseHeight; uiScale = Mathf.Min(scaleX, scaleY); // Minimální škálování uiScale = Mathf.Max(uiScale, 0.5f); // Aplikace matice pro škálování GUI.matrix = Matrix4x4.TRS( Vector3.zero, Quaternion.identity, new Vector3(uiScale, uiScale, 1f) ); } #region ═══════════════════════════════════════════════════════════════════ // HLAVNÍ MENU // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení hlavního menu. /// Obsahuje záložky pro připojení, vytvoření lobby a statistiky. /// protected void DrawMainMenu() { // Reinicializace stylů pokud je potřeba (po změně skinu) if (titleStyle == null) InitializeUIStyles(); // Centrovaný panel float panelWidth = 500; float panelHeight = 600; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = (Screen.height / uiScale - panelHeight) / 2; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); // Pozadí GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); // Použijeme try-catch pro ošetření změn stavu během renderování try { // ═══════════════════════════════════════════════════════════════════ // NADPIS // ═══════════════════════════════════════════════════════════════════ GUILayout.Space(20); GUILayout.Label("GeoSus", titleStyle); GUILayout.Label("GPS Multiplayer Game", subtitleStyle); GUILayout.Space(20); // ═══════════════════════════════════════════════════════════════════ // STAV PŘIPOJENÍ // ═══════════════════════════════════════════════════════════════════ // Cache stav na začátku renderování pro konzistenci bool isConnected = client != null && client.IsConnected; if (isConnected) { GUI.color = Color.green; GUILayout.Label("● Připojeno k serveru", labelStyle); GUI.color = Color.white; } else if (isConnecting) { GUI.color = Color.yellow; GUILayout.Label("● Připojuji...", labelStyle); GUI.color = Color.white; } else { GUI.color = Color.red; GUILayout.Label("● Nepřipojeno", labelStyle); GUI.color = Color.white; } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════════ // JMÉNO HRÁČE // ═══════════════════════════════════════════════════════════════════ GUILayout.Label("Vaše jméno:", labelStyle); inputPlayerName = GUILayout.TextField(inputPlayerName, 20, textFieldStyle); // Uložení jména if (inputPlayerName != displayName && !string.IsNullOrWhiteSpace(inputPlayerName)) { displayName = inputPlayerName; PlayerPrefs.SetString("PlayerName", displayName); if (client != null) client.DisplayName = displayName; } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════════ // PŘIPOJENÍ K SERVERU // ═══════════════════════════════════════════════════════════════════ if (!isConnected) { GUI.enabled = !isConnecting; if (GUILayout.Button("Připojit k serveru", bigButtonStyle)) { ConnectToServer(); } GUI.enabled = true; } else { // ═══════════════════════════════════════════════════════════════ // ZÁLOŽKY // ═══════════════════════════════════════════════════════════════ string[] tabs = { "Připojit se", "Vytvořit hru", "Statistiky" }; mainMenuTab = GUILayout.Toolbar(mainMenuTab, tabs); GUILayout.Space(10); switch (mainMenuTab) { case 0: DrawJoinLobbyTab(); break; case 1: DrawCreateLobbyTab(); break; case 2: DrawStatsTab(); break; } GUILayout.Space(20); // Tlačítko odpojení GUI.color = new Color(1f, 0.5f, 0.5f); if (GUILayout.Button("Odpojit", buttonStyle)) { DisconnectFromServer(); } GUI.color = Color.white; } } // end try catch (System.ArgumentException) { // Ignorujeme chyby při změně stavu GUI mezi Layout a Repaint // Příští frame se vykreslí správně } GUILayout.EndArea(); } /// /// Záložka pro připojení do existujícího lobby /// private void DrawJoinLobbyTab() { GUILayout.Label("Kód lobby:", labelStyle); // Vstup pro kód - automaticky uppercase inputJoinCode = GUILayout.TextField(inputJoinCode.ToUpper(), 6, textFieldStyle); GUILayout.Space(5); GUILayout.Label("Heslo (volitelné):", labelStyle); inputPassword = GUILayout.PasswordField(inputPassword, '*', textFieldStyle); GUILayout.Space(20); GUI.enabled = inputJoinCode.Length == 6; if (GUILayout.Button("Připojit do lobby", bigButtonStyle)) { JoinLobby(inputJoinCode, string.IsNullOrWhiteSpace(inputPassword) ? null : inputPassword); } GUI.enabled = true; } /// /// Záložka pro vytvoření nového lobby /// private void DrawCreateLobbyTab() { // ───────────────────────────────────────────────────────────────── // GPS souřadnice // ───────────────────────────────────────────────────────────────── GUILayout.Label("Střed herní oblasti (GPS):", labelStyle); GUILayout.BeginHorizontal(); GUILayout.Label("Lat:", GUILayout.Width(40)); inputLatitude = GUILayout.TextField(inputLatitude, textFieldStyle); GUILayout.Label("Lon:", GUILayout.Width(40)); inputLongitude = GUILayout.TextField(inputLongitude, textFieldStyle); GUILayout.EndHorizontal(); GUILayout.Space(5); // ───────────────────────────────────────────────────────────────── // Poloměr // ───────────────────────────────────────────────────────────────── GUILayout.Label($"Poloměr herní oblasti: {inputRadius}m", labelStyle); // Slider pro poloměr float radius = 300f; float.TryParse(inputRadius, out radius); radius = GUILayout.HorizontalSlider(radius, 100f, 1000f); inputRadius = Mathf.RoundToInt(radius).ToString(); GUILayout.Space(10); // ───────────────────────────────────────────────────────────────── // Počet impostorů // ───────────────────────────────────────────────────────────────── GUILayout.Label($"Počet impostorů: {inputImpostorCount}", labelStyle); GUILayout.BeginHorizontal(); if (GUILayout.Button("-", GUILayout.Width(40))) inputImpostorCount = Mathf.Max(1, inputImpostorCount - 1); GUILayout.HorizontalSlider(inputImpostorCount, 1, 3); if (GUILayout.Button("+", GUILayout.Width(40))) inputImpostorCount = Mathf.Min(3, inputImpostorCount + 1); GUILayout.EndHorizontal(); GUILayout.Space(5); // ───────────────────────────────────────────────────────────────── // Počet úkolů // ───────────────────────────────────────────────────────────────── GUILayout.Label($"Počet úkolů: {inputTaskCount}", labelStyle); GUILayout.BeginHorizontal(); if (GUILayout.Button("-", GUILayout.Width(40))) inputTaskCount = Mathf.Max(3, inputTaskCount - 1); GUILayout.HorizontalSlider(inputTaskCount, 3, 10); if (GUILayout.Button("+", GUILayout.Width(40))) inputTaskCount = Mathf.Min(10, inputTaskCount + 1); GUILayout.EndHorizontal(); GUILayout.Space(5); // ───────────────────────────────────────────────────────────────── // Heslo // ───────────────────────────────────────────────────────────────── GUILayout.Label("Heslo (volitelné):", labelStyle); inputPassword = GUILayout.PasswordField(inputPassword, '*', textFieldStyle); GUILayout.Space(20); // ───────────────────────────────────────────────────────────────── // Tlačítko vytvoření // ───────────────────────────────────────────────────────────────── if (GUILayout.Button("Vytvořit lobby", bigButtonStyle)) { double lat, lon, rad; if (double.TryParse(inputLatitude, out lat) && double.TryParse(inputLongitude, out lon) && double.TryParse(inputRadius, out rad)) { Position center = new Position(lat, lon); CreateLobby(center, rad, inputImpostorCount, inputTaskCount, string.IsNullOrWhiteSpace(inputPassword) ? null : inputPassword); } else { ShowError("Neplatné souřadnice nebo poloměr!"); } } } /// /// Záložka pro statistiky hráče /// private void DrawStatsTab() { statsScroll = GUILayout.BeginScrollView(statsScroll, GUILayout.Height(350)); // ═══════════════════════════════════════════════════════════════ // SERVER HEALTH // ═══════════════════════════════════════════════════════════════ GUILayout.Label("═══ SERVER STATUS ═══", labelStyle); if (healthStatus == null) { GUILayout.Label("Načítám stav serveru...", labelStyle); } else { Color statusColor = healthStatus.status == "ok" ? Color.green : Color.red; GUI.color = statusColor; DrawStatRow("Status", healthStatus.status.ToUpper()); GUI.color = Color.white; if (healthStatus.uptimeSeconds > 0) { var uptime = TimeSpan.FromSeconds(healthStatus.uptimeSeconds); DrawStatRow("Uptime", $"{uptime.Hours:D2}:{uptime.Minutes:D2}:{uptime.Seconds:D2}"); } DrawStatRow("Aktivní lobby", healthStatus.activeLobbies.ToString()); DrawStatRow("Hráčů online", healthStatus.connectedPlayers.ToString()); if (!string.IsNullOrEmpty(healthStatus.version)) DrawStatRow("Verze", healthStatus.version); } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════ // MOJE STATISTIKY // ═══════════════════════════════════════════════════════════════ GUILayout.Label("═══ MOJE STATISTIKY ═══", labelStyle); if (playerStats == null) { GUILayout.Label("Načítám statistiky...", labelStyle); } else { DrawStatRow("Odehráno her", playerStats.totalGames.ToString()); DrawStatRow("Výher", playerStats.GamesWon.ToString()); DrawStatRow("Win rate", $"{(playerStats.WinRate * 100):F1}%"); DrawStatRow("Zabití", playerStats.totalKills.ToString()); DrawStatRow("Smrti", playerStats.totalDeaths.ToString()); DrawStatRow("K/D ratio", $"{playerStats.killDeathRatio:F2}"); DrawStatRow("Dokončené úkoly", playerStats.tasksCompleted.ToString()); DrawStatRow("Her jako impostor", playerStats.gamesAsImpostor.ToString()); DrawStatRow("Her jako crew", playerStats.gamesAsCrew.ToString()); } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════ // LEADERBOARD // ═══════════════════════════════════════════════════════════════ GUILayout.Label("═══ LEADERBOARD (TOP 5) ═══", labelStyle); if (leaderboard == null || leaderboard.Count == 0) { GUILayout.Label(isLoadingLeaderboard ? "Načítám leaderboard..." : "Žádní hráči v leaderboardu", labelStyle); } else { // Header GUILayout.BeginHorizontal(); GUILayout.Label("#", labelStyle, GUILayout.Width(25)); GUILayout.Label("Hráč", labelStyle, GUILayout.Width(120)); GUILayout.Label("Výhry", labelStyle, GUILayout.Width(50)); GUILayout.Label("Win%", labelStyle, GUILayout.Width(50)); GUILayout.EndHorizontal(); // Entries int rank = 1; foreach (var entry in leaderboard) { GUILayout.BeginHorizontal(); // Barvy pro top 3 if (rank == 1) GUI.color = Color.yellow; else if (rank == 2) GUI.color = new Color(0.8f, 0.8f, 0.8f); else if (rank == 3) GUI.color = new Color(0.8f, 0.5f, 0.2f); else GUI.color = Color.white; GUILayout.Label($"{rank}.", labelStyle, GUILayout.Width(25)); GUILayout.Label(entry.displayName ?? "???", labelStyle, GUILayout.Width(120)); GUILayout.Label(entry.TotalWins.ToString(), labelStyle, GUILayout.Width(50)); GUILayout.Label($"{(entry.WinRate * 100):F0}%", labelStyle, GUILayout.Width(50)); GUI.color = Color.white; GUILayout.EndHorizontal(); rank++; } } GUILayout.EndScrollView(); // Refresh button GUILayout.Space(5); if (GUILayout.Button("Obnovit vše", buttonStyle)) { RefreshAllStats(); } } /// /// Obnoví všechny statistiky (player stats, leaderboard, health) /// private void RefreshAllStats() { FetchPlayerStats(); FetchLeaderboard(5); CheckServerHealth(); } /// /// Pomocná metoda pro vykreslení řádku statistiky /// private void DrawStatRow(string label, string value) { GUILayout.BeginHorizontal(); GUILayout.Label(label, labelStyle, GUILayout.Width(200)); GUILayout.Label(value, labelStyle); GUILayout.EndHorizontal(); } #region ═══════════════════════════════════════════════════════════════════ // LOBBY OBRAZOVKA // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení lobby obrazovky. /// Zobrazuje seznam hráčů, nastavení a tlačítko start. /// protected void DrawLobbyScreen() { if (titleStyle == null) InitializeUIStyles(); float panelWidth = 600; float panelHeight = 700; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = (Screen.height / uiScale - panelHeight) / 2; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); // ═══════════════════════════════════════════════════════════════════ // NADPIS A KÓD // ═══════════════════════════════════════════════════════════════════ GUILayout.Space(10); GUILayout.Label("LOBBY", titleStyle); if (client?.JoinCode != null) { GUILayout.Label($"Kód: {client.JoinCode}", subtitleStyle); } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════════ // SEZNAM HRÁČŮ // ═══════════════════════════════════════════════════════════════════ GUILayout.Label("Hráči:", labelStyle); playerListScroll = GUILayout.BeginScrollView(playerListScroll, GUILayout.Height(200)); if (client?.CurrentLobbyState?.Players != null) { foreach (var player in client.CurrentLobbyState.Players) { GUILayout.BeginHorizontal(boxStyle); // Ikona vlastníka if (player.IsOwner) { GUILayout.Label("👑", GUILayout.Width(25)); } else { GUILayout.Space(25); } // Jméno hráče string nameColor = player.ClientUuid == clientUuid ? "yellow" : "white"; GUILayout.Label($"{player.DisplayName}", richTextStyle); // Stav ready if (player.IsReady) { GUI.color = Color.green; GUILayout.Label("✓ Ready", GUILayout.Width(70)); GUI.color = Color.white; } GUILayout.EndHorizontal(); } } GUILayout.EndScrollView(); // ═══════════════════════════════════════════════════════════════════ // NASTAVENÍ LOBBY (pouze pro vlastníka) // ═══════════════════════════════════════════════════════════════════ bool isOwner = client?.IsOwner ?? false; GUILayout.Space(10); GUILayout.Label("Nastavení hry:", labelStyle); GUILayout.BeginVertical(boxStyle); if (client?.CurrentLobbyState != null) { var state = client.CurrentLobbyState; GUILayout.Label($"Střed: {state.PlayAreaCenter.Lat:F4}, {state.PlayAreaCenter.Lon:F4}"); GUILayout.Label($"Poloměr: {state.PlayAreaRadius}m"); GUILayout.Label($"Impostorů: {state.ImpostorCount}"); GUILayout.Label($"Heslo: {(state.HasPassword ? "Ano" : "Ne")}"); } GUILayout.EndVertical(); // ═══════════════════════════════════════════════════════════════════ // TLAČÍTKA // ═══════════════════════════════════════════════════════════════════ GUILayout.Space(20); if (isOwner) { // Tlačítko START (pouze owner) int playerCount = client?.CurrentLobbyState?.Players?.Count ?? 0; GUI.enabled = playerCount >= 2; // Minimum 2 hráči GUI.color = Color.green; if (GUILayout.Button($"SPUSTIT HRU ({playerCount} hráčů)", bigButtonStyle)) { StartGame(); } GUI.color = Color.white; GUI.enabled = true; if (playerCount < 2) { GUILayout.Label("Potřeba minimálně 2 hráči", richTextStyle); } } else { GUILayout.Label("Čekám na vlastníka lobby...", labelStyle); } GUILayout.Space(10); // Tlačítko opuštění GUI.color = new Color(1f, 0.5f, 0.5f); if (GUILayout.Button("Opustit lobby", buttonStyle)) { LeaveLobby(); } GUI.color = Color.white; GUILayout.EndArea(); } #region ═══════════════════════════════════════════════════════════════════ // LOADING OBRAZOVKA // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení loading obrazovky. /// Zobrazuje progress bar během načítání mapových dat. /// protected void DrawLoadingScreen() { if (titleStyle == null) InitializeUIStyles(); float panelWidth = 400; float panelHeight = 200; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = (Screen.height / uiScale - panelHeight) / 2; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); GUILayout.Space(30); GUILayout.Label("Načítání hry", titleStyle); GUILayout.Space(20); // Progress bar Rect progressRect = GUILayoutUtility.GetRect(panelWidth - 40, 30); progressRect.x = 20; GUI.Box(progressRect, ""); Rect fillRect = new Rect(progressRect.x + 2, progressRect.y + 2, (progressRect.width - 4) * loadingProgress, progressRect.height - 4); GUI.color = Color.green; GUI.DrawTexture(fillRect, Texture2D.whiteTexture); GUI.color = Color.white; GUILayout.Space(10); GUILayout.Label(loadingMessage, labelStyle); GUILayout.EndArea(); } #region ═══════════════════════════════════════════════════════════════════ // HERNÍ HUD // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení herního HUD. /// Zobrazuje roli, úkoly, ovládání a stav hry. /// protected void DrawGameHUD() { if (titleStyle == null) InitializeUIStyles(); // ═══════════════════════════════════════════════════════════════════ // HORNÍ PANEL - Role a stav // ═══════════════════════════════════════════════════════════════════ GUILayout.BeginArea(new Rect(10, 10, 300, 150)); GUI.Box(new Rect(0, 0, 300, 150), "", boxStyle); // Role if (client?.MyRole != null) { string roleText = client.MyRole == PlayerRole.Impostor ? "IMPOSTOR" : "CREWMATE"; Color roleColor = client.MyRole == PlayerRole.Impostor ? Color.red : Color.cyan; GUI.color = roleColor; GUILayout.Label(roleText, subtitleStyle); GUI.color = Color.white; } // Stav (naživu/mrtvý) if (client?.PlayerPositions != null && client.PlayerPositions.TryGetValue(clientUuid, out var myState)) { if (myState.State == PlayerState.Dead) { GUI.color = Color.gray; GUILayout.Label("💀 MRTVÝ (duch)", labelStyle); GUI.color = Color.white; } } // Sabotáž varování if (currentSabotage != null) { GUI.color = Color.red; string sabText = currentSabotage.Type == SabotageType.CriticalMeltdown ? "⚠ MELTDOWN!" : "⚠ COMMS DOWN!"; GUILayout.Label(sabText, labelStyle); if (currentSabotage.Deadline.HasValue) { float remaining = (float)(currentSabotage.Deadline.Value - DateTime.UtcNow).TotalSeconds; GUILayout.Label($"Zbývá: {remaining:F0}s", labelStyle); } GUI.color = Color.white; } // Ping GUILayout.Label($"Ping: {client?.Ping ?? 0}ms", labelStyle); GUILayout.EndArea(); // ═══════════════════════════════════════════════════════════════════ // PRAVÝ PANEL - Úkoly // ═══════════════════════════════════════════════════════════════════ float rightPanelX = Screen.width / uiScale - 260; GUILayout.BeginArea(new Rect(rightPanelX, 10, 250, 400)); GUI.Box(new Rect(0, 0, 250, 400), "", boxStyle); GUILayout.Label("ÚKOLY", subtitleStyle); // Progress bar úkolů GUILayout.Label($"Dokončeno: {totalTasksCompleted}/{totalTasksRequired}"); Rect taskProgressRect = GUILayoutUtility.GetRect(230, 20); GUI.Box(taskProgressRect, ""); if (totalTasksRequired > 0) { float taskProgress = (float)totalTasksCompleted / totalTasksRequired; Rect taskFillRect = new Rect(taskProgressRect.x + 2, taskProgressRect.y + 2, (taskProgressRect.width - 4) * taskProgress, taskProgressRect.height - 4); GUI.color = Color.green; GUI.DrawTexture(taskFillRect, Texture2D.whiteTexture); GUI.color = Color.white; } GUILayout.Space(10); // Seznam úkolů if (client?.MyTasks != null) { foreach (var task in client.MyTasks) { bool completed = myCompletedTaskIds.Contains(task.TaskId); string taskColor = completed ? "green" : "white"; string checkmark = completed ? "✓ " : "○ "; GUILayout.Label($"{checkmark}{task.Name}", richTextStyle); } } GUILayout.EndArea(); // ═══════════════════════════════════════════════════════════════════ // SPODNÍ PANEL - Akce // ═══════════════════════════════════════════════════════════════════ float bottomPanelY = Screen.height / uiScale - 100; GUILayout.BeginArea(new Rect(10, bottomPanelY, Screen.width / uiScale - 20, 90)); GUILayout.BeginHorizontal(); // Tlačítko USE (úkoly, reporty) GUI.enabled = CanPerformAction(); if (GUILayout.Button(GetActionButtonText(), bigButtonStyle, GUILayout.Width(150))) { PerformPrimaryAction(); } GUI.enabled = true; GUILayout.Space(20); // Tlačítko KILL (pouze impostor) if (client?.MyRole == PlayerRole.Impostor) { string nearbyTarget = client.FindNearbyPlayer(5.0, true); GUI.enabled = nearbyTarget != null && !IsOnCooldown(); GUI.color = Color.red; if (GUILayout.Button("🔪 KILL", bigButtonStyle, GUILayout.Width(120))) { if (nearbyTarget != null) { AttemptKill(nearbyTarget); } } GUI.color = Color.white; GUI.enabled = true; GUILayout.Space(10); // Tlačítko SABOTAGE GUI.enabled = currentSabotage == null; if (GUILayout.Button("⚠ SABOTAGE", bigButtonStyle, GUILayout.Width(150))) { showSabotagePanel = !showSabotagePanel; } GUI.enabled = true; } GUILayout.FlexibleSpace(); // Tlačítko MEETING GUI.enabled = CanCallMeeting(); if (GUILayout.Button("🔔 MEETING", bigButtonStyle, GUILayout.Width(150))) { CallEmergencyMeeting(); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.EndArea(); // ═══════════════════════════════════════════════════════════════════ // SABOTAGE PANEL (popup) // ═══════════════════════════════════════════════════════════════════ if (showSabotagePanel && client?.MyRole == PlayerRole.Impostor) { DrawSabotagePanel(); } // ═══════════════════════════════════════════════════════════════════ // PROGRESS BAR OPRAVY // ═══════════════════════════════════════════════════════════════════ if (isRepairing) { DrawProgressBar(); } } /// /// Vykreslení panelu sabotáží /// private void DrawSabotagePanel() { float panelWidth = 200; float panelHeight = 120; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = Screen.height / uiScale - 220; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); GUILayout.Label("Sabotáž", subtitleStyle); if (GUILayout.Button("📡 Comms Blackout", buttonStyle)) { StartSabotage(SabotageType.CommsBlackout); showSabotagePanel = false; } if (GUILayout.Button("☢ Critical Meltdown", buttonStyle)) { StartSabotage(SabotageType.CriticalMeltdown); showSabotagePanel = false; } if (GUILayout.Button("Zavřít", buttonStyle)) { showSabotagePanel = false; } GUILayout.EndArea(); } /// /// Vykreslení progress baru pro úkoly/opravy /// private void DrawProgressBar() { float barWidth = 300; float barHeight = 30; float barX = (Screen.width / uiScale - barWidth) / 2; float barY = Screen.height / uiScale / 2; Rect bgRect = new Rect(barX - 10, barY - 10, barWidth + 20, barHeight + 40); GUI.Box(bgRect, "", boxStyle); GUILayout.BeginArea(new Rect(barX, barY, barWidth, barHeight + 20)); string label = "Opravuji..."; GUILayout.Label(label, labelStyle); Rect progressRect = GUILayoutUtility.GetRect(barWidth, barHeight); GUI.Box(progressRect, ""); Rect fillRect = new Rect(progressRect.x + 2, progressRect.y + 2, (progressRect.width - 4) * repairProgress, progressRect.height - 4); GUI.color = Color.yellow; GUI.DrawTexture(fillRect, Texture2D.whiteTexture); GUI.color = Color.white; GUILayout.EndArea(); } #region ═══════════════════════════════════════════════════════════════════ // MEETING PANEL // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení hlasovacího panelu. /// Zobrazuje seznam hráčů a tlačítka pro hlasování. /// protected void DrawMeetingPanel() { // Kontrola, zda je meeting aktivní nebo zobrazujeme výsledky if (currentMeeting == null && !showVotingResults) return; // Kontrola herní fáze (povolíme i Playing když zobrazujeme výsledky) var phase = client?.CurrentLobbyState?.Phase; bool isInMeeting = phase == GamePhase.Meeting || phase == GamePhase.Voting; if (!isInMeeting && !showVotingResults) return; float panelWidth = 500; float panelHeight = 600; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = (Screen.height / uiScale - panelHeight) / 2; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); // ═══════════════════════════════════════════════════════════════════ // NADPIS // ═══════════════════════════════════════════════════════════════════ GUILayout.Space(10); // Pokud zobrazujeme výsledky, jiný nadpis if (showVotingResults) { GUI.color = Color.yellow; GUILayout.Label("VÝSLEDKY HLASOVÁNÍ", titleStyle); GUI.color = Color.white; } else if (currentMeeting != null) { string meetingType = currentMeeting.Type == MeetingType.BodyReport ? "TĚLO NALEZENO!" : "EMERGENCY MEETING!"; GUI.color = Color.red; GUILayout.Label(meetingType, titleStyle); GUI.color = Color.white; } // ═══════════════════════════════════════════════════════════════════ // ČASOVAČ A FÁZE MEETINGU // ═══════════════════════════════════════════════════════════════════ if (currentMeeting != null && !showVotingResults) { DateTime now = DateTime.UtcNow; bool isArrival = now < currentMeeting.ArrivalDeadline; bool isDiscussion = !isArrival && currentMeeting.DiscussionEndTime.HasValue && now < currentMeeting.DiscussionEndTime.Value; bool isVoting = !isArrival && !isDiscussion && now < currentMeeting.VotingEndTime; // Fáze 1: Příchod na meeting if (isArrival) { float remaining = (float)(currentMeeting.ArrivalDeadline - now).TotalSeconds; if (!iArrivedAtMeeting) { GUI.color = Color.yellow; GUILayout.Label($"⚠ BĚŽ NA MEETING! ({remaining:F0}s)", subtitleStyle); GUI.color = Color.white; GUILayout.Label("Pokud nedorazíš včas, nebudeš moci hlasovat!", labelStyle); } else { GUI.color = Color.green; GUILayout.Label($"✓ Dorazil/a jsi! Čekání: {remaining:F0}s", subtitleStyle); GUI.color = Color.white; } // Zobraz počet dorazivších int arrivedCount = arrivedAtMeeting.Count; int totalAlive = 0; if (client?.CurrentLobbyState?.Players != null) { foreach (var p in client.CurrentLobbyState.Players) { if (client.PlayerPositions != null && client.PlayerPositions.TryGetValue(p.ClientUuid, out var info)) { if (info.State == PlayerState.Alive) totalAlive++; } else { totalAlive++; } } } GUILayout.Label($"Dorazilo: {arrivedCount}/{totalAlive}", labelStyle); canVote = false; } // Fáze 2: Diskuze else if (isDiscussion && currentMeeting.DiscussionEndTime.HasValue) { float remaining = (float)(currentMeeting.DiscussionEndTime.Value - now).TotalSeconds; GUILayout.Label($"Diskuze: {remaining:F0}s", subtitleStyle); canVote = false; } // Fáze 3: Hlasování else if (isVoting) { float remaining = (float)(currentMeeting.VotingEndTime - now).TotalSeconds; GUILayout.Label($"Hlasování: {remaining:F0}s", subtitleStyle); // Můžeme hlasovat pouze pokud jsme dorazili a jsme naživu if (!iArrivedAtMeeting) { GUI.color = Color.red; GUILayout.Label("❌ Nedorazil/a jsi včas - nemůžeš hlasovat!", labelStyle); GUI.color = Color.white; canVote = false; } else { canVote = canVoteInMeeting && myVote == null; } } else { GUILayout.Label("Čekám na výsledky...", subtitleStyle); canVote = false; } } GUILayout.Space(10); // ═══════════════════════════════════════════════════════════════════ // VÝSLEDKY HLASOVÁNÍ // ═══════════════════════════════════════════════════════════════════ if (showVotingResults && votingResults != null) { GUILayout.BeginVertical(boxStyle); GUI.color = Color.yellow; GUILayout.Label("═══ VÝSLEDKY HLASOVÁNÍ ═══", subtitleStyle); GUI.color = Color.white; GUILayout.Space(5); if (votingResults.VoteCounts != null) { foreach (var kvp in votingResults.VoteCounts) { // Server používá "__SKIP__" pro skip vote string name = (kvp.Key == "__SKIP__" || kvp.Key == "skip") ? "⏭ Přeskočit" : $"👤 {GetPlayerName(kvp.Key)}"; // Zvýrazníme vyhozeného hráče if (kvp.Key == votingResults.EjectedPlayerId) { GUI.color = Color.red; GUILayout.Label($"🚪 {name}: {kvp.Value} hlasů", labelStyle); GUI.color = Color.white; } else { GUILayout.Label($" {name}: {kvp.Value} hlasů", labelStyle); } } } GUILayout.Space(10); // Výsledek hlasování if (votingResults.EjectedPlayerId != null) { string ejectedName = GetPlayerName(votingResults.EjectedPlayerId); GUI.color = Color.red; GUILayout.Label($"🚪 {ejectedName} byl/a vyhozen/a!", subtitleStyle); GUI.color = Color.white; } else if (votingResults.WasTie) { GUI.color = Color.yellow; GUILayout.Label("⚖ Remíza - nikdo nebyl vyhozen", subtitleStyle); GUI.color = Color.white; } else { GUI.color = Color.gray; GUILayout.Label("✗ Nikdo nebyl vyhozen", subtitleStyle); GUI.color = Color.white; } GUILayout.EndVertical(); GUILayout.Space(10); if (Time.time > votingResultsEndTime) { showVotingResults = false; currentMeeting = null; } } // ═══════════════════════════════════════════════════════════════════ // SEZNAM HRÁČŮ PRO HLASOVÁNÍ // ═══════════════════════════════════════════════════════════════════ if (!showVotingResults) { playerListScroll = GUILayout.BeginScrollView(playerListScroll, GUILayout.Height(350)); if (client?.CurrentLobbyState?.Players != null) { foreach (var player in client.CurrentLobbyState.Players) { // Získáme stav hráče - bezpečný přístup bool isAlive = true; if (client.PlayerPositions != null && client.PlayerPositions.TryGetValue(player.ClientUuid, out var pInfo)) { isAlive = pInfo.State == PlayerState.Alive; } GUILayout.BeginHorizontal(boxStyle); // Ikona stavu if (!isAlive) { GUI.color = Color.gray; GUILayout.Label("💀", GUILayout.Width(25)); } else { GUILayout.Label("👤", GUILayout.Width(25)); } // Jméno string nameColor = player.ClientUuid == clientUuid ? "yellow" : (isAlive ? "white" : "gray"); GUILayout.Label($"{player.DisplayName}", richTextStyle, GUILayout.Width(150)); // Indikátor hlasování if (meetingVotes.ContainsKey(player.ClientUuid)) { GUILayout.Label("✓ Hlasoval", GUILayout.Width(80)); } // Tlačítko hlasování GUI.enabled = canVote && isAlive && player.ClientUuid != clientUuid; if (GUILayout.Button("Hlasovat", GUILayout.Width(80))) { CastVote(player.ClientUuid); } GUI.enabled = true; GUI.color = Color.white; GUILayout.EndHorizontal(); } } GUILayout.EndScrollView(); // ═══════════════════════════════════════════════════════════════ // TLAČÍTKO SKIP // ═══════════════════════════════════════════════════════════════ GUILayout.Space(10); GUI.enabled = canVote; GUI.color = Color.gray; if (GUILayout.Button("⏭ Přeskočit hlasování", bigButtonStyle)) { CastVote(null); // null = skip } GUI.color = Color.white; GUI.enabled = true; } GUILayout.EndArea(); } #region ═══════════════════════════════════════════════════════════════════ // KONEC HRY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení obrazovky konce hry. /// Zobrazuje vítěze a statistiky. /// protected void DrawGameEndScreen() { if (titleStyle == null) InitializeUIStyles(); float panelWidth = 500; float panelHeight = 400; float panelX = (Screen.width / uiScale - panelWidth) / 2; float panelY = (Screen.height / uiScale - panelHeight) / 2; GUILayout.BeginArea(new Rect(panelX, panelY, panelWidth, panelHeight)); GUI.Box(new Rect(0, 0, panelWidth, panelHeight), "", boxStyle); GUILayout.Space(20); // ═══════════════════════════════════════════════════════════════════ // VÍTĚZ // ═══════════════════════════════════════════════════════════════════ if (gameEndData != null) { // Určíme, zda jsme vyhráli bool weWon = gameEndData.Winners.Contains(clientUuid); if (weWon) { GUI.color = Color.green; GUILayout.Label("🏆 VÍTĚZSTVÍ!", titleStyle); } else { GUI.color = Color.red; GUILayout.Label("💔 PROHRA", titleStyle); } GUI.color = Color.white; GUILayout.Space(20); // Vítězná frakce string faction = gameEndData.WinningFaction == "Impostor" ? "Impostoři" : "Posádka"; GUILayout.Label($"Vyhráli: {faction}", subtitleStyle); GUILayout.Space(10); // Důvod GUILayout.Label($"Důvod: {gameEndData.Reason}", labelStyle); GUILayout.Space(20); // Seznam vítězů GUILayout.Label("Vítězové:", labelStyle); foreach (var winnerId in gameEndData.Winners) { string name = GetPlayerName(winnerId); GUILayout.Label($" • {name}", labelStyle); } } GUILayout.Space(30); // ═══════════════════════════════════════════════════════════════════ // TLAČÍTKA // ═══════════════════════════════════════════════════════════════════ // Pouze owner může restartovat hru bool isOwner = client?.IsOwner ?? false; if (isOwner) { if (GUILayout.Button("🔄 Nová hra (zpět do lobby)", bigButtonStyle)) { // Pošleme serveru požadavek na návrat do lobby client?.ReturnToLobby(); } } else { GUI.enabled = false; GUILayout.Button("Čekám na hostitele...", bigButtonStyle); GUI.enabled = true; } GUILayout.Space(10); if (GUILayout.Button("Hlavní menu", buttonStyle)) { LeaveLobby(); gameEndData = null; } GUILayout.EndArea(); } #region ═══════════════════════════════════════════════════════════════════ // NOTIFIKACE A CHYBY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení notifikací /// protected void DrawNotifications() { if (currentNotification.message == null) return; float notifWidth = 400; float notifHeight = 60; float notifX = (Screen.width / uiScale - notifWidth) / 2; float notifY = 50; // Fade efekt float remaining = notificationEndTime - Time.time; float alpha = Mathf.Clamp01(remaining); Color bgColor = currentNotification.color; bgColor.a = alpha * 0.9f; GUI.color = bgColor; GUI.Box(new Rect(notifX, notifY, notifWidth, notifHeight), "", notificationStyle); GUI.color = new Color(1, 1, 1, alpha); GUI.Label(new Rect(notifX, notifY, notifWidth, notifHeight), $"{currentNotification.icon} {currentNotification.message}", notificationStyle); GUI.color = Color.white; } /// /// Vykreslení chybové zprávy /// protected void DrawErrorMessage() { if (string.IsNullOrEmpty(errorMessage) || Time.time > errorMessageEndTime) { errorMessage = null; return; } float errorWidth = 500; float errorHeight = 40; float errorX = (Screen.width / uiScale - errorWidth) / 2; float errorY = Screen.height / uiScale - 60; GUI.color = new Color(0.8f, 0.2f, 0.2f, 0.9f); GUI.Box(new Rect(errorX, errorY, errorWidth, errorHeight), "", notificationStyle); GUI.color = Color.white; GUI.Label(new Rect(errorX, errorY, errorWidth, errorHeight), $"⚠ {errorMessage}", notificationStyle); } #region ═══════════════════════════════════════════════════════════════════ // POMOCNÉ UI METODY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Získá text pro hlavní akční tlačítko /// private string GetActionButtonText() { // Kontrola těla poblíž var body = client?.FindNearbyBody(5.0); if (body != null) return "🚨 REPORT"; // Kontrola úkolu poblíž var task = client?.FindNearbyTask(5.0); if (task != null) return "✋ USE"; // Kontrola opravné stanice poblíž if (currentSabotage != null) { var station = FindNearbyRepairStation(5.0); if (station != null) return "🔧 REPAIR"; } return "USE"; } /// /// Zjistí, zda můžeme provést akci /// private bool CanPerformAction() { if (client == null) return false; // Zjistíme, jestli jsme duch bool isGhost = false; if (client.PlayerPositions != null && client.PlayerPositions.TryGetValue(clientUuid, out var playerInfo)) { isGhost = playerInfo.State != PlayerState.Alive; } // Duchové NEMOHOU reportovat těla ani volat meeting // Ale MOHOU plnit úkoly! if (!isGhost && client.FindNearbyBody(5.0) != null) return true; // Kontrola úkolu - duchové i živí crew mohou dělat úkoly if (client.FindNearbyTask(5.0) != null && client.MyRole == PlayerRole.Crew) return true; // Kontrola opravné stanice - pouze živí mohou opravovat if (!isGhost && currentSabotage != null && FindNearbyRepairStation(5.0) != null) return true; return false; } /// /// Zjistí, zda můžeme svolat meeting /// private bool CanCallMeeting() { if (client == null) return false; if (currentSabotage?.Type == SabotageType.CommsBlackout) return false; // Kontrola stavu hráče - bezpečný přístup if (client.PlayerPositions != null && client.PlayerPositions.TryGetValue(clientUuid, out var playerInfo)) { if (playerInfo.State != PlayerState.Alive) return false; } return true; } /// /// Zjistí, zda je kill na cooldownu /// private bool IsOnCooldown() { // TODO: Implementovat cooldown tracking return false; } }