This commit is contained in:
2026-01-27 21:36:29 +01:00
commit c402c5513b
125 changed files with 18530 additions and 0 deletions

View File

@@ -0,0 +1,589 @@
/*
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ 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;
}
}
}