2026-10-31 17:05:17 +01:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Bogatyri.World;
|
|
|
|
|
|
using TMPro;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
// Разведка на карте мира (М1): туман войны и чужие армии. Правил тут нет -- всё знает модуль Разведки (IIntelligence),
|
|
|
|
|
|
// здесь только рисуем то, что он вернул.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Туман: неисследованное -- тёмное, исследованное -- притушено, в зоне обзора -- чисто.
|
2026-11-04 14:55:41 +01:00
|
|
|
|
// 2.5D (04.11.2026): туман рисуется прямо в текстуре земли -- объекты (города, лагеря, точки) стоят над ней и видны всегда,
|
|
|
|
|
|
// как требует решение 31.10, п. 1. hideUnexploredObjects в 3D пока только сильнее затемняет землю.
|
2026-10-31 17:05:17 +01:00
|
|
|
|
// Чужие армии -- только те, что знает Разведка: в обзоре -- красные, с примерной численностью «~min–max»
|
|
|
|
|
|
// и владельцем, если опознан; вышли из обзора -- бледная метка последнего контакта.
|
|
|
|
|
|
public class IntelligenceOverlay : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
static readonly Color ObservedColor = new Color(0.9f, 0.25f, 0.2f);
|
2026-11-04 14:55:41 +01:00
|
|
|
|
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;
|
2026-10-31 17:05:17 +01:00
|
|
|
|
const int CellSize = 1000;
|
|
|
|
|
|
|
|
|
|
|
|
private Transform mapRoot;
|
|
|
|
|
|
private WorldSimulation world;
|
|
|
|
|
|
private Texture2D fog;
|
|
|
|
|
|
private Color32[] pixels;
|
|
|
|
|
|
private int gridW, gridH;
|
|
|
|
|
|
private float nextFogRefresh;
|
2026-11-04 14:55:41 +01:00
|
|
|
|
private Renderer ground;
|
|
|
|
|
|
private readonly Dictionary<string, (Renderer sprite, TextMeshPro label)> markers = new Dictionary<string, (Renderer, TextMeshPro)>();
|
2026-10-31 17:05:17 +01:00
|
|
|
|
private readonly HashSet<string> alive = new HashSet<string>();
|
|
|
|
|
|
private readonly List<string> stale = new List<string>();
|
|
|
|
|
|
|
2026-11-04 14:55:41 +01:00
|
|
|
|
public static IntelligenceOverlay Create(Transform mapRoot, WorldSimulation world, Renderer ground)
|
2026-10-31 17:05:17 +01:00
|
|
|
|
{
|
|
|
|
|
|
var go = new GameObject("Intelligence");
|
|
|
|
|
|
go.transform.SetParent(mapRoot, false);
|
|
|
|
|
|
var overlay = go.AddComponent<IntelligenceOverlay>();
|
|
|
|
|
|
overlay.mapRoot = mapRoot;
|
|
|
|
|
|
overlay.world = world;
|
2026-11-04 14:55:41 +01:00
|
|
|
|
overlay.ground = ground;
|
2026-10-31 17:05:17 +01:00
|
|
|
|
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];
|
2026-11-04 14:55:41 +01:00
|
|
|
|
// 1 пиксель = 1 клетка карты; текстура натянута на всю землю (UV 0..1 -- от левого нижнего угла карты)
|
2026-11-04 15:03:16 +01:00
|
|
|
|
if (ground != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ground.sharedMaterial.HasProperty("_BaseMap")) ground.sharedMaterial.SetTexture("_BaseMap", fog);
|
|
|
|
|
|
if (ground.sharedMaterial.HasProperty("_MainTex")) ground.sharedMaterial.SetTexture("_MainTex", fog);
|
|
|
|
|
|
}
|
2026-10-31 17:05:17 +01:00
|
|
|
|
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;
|
2026-10-31 17:15:47 +01:00
|
|
|
|
byte unexplored = (byte)(hide ? 250 : 220), explored = 110;
|
2026-10-31 17:05:17 +01:00
|
|
|
|
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;
|
2026-11-04 15:03:16 +01:00
|
|
|
|
// трава разных оттенков (пятнами), поверх -- туман
|
|
|
|
|
|
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);
|
2026-11-04 14:55:41 +01:00
|
|
|
|
c.a = 1f;
|
|
|
|
|
|
pixels[cy * gridW + cx] = c;
|
2026-10-31 17:05:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-11-04 14:55:41 +01:00
|
|
|
|
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);
|
2026-10-31 17:05:17 +01:00
|
|
|
|
|
|
|
|
|
|
string owner = c.Identified ? OwnerName(c.OwnerId) : "чужая армия";
|
|
|
|
|
|
m.label.text = c.Observed
|
|
|
|
|
|
? $"{owner}\n~{c.ApproxMin}–{c.ApproxMax} воинов"
|
|
|
|
|
|
: $"<color=#BBBBBB>{owner}, ~{c.ApproxMin}–{c.ApproxMax}\nконтакт {Ago(now - c.LastSeenMs)} назад</color>";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Метки армий, которых Разведка больше не знает (контакт устарел)
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:55:41 +01:00
|
|
|
|
(Renderer, TextMeshPro) CreateMarker(string armyId)
|
2026-10-31 17:05:17 +01:00
|
|
|
|
{
|
2026-11-04 14:55:41 +01:00
|
|
|
|
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);
|
2026-10-31 17:05:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Имя владельца -- по его поселению на карте («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} сек";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|