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,675 @@
/*
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ GEOSUS UNITY TEST CLIENT - KOMPLETNÍ PRŮVODCE ║
║ ║
║ Tento soubor je součástí testovacího klienta pro hru GeoSus - multiplayer GPS hru inspirovanou ║
║ hrou Among Us. Tento klient slouží jako referenční implementace pro studenty, kteří chtějí ║
║ postavit vlastní hru na základě GeoSus serveru. ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 📚 OBSAH DOKUMENTACE: ║
║ ═══════════════════ ║
║ ║
║ 1. ARCHITEKTURA APLIKACE ║
║ 2. SÍŤOVÁ KOMUNIKACE ║
║ 3. HERNÍ LOGIKA ║
║ 4. UŽIVATELSKÉ ROZHRANÍ ║
║ 5. VYKRESLOVÁNÍ MAPY ║
║ 6. STATISTIKY HRÁČŮ ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 📁 STRUKTURA SOUBORŮ: ║
║ ═════════════════════ ║
║ ║
║ UnityTestClient_Main.cs - Hlavní třída, inicializace, herní smyčka ║
║ UnityTestClient_Network.cs - Síťová komunikace, zpracování zpráv ║
║ UnityTestClient_UI.cs - Veškeré uživatelské rozhraní (IMGUI) ║
║ UnityTestClient_Map.cs - Vykreslování mapy pomocí 3D objektů ║
║ UnityTestClient_Game.cs - Herní mechaniky (pohyb, tasky, sabotáže) ║
║ UnityTestClient_Stats.cs - HTTP API pro statistiky hráčů ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🎮 1. ARCHITEKTURA APLIKACE ║
║ ═══════════════════════════ ║
║ ║
║ Aplikace používá PARTIAL CLASS pattern - všechny soubory definují stejnou třídu ║
║ UnityTestClient, která dědí z MonoBehaviour. Unity je automaticky spojí dohromady. ║
║ ║
║ Hlavní herní stavy (AppState): ║
║ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ║
║ │ MainMenu │───▶│ Lobby │───▶│ Loading │───▶│ InGame │ ║
║ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ║
║ │ │ │ ║
║ │ │ ▼ ║
║ │ │ ┌─────────────┐ ║
║ │ │ │ Meeting │ ║
║ │ │ └─────────────┘ ║
║ │ │ │ ║
║ ▼ ▼ ▼ ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ GameEnded │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🌐 2. SÍŤOVÁ KOMUNIKACE ║
║ ═══════════════════════ ║
║ ║
║ Komunikace probíhá přes TCP socket s AES-256 šifrováním. ║
║ ║
║ Handshake proces: ║
║ 1. Klient posílá ClientHello s UUID a jménem ║
║ 2. Server odpovídá ServerHello s RSA veřejným klíčem ║
║ 3. Klient generuje AES klíč, zašifruje ho RSA a pošle KeyExchange ║
║ 4. Server potvrdí KeyExchangeAck - od teď je vše šifrované AES ║
║ ║
║ Formát zpráv: ║
║ ┌────────────┬──────────────────────────────────────────┐ ║
║ │ 4 bajty │ N bajtů │ ║
║ │ (délka) │ (JSON data, po handshake šifrovaná AES) │ ║
║ └────────────┴──────────────────────────────────────────┘ ║
║ ║
║ Důležité typy zpráv: ║
║ • CreateLobby/JoinLobby - vytvoření/připojení do lobby ║
║ • StartGame - spuštění hry (pouze owner) ║
║ • UpdatePosition - aktualizace pozice hráče ║
║ • KillAttempt - pokus o zabití (pouze impostor) ║
║ • ReportBody/CallEmergencyMeeting - svolání schůze ║
║ • CastVote - hlasování ║
║ • CompleteTask - dokončení úkolu (server validuje pozici) ║
║ • StartSabotage - spuštění sabotáže (pouze impostor) ║
║ • ActivateRepairStation - oprava sabotáže ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🎯 3. HERNÍ LOGIKA ║
║ ═════════════════ ║
║ ║
║ Role hráčů: ║
║ • Crew (posádka) - plní úkoly, hlasuje, reportuje těla ║
║ • Impostor - zabíjí, sabotuje, předstírá plnění úkolů ║
║ ║
║ Herní fáze (GamePhase): ║
║ • Lobby - čekání na hráče, nastavení hry ║
║ • Loading - načítání mapových dat z Overpass API ║
║ • Playing - hlavní herní fáze ║
║ • Meeting - diskuze před hlasováním ║
║ • Voting - hlasování o vyloučení ║
║ • Ended - konec hry ║
║ ║
║ Typy úkolů: ║
║ • Všechny úkoly jsou INSTANT - stačí přijít na místo a stisknout USE ║
║ • Server validuje pozici hráče (musí být do 5m od úkolu) ║
║ • Duchové (mrtví crew) mohou také plnit úkoly ║
║ ║
║ Sabotáže (SabotageType): ║
║ • CommsBlackout - blokuje reporty a emergency meetings, 1 opravná stanice ║
║ • CriticalMeltdown - časový limit, 2 stanice musí opravovat současně ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🖥️ 4. UŽIVATELSKÉ ROZHRANÍ ║
║ ═══════════════════════════ ║
║ ║
║ UI používá Unity IMGUI (OnGUI) pro jednoduchost a přenositelnost. ║
║ Pro produkční hru doporučujeme použít Unity UI (Canvas) nebo UI Toolkit. ║
║ ║
║ Škálování UI: ║
║ • Používáme relativní jednotky (procenta obrazovky) ║
║ • GUI.matrix pro globální škálování ║
║ • Responzivní layout pomocí GUILayout ║
║ ║
║ Hlavní obrazovky: ║
║ • MainMenu - připojení, vytvoření lobby, statistiky ║
║ • Lobby - seznam hráčů, nastavení, chat ║
║ • HUD - role, úkoly, minimap, sabotáže ║
║ • Meeting - hlasovací panel, timer ║
║ • GameEnd - výsledky, statistiky ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🗺️ 5. VYKRESLOVÁNÍ MAPY ║
║ ═══════════════════════ ║
║ ║
║ Mapa se vykresluje pomocí 3D GameObjects v prostoru Unity: ║
║ ║
║ Převod GPS → Unity souřadnice: ║
║ • Střed mapy (playAreaCenter) = Vector3(0, 0, 0) ║
║ • 1 metr reálně = 1 Unity jednotka ║
║ • Latitude → Z osa, Longitude → X osa ║
║ ║
║ Vrstvy mapy (od spodu): ║
║ 1. Podklad (zelená plocha) - Y = 0 ║
║ 2. Oblasti (parky, voda) - Y = 0.01 ║
║ 3. Cesty - Y = 0.02 ║
║ 4. Budovy - Y = 0 až výška budovy ║
║ 5. POI markery - Y = 0.5 ║
║ 6. Hráči - Y = 1 ║
║ 7. Markery (úkoly, těla) - Y = 0.3 ║
║ ║
║ Dynamické objekty: ║
║ • Hráči - capsule s barvou podle stavu ║
║ • Těla - ležící kapsle ║
║ • Úkoly - žluté diamanty ║
║ • Opravné stanice - červené/zelené krychle ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 📊 6. STATISTIKY HRÁČŮ ║
║ ═══════════════════════ ║
║ ║
║ Server poskytuje HTTP REST API na portu 8088: ║
║ ║
║ GET /stats/{playerId} - statistiky konkrétního hráče ║
║ GET /leaderboard - žebříček hráčů ║
║ GET /health - stav serveru ║
║ ║
║ Statistiky zahrnují: ║
║ • Počet her, výher, proher ║
║ • Zabití, smrti, K/D ratio ║
║ • Dokončené úkoly ║
║ • Win rate jako impostor/crew ║
║ • Přesnost hlasování ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 🚀 JAK ZAČÍT: ║
║ ═════════════ ║
║ ║
║ 1. Přidejte VŠECHNY soubory UnityTestClient_*.cs do Unity projektu ║
║ 2. Přidejte ClientSDK (Protocol.cs, Encryption.cs, EventDispatcher.cs, GameClient.cs) ║
║ 3. Vytvořte prázdný GameObject a přidejte komponentu UnityTestClient ║
║ 4. Nastavte v Inspectoru serverHost a serverPort ║
║ 5. Spusťte server (dotnet run v Server/) ║
║ 6. Spusťte hru v Unity ║
║ ║
║ Pro vlastní hru: ║
║ • Nahraďte IMGUI za Unity UI Canvas ║
║ • Přidejte vlastní 3D modely místo primitivních tvarů ║
║ • Implementujte GPS pohyb místo WASD (Input.location) ║
║ • Přidejte zvuky, particle efekty, animace ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ⚠️ DŮLEŽITÉ POZNÁMKY: ║
║ ═════════════════════ ║
║ ║
║ • GameClient.Update() MUSÍ být volán každý frame pro zpracování síťových událostí ║
║ • Pozice hráče se odesílá automaticky každých 100ms ║
║ • Server má anti-cheat - příliš rychlý pohyb způsobí varování/kick ║
║ • Všechny herní akce (zabití, hlasování) validuje server ║
║ • MapData může být null pokud Overpass API není dostupné ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*/
using UnityEngine;
using System;
using System.Collections.Generic;
using GeoSus.Client;
/// <summary>
/// Hlavní třída Unity testovacího klienta pro GeoSus.
/// Tato partial třída obsahuje inicializaci, herní smyčku a správu stavů.
///
/// POUŽITÍ:
/// 1. Přidejte tento skript na prázdný GameObject
/// 2. Nastavte serverHost a serverPort v Inspectoru
/// 3. Spusťte hru
/// </summary>
public partial class UnityTestClient : MonoBehaviour
{
#region
// KONFIGURACE SERVERU
// ════════════════════════════════════════════════════════════════════════
// Tyto hodnoty nastavte v Unity Inspectoru nebo zde přímo v kódu.
// ServerHost je IP adresa nebo hostname serveru.
// ServerPort je TCP port pro herní komunikaci (výchozí 7777).
// HttpPort je port pro REST API statistik (výchozí 8088).
#endregion
[Header("Nastavení serveru")]
[Tooltip("IP adresa nebo hostname GeoSus serveru")]
public string serverHost = "127.0.0.1";
[Tooltip("TCP port pro herní komunikaci")]
public int serverPort = 7777;
[Tooltip("HTTP port pro statistiky API (ignorováno pokud useHttps=true)")]
public int httpPort = 8088;
[Tooltip("Použít HTTPS pro Stats API (pro produkční server)")]
public bool useHttps = false;
#region
// HERNÍ STAVY
// ════════════════════════════════════════════════════════════════════════
// AppState určuje, která obrazovka se zobrazuje a jaká logika běží.
// Toto je KLIENTSKÝ stav, nezaměňovat s GamePhase ze serveru.
#endregion
/// <summary>
/// Stav aplikace určující aktuální obrazovku.
/// MainMenu -> Lobby -> Loading -> InGame -> GameEnded
/// </summary>
public enum AppState
{
/// <summary>Hlavní menu - připojení, vytvoření/vstup do lobby</summary>
MainMenu,
/// <summary>V lobby - čekání na hráče, nastavení hry</summary>
Lobby,
/// <summary>Načítání - stahování mapových dat</summary>
Loading,
/// <summary>Ve hře - hlavní gameplay</summary>
InGame,
/// <summary>Konec hry - zobrazení výsledků</summary>
GameEnded
}
#region
// HLAVNÍ PROMĚNNÉ
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Aktuální stav aplikace (která obrazovka je aktivní)
/// </summary>
[HideInInspector]
public AppState currentState = AppState.MainMenu;
/// <summary>
/// Instance herního klienta z ClientSDK.
/// Obsahuje veškerou síťovou logiku a herní data.
/// </summary>
protected GameClient client;
/// <summary>
/// Unikátní ID tohoto klienta (generuje se při startu)
/// </summary>
protected string clientUuid;
/// <summary>
/// Zobrazované jméno hráče
/// </summary>
protected string displayName = "Hráč";
/// <summary>
/// Fronta notifikací k zobrazení na obrazovce
/// </summary>
protected Queue<NotificationData> notifications = new Queue<NotificationData>();
/// <summary>
/// Aktuálně zobrazená notifikace (null = žádná)
/// </summary>
protected NotificationData currentNotification;
/// <summary>
/// Čas konce zobrazení aktuální notifikace
/// </summary>
protected float notificationEndTime;
/// <summary>
/// Chybová zpráva k zobrazení (null = žádná chyba)
/// </summary>
protected string errorMessage;
/// <summary>
/// Čas zmizení chybové zprávy
/// </summary>
protected float errorMessageEndTime;
/// <summary>
/// Hlavní herní kamera
/// </summary>
protected Camera playerCamera;
/// <summary>
/// Výška kamery nad mapou
/// </summary>
protected float cameraHeight = 100f;
#region
// DATOVÉ STRUKTURY
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Struktura pro notifikaci na obrazovce
/// </summary>
protected struct NotificationData
{
/// <summary>Text notifikace</summary>
public string message;
/// <summary>Barva pozadí</summary>
public Color color;
/// <summary>Doba zobrazení v sekundách</summary>
public float duration;
/// <summary>Ikona (emoji nebo text)</summary>
public string icon;
}
#region
// UNITY LIFECYCLE
// ════════════════════════════════════════════════════════════════════════
// Unity volá tyto metody automaticky:
// Awake() - při vytvoření objektu (před Start)
// Start() - před prvním Update
// Update() - každý frame
// OnDestroy() - při zničení objektu
// OnGUI() - pro vykreslení IMGUI (může být vícekrát za frame)
#endregion
/// <summary>
/// Inicializace při vytvoření objektu.
/// Generujeme unikátní UUID pro tohoto klienta.
/// </summary>
void Awake()
{
// Generujeme unikátní ID klienta
// V produkční hře byste toto ukládali do PlayerPrefs
clientUuid = Guid.NewGuid().ToString("N").Substring(0, 8);
// Načteme uložené jméno hráče (pokud existuje)
displayName = PlayerPrefs.GetString("PlayerName", "Hráč" + UnityEngine.Random.Range(1, 999));
Debug.Log($"[GeoSus] Klient inicializován s UUID: {clientUuid}");
}
/// <summary>
/// Inicializace před prvním frame.
/// </summary>
void Start()
{
// Nastavení kamery pro top-down pohled
SetupCamera();
Debug.Log("[GeoSus] Test klient připraven");
}
/// <summary>
/// Hlavní herní smyčka - volá se každý frame.
/// DŮLEŽITÉ: Zde musíme volat client.Update() pro zpracování síťových událostí!
/// </summary>
void Update()
{
// ═══════════════════════════════════════════════════════════════════
// KRITICKÉ: Zpracování síťových událostí
// ═══════════════════════════════════════════════════════════════════
// GameClient používá EventDispatcher pro thread-safe přenos událostí
// ze síťového vlákna do hlavního Unity vlákna.
// BEZ TOHOTO VOLÁNÍ nebudete dostávat žádné události ze serveru!
if (client != null)
{
client.Update();
}
// Zpracování vstupu podle aktuálního stavu
switch (currentState)
{
case AppState.InGame:
// Herní logika - pohyb, akce
HandlePlayerInput();
UpdateGameLogic();
break;
}
// Aktualizace notifikací
UpdateNotifications();
// Aktualizace herních objektů na mapě
if (currentState == AppState.InGame || currentState == AppState.Loading)
{
UpdateMapObjects();
}
// Automatické obnovování statistik
UpdateStatsAutoRefresh();
}
/// <summary>
/// Úklid při zničení objektu.
/// DŮLEŽITÉ: Vždy se odpojte od serveru při ukončení!
/// </summary>
void OnDestroy()
{
// Odpojení od serveru
if (client != null)
{
client.Disconnect("Aplikace ukončena");
client.Dispose();
client = null;
}
// Úklid mapových objektů
CleanupMapObjects();
Debug.Log("[GeoSus] Klient ukončen");
}
/// <summary>Příznak, zda jsou GUI styly inicializované</summary>
private bool stylesInitialized = false;
/// <summary>
/// Vykreslení IMGUI - volá se každý frame (může i vícekrát).
/// Veškeré UI se vykresluje zde.
/// </summary>
void OnGUI()
{
// Inicializace stylů při prvním volání OnGUI
if (!stylesInitialized)
{
InitializeUIStyles();
stylesInitialized = true;
}
// Aplikace škálování UI pro různé rozlišení
ApplyUIScaling();
// Vykreslení UI podle aktuálního stavu
switch (currentState)
{
case AppState.MainMenu:
DrawMainMenu();
break;
case AppState.Lobby:
DrawLobbyScreen();
break;
case AppState.Loading:
DrawLoadingScreen();
break;
case AppState.InGame:
DrawGameHUD();
DrawMeetingPanel(); // Zobrazí se jen pokud je meeting aktivní
break;
case AppState.GameEnded:
DrawGameEndScreen();
break;
}
// Overlay prvky (notifikace, chyby) - vždy navrchu
DrawNotifications();
DrawErrorMessage();
// Debug panel (pouze v editoru)
#if UNITY_EDITOR
DrawDebugPanel();
#endif
}
#region
// POMOCNÉ METODY
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Zobrazí notifikaci na obrazovce.
/// Notifikace se řadí do fronty a zobrazují postupně.
/// </summary>
/// <param name="message">Text zprávy</param>
/// <param name="color">Barva pozadí</param>
/// <param name="icon">Ikona/emoji (volitelné)</param>
protected void ShowNotification(string message, Color color, string icon)
{
ShowNotification(message, color, 3f, icon);
}
/// <summary>
/// Zobrazí notifikaci na obrazovce.
/// Notifikace se řadí do fronty a zobrazují postupně.
/// </summary>
/// <param name="message">Text zprávy</param>
/// <param name="color">Barva pozadí</param>
/// <param name="duration">Doba zobrazení v sekundách</param>
/// <param name="icon">Ikona/emoji (volitelné)</param>
protected void ShowNotification(string message, Color color, float duration = 3f, string icon = "")
{
notifications.Enqueue(new NotificationData
{
message = message,
color = color,
duration = duration,
icon = icon
});
}
/// <summary>
/// Aktualizace fronty notifikací
/// </summary>
private void UpdateNotifications()
{
// Pokud aktuální notifikace vypršela, zobrazíme další
if (currentNotification.message != null && Time.time > notificationEndTime)
{
currentNotification = default;
}
// Pokud není žádná notifikace a fronta není prázdná, zobrazíme další
if (currentNotification.message == null && notifications.Count > 0)
{
currentNotification = notifications.Dequeue();
notificationEndTime = Time.time + currentNotification.duration;
}
}
/// <summary>
/// Zobrazí chybovou zprávu
/// </summary>
/// <param name="message">Text chyby</param>
/// <param name="duration">Doba zobrazení</param>
protected void ShowError(string message, float duration = 5f)
{
errorMessage = message;
errorMessageEndTime = Time.time + duration;
Debug.LogError($"[GeoSus] {message}");
}
/// <summary>
/// Nastavení kamery pro top-down pohled
/// </summary>
private void SetupCamera()
{
// Najdeme nebo vytvoříme hlavní kameru
playerCamera = Camera.main;
if (playerCamera == null)
{
GameObject camObj = new GameObject("MainCamera");
playerCamera = camObj.AddComponent<Camera>();
camObj.tag = "MainCamera";
}
// Nastavení pro top-down pohled
playerCamera.orthographic = true;
playerCamera.orthographicSize = 50f; // Výchozí zoom (50 metrů od středu)
playerCamera.transform.position = new Vector3(0, cameraHeight, 0);
playerCamera.transform.rotation = Quaternion.Euler(90, 0, 0);
playerCamera.backgroundColor = new Color(0.2f, 0.3f, 0.2f); // Tmavě zelená
playerCamera.clearFlags = CameraClearFlags.SolidColor;
// Přidáme naši komponentu pro ovládání kamery
if (playerCamera.GetComponent<CameraController>() == null)
{
playerCamera.gameObject.AddComponent<CameraController>();
}
}
#region
// DEBUG PANEL
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Debug panel - zobrazuje interní stav klienta
/// Pouze v Unity Editoru
/// </summary>
private void DrawDebugPanel()
{
GUILayout.BeginArea(new Rect(10, Screen.height - 150, 300, 140));
GUI.Box(new Rect(0, 0, 300, 140), "");
GUILayout.Label($"<color=yellow><b>DEBUG</b></color>", richTextStyle);
GUILayout.Label($"State: {currentState}");
GUILayout.Label($"Connected: {client?.IsConnected ?? false}");
GUILayout.Label($"LobbyId: {client?.LobbyId ?? "null"}");
GUILayout.Label($"Phase: {client?.CurrentLobbyState?.Phase.ToString() ?? "null"}");
GUILayout.Label($"Role: {client?.MyRole?.ToString() ?? "null"}");
GUILayout.Label($"Ping: {client?.Ping ?? 0}ms");
GUILayout.EndArea();
}
}
/// <summary>
/// Jednoduchý kontroler kamery pro top-down pohled.
/// Umožňuje pohyb pomocí WASD/šipek a zoom pomocí kolečka myši.
///
/// POZNÁMKA: V produkční hře byste toto pravděpodobně spojili
/// s pohybem hráče - kamera sleduje hráče.
/// </summary>
public class CameraController : MonoBehaviour
{
[Header("Nastavení kamery")]
[Tooltip("Rychlost pohybu kamery")]
public float moveSpeed = 50f;
[Tooltip("Rychlost zoomu")]
public float zoomSpeed = 10f;
[Tooltip("Minimální zoom (orthographic size)")]
public float minZoom = 10f;
[Tooltip("Maximální zoom (orthographic size)")]
public float maxZoom = 200f;
private Camera cam;
void Start()
{
cam = GetComponent<Camera>();
}
void Update()
{
// Pohyb kamery - WASD nebo šipky
// POZNÁMKA: V plné hře se kamera obvykle pohybuje s hráčem
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(h, 0, v) * moveSpeed * Time.deltaTime;
transform.position += movement;
// Zoom - kolečko myši
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (scroll != 0 && cam.orthographic)
{
cam.orthographicSize = Mathf.Clamp(
cam.orthographicSize - scroll * zoomSpeed,
minZoom,
maxZoom
);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 719b1c3bbdc301646b2dbfa653d9a455

View File

@@ -0,0 +1,673 @@
/*
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ HERNÍ MECHANIKY - UnityTestClient_Game.cs ║
║ ║
║ Tento soubor obsahuje veškeré herní mechaniky: ║
║ • Pohyb hráče (WASD + myš) ║
║ • Interakce (úkoly, reporty, opravy) ║
║ • Kill mechanika (pro impostory) ║
║ • Sabotáže a jejich opravy ║
║ • Emergency meeting ║
║ • Hlasování ║
║ ║
║ OVLÁDÁNÍ: ║
║ • WASD - pohyb hráče ║
║ • Myš - otáčení kamery (volitelné) ║
║ • E - interakce (úkoly, report, oprava) ║
║ • Q - kill (pouze impostor) ║
║ • Tab - mapa/statistiky ║
║ • Escape - menu ║
║ ║
║ SYSTÉM ÚKOLŮ: ║
║ • Všechny úkoly jsou INSTANT - stačí přijít na místo (do 5m) a stisknout E ║
║ • Server validuje pozici hráče - nemůžete dokončit úkol na dálku ║
║ • Duchové (mrtví crew) mohou dokončovat úkoly - pomáhají týmu vyhrát ║
║ • Impostoři NEMOHOU dokončovat úkoly ║
║ ║
║ SABOTÁŽE: ║
║ • CommsBlackout - blokuje reporty a emergency meetings ║
║ - 1 opravná stanice, libovolný hráč opraví sám ║
║ • CriticalMeltdown - časový limit na opravu! ║
║ - 2 stanice musí být opravovány SOUČASNĚ dvěma hráči ║
║ - Pokud čas vyprší, impostoři vyhrávají ║
║ ║
║ POZNÁMKA: ║
║ V reálné mobilní hře by se pozice hráče aktualizovala podle GPS (Input.location). ║
║ Tento test klient umožňuje simulovat pohyb pomocí WASD pro testování. ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*/
using UnityEngine;
using System;
using System.Collections.Generic;
using GeoSus.Client;
// ═══════════════════════════════════════════════════════════════════════════════
// LOKÁLNÍ TYPY PRO UNITY (kopie z Protocol.cs pro kompatibilitu)
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Reprezentace úkolu hráče
/// </summary>
[System.Serializable]
public class PlayerTask
{
public string Id;
public string Name;
public string Description;
public TaskType Type;
public Position Location;
public bool IsCompleted;
}
/// <summary>
/// Opravná stanice pro sabotáže
/// </summary>
[System.Serializable]
public class RepairStation
{
public string Id;
public string Name;
public Position Position;
public bool IsActive;
}
public partial class UnityTestClient
{
#region
// GAME PROMĚNNÉ
// ════════════════════════════════════════════════════════════════════════
#endregion
// ─────────────────────────────────────────────────────────────────────────
// Pohyb
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Rychlost pohybu hráče (Unity jednotky/s)</summary>
protected float moveSpeed = 10f;
/// <summary>Aktuální pozice hráče v Unity souřadnicích</summary>
protected Vector3 currentPlayerPosition;
/// <summary>Interval odesílání pozice na server (sekundy)</summary>
protected float positionUpdateInterval = 0.5f;
/// <summary>Čas posledního odeslání pozice</summary>
protected float lastPositionUpdate;
// ─────────────────────────────────────────────────────────────────────────
// Opravy
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Právě opravujeme?</summary>
protected bool isRepairing = false;
/// <summary>ID aktivní opravné stanice</summary>
protected string activeRepairStation = null;
/// <summary>Progress opravy (0-1)</summary>
protected float repairProgress = 0f;
// ─────────────────────────────────────────────────────────────────────────
// Sabotáže
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Aktuální sabotáž (null = žádná)</summary>
protected SabotageStartedPayload currentSabotage = null;
// ─────────────────────────────────────────────────────────────────────────
// Kill
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Cooldown killu (sekundy)</summary>
protected float killCooldown = 25f;
/// <summary>Čas posledního killu</summary>
protected float lastKillTime = -100f;
// ─────────────────────────────────────────────────────────────────────────
// Konec hry
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Data o konci hry</summary>
protected GameEndedPayload gameEndData = null;
#region
// POHYB HRÁČE
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Zpracování vstupu hráče.
/// Volá se každý frame v Update().
///
/// POZNÁMKA PRO STUDENTY:
/// Input.GetAxis vrací hodnotu -1 až 1 pro plynulý pohyb.
/// "Horizontal" = A/D nebo šipky vlevo/vpravo
/// "Vertical" = W/S nebo šipky nahoru/dolů
/// </summary>
protected void HandlePlayerInput()
{
// Pohyb pouze během hraní
if (currentState != AppState.InGame) return;
// Kontrola, zda jsme naživu - bezpečný přístup
if (client?.PlayerPositions != null &&
client.PlayerPositions.TryGetValue(clientUuid, out var myInfo))
{
if (myInfo.State != PlayerState.Alive)
{
// Duch může létat, ale neposílá pozici
HandleGhostMovement();
return;
}
}
// Kontrola, zda není meeting - ale během arrival fáze se můžeme hýbat
var phase = client?.CurrentLobbyState?.Phase;
if (phase == GamePhase.Meeting)
{
// Během arrival fáze (před ArrivalDeadline) se můžeme hýbat
if (currentMeeting != null && DateTime.UtcNow < currentMeeting.ArrivalDeadline)
{
// OK - můžeme se hýbat k meeting pointu
}
else
{
return; // Po arrival deadline se nehýbeme
}
}
// ═══════════════════════════════════════════════════════════════════
// WASD POHYB
// ═══════════════════════════════════════════════════════════════════
float horizontal = Input.GetAxis("Horizontal"); // A/D
float vertical = Input.GetAxis("Vertical"); // W/S
if (horizontal != 0 || vertical != 0)
{
// Směr pohybu
Vector3 movement = new Vector3(horizontal, 0, vertical).normalized;
// Aplikace rychlosti a delta time
Vector3 newPosition = currentPlayerPosition + movement * moveSpeed * Time.deltaTime;
// Kontrola hranic herní oblasti
if (IsPositionInPlayArea(newPosition))
{
currentPlayerPosition = newPosition;
UpdateCameraPosition();
}
}
// ═══════════════════════════════════════════════════════════════════
// ODESÍLÁNÍ POZICE NA SERVER
// ═══════════════════════════════════════════════════════════════════
if (Time.time - lastPositionUpdate >= positionUpdateInterval)
{
SendPositionToServer();
lastPositionUpdate = Time.time;
}
// ═══════════════════════════════════════════════════════════════════
// KLÁVESOVÉ ZKRATKY PRO AKCE
// ═══════════════════════════════════════════════════════════════════
// E - Interakce (USE)
if (Input.GetKeyDown(KeyCode.E))
{
PerformPrimaryAction();
}
// Q - Kill (pouze impostor)
if (Input.GetKeyDown(KeyCode.Q) && client?.MyRole == PlayerRole.Impostor)
{
TryKillNearbyPlayer();
}
// R - Emergency meeting
if (Input.GetKeyDown(KeyCode.R))
{
CallEmergencyMeeting();
}
// Escape - Menu
if (Input.GetKeyDown(KeyCode.Escape))
{
// TODO: Toggle pause menu
}
}
/// <summary>
/// Pohyb ducha (mrtvý hráč)
/// </summary>
private void HandleGhostMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (horizontal != 0 || vertical != 0)
{
Vector3 movement = new Vector3(horizontal, 0, vertical).normalized;
currentPlayerPosition += movement * moveSpeed * 1.5f * Time.deltaTime; // Duch je rychlejší
UpdateCameraPosition();
}
}
/// <summary>
/// Kontrola, zda je pozice v hrací oblasti
/// </summary>
private bool IsPositionInPlayArea(Vector3 position)
{
// Vzdálenost od středu
float distance = Vector3.Distance(position, Vector3.zero);
return distance <= (float)mapRadius;
}
/// <summary>
/// Aktualizace pozice kamery podle hráče
/// </summary>
private void UpdateCameraPosition()
{
if (playerCamera == null) return;
// Top-down kamera následuje hráče
Vector3 cameraPos = currentPlayerPosition;
cameraPos.y = cameraHeight;
playerCamera.transform.position = cameraPos;
// Aktualizace lokálního hráče vizuálu
UpdateLocalPlayerVisual();
}
/// <summary>
/// Inicializace pozice hráče při startu hry
/// </summary>
protected void InitializePlayerPosition()
{
// Nastavíme hráče na střed mapy
currentPlayerPosition = Vector3.zero;
UpdateCameraPosition();
Debug.Log("[GeoSus] Pozice hráče inicializována na střed mapy");
}
/// <summary>
/// Odeslání pozice hráče na server
/// </summary>
private void SendPositionToServer()
{
if (client == null || !client.IsConnected) return;
// Převod na GPS
Position gpsPosition = UnityToGPS(currentPlayerPosition);
// Odeslání
client.UpdatePosition(gpsPosition);
}
#region
// INTERAKCE
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Provede primární akci (USE/REPORT/REPAIR)
/// </summary>
protected void PerformPrimaryAction()
{
if (client == null) return;
// Kontrola stavu hráče - bezpečný přístup
if (client.PlayerPositions != null && client.PlayerPositions.TryGetValue(clientUuid, out var myState))
{
if (myState.State != PlayerState.Alive)
{
ShowNotification("Jsi mrtvý!", Color.gray, 2f, "💀");
return;
}
}
// ═══════════════════════════════════════════════════════════════════
// PRIORITA 1: Report těla
// ═══════════════════════════════════════════════════════════════════
var body = client.FindNearbyBody(5.0);
if (body != null)
{
// Použijeme přímo BodyId property
ReportBody(body.BodyId);
return;
}
// ═══════════════════════════════════════════════════════════════════
// PRIORITA 2: Oprava stanice
// ═══════════════════════════════════════════════════════════════════
if (currentSabotage != null)
{
var station = FindNearbyRepairStation(5.0);
if (station != null)
{
if (!isRepairing)
{
StartRepair(station.StationId);
}
return;
}
}
// ═══════════════════════════════════════════════════════════════════
// PRIORITA 3: Úkol (pouze crew)
// ═══════════════════════════════════════════════════════════════════
if (client.MyRole == PlayerRole.Crew)
{
var task = client.FindNearbyTask(5.0);
if (task != null)
{
TryCompleteTask(task);
return;
}
}
ShowNotification("Nic v dosahu!", Color.yellow, "❓");
}
#region
// ÚKOLY
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Pokus o dokončení úkolu - pošle CompleteTask na server.
/// Server ověří pozici a označí jako dokončený.
/// </summary>
private void TryCompleteTask(object taskObj)
{
if (client == null) return;
// Dynamicky získáme vlastnosti úkolu přes reflexi
var taskType = taskObj.GetType();
string taskId = taskType.GetField("TaskId")?.GetValue(taskObj) as string ??
taskType.GetProperty("TaskId")?.GetValue(taskObj) as string ?? "unknown";
string taskName = taskType.GetField("Name")?.GetValue(taskObj) as string ??
taskType.GetProperty("Name")?.GetValue(taskObj) as string ?? "Úkol";
// Pošleme CompleteTask na server
client.CompleteTask(taskId);
ShowNotification($"Provádím: {taskName}...", Color.cyan, "📋");
}
#region
// REPORT TĚLA
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Nahlášení těla
/// </summary>
/// <param name="bodyId">ID těla</param>
protected void ReportBody(string bodyId)
{
if (client == null) return;
client.ReportBody(bodyId);
ShowNotification("Tělo nahlášeno!", Color.red, "🚨");
}
#region
// KILL
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Pokus o zabití blízkého hráče
/// </summary>
private void TryKillNearbyPlayer()
{
if (client?.MyRole != PlayerRole.Impostor) return;
// Kontrola cooldownu
if (Time.time - lastKillTime < killCooldown)
{
float remaining = killCooldown - (Time.time - lastKillTime);
ShowNotification($"Cooldown: {remaining:F0}s", Color.yellow, "⏳");
return;
}
// Najdi blízkého hráče
string targetId = client.FindNearbyPlayer(5.0, true);
if (targetId != null)
{
AttemptKill(targetId);
}
else
{
ShowNotification("Nikdo v dosahu!", Color.yellow, "🔪");
}
}
/// <summary>
/// Pokus o zabití konkrétního hráče
/// </summary>
/// <param name="targetId">UUID cíle</param>
protected void AttemptKill(string targetId)
{
if (client == null) return;
client.Kill(targetId);
lastKillTime = Time.time;
string targetName = GetPlayerName(targetId);
ShowNotification($"Útočíš na {targetName}!", Color.red, "🔪");
}
#region
// SABOTÁŽE
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Spuštění sabotáže
/// </summary>
/// <param name="type">Typ sabotáže</param>
protected void StartSabotage(SabotageType type)
{
if (client?.MyRole != PlayerRole.Impostor) return;
if (currentSabotage != null) return; // Již běží sabotáž
client.Send(new StartSabotage { SabotageType = type });
string sabName = type == SabotageType.CommsBlackout ? "Comms Blackout" : "Critical Meltdown";
ShowNotification($"Sabotáž: {sabName}!", new Color(1f, 0.5f, 0f), "⚠");
}
#region
// OPRAVY
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Oprava stanice - INSTANT
/// Server opraví stanici okamžitě při přijetí ActivateRepairStation.
/// Budoucí pokročilý klient může simulovat progress bar a poslat request až po uplynutí času.
/// </summary>
/// <param name="stationId">ID opravné stanice</param>
private void StartRepair(string stationId)
{
if (currentSabotage == null) return;
// Pošleme serveru - ten opraví INSTANT
client.Send(new ActivateRepairStation { StationId = stationId });
ShowNotification("Stanice opravena!", Color.green, "✅");
}
/// <summary>
/// Aktualizace průběhu opravy - NEPOUŽÍVÁ SE (oprava je instant)
/// Ponecháno pro budoucí implementaci s progress barem.
/// </summary>
protected void UpdateRepairProgress()
{
// Oprava je nyní instant - tato metoda není potřeba
// Budoucí klient může implementovat lokální progress bar:
// if (!isRepairing) return;
// repairProgress += Time.deltaTime / 3f; // 3 sekundy
// if (repairProgress >= 1f) {
// client.Send(new ActivateRepairStation { StationId = activeRepairStation });
// isRepairing = false;
// }
}
/// <summary>
/// Ukončení opravy (úspěšné) - NEPOUŽÍVÁ SE (oprava je instant)
/// </summary>
private void StopRepair()
{
// Oprava je instant - tato metoda není potřeba
}
/// <summary>
/// Zrušení opravy - NEPOUŽÍVÁ SE (oprava je instant)
/// </summary>
private void CancelRepair()
{
// Oprava je instant - tato metoda není potřeba
}
#region
// EMERGENCY MEETING
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Svolání emergency meetingu
/// </summary>
protected void CallEmergencyMeeting()
{
if (client == null) return;
// Kontrola sabotáže - nelze volat během comms blackout
if (currentSabotage?.Type == SabotageType.CommsBlackout)
{
ShowNotification("Komunikace odpojeny!", Color.red, "📡");
return;
}
client.CallEmergencyMeeting();
ShowNotification("Emergency Meeting!", Color.red, "🔔");
}
#region
// HLASOVÁNÍ
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Odeslání hlasu
/// </summary>
/// <param name="targetId">UUID hráče nebo null pro skip</param>
protected void CastVote(string targetId)
{
if (client == null)
{
Debug.LogError("[GeoSus] CastVote: client je null!");
return;
}
if (!canVote)
{
Debug.LogWarning("[GeoSus] CastVote: canVote je false!");
return;
}
string voteText = targetId == null ? "SKIP" : GetPlayerName(targetId);
Debug.Log($"[GeoSus] Hlasování: {voteText} (targetId: {targetId ?? "null"})");
client.Vote(targetId);
myVote = targetId ?? "skip";
canVote = false;
ShowNotification($"Hlasoval jsi: {voteText}", Color.cyan, 2f, "🗳");
}
#region
// POMOCNÉ METODY
// ════════════════════════════════════════════════════════════════════════
#endregion
/// <summary>
/// Získání jména hráče podle UUID
/// </summary>
protected string GetPlayerName(string playerId)
{
if (client?.CurrentLobbyState?.Players == null) return playerId;
foreach (var player in client.CurrentLobbyState.Players)
{
if (player.ClientUuid == playerId)
{
return player.DisplayName;
}
}
return playerId.Substring(0, 8) + "...";
}
/// <summary>
/// Inicializace herního stavu při startu hry
/// </summary>
protected void InitializeGameState()
{
// Reset pozice na střed
currentPlayerPosition = Vector3.zero;
// Reset úkolů
totalTasksCompleted = 0;
totalTasksRequired = client?.MyTasks?.Count ?? 0;
myCompletedTaskIds.Clear();
// Reset oprav
isRepairing = false;
activeRepairStation = null;
repairProgress = 0f;
// Reset kill cooldownu
lastKillTime = Time.time; // Cooldown na začátku
// Reset sabotáže
currentSabotage = null;
// Reset meetingu
currentMeeting = null;
meetingVotes.Clear();
myVote = null;
canVote = false;
// Reset konce hry
gameEndData = null;
}
/// <summary>
/// Game loop - aktualizace herní logiky.
/// Volá se v Update().
/// </summary>
protected void UpdateGameLogic()
{
if (currentState != AppState.InGame) return;
// Aktualizace opravy
UpdateRepairProgress();
// Aktualizace mapových objektů
UpdateMapObjects();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 829e2eaa53b86d347912031dba7711da

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2d9f27ae239808c44b27975df7eefd38

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cdc71cfbe967d754189f3382fee78c6c

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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8b7bb01435f46b842b79fe4836d1b44e

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ebca6f90ee582ea448b7a028626d8636