Files
MVP1/Assets/Scripts/WorldMap/IntelligenceOverlay.cs
T
AlexanderandClaude Opus 5.5 9a6e5a2e7b Карта мира в 2.5D на примитивах
Карта на земле (X/Z): замок, города, лагеря, ресурсные точки, отряды и метки разведки -- примитивы,
подписи смотрят в камеру; наклонная перспективная камера с тем же поведением (перетаскивание, щипок, тап);
туман -- в текстуре земли; солнце и 3D-рендерер URP для камеры (Map3D). Логика и окна не менялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:55:41 +01:00

153 lines
6.9 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),
// здесь только рисуем то, что он вернул.
//
// Туман: неисследованное -- тёмное, исследованное -- притушено, в зоне обзора -- чисто.
// 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<string, (Renderer sprite, TextMeshPro label)> markers = new Dictionary<string, (Renderer, 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, Renderer ground)
{
var go = new GameObject("Intelligence");
go.transform.SetParent(mapRoot, false);
var overlay = go.AddComponent<IntelligenceOverlay>();
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) ground.sharedMaterial.mainTexture = 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 c = Color.Lerp(GroundColor, 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} воинов"
: $"<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);
}
}
(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} сек";
}
}