using System.Collections.Generic; using Bogatyri.World; using TMPro; using UnityEngine; // Разведка на карте мира (М1): туман войны и чужие армии. Правил тут нет -- всё знает модуль Разведки (IIntelligence), // здесь только рисуем то, что он вернул. // // Туман: неисследованное -- тёмное, исследованное -- притушено, в зоне обзора -- чисто. // 2.5D (04.11.2026): туман рисуется прямо в текстуре земли -- объекты (города, лагеря, точки) стоят над ней и видны всегда, // как требует решение 31.10, п. 1. hideUnexploredObjects в 3D пока только сильнее затемняет землю. // Чужие армии -- только те, что знает Разведка: в обзоре -- красные, с примерной численностью «~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.75f, 0.55f, 0.55f); // бледная -- последний контакт static readonly Color GroundColor = new Color(0.3f, 0.42f, 0.25f); static readonly Color FogColor = new Color(0.04f, 0.055f, 0.08f); const float MarkerSize = 0.55f, MarkerHeight = 0.5f; const int CellSize = 1000; private Transform mapRoot; private WorldSimulation world; private Texture2D fog; private Color32[] pixels; private int gridW, gridH; private float nextFogRefresh; private Renderer ground; 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, Renderer ground) { var go = new GameObject("Intelligence"); go.transform.SetParent(mapRoot, false); var overlay = go.AddComponent(); overlay.mapRoot = mapRoot; overlay.world = world; overlay.ground = ground; 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]; // 1 пиксель = 1 клетка карты; текстура натянута на всю землю (UV 0..1 -- от левого нижнего угла карты) if (ground != null) { if (ground.sharedMaterial.HasProperty("_BaseMap")) ground.sharedMaterial.SetTexture("_BaseMap", fog); if (ground.sharedMaterial.HasProperty("_MainTex")) ground.sharedMaterial.SetTexture("_MainTex", fog); } 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; // трава разных оттенков (пятнами), поверх -- туман Color grass = Color.Lerp(GroundColor, new Color(0.42f, 0.55f, 0.3f), Mathf.PerlinNoise(cx * 0.35f, cy * 0.35f)); Color c = Color.Lerp(grass, FogColor, a / 255f); c.a = 1f; pixels[cy * gridW + cx] = c; } 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, MarkerHeight / 2f, c.Y / 1000f); m.label.transform.position = new Vector3(c.X / 1000f, MarkerHeight + 0.8f, c.Y / 1000f); m.sprite.sharedMaterial = Map3D.Mat(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); } } (Renderer, TextMeshPro) CreateMarker(string armyId) { var r = Map3D.Block(mapRoot, "Contact_" + armyId, PrimitiveType.Cube, ObservedColor, Vector3.zero, Vector2.one * MarkerSize, MarkerHeight); r.transform.rotation = Quaternion.Euler(0, 45, 0); // ромб-флажок: не путается со своими кубиками var label = Map3D.Label(mapRoot, "", Vector3.zero, 2.6f); label.gameObject.name = "ContactLabel_" + armyId; return (r, 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} сек"; } }