Files
MVP1/Assets/Scripts/WorldMap/IntelligenceOverlay.cs
T

165 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, (SpriteRenderer sprite, TextMeshPro label)> markers = new Dictionary<string, (SpriteRenderer, TextMeshPro)>();
private readonly HashSet<string> alive = new HashSet<string>();
private readonly List<string> stale = new List<string>();
public static IntelligenceOverlay Create(Transform mapRoot, WorldSimulation world)
{
var go = new GameObject("Intelligence");
go.transform.SetParent(mapRoot, false);
var overlay = go.AddComponent<IntelligenceOverlay>();
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<SpriteRenderer>();
// 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} воинов"
: $"<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);
}
}
(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<SpriteRenderer>();
sr.sprite = PrimitiveSprites.Square;
sr.sortingOrder = 32;
var lgo = new GameObject("ContactLabel_" + armyId);
lgo.transform.SetParent(mapRoot, false);
var label = lgo.AddComponent<TextMeshPro>();
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} сек";
}
}