590 lines
24 KiB
C#
590 lines
24 KiB
C#
/*
|
|
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ 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
|
|
|
|
/// <summary>Port pro Stats HTTP API</summary>
|
|
protected int statsApiPort = 8088;
|
|
|
|
/// <summary>URL pro Stats API (automaticky používá HTTPS pokud je povoleno)</summary>
|
|
protected string StatsApiUrl => useHttps
|
|
? $"https://{serverHost}"
|
|
: $"http://{serverHost}:{statsApiPort}";
|
|
|
|
/// <summary>Načtené statistiky hráče</summary>
|
|
protected PlayerStatistics playerStats = null;
|
|
|
|
/// <summary>Načtený leaderboard</summary>
|
|
protected List<LeaderboardEntry> leaderboard = null;
|
|
|
|
/// <summary>Stav zdraví serveru</summary>
|
|
protected ServerHealthStatus healthStatus = null;
|
|
|
|
/// <summary>Probíhá načítání statistik?</summary>
|
|
protected bool isLoadingStats = false;
|
|
|
|
/// <summary>Probíhá načítání leaderboardu?</summary>
|
|
protected bool isLoadingLeaderboard = false;
|
|
|
|
#region ═══════════════════════════════════════════════════════════════════
|
|
// DATOVÉ STRUKTURY
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Statistiky hráče.
|
|
/// Mapuje se z JSON response z /stats/{playerId}.
|
|
/// POZOR: Názvy polí musí přesně odpovídat JSON z serveru!
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public class PlayerStatistics
|
|
{
|
|
/// <summary>UUID hráče</summary>
|
|
public string clientUuid;
|
|
|
|
/// <summary>Zobrazované jméno</summary>
|
|
public string displayName;
|
|
|
|
/// <summary>Celkový počet odehraných her</summary>
|
|
public int totalGames;
|
|
|
|
/// <summary>Počet her jako crew</summary>
|
|
public int gamesAsCrew;
|
|
|
|
/// <summary>Počet her jako impostor</summary>
|
|
public int gamesAsImpostor;
|
|
|
|
/// <summary>Počet výher jako crew</summary>
|
|
public int crewWins;
|
|
|
|
/// <summary>Počet výher jako impostor</summary>
|
|
public int impostorWins;
|
|
|
|
/// <summary>Win rate jako crew (0-1)</summary>
|
|
public float crewWinRate;
|
|
|
|
/// <summary>Win rate jako impostor (0-1)</summary>
|
|
public float impostorWinRate;
|
|
|
|
/// <summary>Počet zabití (jako impostor)</summary>
|
|
public int totalKills;
|
|
|
|
/// <summary>Počet smrtí</summary>
|
|
public int totalDeaths;
|
|
|
|
/// <summary>Kill/Death ratio</summary>
|
|
public float killDeathRatio;
|
|
|
|
/// <summary>Počet dokončených úkolů</summary>
|
|
public int tasksCompleted;
|
|
|
|
/// <summary>Průměrný počet tasků za hru</summary>
|
|
public float averageTasksPerGame;
|
|
|
|
/// <summary>Počet nahlášených těl</summary>
|
|
public int bodiesReported;
|
|
|
|
/// <summary>Počet svolaných emergency meetingů</summary>
|
|
public int emergencyMeetingsCalled;
|
|
|
|
/// <summary>Kolikrát byl vyhozen hlasováním</summary>
|
|
public int timesVotedOut;
|
|
|
|
/// <summary>Počet úspěšných hlasů (správně identifikovaný impostor)</summary>
|
|
public int successfulVotes;
|
|
|
|
/// <summary>Celkový čas hraní v sekundách</summary>
|
|
public long totalPlaytimeSeconds;
|
|
|
|
/// <summary>Počet cheat incidentů</summary>
|
|
public int cheatIncidents;
|
|
|
|
/// <summary>Kdy byl naposledy viděn</summary>
|
|
public string lastSeen;
|
|
|
|
// Pomocné vlastnosti pro zobrazení v UI
|
|
public int GamesWon => crewWins + impostorWins;
|
|
public float WinRate => totalGames > 0 ? (float)GamesWon / totalGames : 0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Položka leaderboardu.
|
|
/// POZOR: Názvy polí musí přesně odpovídat JSON z serveru!
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public class LeaderboardEntry
|
|
{
|
|
/// <summary>UUID hráče</summary>
|
|
public string clientUuid;
|
|
|
|
/// <summary>Zobrazované jméno</summary>
|
|
public string displayName;
|
|
|
|
/// <summary>Celkový počet her</summary>
|
|
public int totalGames;
|
|
|
|
/// <summary>Výhry jako crew</summary>
|
|
public int crewWins;
|
|
|
|
/// <summary>Výhry jako impostor</summary>
|
|
public int impostorWins;
|
|
|
|
/// <summary>Počet zabití</summary>
|
|
public int totalKills;
|
|
|
|
/// <summary>Počet dokončených tasků</summary>
|
|
public int tasksCompleted;
|
|
|
|
// Pomocné vlastnosti
|
|
public int TotalWins => crewWins + impostorWins;
|
|
public float WinRate => totalGames > 0 ? (float)TotalWins / totalGames : 0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wrapper pro leaderboard response (JSON array).
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public class LeaderboardResponse
|
|
{
|
|
public List<LeaderboardEntry> entries;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stav serveru.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public class ServerHealthStatus
|
|
{
|
|
/// <summary>Stav: "healthy" nebo "unhealthy"</summary>
|
|
public string status;
|
|
|
|
/// <summary>Verze serveru</summary>
|
|
public string version;
|
|
|
|
/// <summary>Uptime v sekundách</summary>
|
|
public long uptimeSeconds;
|
|
|
|
/// <summary>Počet aktivních lobby</summary>
|
|
public int activeLobbies;
|
|
|
|
/// <summary>Počet připojených hráčů</summary>
|
|
public int connectedPlayers;
|
|
}
|
|
|
|
#region ═══════════════════════════════════════════════════════════════════
|
|
// NAČÍTÁNÍ STATISTIK
|
|
// ════════════════════════════════════════════════════════════════════════
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Spustí načítání statistik hráče.
|
|
/// Výsledek bude v playerStats.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="playerId">UUID hráče</param>
|
|
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<PlayerStatistics>(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
|
|
|
|
/// <summary>
|
|
/// Spustí načítání leaderboardu.
|
|
/// </summary>
|
|
/// <param name="limit">Počet hráčů k načtení (max 100)</param>
|
|
protected void FetchLeaderboard(int limit = 10)
|
|
{
|
|
if (isLoadingLeaderboard) return;
|
|
|
|
StartCoroutine(FetchLeaderboardCoroutine(limit));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coroutine pro načtení leaderboardu.
|
|
/// </summary>
|
|
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<LeaderboardResponse>(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
|
|
|
|
/// <summary>
|
|
/// Spustí health check serveru.
|
|
/// </summary>
|
|
protected void CheckServerHealth()
|
|
{
|
|
StartCoroutine(CheckServerHealthCoroutine());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coroutine pro health check.
|
|
/// </summary>
|
|
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<ServerHealthStatus>(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
|
|
|
|
/// <summary>
|
|
/// Vykreslení leaderboard panelu.
|
|
/// Volá se z DrawStatsTab().
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Vykreslení panelu stavu serveru.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Interval automatického obnovení statistik (sekundy)
|
|
/// </summary>
|
|
protected float statsRefreshInterval = 60f;
|
|
|
|
/// <summary>Čas posledního obnovení</summary>
|
|
protected float lastStatsRefresh = 0f;
|
|
|
|
/// <summary>
|
|
/// Automatické načítání statistik.
|
|
/// Volá se v Update().
|
|
/// </summary>
|
|
protected void UpdateStatsAutoRefresh()
|
|
{
|
|
if (currentState != AppState.MainMenu) return;
|
|
if (string.IsNullOrEmpty(clientUuid)) return;
|
|
|
|
if (Time.time - lastStatsRefresh >= statsRefreshInterval)
|
|
{
|
|
FetchPlayerStats();
|
|
CheckServerHealth();
|
|
lastStatsRefresh = Time.time;
|
|
}
|
|
}
|
|
}
|