Files
TestClient/Assets/UnityTestClient/UnityTestClient_Game.cs
2026-01-27 21:36:29 +01:00

674 lines
31 KiB
C#

/*
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ 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();
}
}