using System.Collections.Generic; using Bogatyri.World; using TMPro; using UnityEngine; // Разведка на карте мира (М1): туман войны и чужие армии. Правил тут нет -- всё знает модуль Разведки (IIntelligence), // здесь только рисуем то, что он вернул. // // Туман: неисследованное -- тёмное, исследованное -- притушено, в зоне обзора -- чисто. // По умолчанию туман только затемняет землю: города, лагеря и точки видны (IntelligenceConfig.hideUnexploredObjects). // Чужие армии -- только те, что знает Разведка: в обзоре -- красные, с примерной численностью «~min–max» // и владельцем, если опознан; вышли из обзора -- бледная метка последнего контакта. public class IntelligenceOverlay : MonoBehaviour { static readonly Color ObservedColor = new Color(0.9f, 0.25f, 0.2f); static readonly Color GhostColor = new Color(0.9f, 0.25f, 0.2f, 0.35f); const float MarkerSize = 0.55f; const int CellSize = 1000; private Transform mapRoot; private WorldSimulation world; private Texture2D fog; private Color32[] pixels; private int gridW, gridH; private float nextFogRefresh; private readonly Dictionary markers = new Dictionary(); private readonly HashSet alive = new HashSet(); private readonly List stale = new List(); public static IntelligenceOverlay Create(Transform mapRoot, WorldSimulation world) { var go = new GameObject("Intelligence"); go.transform.SetParent(mapRoot, false); var overlay = go.AddComponent(); overlay.mapRoot = mapRoot; overlay.world = world; overlay.CreateFog(); return overlay; } void CreateFog() { gridW = (2 * WorldMapLayout.HalfWidth + CellSize - 1) / CellSize; gridH = (2 * WorldMapLayout.HalfHeight + CellSize - 1) / CellSize; fog = new Texture2D(gridW, gridH, TextureFormat.RGBA32, false) { filterMode = FilterMode.Bilinear, // мягкий край тумана wrapMode = TextureWrapMode.Clamp }; pixels = new Color32[gridW * gridH]; var go = new GameObject("Fog"); go.transform.SetParent(transform, false); var sr = go.AddComponent(); // 1 пиксель = 1 клетка карты = 1 единица Unity; центр спрайта -- центр карты sr.sprite = Sprite.Create(fog, new Rect(0, 0, gridW, gridH), new Vector2(0.5f, 0.5f), 1f); go.transform.position = new Vector3((gridW * CellSize / 2f - WorldMapLayout.HalfWidth) / 1000f, (gridH * CellSize / 2f - WorldMapLayout.HalfHeight) / 1000f, 0f); // Объекты видны -- туман между землёй (-10) и объектами (0+). Скрыты -- поверх объектов, под войсками (30) sr.sortingOrder = IntelligenceConfig.Load().hideUnexploredObjects ? 20 : -9; RefreshFog(); } void OnDestroy() { if (fog != null) Destroy(fog); } void Update() { if (!GameServices.TryGet(out IIntelligence intel)) return; if (Time.unscaledTime >= nextFogRefresh) { nextFogRefresh = Time.unscaledTime + 0.5f; RefreshFog(); } RefreshContacts(intel); } void RefreshFog() { if (!GameServices.TryGet(out IIntelligence intel)) return; bool hide = IntelligenceConfig.Load().hideUnexploredObjects; byte unexplored = (byte)(hide ? 250 : 220), explored = 110; for (int cy = 0; cy < gridH; cy++) for (int cx = 0; cx < gridW; cx++) { int x = cx * CellSize + CellSize / 2 - WorldMapLayout.HalfWidth; int y = cy * CellSize + CellSize / 2 - WorldMapLayout.HalfHeight; MapVisibility v = intel.GetVisibility(x, y); byte a = v == MapVisibility.Observed ? (byte)0 : v == MapVisibility.Explored ? explored : unexplored; pixels[cy * gridW + cx] = new Color32(10, 14, 20, a); } fog.SetPixels32(pixels); fog.Apply(false); } void RefreshContacts(IIntelligence intel) { long now = MarchManager.NowMs; alive.Clear(); foreach (var c in intel.Contacts) { alive.Add(c.ArmyId); if (!markers.TryGetValue(c.ArmyId, out var m)) { m = CreateMarker(c.ArmyId); markers[c.ArmyId] = m; } m.sprite.transform.position = new Vector3(c.X / 1000f, c.Y / 1000f, 0f); m.label.transform.position = m.sprite.transform.position + new Vector3(0, 0.75f, 0); m.sprite.color = c.Observed ? ObservedColor : GhostColor; string owner = c.Identified ? OwnerName(c.OwnerId) : "чужая армия"; m.label.text = c.Observed ? $"{owner}\n~{c.ApproxMin}–{c.ApproxMax} воинов" : $"{owner}, ~{c.ApproxMin}–{c.ApproxMax}\nконтакт {Ago(now - c.LastSeenMs)} назад"; } // Метки армий, которых Разведка больше не знает (контакт устарел) stale.Clear(); foreach (var id in markers.Keys) if (!alive.Contains(id)) stale.Add(id); foreach (var id in stale) { Destroy(markers[id].sprite.gameObject); Destroy(markers[id].label.gameObject); markers.Remove(id); } } (SpriteRenderer, TextMeshPro) CreateMarker(string armyId) { var go = new GameObject("Contact_" + armyId); go.transform.SetParent(mapRoot, false); go.transform.localScale = Vector3.one * MarkerSize; go.transform.rotation = Quaternion.Euler(0, 0, 45); // ромб-флажок: не путается со своими квадратиками var sr = go.AddComponent(); sr.sprite = PrimitiveSprites.Square; sr.sortingOrder = 32; var lgo = new GameObject("ContactLabel_" + armyId); lgo.transform.SetParent(mapRoot, false); var label = lgo.AddComponent(); label.fontSize = 2.6f; label.alignment = TextAlignmentOptions.Center; label.rectTransform.sizeDelta = new Vector2(6f, 1.2f); label.sortingOrder = 33; return (sr, label); } // Имя владельца -- по его поселению на карте («npc_3» -> «Велимир») string OwnerName(string ownerId) { foreach (var n in world.Nodes) if (n.ownerId == ownerId && n.type == MapNodeType.NeighborCity) return n.displayName; return ownerId ?? "?"; } static string Ago(long ms) { long sec = System.Math.Max(0, ms) / 1000; return sec >= 60 ? $"{sec / 60} мин" : $"{sec} сек"; } }