/* ╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ VYKRESLOVÁNÍ MAPY - UnityTestClient_Map.cs ║ ║ ║ ║ Tento soubor obsahuje veškeré vykreslování 3D mapy pomocí GameObjectů: ║ ║ • Budovy (Buildings) - 3D kvádry s výškou ║ ║ • Cesty (Pathways) - LineRenderer nebo extrudované polygony ║ ║ • Oblasti (Areas) - Ploché 3D objekty (parky, voda) ║ ║ • POI (Points of Interest) - Markery s ikonami ║ ║ • Hráči - Capsule s barvou podle role/stavu ║ ║ • Těla - Ležící capsule (po zabití) ║ ║ • Úkoly - Žluté diamanty (markery) ║ ║ • Opravné stanice - Oranžové věže (při sabotáži) ║ ║ ║ ║ GPS → UNITY PŘEVOD: ║ ║ 1. Střed mapy (PlayAreaCenter) = Vector3(0, 0, 0) ║ ║ 2. 1 metr GPS ≈ 1 Unity jednotka ║ ║ 3. Latitude → Z osa (sever = +Z) ║ ║ 4. Longitude → X osa (východ = +X) ║ ║ 5. Haversine formula pro přesný výpočet vzdáleností ║ ║ ║ ║ VRSTVY (Y osa): ║ ║ 0.00 - Zelený podklad ║ ║ 0.01 - Oblasti (parky, voda) ║ ║ 0.02 - Cesty ║ ║ 0.XX - Budovy (výška podle dat) ║ ║ 0.30 - Markery (úkoly, těla) ║ ║ 0.50 - POI ║ ║ 1.00 - Hráči ║ ║ 5.00+ - Opravné stanice (věže) ║ ║ ║ ║ MAPOVÁ DATA: ║ ║ Server stahuje data z OpenStreetMap (Overpass API) a posílá je klientovi. ║ ║ Pokud Overpass API není dostupné, mapa se vykreslí bez detailů. ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ */ using UnityEngine; using System; using System.Collections.Generic; using System.Linq; using GeoSus.Client; public partial class UnityTestClient { #region ═══════════════════════════════════════════════════════════════════ // MAP PROMĚNNÉ // ════════════════════════════════════════════════════════════════════════ #endregion // ───────────────────────────────────────────────────────────────────────── // Kontejnery pro mapové objekty // ───────────────────────────────────────────────────────────────────────── /// Rodičovský objekt pro celou mapu protected GameObject mapContainer; /// Kontejner pro budovy protected GameObject buildingsContainer; /// Kontejner pro cesty protected GameObject pathwaysContainer; /// Kontejner pro oblasti protected GameObject areasContainer; /// Kontejner pro POI protected GameObject poisContainer; /// Kontejner pro hráče protected GameObject playersContainer; /// Kontejner pro těla protected GameObject bodiesContainer; /// Kontejner pro úkoly protected GameObject tasksContainer; /// Kontejner pro opravné stanice protected GameObject repairStationsContainer; /// Emergency button marker (střed mapy) protected GameObject emergencyButtonMarker; /// Meeting lokace marker (během meetingu) protected GameObject meetingLocationMarker; // ───────────────────────────────────────────────────────────────────────── // Mapování objektů na ID // ───────────────────────────────────────────────────────────────────────── /// Mapování ID hráče na GameObject protected Dictionary playerObjects = new Dictionary(); /// Mapování ID těla na GameObject protected Dictionary bodyObjects = new Dictionary(); /// Mapování ID úkolu na GameObject protected Dictionary taskObjects = new Dictionary(); /// Mapování ID opravné stanice na GameObject protected Dictionary repairStationObjects = new Dictionary(); // ───────────────────────────────────────────────────────────────────────── // GPS referenční bod // ───────────────────────────────────────────────────────────────────────── /// Střed herní oblasti (GPS) protected Position? mapCenter; /// Poloměr herní oblasti (metry) protected double mapRadius; // ───────────────────────────────────────────────────────────────────────── // Materiály // ───────────────────────────────────────────────────────────────────────── /// Materiál pro budovy protected Material buildingMaterial; /// Materiál pro cesty protected Material pathwayMaterial; /// Materiál pro oblasti protected Material areaMaterial; /// Materiál pro POI protected Material poiMaterial; /// Materiál pro hráče protected Material playerMaterial; /// Materiál pro těla protected Material bodyMaterial; /// Materiál pro úkoly protected Material taskMaterial; /// Materiál pro opravné stanice protected Material repairMaterial; #region ═══════════════════════════════════════════════════════════════════ // INICIALIZACE MAPY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření kontejnerů a materiálů pro mapu. /// Volá se při startu hry. /// protected void InitializeMapSystem() { // Vytvoření hlavního kontejneru if (mapContainer == null) { mapContainer = new GameObject("MapContainer"); } // Vytvoření sub-kontejnerů pro lepší organizaci buildingsContainer = CreateContainer("Buildings"); pathwaysContainer = CreateContainer("Pathways"); areasContainer = CreateContainer("Areas"); poisContainer = CreateContainer("POIs"); playersContainer = CreateContainer("Players"); bodiesContainer = CreateContainer("Bodies"); tasksContainer = CreateContainer("Tasks"); repairStationsContainer = CreateContainer("RepairStations"); // Inicializace materiálů InitializeMaterials(); // Vytvoření podlahy herní oblasti CreateGroundPlane(); } /// /// Pomocná metoda pro vytvoření kontejneru /// private GameObject CreateContainer(string name) { var container = new GameObject(name); container.transform.SetParent(mapContainer.transform); return container; } /// /// Inicializace materiálů pro různé typy objektů. /// BUILD-SAFE: Všechny materiály jsou odvozeny z primitiv. /// private void InitializeMaterials() { // Budovy - různé barvy podle typu buildingMaterial = CreateUnlitMaterial(new Color(0.7f, 0.65f, 0.6f)); // Světle šedá/béžová // Cesty - tmavší šedá pathwayMaterial = CreateUnlitMaterial(new Color(0.5f, 0.5f, 0.5f)); // Oblasti (parky) - zelená s průhledností areaMaterial = CreateTransparentMaterial(new Color(0.3f, 0.7f, 0.3f, 0.4f)); // POI - jasně žlutá poiMaterial = CreateUnlitMaterial(Color.yellow); // Hráči - modrá playerMaterial = CreateUnlitMaterial(new Color(0.2f, 0.5f, 1f)); // Těla - červená bodyMaterial = CreateUnlitMaterial(new Color(0.8f, 0.2f, 0.2f)); // Úkoly - cyan/tyrkysová taskMaterial = CreateUnlitMaterial(new Color(0f, 0.9f, 0.9f)); // Opravné stanice - oranžová repairMaterial = CreateUnlitMaterial(new Color(1f, 0.6f, 0.1f)); } /// /// Vytvoří průhledný materiál /// BUILD-SAFE: Používá materiál z primitivu jako základ /// private Material CreateTransparentMaterial(Color color) { var mat = CreateUnlitMaterial(color); // Nastavení průhlednosti mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.renderQueue = 3000; // Pro URP if (mat.HasProperty("_Surface")) { mat.SetFloat("_Surface", 1); // 1 = Transparent } return mat; } /// /// Nastaví materiál jako průhledný /// private void SetMaterialTransparent(Material mat) { mat.SetFloat("_Mode", 3); // Transparent mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); mat.renderQueue = 3000; } /// /// Nastaví materiál jako emisivní (svítící) /// private void SetMaterialEmissive(Material mat, Color emissionColor) { mat.EnableKeyword("_EMISSION"); mat.SetColor("_EmissionColor", emissionColor); } /// /// Vytvoření podlahy herní oblasti /// private void CreateGroundPlane() { var ground = GameObject.CreatePrimitive(PrimitiveType.Plane); ground.name = "Ground"; ground.transform.SetParent(mapContainer.transform); ground.transform.localPosition = Vector3.zero; // Škálování podle poloměru (Plane má základní velikost 10x10) float scale = (float)mapRadius * 0.3f; // Mírně větší než poloměr ground.transform.localScale = new Vector3(scale, 1, scale); // Materiál podlahy - použijeme Unlit shader pro konzistentní barvu var groundMat = CreateUnlitMaterial(new Color(0.25f, 0.45f, 0.2f)); // Tráva ground.GetComponent().material = groundMat; // Vytvoříme emergency button marker uprostřed mapy CreateEmergencyButtonMarker(); } /// /// Vytvoří výrazný marker pro emergency button (střed mapy) /// private void CreateEmergencyButtonMarker() { emergencyButtonMarker = new GameObject("EmergencyButton"); emergencyButtonMarker.transform.SetParent(mapContainer.transform); emergencyButtonMarker.transform.localPosition = new Vector3(0, 0.1f, 0); // Střed mapy // Velký červený kruh na zemi var platform = GameObject.CreatePrimitive(PrimitiveType.Cylinder); platform.name = "Platform"; platform.transform.SetParent(emergencyButtonMarker.transform); platform.transform.localPosition = Vector3.zero; platform.transform.localScale = new Vector3(8f, 0.2f, 8f); var platformMat = CreateEmissiveMaterial(new Color(0.8f, 0.2f, 0.2f), 1.5f); platform.GetComponent().material = platformMat; // Tlačítko uprostřed var button = GameObject.CreatePrimitive(PrimitiveType.Cylinder); button.name = "Button"; button.transform.SetParent(emergencyButtonMarker.transform); button.transform.localPosition = new Vector3(0, 0.5f, 0); button.transform.localScale = new Vector3(3f, 0.5f, 3f); var buttonMat = CreateEmissiveMaterial(Color.red, 3f); button.GetComponent().material = buttonMat; // Světelná koule nahoře var light = GameObject.CreatePrimitive(PrimitiveType.Sphere); light.name = "Light"; light.transform.SetParent(emergencyButtonMarker.transform); light.transform.localPosition = new Vector3(0, 3f, 0); light.transform.localScale = new Vector3(2f, 2f, 2f); light.GetComponent().material = buttonMat; } /// /// Vytvoří nebo aktualizuje marker pro meeting lokaci /// protected void UpdateMeetingLocationMarker(Position? location, bool isActive) { if (!isActive || location == null) { if (meetingLocationMarker != null) { Destroy(meetingLocationMarker); meetingLocationMarker = null; } return; } if (meetingLocationMarker == null) { meetingLocationMarker = new GameObject("MeetingLocation"); meetingLocationMarker.transform.SetParent(mapContainer.transform); // Velký pulsující kruh var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ring.name = "Ring"; ring.transform.SetParent(meetingLocationMarker.transform); ring.transform.localPosition = new Vector3(0, 0.1f, 0); ring.transform.localScale = new Vector3(15f, 0.1f, 15f); var ringMat = CreateEmissiveMaterial(new Color(1f, 1f, 0f, 0.6f), 2f); ring.GetComponent().material = ringMat; // Šipka dolů var arrow = GameObject.CreatePrimitive(PrimitiveType.Cube); arrow.name = "Arrow"; arrow.transform.SetParent(meetingLocationMarker.transform); arrow.transform.localPosition = new Vector3(0, 10f, 0); arrow.transform.localScale = new Vector3(2f, 8f, 0.5f); var arrowMat = CreateEmissiveMaterial(Color.yellow, 4f); arrow.GetComponent().material = arrowMat; } Vector3 pos = GPSToUnity(location.Value); meetingLocationMarker.transform.position = pos; } /// /// Cache pro základní materiál - získaný z primitivu při prvním volání /// private static Material _cachedBaseMaterial = null; /// /// Vytvoří materiál pro konzistentní barvu /// BUILD-SAFE: Používá Sprites/Default shader který je vždy zahrnut /// private Material CreateUnlitMaterial(Color color) { // Sprites/Default shader je VŽDY zahrnut v Unity buildech var shader = Shader.Find("Sprites/Default"); if (shader == null) { // Absolutní fallback - nemělo by nikdy nastat Debug.LogError("[GeoSus] Sprites/Default shader nenalezen!"); var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube); var mat = new Material(tempCube.GetComponent().sharedMaterial); DestroyImmediate(tempCube); mat.color = color; return mat; } var material = new Material(shader); material.color = color; return material; } #region ═══════════════════════════════════════════════════════════════════ // GPS KONVERZE // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Konverze GPS souřadnic na Unity pozici. /// Střed mapy je na (0, 0, 0). /// /// MATEMATIKA: /// - Latitude (zeměpisná šířka) → Z osa /// - Longitude (zeměpisná délka) → X osa /// - Používáme Haversine formuli pro přesnou vzdálenost /// /// GPS souřadnice /// Unity pozice ve 3D prostoru protected Vector3 GPSToUnity(Position position) { if (!mapCenter.HasValue) return Vector3.zero; // Výpočet vzdálenosti a směru od středu // Zjednodušená verze - pro malé vzdálenosti je dost přesná double latDiff = position.Lat - mapCenter.Value.Lat; double lonDiff = position.Lon - mapCenter.Value.Lon; // Převod stupňů na metry // 1 stupeň latitude ≈ 111320 metrů // 1 stupeň longitude závisí na latitude: 111320 * cos(latitude) double metersPerDegreeLat = 111320.0; double metersPerDegreeLon = 111320.0 * Math.Cos(mapCenter.Value.Lat * Math.PI / 180.0); float x = (float)(lonDiff * metersPerDegreeLon); float z = (float)(latDiff * metersPerDegreeLat); return new Vector3(x, 0, z); } /// /// Konverze Unity pozice zpět na GPS souřadnice. /// Inverzní funkce k GPSToUnity. /// /// Unity pozice /// GPS souřadnice protected Position UnityToGPS(Vector3 unityPos) { if (!mapCenter.HasValue) return new Position(0, 0); double metersPerDegreeLat = 111320.0; double metersPerDegreeLon = 111320.0 * Math.Cos(mapCenter.Value.Lat * Math.PI / 180.0); double latDiff = unityPos.z / metersPerDegreeLat; double lonDiff = unityPos.x / metersPerDegreeLon; return new Position(mapCenter.Value.Lat + latDiff, mapCenter.Value.Lon + lonDiff); } #region ═══════════════════════════════════════════════════════════════════ // BUDOVY (BUILDINGS) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Získá barvu budovy podle typu /// private Color GetBuildingColor(string buildingType) { if (string.IsNullOrEmpty(buildingType)) return new Color(0.7f, 0.65f, 0.6f); switch (buildingType.ToLower()) { case "residential": case "apartments": case "house": return new Color(0.85f, 0.75f, 0.65f); // Teplá béžová case "commercial": case "retail": case "shop": return new Color(0.7f, 0.7f, 0.85f); // Světle modrá case "industrial": case "warehouse": return new Color(0.6f, 0.55f, 0.5f); // Tmavě šedá case "school": case "university": return new Color(0.9f, 0.85f, 0.7f); // Světle žlutá case "hospital": case "clinic": return new Color(0.9f, 0.9f, 0.95f); // Bílá case "church": case "cathedral": return new Color(0.8f, 0.75f, 0.85f); // Světle fialová case "garage": case "garages": return new Color(0.5f, 0.5f, 0.5f); // Šedá default: return new Color(0.75f, 0.7f, 0.65f); // Výchozí béžová } } /// /// Vytvoření budovy z mapových dat. /// Budova je vykreslena jako 3D kvádr s výškou. /// /// Data budovy z mapy protected void CreateBuildingObject(MapBuilding building) { // Vytvoření GameObjectu var buildingObj = new GameObject($"Building_{building.Name ?? "Unknown"}"); buildingObj.transform.SetParent(buildingsContainer.transform); // Výpočet středu budovy Vector3 center = CalculatePolygonCenter(building.Outline); buildingObj.transform.position = center; // Vytvoření mesh pro budovu MeshFilter meshFilter = buildingObj.AddComponent(); MeshRenderer meshRenderer = buildingObj.AddComponent(); // Generování mesh z polygonu (výška implicitně 10m) Mesh mesh = CreateExtrudedPolygonMesh(building.Outline, 10f); meshFilter.mesh = mesh; // Použijeme barvu podle typu budovy Color buildingColor = GetBuildingColor(building.BuildingType); meshRenderer.material = CreateUnlitMaterial(buildingColor); // Přidání collideru pro interakci buildingObj.AddComponent(); } /// /// Výpočet středu polygonu /// private Vector3 CalculatePolygonCenter(List points) { Vector3 center = Vector3.zero; foreach (var point in points) { center += GPSToUnity(point); } return center / points.Count; } /// /// Vytvoření 3D mesh z polygonu vytažením do výšky. /// /// ALGORITMUS: /// 1. Triangulace spodní podstavy (ear clipping) /// 2. Kopie vrcholů nahoru pro horní podstavu /// 3. Vytvoření bočních stěn spojením okrajů /// private Mesh CreateExtrudedPolygonMesh(List outline, float height) { Mesh mesh = new Mesh(); int vertexCount = outline.Count; // Vertices - spodní a horní podstava Vector3[] vertices = new Vector3[vertexCount * 2]; Vector3 center = CalculatePolygonCenter(outline); for (int i = 0; i < vertexCount; i++) { Vector3 pos = GPSToUnity(outline[i]) - center; vertices[i] = pos; // Spodní vertices[i + vertexCount] = pos + Vector3.up * height; // Horní } // Triangles - jen boční stěny pro jednoduchost List triangles = new List(); for (int i = 0; i < vertexCount; i++) { int next = (i + 1) % vertexCount; // Boční stěna - dva trojúhelníky triangles.Add(i); triangles.Add(i + vertexCount); triangles.Add(next); triangles.Add(next); triangles.Add(i + vertexCount); triangles.Add(next + vertexCount); } // Horní podstava - zjednodušená triangulace (fan) if (vertexCount >= 3) { for (int i = 1; i < vertexCount - 1; i++) { triangles.Add(vertexCount); // Střed (první bod horní) triangles.Add(vertexCount + i); triangles.Add(vertexCount + i + 1); } } mesh.vertices = vertices; mesh.triangles = triangles.ToArray(); mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } #region ═══════════════════════════════════════════════════════════════════ // CESTY (PATHWAYS) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření cesty z mapových dat. /// Cesta je vykreslena jako LineRenderer nebo plochý polygon. /// /// Data cesty protected void CreatePathwayObject(MapPathway pathway) { var pathObj = new GameObject($"Path_{pathway.Name ?? "Unknown"}"); pathObj.transform.SetParent(pathwaysContainer.transform); // Použijeme LineRenderer pro jednoduchost LineRenderer line = pathObj.AddComponent(); line.material = pathwayMaterial; line.widthMultiplier = GetPathWidth(pathway.PathType); // Nastavení bodů cesty line.positionCount = pathway.Points.Count; for (int i = 0; i < pathway.Points.Count; i++) { Vector3 pos = GPSToUnity(pathway.Points[i]); pos.y = 0.1f; // Mírně nad zemí line.SetPosition(i, pos); } } /// /// Získání šířky cesty podle typu /// private float GetPathWidth(PathType type) { switch (type) { case PathType.Road: return 5f; case PathType.Pedestrian: return 2f; // Sidewalk case PathType.Path: return 1f; // Trail case PathType.Footway: return 1.5f; default: return 2f; } } #region ═══════════════════════════════════════════════════════════════════ // OBLASTI (AREAS) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření oblasti z mapových dat. /// Oblast je vykreslena jako plochý 3D polygon. /// /// Data oblasti protected void CreateAreaObject(MapArea area) { var areaObj = new GameObject($"Area_{area.Name ?? "Unknown"}"); areaObj.transform.SetParent(areasContainer.transform); MeshFilter meshFilter = areaObj.AddComponent(); MeshRenderer meshRenderer = areaObj.AddComponent(); // Vytvoření plochého mesh Mesh mesh = CreateFlatPolygonMesh(area.Outline); meshFilter.mesh = mesh; // Materiál podle typu Material mat = new Material(areaMaterial); mat.color = GetAreaColor(area.AreaType); meshRenderer.material = mat; areaObj.transform.position = new Vector3(0, 0.05f, 0); // Těsně nad zemí } /// /// Vytvoření plochého mesh z polygonu /// private Mesh CreateFlatPolygonMesh(List outline) { Mesh mesh = new Mesh(); int vertexCount = outline.Count; Vector3[] vertices = new Vector3[vertexCount]; Vector3 center = CalculatePolygonCenter(outline); for (int i = 0; i < vertexCount; i++) { vertices[i] = GPSToUnity(outline[i]) - center; } // Triangulace - fan pattern List triangles = new List(); if (vertexCount >= 3) { for (int i = 1; i < vertexCount - 1; i++) { triangles.Add(0); triangles.Add(i); triangles.Add(i + 1); } } mesh.vertices = vertices; mesh.triangles = triangles.ToArray(); mesh.RecalculateNormals(); return mesh; } /// /// Získání barvy oblasti podle typu /// private Color GetAreaColor(MapAreaType type) { switch (type) { case MapAreaType.Park: return new Color(0.2f, 0.8f, 0.2f, 0.5f); case MapAreaType.Garden: return new Color(0.3f, 0.7f, 0.3f, 0.5f); case MapAreaType.Water: return new Color(0.2f, 0.5f, 0.9f, 0.7f); case MapAreaType.Forest: return new Color(0.1f, 0.5f, 0.1f, 0.5f); case MapAreaType.Grass: return new Color(0.4f, 0.8f, 0.4f, 0.5f); case MapAreaType.Playground: return new Color(0.6f, 0.5f, 0.4f, 0.5f); default: return new Color(0.5f, 0.5f, 0.5f, 0.5f); } } #region ═══════════════════════════════════════════════════════════════════ // POI (POINTS OF INTEREST) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření POI markeru - VÝRAZNÝ marker s ikonou a popisem. /// POI jsou body zájmu pro orientaci na mapě. /// /// Data POI protected void CreatePOIObject(MapPOI poi) { var poiObj = new GameObject($"POI_{poi.Name ?? "Unknown"}"); poiObj.transform.SetParent(poisContainer.transform); Vector3 pos = GPSToUnity(poi.Location); pos.y = 0.5f; poiObj.transform.position = pos; // Vytvoření vizuálu - větší a viditelnější var visual = GameObject.CreatePrimitive(PrimitiveType.Cylinder); visual.transform.SetParent(poiObj.transform); visual.transform.localPosition = new Vector3(0, 0.5f, 0); visual.transform.localScale = new Vector3(1.2f, 0.5f, 1.2f); // Barva podle typu - svítící materiál Color poiColor = GetPOIColor(poi.POIType); Material mat = CreateEmissiveMaterial(poiColor, 1.5f); visual.GetComponent().material = mat; // Ikona/symbol nahoře - malá koule var icon = GameObject.CreatePrimitive(PrimitiveType.Sphere); icon.transform.SetParent(poiObj.transform); icon.transform.localPosition = new Vector3(0, 1.3f, 0); icon.transform.localScale = new Vector3(0.8f, 0.8f, 0.8f); icon.GetComponent().material = mat; } /// /// Získání barvy POI podle typu /// private Color GetPOIColor(MapPOIType type) { switch (type) { case MapPOIType.Shop: return Color.magenta; case MapPOIType.FoodDrink: return new Color(1f, 0.5f, 0f); case MapPOIType.Transport: return Color.blue; case MapPOIType.Landmark: return Color.yellow; case MapPOIType.Health: return Color.green; case MapPOIType.Culture: return Color.cyan; case MapPOIType.Recreation: return new Color(0.5f, 1f, 0.5f); default: return Color.white; } } #region ═══════════════════════════════════════════════════════════════════ // HRÁČI // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Barvy pro hráče - každý hráč má unikátní barvu /// private static readonly Color[] PlayerColors = new Color[] { Color.red, Color.blue, Color.green, Color.yellow, Color.cyan, Color.magenta, new Color(1f, 0.5f, 0f), // Orange new Color(0.5f, 0f, 1f), // Purple new Color(0f, 1f, 0.5f), // Mint new Color(1f, 0.75f, 0.8f), // Pink }; /// /// Vytvoření nebo aktualizace objektu hráče - VÝRAZNÝ marker. /// Hráč je vykreslován jako Capsule s barvou a jménem. /// /// UUID hráče /// GPS pozice /// Stav hráče protected void UpdatePlayerObject(string playerId, Position position, PlayerState state) { // Skip pro lokálního hráče - ten je vykreslen jinak if (playerId == clientUuid) return; GameObject playerObj; if (!playerObjects.TryGetValue(playerId, out playerObj)) { // Kontejner pro hráče playerObj = new GameObject($"Player_{GetPlayerName(playerId)}"); playerObj.transform.SetParent(playersContainer.transform); // Tělo hráče - větší capsule var body = GameObject.CreatePrimitive(PrimitiveType.Capsule); body.name = "Body"; body.transform.SetParent(playerObj.transform); body.transform.localPosition = new Vector3(0, 2f, 0); body.transform.localScale = new Vector3(2f, 2.5f, 2f); // Přidělení výrazné barvy int colorIndex = playerObjects.Count % PlayerColors.Length; var mat = CreateEmissiveMaterial(PlayerColors[colorIndex], 0.5f); body.GetComponent().material = mat; // Kroužek pod hráčem var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ring.name = "Ring"; ring.transform.SetParent(playerObj.transform); ring.transform.localPosition = new Vector3(0, 0.1f, 0); ring.transform.localScale = new Vector3(4f, 0.1f, 4f); var ringMat = CreateEmissiveMaterial(PlayerColors[colorIndex] * 0.7f, 0.3f); ring.GetComponent().material = ringMat; playerObjects[playerId] = playerObj; } // Aktualizace pozice Vector3 unityPos = GPSToUnity(position); playerObj.transform.position = unityPos; // Viditelnost podle stavu playerObj.SetActive(state == PlayerState.Alive); } /// /// Aktualizace lokálního hráče (kurzor/avatar) - VELMI VÝRAZNÝ /// protected void UpdateLocalPlayerVisual() { // Vytvoření lokálního hráče pokud neexistuje if (localPlayerObject == null) { // Hlavní objekt hráče localPlayerObject = new GameObject("LocalPlayer"); localPlayerObject.transform.SetParent(playersContainer.transform); // Tělo hráče - VELKÁ jasně zelená capsule var body = GameObject.CreatePrimitive(PrimitiveType.Capsule); body.name = "Body"; body.transform.SetParent(localPlayerObject.transform); body.transform.localPosition = new Vector3(0, 2.5f, 0); body.transform.localScale = new Vector3(3f, 3f, 3f); // Výrazná zelená barva se svícením var bodyMat = CreateEmissiveMaterial(new Color(0.3f, 1f, 0.3f), 1.5f); body.GetComponent().material = bodyMat; // VELKÝ indikátor - kroužek kolem hráče var indicator = GameObject.CreatePrimitive(PrimitiveType.Cylinder); indicator.name = "Indicator"; indicator.transform.SetParent(localPlayerObject.transform); indicator.transform.localPosition = new Vector3(0, 0.15f, 0); indicator.transform.localScale = new Vector3(8f, 0.15f, 8f); // Žlutý svítící kroužek var indicatorMat = CreateEmissiveMaterial(new Color(1f, 1f, 0f), 2f); indicator.GetComponent().material = indicatorMat; // Šipka směru var arrow = GameObject.CreatePrimitive(PrimitiveType.Cube); arrow.name = "Arrow"; arrow.transform.SetParent(localPlayerObject.transform); arrow.transform.localPosition = new Vector3(0, 0.3f, 3f); arrow.transform.localScale = new Vector3(1.5f, 0.3f, 3f); arrow.GetComponent().material = bodyMat; // Druhý vnější ring pro ještě lepší viditelnost var outerRing = GameObject.CreatePrimitive(PrimitiveType.Cylinder); outerRing.name = "OuterRing"; outerRing.transform.SetParent(localPlayerObject.transform); outerRing.transform.localPosition = new Vector3(0, 0.1f, 0); outerRing.transform.localScale = new Vector3(12f, 0.1f, 12f); var outerMat = CreateEmissiveMaterial(new Color(0.5f, 1f, 0.5f, 0.3f), 0.5f); outerRing.GetComponent().material = outerMat; } // Pozice podle currentPlayerPosition (ne kamery!) localPlayerObject.transform.position = new Vector3( currentPlayerPosition.x, 0, currentPlayerPosition.z ); } /// Objekt lokálního hráče protected GameObject localPlayerObject; #region ═══════════════════════════════════════════════════════════════════ // TĚLA (BODIES) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření objektu těla mrtvého hráče. /// Tělo je vykresleno jako ležící capsule. /// /// ID těla /// GPS pozice /// Jméno oběti protected void CreateBodyObject(string bodyId, Position position, string victimName) { if (bodyObjects.ContainsKey(bodyId)) return; // Kontejner pro tělo var container = new GameObject($"Body_{victimName}"); container.transform.SetParent(bodiesContainer.transform); Vector3 unityPos = GPSToUnity(position); container.transform.position = unityPos; // Ležící tělo var bodyObj = GameObject.CreatePrimitive(PrimitiveType.Capsule); bodyObj.name = "Corpse"; bodyObj.transform.SetParent(container.transform); bodyObj.transform.localPosition = new Vector3(0, 0.3f, 0); bodyObj.transform.rotation = Quaternion.Euler(90, 0, 0); bodyObj.transform.localScale = new Vector3(1.5f, 2f, 1.5f); // Výrazná červená barva se svícením var bodyMat = CreateEmissiveMaterial(new Color(1f, 0.2f, 0.2f), 1.5f); bodyObj.GetComponent().material = bodyMat; // Výstražný kroužek var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ring.name = "WarningRing"; ring.transform.SetParent(container.transform); ring.transform.localPosition = new Vector3(0, 0.1f, 0); ring.transform.localScale = new Vector3(6f, 0.1f, 6f); var ringMat = CreateEmissiveMaterial(new Color(1f, 0f, 0f, 0.6f), 2f); ring.GetComponent().material = ringMat; // Výstražná značka nahoře var warning = GameObject.CreatePrimitive(PrimitiveType.Cube); warning.name = "Warning"; warning.transform.SetParent(container.transform); warning.transform.localPosition = new Vector3(0, 4f, 0); warning.transform.localScale = new Vector3(2f, 2f, 0.3f); warning.GetComponent().material = bodyMat; bodyObjects[bodyId] = container; } /// /// Odstranění objektu těla (po reportu) /// protected void RemoveBodyObject(string bodyId) { if (bodyObjects.TryGetValue(bodyId, out var bodyObj)) { Destroy(bodyObj); bodyObjects.Remove(bodyId); } } #region ═══════════════════════════════════════════════════════════════════ // ÚKOLY (TASKS) // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření markerů úkolů pro hráče. /// Úkoly jsou vykresleny jako svítící válce. /// protected void CreateTaskMarkers() { if (client?.MyTasks == null) return; foreach (var task in client.MyTasks) { CreateTaskObject(task); } } /// /// Vytvoření markeru úkolu - VÝRAZNÝ marker s popiskem /// private void CreateTaskObject(object taskObj) { // Dynamický přístup k vlastnostem var taskType = taskObj.GetType(); string taskId = taskType.GetField("TaskId")?.GetValue(taskObj) as string ?? taskType.GetProperty("TaskId")?.GetValue(taskObj) as string ?? taskType.GetField("Id")?.GetValue(taskObj) as string ?? taskType.GetProperty("Id")?.GetValue(taskObj) as string ?? System.Guid.NewGuid().ToString(); string taskName = taskType.GetField("Name")?.GetValue(taskObj) as string ?? taskType.GetProperty("Name")?.GetValue(taskObj) as string ?? "Task"; // Získání lokace object locationObj = taskType.GetField("Location")?.GetValue(taskObj) ?? taskType.GetProperty("Location")?.GetValue(taskObj); Position location = locationObj is Position pos ? pos : new Position(0, 0); if (taskObjects.ContainsKey(taskId)) return; // Kontejner pro task marker var container = new GameObject($"Task_{taskName}"); container.transform.SetParent(tasksContainer.transform); Vector3 unityPos = GPSToUnity(location); container.transform.position = unityPos; // Hlavní marker - VĚTŠÍ svítící válec var marker = GameObject.CreatePrimitive(PrimitiveType.Cylinder); marker.name = "Marker"; marker.transform.SetParent(container.transform); marker.transform.localPosition = new Vector3(0, 2f, 0); marker.transform.localScale = new Vector3(4f, 2f, 4f); // Výrazný svítící cyan materiál var mat = CreateEmissiveMaterial(new Color(0f, 1f, 1f), 2f); marker.GetComponent().material = mat; // Vrchní kužel jako "šipka dolů" var cone = GameObject.CreatePrimitive(PrimitiveType.Cylinder); cone.name = "Cone"; cone.transform.SetParent(container.transform); cone.transform.localPosition = new Vector3(0, 5f, 0); cone.transform.localScale = new Vector3(2f, 1f, 2f); cone.GetComponent().material = mat; // Pulsující ring na zemi var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ring.name = "Ring"; ring.transform.SetParent(container.transform); ring.transform.localPosition = new Vector3(0, 0.1f, 0); ring.transform.localScale = new Vector3(8f, 0.1f, 8f); var ringMat = CreateEmissiveMaterial(new Color(0f, 0.8f, 0.8f, 0.5f), 1f); ring.GetComponent().material = ringMat; taskObjects[taskId] = container; } /// /// Vytvoří svítící materiál /// BUILD-SAFE: Používá Sprites/Default - emise simulována jasnou barvou /// private Material CreateEmissiveMaterial(Color color, float intensity = 1f) { // Simulujeme emisi zjasnením barvy Color brightColor = new Color( Mathf.Min(1f, color.r * (1f + intensity * 0.5f)), Mathf.Min(1f, color.g * (1f + intensity * 0.5f)), Mathf.Min(1f, color.b * (1f + intensity * 0.5f)), color.a ); return CreateUnlitMaterial(brightColor); } /// /// Aktualizace stavu úkolu (dokončen = skryje marker) /// protected void UpdateTaskMarker(string taskId, bool completed) { if (taskObjects.TryGetValue(taskId, out var taskObj)) { if (completed) { // Skryjeme celý marker taskObj.SetActive(false); } } } #region ═══════════════════════════════════════════════════════════════════ // OPRAVNÉ STANICE // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vytvoření markerů opravných stanic. /// Zobrazují se pouze při aktivní sabotáži. /// protected void CreateRepairStationMarkers() { if (currentSabotage?.RepairStations == null) return; foreach (var station in currentSabotage.RepairStations) { CreateRepairStationObject(station.StationId, station.Name, station.Location); } } /// /// Vytvoření markeru opravné stanice - VELMI VÝRAZNÝ marker /// private void CreateRepairStationObject(string stationId, string stationName, Position stationPos) { if (string.IsNullOrEmpty(stationId)) stationId = System.Guid.NewGuid().ToString(); if (string.IsNullOrEmpty(stationName)) stationName = "Repair"; if (repairStationObjects.ContainsKey(stationId)) return; Debug.Log($"[GeoSus] Creating repair station marker: {stationId} '{stationName}' @ {stationPos.Lat:F6}, {stationPos.Lon:F6}"); // Kontejner pro opravnou stanici var container = new GameObject($"Repair_{stationName}"); container.transform.SetParent(repairStationsContainer.transform); Vector3 unityPos = GPSToUnity(stationPos); container.transform.position = unityPos; // VELKÝ hlavní marker - věž var tower = GameObject.CreatePrimitive(PrimitiveType.Cylinder); tower.name = "Tower"; tower.transform.SetParent(container.transform); tower.transform.localPosition = new Vector3(0, 5f, 0); tower.transform.localScale = new Vector3(3f, 5f, 3f); // Výrazná oranžová/žlutá barva var towerMat = CreateEmissiveMaterial(new Color(1f, 0.6f, 0f), 3f); tower.GetComponent().material = towerMat; // Výstražná koule nahoře var beacon = GameObject.CreatePrimitive(PrimitiveType.Sphere); beacon.name = "Beacon"; beacon.transform.SetParent(container.transform); beacon.transform.localPosition = new Vector3(0, 11f, 0); beacon.transform.localScale = new Vector3(4f, 4f, 4f); var beaconMat = CreateEmissiveMaterial(new Color(1f, 0.8f, 0f), 5f); beacon.GetComponent().material = beaconMat; // Velký kroužek na zemi - blikající var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ring.name = "Ring"; ring.transform.SetParent(container.transform); ring.transform.localPosition = new Vector3(0, 0.15f, 0); ring.transform.localScale = new Vector3(12f, 0.15f, 12f); var ringMat = CreateEmissiveMaterial(new Color(1f, 0.5f, 0f, 0.6f), 2f); ring.GetComponent().material = ringMat; // Druhá výstražná značka - šipka dolů var arrow1 = GameObject.CreatePrimitive(PrimitiveType.Cube); arrow1.name = "Arrow1"; arrow1.transform.SetParent(container.transform); arrow1.transform.localPosition = new Vector3(0, 15f, 0); arrow1.transform.localScale = new Vector3(2f, 4f, 0.5f); arrow1.GetComponent().material = beaconMat; repairStationObjects[stationId] = container; } /// /// Aktualizace stavu opravné stanice /// protected void UpdateRepairStationMarker(string stationId, bool active) { if (repairStationObjects.TryGetValue(stationId, out var stationObj)) { // Najdeme Beacon (kouli nahoře) pro blikání var beacon = stationObj.transform.Find("Beacon"); var ring = stationObj.transform.Find("Ring"); if (beacon != null) { var renderer = beacon.GetComponent(); if (renderer != null) { float pulse = active ? Mathf.Abs(Mathf.Sin(Time.time * 8f)) : 0.5f; Color emissionColor = new Color(1f, 0.8f, 0f) * (1f + pulse * 3f); renderer.material.color = emissionColor; } } if (ring != null) { var renderer = ring.GetComponent(); if (renderer != null) { float pulse = Mathf.Abs(Mathf.Sin(Time.time * 3f)); Color ringColor = active ? new Color(1f, 1f, 0f, 0.6f + pulse * 0.4f) : new Color(1f, 0.5f, 0f, 0.4f); renderer.material.color = ringColor; } } } } /// /// Odstranění všech opravných stanic (po opravě sabotáže) /// protected void ClearRepairStationMarkers() { foreach (var kvp in repairStationObjects) { Destroy(kvp.Value); } repairStationObjects.Clear(); } #region ═══════════════════════════════════════════════════════════════════ // NAČTENÍ MAPY // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Zpracování přijatých mapových dat. /// Vytvoří všechny 3D objekty mapy. /// /// POZNÁMKA: MapDataPayload používá kompaktní formát: /// - Buildings: List kde každý double[] je [lat1,lon1,lat2,lon2,...] /// - Pathways: List podobně /// - Areas: List podobně /// - POIs: List ve formátu [lat,lon,type,lat,lon,type,...] /// /// Data z Overpass API protected void ProcessMapData(MapDataPayload mapData) { if (mapData == null) return; // Uložení referenčních bodů mapCenter = mapData.Center; mapRadius = mapData.RadiusMeters; // Inicializace systému InitializeMapSystem(); // Vytvoření budov z kompaktního formátu if (mapData.Buildings != null) { loadingMessage = "Vytvářím budovy..."; for (int i = 0; i < mapData.Buildings.Count; i++) { var coords = mapData.Buildings[i]; string buildingType = (mapData.BuildingTypes != null && i < mapData.BuildingTypes.Count) ? mapData.BuildingTypes[i] : null; CreateBuildingFromCoords(coords, buildingType, i); } } // Vytvoření cest z kompaktního formátu if (mapData.Pathways != null) { loadingMessage = "Vytvářím cesty..."; for (int i = 0; i < mapData.Pathways.Count; i++) { var coords = mapData.Pathways[i]; PathType pathType = (mapData.PathwayTypes != null && i < mapData.PathwayTypes.Count) ? (PathType)mapData.PathwayTypes[i] : PathType.Footway; CreatePathwayFromCoords(coords, pathType, i); } } // Vytvoření oblastí z kompaktního formátu if (mapData.Areas != null) { loadingMessage = "Vytvářím oblasti..."; for (int i = 0; i < mapData.Areas.Count; i++) { var coords = mapData.Areas[i]; MapAreaType areaType = (mapData.AreaTypes != null && i < mapData.AreaTypes.Count) ? (MapAreaType)mapData.AreaTypes[i] : MapAreaType.Other; CreateAreaFromCoords(coords, areaType, i); } } // Vytvoření POI z kompaktního formátu [lat,lon,type,lat,lon,type,...] if (mapData.POIs != null && mapData.POIs.Count >= 3) { loadingMessage = "Vytvářím body zájmu..."; for (int i = 0; i + 2 < mapData.POIs.Count; i += 3) { double lat = mapData.POIs[i]; double lon = mapData.POIs[i + 1]; int typeIndex = (int)mapData.POIs[i + 2]; MapPOIType poiType = (MapPOIType)typeIndex; CreatePOIFromCoords(lat, lon, poiType, i / 3); } } loadingProgress = 1f; loadingMessage = "Mapa připravena!"; } /// /// Vytvoření budovy z kompaktních souřadnic /// private void CreateBuildingFromCoords(double[] coords, string buildingType, int index) { if (coords == null || coords.Length < 6) return; // Potřeba alespoň 3 body // Převod na List List outline = new List(); for (int i = 0; i + 1 < coords.Length; i += 2) { outline.Add(new Position(coords[i], coords[i + 1])); } // Vytvoření MapBuilding objektu var building = new MapBuilding { Id = index, Outline = outline, Name = buildingType ?? "Building", BuildingType = buildingType }; CreateBuildingObject(building); } /// /// Vytvoření cesty z kompaktních souřadnic /// private void CreatePathwayFromCoords(double[] coords, PathType pathType, int index) { if (coords == null || coords.Length < 4) return; // Potřeba alespoň 2 body // Převod na List List points = new List(); for (int i = 0; i + 1 < coords.Length; i += 2) { points.Add(new Position(coords[i], coords[i + 1])); } // Vytvoření MapPathway objektu var pathway = new MapPathway { Id = index, Points = points, PathType = pathType, Name = pathType.ToString() }; CreatePathwayObject(pathway); } /// /// Vytvoření oblasti z kompaktních souřadnic /// private void CreateAreaFromCoords(double[] coords, MapAreaType areaType, int index) { if (coords == null || coords.Length < 6) return; // Potřeba alespoň 3 body // Převod na List List outline = new List(); for (int i = 0; i + 1 < coords.Length; i += 2) { outline.Add(new Position(coords[i], coords[i + 1])); } // Vytvoření MapArea objektu var area = new MapArea { Id = index, Outline = outline, AreaType = areaType, Name = areaType.ToString() }; CreateAreaObject(area); } /// /// Vytvoření POI z souřadnic /// private void CreatePOIFromCoords(double lat, double lon, MapPOIType poiType, int index) { var poi = new MapPOI { Id = index, Location = new Position(lat, lon), POIType = poiType, Name = poiType.ToString() }; CreatePOIObject(poi); } #region ═══════════════════════════════════════════════════════════════════ // ÚKLID // ════════════════════════════════════════════════════════════════════════ #endregion /// /// Vyčištění všech mapových objektů. /// Volá se při ukončení hry nebo odpojení. /// protected void CleanupMapObjects() { // Zničení hlavního kontejneru zničí všechny potomky if (mapContainer != null) { Destroy(mapContainer); mapContainer = null; } // Vyčištění slovníků playerObjects.Clear(); bodyObjects.Clear(); taskObjects.Clear(); repairStationObjects.Clear(); // Zničení lokálního hráče if (localPlayerObject != null) { Destroy(localPlayerObject); localPlayerObject = null; } } /// /// Aktualizace všech dynamických objektů na mapě. /// Volá se každý frame v Update(). /// protected void UpdateMapObjects() { if (client == null || currentState != AppState.InGame) return; // Aktualizace pozic hráčů - bezpečně kopírujeme klíče if (client.PlayerPositions != null) { try { // Použijeme ToArray() místo new List() - je rychlejší a bezpečnější var playerIds = client.PlayerPositions.Keys.ToArray(); foreach (var playerId in playerIds) { if (client.PlayerPositions.TryGetValue(playerId, out var playerInfo)) { UpdatePlayerObject(playerId, playerInfo.Position, playerInfo.State); } } } catch (System.InvalidOperationException) { // Dictionary byl změněn během iterace - přeskočíme tento frame } } // Aktualizace lokálního hráče UpdateLocalPlayerVisual(); // Aktualizace opravných stanic (blikání) - bezpečně kopírujeme klíče if (currentSabotage != null) { try { var stationIds = repairStationObjects.Keys.ToArray(); foreach (var stationId in stationIds) { bool isActive = activeRepairStation == stationId; UpdateRepairStationMarker(stationId, isActive); } } catch (System.InvalidOperationException) { // Dictionary byl změněn během iterace - přeskočíme tento frame } } // Aktualizace meeting location markeru bool hasMeeting = currentMeeting != null && client?.CurrentLobbyState?.Phase == GamePhase.Meeting; if (hasMeeting && currentMeeting != null) { UpdateMeetingLocationMarker(currentMeeting.MeetingLocation, true); } else { UpdateMeetingLocationMarker(null, false); } } /// /// Hledání nejbližší opravné stanice /// protected RepairStationInfo FindNearbyRepairStation(double maxDistance) { if (currentSabotage?.RepairStations == null) return null; if (localPlayerObject == null) return null; Position myPos = UnityToGPS(localPlayerObject.transform.position); foreach (var station in currentSabotage.RepairStations) { double dist = CalculateDistance(myPos, station.Location); if (dist <= maxDistance) return station; } return null; } /// /// Výpočet vzdálenosti mezi dvěma GPS body (Haversine) /// protected double CalculateDistance(Position a, Position b) { const double R = 6371000; // Poloměr Země v metrech double lat1 = a.Lat * Math.PI / 180; double lat2 = b.Lat * Math.PI / 180; double dLat = (b.Lat - a.Lat) * Math.PI / 180; double dLon = (b.Lon - a.Lon) * Math.PI / 180; double h = Math.Sin(dLat/2) * Math.Sin(dLat/2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Sin(dLon/2) * Math.Sin(dLon/2); return 2 * R * Math.Asin(Math.Sqrt(h)); } }