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(); 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(); 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(); } // Перестраиваем квадратики, только когда поменялось число солдат (после боя) 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()); 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(); 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()); 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().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}"; } }