/* ╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ STATISTIKY A HTTP API - UnityTestClient_Stats.cs ║ ║ ║ ║ Tento soubor obsahuje komunikaci s HTTP Stats API serveru: ║ ║ • Načítání statistik hráče ║ ║ • Načítání leaderboardu ║ ║ • Health check serveru ║ ║ ║ ║ API ENDPOINTY (port 8088): ║ ║ • GET /stats/{playerId} - statistiky konkrétního hráče ║ ║ • GET /leaderboard?limit=N - top N hráčů podle win rate ║ ║ • GET /health - stav serveru (aktivní lobby, hráči) ║ ║ ║ ║ STATISTIKY ZAHRNUJÍ: ║ ║ • Počet her, výher, proher ║ ║ • Win rate celkově, jako impostor, jako crew ║ ║ • Počet zabití, smrtí, K/D ratio ║ ║ • Dokončené úkoly ║ ║ • Přesnost hlasování (správně odhalení impostoři) ║ ║ ║ ║ POZNÁMKA PRO STUDENTY: ║ ║ Unity používá UnityWebRequest pro HTTP komunikaci. ║ ║ Všechny requesty musí běžet jako coroutiny (StartCoroutine). ║ ║ JSON parsing: JsonUtility (jednodušší) nebo Newtonsoft.Json (flexibilnější). ║ ║ ║ ║ Pro vlastní hru můžete rozšířit statistiky o: ║ ║ • Achievements/Badges ║ ║ • Sezónní ranky ║ ║ • Historii her ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ */ using UnityEngine; using UnityEngine.Networking; using System; using System.Collections; using System.Collections.Generic; public partial class UnityTestClient { #region ═══════════════════════════════════════════════════════════════════ // STATS PROMĚNNÉ // ════════════════════════════════════════════════════════════════════════ #endregion /// Port pro Stats HTTP API protected int statsApiPort = 8088; /// URL pro Stats API (automaticky používá HTTPS pokud je povoleno) protected string StatsApiUrl => useHttps ? $"https://{serverHost}" : $"http://{serverHost}:{statsApiPort}"; /// Načtené statistiky hráče protected PlayerStatistics playerStats = null; /// Načtený leaderboard protected List leaderboard = null; /// Stav zdraví serveru protected ServerHealthStatus healthStatus = null; /// Probíhá načítání statistik? protected bool isLoadingStats = false; /// Probíhá načítání leaderboardu? protected bool isLoadingLeaderboard = false; #region ═══════════════════════════════════════════════════════════════════ // DATOVÉ STRUKTURY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Statistiky hráče. /// Mapuje se z JSON response z /stats/{playerId}. /// POZOR: Názvy polí musí přesně odpovídat JSON z serveru! /// [System.Serializable] public class PlayerStatistics { /// UUID hráče public string clientUuid; /// Zobrazované jméno public string displayName; /// Celkový počet odehraných her public int totalGames; /// Počet her jako crew public int gamesAsCrew; /// Počet her jako impostor public int gamesAsImpostor; /// Počet výher jako crew public int crewWins; /// Počet výher jako impostor public int impostorWins; /// Win rate jako crew (0-1) public float crewWinRate; /// Win rate jako impostor (0-1) public float impostorWinRate; /// Počet zabití (jako impostor) public int totalKills; /// Počet smrtí public int totalDeaths; /// Kill/Death ratio public float killDeathRatio; /// Počet dokončených úkolů public int tasksCompleted; /// Průměrný počet tasků za hru public float averageTasksPerGame; /// Počet nahlášených těl public int bodiesReported; /// Počet svolaných emergency meetingů public int emergencyMeetingsCalled; /// Kolikrát byl vyhozen hlasováním public int timesVotedOut; /// Počet úspěšných hlasů (správně identifikovaný impostor) public int successfulVotes; /// Celkový čas hraní v sekundách public long totalPlaytimeSeconds; /// Počet cheat incidentů public int cheatIncidents; /// Kdy byl naposledy viděn public string lastSeen; // Pomocné vlastnosti pro zobrazení v UI public int GamesWon => crewWins + impostorWins; public float WinRate => totalGames > 0 ? (float)GamesWon / totalGames : 0f; } /// /// Položka leaderboardu. /// POZOR: Názvy polí musí přesně odpovídat JSON z serveru! /// [System.Serializable] public class LeaderboardEntry { /// UUID hráče public string clientUuid; /// Zobrazované jméno public string displayName; /// Celkový počet her public int totalGames; /// Výhry jako crew public int crewWins; /// Výhry jako impostor public int impostorWins; /// Počet zabití public int totalKills; /// Počet dokončených tasků public int tasksCompleted; // Pomocné vlastnosti public int TotalWins => crewWins + impostorWins; public float WinRate => totalGames > 0 ? (float)TotalWins / totalGames : 0f; } /// /// Wrapper pro leaderboard response (JSON array). /// [System.Serializable] public class LeaderboardResponse { public List entries; } /// /// Stav serveru. /// [System.Serializable] public class ServerHealthStatus { /// Stav: "healthy" nebo "unhealthy" public string status; /// Verze serveru public string version; /// Uptime v sekundách public long uptimeSeconds; /// Počet aktivních lobby public int activeLobbies; /// Počet připojených hráčů public int connectedPlayers; } #region ═══════════════════════════════════════════════════════════════════ // NAČÍTÁNÍ STATISTIK // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Spustí načítání statistik hráče. /// Výsledek bude v playerStats. /// protected void FetchPlayerStats() { Debug.Log($"[Stats] FetchPlayerStats called. isLoadingStats={isLoadingStats}, clientUuid={clientUuid}"); if (isLoadingStats) { Debug.Log("[Stats] Already loading, skipping"); return; } if (string.IsNullOrEmpty(clientUuid)) { Debug.Log("[Stats] clientUuid is empty, skipping"); return; } Debug.Log($"[Stats] Starting coroutine for player {clientUuid}"); StartCoroutine(FetchPlayerStatsCoroutine(clientUuid)); } /// /// Coroutine pro načtení statistik hráče. /// /// POZNÁMKA PRO STUDENTY: /// Coroutiny v Unity umožňují asynchronní operace. /// yield return čeká na dokončení operace před pokračováním. /// UnityWebRequest.Get vytváří HTTP GET request. /// /// UUID hráče private IEnumerator FetchPlayerStatsCoroutine(string playerId) { isLoadingStats = true; string url = $"{StatsApiUrl}/stats/{playerId}"; Debug.Log($"[Stats] Fetching from: {url}"); using (UnityWebRequest request = UnityWebRequest.Get(url)) { // Nastavení timeoutu request.timeout = 10; // Odeslání requestu a čekání na odpověď yield return request.SendWebRequest(); Debug.Log($"[Stats] Request completed. Result: {request.result}, Code: {request.responseCode}"); // Kontrola chyb if (request.result == UnityWebRequest.Result.Success) { // Parsování JSON try { string json = request.downloadHandler.text; playerStats = JsonUtility.FromJson(json); Debug.Log($"[Stats] Loaded stats for {playerStats.displayName}"); } catch (Exception e) { Debug.LogError($"[Stats] Failed to parse stats: {e.Message}"); playerStats = null; } } else { // Chyba při requestu Debug.LogWarning($"[Stats] Failed to fetch stats: {request.error}"); // Pokud hráč neexistuje (404), vytvoříme prázdné statistiky if (request.responseCode == 404) { playerStats = new PlayerStatistics { clientUuid = playerId, displayName = displayName, totalGames = 0, gamesAsCrew = 0, gamesAsImpostor = 0, crewWins = 0, impostorWins = 0, crewWinRate = 0, impostorWinRate = 0, totalKills = 0, totalDeaths = 0, killDeathRatio = 0, tasksCompleted = 0 }; } } } isLoadingStats = false; } #region ═══════════════════════════════════════════════════════════════════ // LEADERBOARD // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Spustí načítání leaderboardu. /// /// Počet hráčů k načtení (max 100) protected void FetchLeaderboard(int limit = 10) { if (isLoadingLeaderboard) return; StartCoroutine(FetchLeaderboardCoroutine(limit)); } /// /// Coroutine pro načtení leaderboardu. /// private IEnumerator FetchLeaderboardCoroutine(int limit) { isLoadingLeaderboard = true; string url = $"{StatsApiUrl}/leaderboard?limit={limit}"; using (UnityWebRequest request = UnityWebRequest.Get(url)) { request.timeout = 10; yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { try { string json = request.downloadHandler.text; // JsonUtility nemá přímou podporu pro arrays, potřebujeme wrapper // nebo použijeme jednoduchý workaround string wrappedJson = "{\"entries\":" + json + "}"; LeaderboardResponse response = JsonUtility.FromJson(wrappedJson); leaderboard = response.entries; Debug.Log($"[Stats] Loaded leaderboard with {leaderboard.Count} entries"); } catch (Exception e) { Debug.LogError($"[Stats] Failed to parse leaderboard: {e.Message}"); leaderboard = null; } } else { Debug.LogWarning($"[Stats] Failed to fetch leaderboard: {request.error}"); } } isLoadingLeaderboard = false; } #region ═══════════════════════════════════════════════════════════════════ // HEALTH CHECK // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Spustí health check serveru. /// protected void CheckServerHealth() { StartCoroutine(CheckServerHealthCoroutine()); } /// /// Coroutine pro health check. /// private IEnumerator CheckServerHealthCoroutine() { string url = $"{StatsApiUrl}/health"; using (UnityWebRequest request = UnityWebRequest.Get(url)) { request.timeout = 5; yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { try { string json = request.downloadHandler.text; healthStatus = JsonUtility.FromJson(json); Debug.Log($"[Stats] Server health: {healthStatus.status}, " + $"uptime: {healthStatus.uptimeSeconds}s, " + $"lobbies: {healthStatus.activeLobbies}, " + $"players: {healthStatus.connectedPlayers}"); } catch (Exception e) { Debug.LogError($"[Stats] Failed to parse health: {e.Message}"); healthStatus = null; } } else { Debug.LogWarning($"[Stats] Health check failed: {request.error}"); healthStatus = new ServerHealthStatus { status = "unreachable", version = "unknown" }; } } } #region ═══════════════════════════════════════════════════════════════════ // UI PRO LEADERBOARD // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení leaderboard panelu. /// Volá se z DrawStatsTab(). /// protected void DrawLeaderboardPanel() { if (leaderboard == null) { GUILayout.Label("Načítám leaderboard...", labelStyle); if (GUILayout.Button("Načíst", buttonStyle)) { FetchLeaderboard(10); } return; } GUILayout.Label("TOP HRÁČI", subtitleStyle); GUILayout.Space(5); // Hlavička GUILayout.BeginHorizontal(); GUILayout.Label("#", GUILayout.Width(30)); GUILayout.Label("Jméno", GUILayout.Width(150)); GUILayout.Label("Skóre", GUILayout.Width(80)); GUILayout.Label("Win%", GUILayout.Width(60)); GUILayout.EndHorizontal(); // Položky int rank = 1; foreach (var entry in leaderboard) { GUILayout.BeginHorizontal(); // Speciální 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}.", GUILayout.Width(30)); GUILayout.Label(entry.displayName, GUILayout.Width(150)); GUILayout.Label(entry.TotalWins.ToString(), GUILayout.Width(80)); GUILayout.Label($"{entry.WinRate * 100:F0}%", GUILayout.Width(60)); GUI.color = Color.white; GUILayout.EndHorizontal(); rank++; } GUILayout.Space(10); if (GUILayout.Button("Obnovit", buttonStyle)) { leaderboard = null; FetchLeaderboard(10); } } #region ═══════════════════════════════════════════════════════════════════ // UI PRO SERVER HEALTH // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vykreslení panelu stavu serveru. /// protected void DrawServerHealthPanel() { GUILayout.BeginVertical(boxStyle); GUILayout.Label("STAV SERVERU", subtitleStyle); if (healthStatus == null) { GUILayout.Label("Neznámý", labelStyle); if (GUILayout.Button("Zkontrolovat", buttonStyle)) { CheckServerHealth(); } } else { // Status if (healthStatus.status == "healthy") { GUI.color = Color.green; GUILayout.Label("● Online", labelStyle); } else { GUI.color = Color.red; GUILayout.Label("● Offline", labelStyle); } GUI.color = Color.white; // Detaily GUILayout.Label($"Verze: {healthStatus.version}", labelStyle); // Uptime TimeSpan uptime = TimeSpan.FromSeconds(healthStatus.uptimeSeconds); GUILayout.Label($"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m", labelStyle); // Aktivita GUILayout.Label($"Lobby: {healthStatus.activeLobbies}", labelStyle); GUILayout.Label($"Hráči: {healthStatus.connectedPlayers}", labelStyle); GUILayout.Space(5); if (GUILayout.Button("Obnovit", buttonStyle)) { healthStatus = null; CheckServerHealth(); } } GUILayout.EndVertical(); } #region ═══════════════════════════════════════════════════════════════════ // AUTOMATICKÉ NAČÍTÁNÍ // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Interval automatického obnovení statistik (sekundy) /// protected float statsRefreshInterval = 60f; /// Čas posledního obnovení protected float lastStatsRefresh = 0f; /// /// Automatické načítání statistik. /// Volá se v Update(). /// protected void UpdateStatsAutoRefresh() { if (currentState != AppState.MainMenu) return; if (string.IsNullOrEmpty(clientUuid)) return; if (Time.time - lastStatsRefresh >= statsRefreshInterval) { FetchPlayerStats(); CheckServerHealth(); lastStatsRefresh = Time.time; } } }