Files
MVP1/Assets/Scripts/WorldMap/MarchView.cs
T
AlexanderandClaude Opus 5.5 ae9c141b49 Ресурсные точки на карте мира (регламент 31.10, наша часть по решению №46)
Мир (Bogatyri.World.Simulation, WorldSimulation.Points.cs):
- точки дерева, камня, металла, золота трёх размеров; запас, добыча воина за игровой час
  (1 игровой час = 5 реальных минут), груз на воина, срок жизни 12/24/48 реальных часов — ТЕСТ
- сбор: дошли → стоят на точке (MarchState.Gathering) до полного груза, истощения или исчезновения точки,
  затем физический возврат с грузом; всё считается в момент прихода — работает и офлайн
- осадные груза не несут; одна точка — одно войско; точка исчезла до прихода — возврат без добычи
- истощённая/исчезнувшая точка заменяется новой того же ресурса в другом месте (детерминированно)
- события GatherStarted, GatherFinished; порядок событий по времени с учётом сбора и исчезновения точек
- сохранение в секции world (points); старые сохранения без точек читаются

Зачисление груза — Экономика через IEconomy (как трофеи боя), код Валдемара не менялся.
Таблица WorldResourceConfig (Assets/Resources/WorldResourceConfig.asset).

Карта мира: точки — ромбы цвета ресурса с запасом и сроком; окно отправки с запасом, добычей,
грузом и прогнозом «унесут N за T»; окно «ТОЧКИ НЕТ»; подпись отряда «сбор», время с часами.
Город: окно «ВОЙСКО ВЕРНУЛОСЬ» пишет «Сбор … ресурсы собраны».

Открыто (не задано регламентом): число точек на карте (сейчас по 2 каждого ресурса), выбор размера,
предел добычи на точку. Позже: нападение на занятую точку, «Связь Волхвов», переполнение склада.

Тесты: появление и расстановка, сбор и груз, истощение и новая точка, осадные без груза, исчезновение
до прихода и во время сбора, одна точка — одно войско, сохранение посреди сбора. Все 54 теста проходят вне Unity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:28:50 +01:00

135 lines
5.6 KiB
C#

using Bogatyri.World;
using TMPro;
using UnityEngine;
// Войско в походе на карте: группа маленьких квадратиков + подпись с числом солдат и таймером.
// Сам ничего не решает -- позицию и состояние берёт из 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 SpriteRenderer[] 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, 0.75f, 0);
label = go.AddComponent<TextMeshPro>();
label.fontSize = 3f;
label.alignment = TextAlignmentOptions.Center;
label.rectTransform.sizeDelta = new Vector2(4f, 0.8f);
label.sortingOrder = 31;
}
// Перестраиваем квадратики, только когда поменялось число солдат (после боя)
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 SpriteRenderer[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 = new GameObject("Soldier");
go.transform.SetParent(transform, false);
go.transform.localPosition = new Vector3(
(col - (inThisRow - 1) / 2f) * SquareSpacing,
((rows - 1) / 2f - row) * SquareSpacing,
0f);
go.transform.localScale = Vector3.one * SquareSize;
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = PrimitiveSprites.Square;
sr.color = color;
sr.sortingOrder = 30;
squares[i] = sr;
}
}
// Линия от города до цели -- видно, куда идёт войско. Лежит отдельно, чтобы не ездить вместе с отрядом.
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 = new GameObject($"Route_{march.id}");
routeLine.transform.SetParent(transform.parent, false);
routeLine.transform.position = (home + target) / 2f;
routeLine.transform.localScale = new Vector3(delta.magnitude, 0.08f, 1f);
routeLine.transform.rotation = Quaternion.Euler(0, 0, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
var sr = routeLine.AddComponent<SpriteRenderer>();
sr.sprite = PrimitiveSprites.Square;
sr.color = new Color(1f, 1f, 1f, 0.35f);
sr.sortingOrder = -5;
}
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.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}";
}
}