/* ╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ 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) // ═══════════════════════════════════════════════════════════════════════════════ /// /// Reprezentace úkolu hráče /// [System.Serializable] public class PlayerTask { public string Id; public string Name; public string Description; public TaskType Type; public Position Location; public bool IsCompleted; } /// /// Opravná stanice pro sabotáže /// [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 // ───────────────────────────────────────────────────────────────────────── /// Rychlost pohybu hráče (Unity jednotky/s) protected float moveSpeed = 10f; /// Aktuální pozice hráče v Unity souřadnicích protected Vector3 currentPlayerPosition; /// Interval odesílání pozice na server (sekundy) protected float positionUpdateInterval = 0.5f; /// Čas posledního odeslání pozice protected float lastPositionUpdate; // ───────────────────────────────────────────────────────────────────────── // Opravy // ───────────────────────────────────────────────────────────────────────── /// Právě opravujeme? protected bool isRepairing = false; /// ID aktivní opravné stanice protected string activeRepairStation = null; /// Progress opravy (0-1) protected float repairProgress = 0f; // ───────────────────────────────────────────────────────────────────────── // Sabotáže // ───────────────────────────────────────────────────────────────────────── /// Aktuální sabotáž (null = žádná) protected SabotageStartedPayload currentSabotage = null; // ───────────────────────────────────────────────────────────────────────── // Kill // ───────────────────────────────────────────────────────────────────────── /// Cooldown killu (sekundy) protected float killCooldown = 25f; /// Čas posledního killu protected float lastKillTime = -100f; // ───────────────────────────────────────────────────────────────────────── // Konec hry // ───────────────────────────────────────────────────────────────────────── /// Data o konci hry protected GameEndedPayload gameEndData = null; #region ═══════════════════════════════════════════════════════════════════ // POHYB HRÁČE // ════════════════════════════════════════════════════════════════════════ #endregion /// /// 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ů /// 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 } } /// /// Pohyb ducha (mrtvý hráč) /// 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(); } } /// /// Kontrola, zda je pozice v hrací oblasti /// private bool IsPositionInPlayArea(Vector3 position) { // Vzdálenost od středu float distance = Vector3.Distance(position, Vector3.zero); return distance <= (float)mapRadius; } /// /// Aktualizace pozice kamery podle hráče /// 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(); } /// /// Inicializace pozice hráče při startu hry /// 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"); } /// /// Odeslání pozice hráče na server /// 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 /// /// Provede primární akci (USE/REPORT/REPAIR) /// 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 /// /// Pokus o dokončení úkolu - pošle CompleteTask na server. /// Server ověří pozici a označí jako dokončený. /// 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 /// /// Nahlášení těla /// /// ID těla protected void ReportBody(string bodyId) { if (client == null) return; client.ReportBody(bodyId); ShowNotification("Tělo nahlášeno!", Color.red, "🚨"); } #region ═══════════════════════════════════════════════════════════════════ // KILL // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Pokus o zabití blízkého hráče /// 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, "🔪"); } } /// /// Pokus o zabití konkrétního hráče /// /// UUID cíle 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 /// /// Spuštění sabotáže /// /// Typ sabotáže 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 /// /// 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. /// /// ID opravné stanice 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, "✅"); } /// /// Aktualizace průběhu opravy - NEPOUŽÍVÁ SE (oprava je instant) /// Ponecháno pro budoucí implementaci s progress barem. /// 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; // } } /// /// Ukončení opravy (úspěšné) - NEPOUŽÍVÁ SE (oprava je instant) /// private void StopRepair() { // Oprava je instant - tato metoda není potřeba } /// /// Zrušení opravy - NEPOUŽÍVÁ SE (oprava je instant) /// private void CancelRepair() { // Oprava je instant - tato metoda není potřeba } #region ═══════════════════════════════════════════════════════════════════ // EMERGENCY MEETING // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Svolání emergency meetingu /// 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 /// /// Odeslání hlasu /// /// UUID hráče nebo null pro skip 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 /// /// Získání jména hráče podle UUID /// 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) + "..."; } /// /// Inicializace herního stavu při startu hry /// 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; } /// /// Game loop - aktualizace herní logiky. /// Volá se v Update(). /// protected void UpdateGameLogic() { if (currentState != AppState.InGame) return; // Aktualizace opravy UpdateRepairProgress(); // Aktualizace mapových objektů UpdateMapObjects(); } }