Цвет -- в _BaseColor (URP), раньше всё было белым. Настройка 3D-рендерера добавляет его в URP Asset каждого уровня качества (в проекте URP задан только в Quality) + отдельная команда меню. Карта мира: замок со стенами и башнями, города -- домики, лагеря -- шатры с костром, кристаллы точек, рощи. 3D-поселение: здания с двускатными крышами и приметами, ели и лиственные, песчаные берега, снег на скалах. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
7.3 KiB
C#
159 lines
7.3 KiB
C#
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)
|
||
{
|
||
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} воинов"
|
||
: $"<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} сек";
|
||
}
|
||
}
|