Карта на земле (X/Z): замок, города, лагеря, ресурсные точки, отряды и метки разведки -- примитивы, подписи смотрят в камеру; наклонная перспективная камера с тем же поведением (перетаскивание, щипок, тап); туман -- в текстуре земли; солнце и 3D-рендерер URP для камеры (Map3D). Логика и окна не менялись. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.9 KiB
C#
137 lines
5.9 KiB
C#
using Bogatyri.World;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
|
|
// Войско в походе на карте: группа маленьких кубиков на земле + подпись с числом солдат и таймером (2.5D, 04.11.2026).
|
|
// Сам ничего не решает -- позицию и состояние берёт из WorldSimulation.
|
|
public class MarchView : MonoBehaviour
|
|
{
|
|
const int MaxSquares = 15; // больше квадратиков не рисуем, чтобы не загромождать карту
|
|
const int SquaresPerRow = 5;
|
|
const float SquareSize = 0.18f;
|
|
const float SquareSpacing = 0.26f;
|
|
|
|
static readonly Color OutboundColor = new Color(0.3f, 0.55f, 1f);
|
|
static readonly Color ReturningColor = new Color(0.55f, 0.8f, 1f);
|
|
|
|
public int MarchId { get; private set; }
|
|
|
|
private Renderer[] squares;
|
|
private TextMeshPro label;
|
|
private GameObject routeLine;
|
|
private MarchState shownState;
|
|
private int shownSoldiers = -1;
|
|
|
|
public static MarchView Create(Transform parent, MarchData march)
|
|
{
|
|
var go = new GameObject($"March_{march.id}");
|
|
go.transform.SetParent(parent, false);
|
|
var view = go.AddComponent<MarchView>();
|
|
view.MarchId = march.id;
|
|
view.CreateLabel();
|
|
return view;
|
|
}
|
|
|
|
void CreateLabel()
|
|
{
|
|
var go = new GameObject("Label");
|
|
go.transform.SetParent(transform, false);
|
|
go.transform.localPosition = new Vector3(0, 1.0f, 0);
|
|
label = go.AddComponent<TextMeshPro>();
|
|
label.fontSize = 3f;
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
label.rectTransform.sizeDelta = new Vector2(5f, 0.9f);
|
|
label.color = Color.white;
|
|
label.outlineWidth = 0.25f;
|
|
label.outlineColor = Color.black;
|
|
go.AddComponent<FaceCamera>();
|
|
}
|
|
|
|
// Перестраиваем квадратики, только когда поменялось число солдат (после боя)
|
|
void RebuildSquares(int soldiers, Color color)
|
|
{
|
|
if (squares != null)
|
|
{
|
|
foreach (var sq in squares) Destroy(sq.gameObject);
|
|
}
|
|
|
|
int count = Mathf.Clamp(soldiers, 1, MaxSquares);
|
|
int rows = Mathf.CeilToInt(count / (float)SquaresPerRow);
|
|
squares = new Renderer[count];
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int col = i % SquaresPerRow;
|
|
int row = i / SquaresPerRow;
|
|
int inThisRow = Mathf.Min(SquaresPerRow, count - row * SquaresPerRow);
|
|
|
|
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
go.name = "Soldier";
|
|
Destroy(go.GetComponent<Collider>());
|
|
go.transform.SetParent(transform, false);
|
|
go.transform.localPosition = new Vector3(
|
|
(col - (inThisRow - 1) / 2f) * SquareSpacing,
|
|
SquareSize / 2f,
|
|
((rows - 1) / 2f - row) * SquareSpacing);
|
|
go.transform.localScale = Vector3.one * SquareSize;
|
|
|
|
var r = go.GetComponent<Renderer>();
|
|
r.sharedMaterial = Map3D.Mat(color);
|
|
squares[i] = r;
|
|
}
|
|
}
|
|
|
|
// Линия от города до цели -- видно, куда идёт войско. Лежит отдельно, чтобы не ездить вместе с отрядом.
|
|
void CreateRouteLine(WorldSimulation world, MarchData march)
|
|
{
|
|
Vector3 home = WorldMapController.ToWorld(world.GetNode(march.fromNodeId));
|
|
Vector3 target = WorldMapController.ToWorld(world.GetNode(march.targetNodeId));
|
|
Vector3 delta = target - home;
|
|
|
|
// Тонкая полоса на земле от города до цели
|
|
routeLine = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
routeLine.name = $"Route_{march.id}";
|
|
Destroy(routeLine.GetComponent<Collider>());
|
|
routeLine.transform.SetParent(transform.parent, false);
|
|
routeLine.transform.position = (home + target) / 2f + Vector3.up * 0.02f;
|
|
routeLine.transform.localScale = new Vector3(delta.magnitude, 0.02f, 0.08f);
|
|
routeLine.transform.rotation = Quaternion.Euler(0, -Mathf.Atan2(delta.z, delta.x) * Mathf.Rad2Deg, 0);
|
|
routeLine.GetComponent<Renderer>().sharedMaterial = Map3D.Mat(new Color(0.85f, 0.85f, 0.8f));
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (routeLine != null) Destroy(routeLine);
|
|
}
|
|
|
|
public void Refresh(WorldSimulation world, MarchData march, long nowMs)
|
|
{
|
|
if (routeLine == null) CreateRouteLine(world, march);
|
|
|
|
world.GetMarchSegment(march, nowMs, out MapNode from, out MapNode to, out double progress);
|
|
transform.position = Vector3.Lerp(WorldMapController.ToWorld(from), WorldMapController.ToWorld(to), (float)progress);
|
|
|
|
bool returning = march.state == MarchState.Returning;
|
|
int soldiers = returning ? march.survivors : march.soldiers;
|
|
|
|
if (soldiers != shownSoldiers || march.state != shownState)
|
|
{
|
|
shownSoldiers = soldiers;
|
|
shownState = march.state;
|
|
RebuildSquares(soldiers, returning ? ReturningColor : OutboundColor);
|
|
}
|
|
|
|
bool gathering = march.state == MarchState.Gathering;
|
|
long endMs = returning ? march.returnMs : gathering ? march.gatherEndMs : march.arriveMs;
|
|
long secondsLeft = System.Math.Max(0, (endMs - nowMs + 999) / 1000);
|
|
string direction = returning ? (march.cargo > 0 ? $"домой, груз {march.cargo}" : "домой")
|
|
: gathering ? $"сбор {march.cargo}"
|
|
: march.scout ? "разведка" : march.gather ? "на сбор" : "в бой";
|
|
// Марши идут часами (регламент 31.10) -- «1:05:30», меньше часа -- «12:30»
|
|
string time = secondsLeft >= 3600
|
|
? $"{secondsLeft / 3600}:{secondsLeft / 60 % 60:00}:{secondsLeft % 60:00}"
|
|
: $"{secondsLeft / 60}:{secondsLeft % 60:00}";
|
|
label.text = $"{soldiers} сол. {direction} {time}";
|
|
}
|
|
}
|