Files
MVP1/Assets/Scripts/WorldMap/ArmiesWindow.cs
T

205 lines
9.6 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using Bogatyri.World;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
// Окно «Армии» (слой приложения): где каждое войско игрока, что делает, состав, груз и сколько ждать.
// Регламент 31.10, раздел 7: «Панель армий нужна всем игрокам: местонахождение, действие, состав, груз,
// время марша, добычи и ожидаемого возвращения». Данные -- модули Мира (походы) и Армии (дома, гарнизон, слоты).
// Документ: Docs/Modules/World.md
//
// Открыть: ArmiesWindow.Open() -- кнопка «Армии» на карте мира. Таймеры обновляются сами, пока окно открыто.
public static class ArmiesWindow
{
const float ListHeight = 620f;
static GameObject root;
public static bool IsOpen => root != null;
public static void Open()
{
Close();
if (MarchManager.Instance == null) return;
RectTransform card = UIKit.Modal("Армии", out root);
var view = root.AddComponent<ArmiesView>();
view.Build(card);
var close = UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, Close, -1, 90, 30);
UIKit.Size(close, -1, 90);
}
public static void Close()
{
if (root != null) Object.Destroy(root);
root = null;
}
// Живая часть окна: строки походов обновляются 2 раза в секунду, список -- когда походы меняются
class ArmiesView : MonoBehaviour
{
TextMeshProUGUI summary;
RectTransform list;
readonly List<(int marchId, TextMeshProUGUI text)> rows = new List<(int, TextMeshProUGUI)>();
string shownKey;
float nextRefresh;
WorldSimulation World => MarchManager.Instance.World;
public void Build(RectTransform card)
{
summary = UIKit.Text(card, "", 30, UIKit.Theme.Text, TextAlignmentOptions.Center);
var holder = UIKit.Make("ListHolder", card);
UIKit.Size(holder, -1, ListHeight);
list = UIKit.ScrollList(holder, out ScrollRect _, 10);
Refresh();
}
void Update()
{
if (Time.unscaledTime < nextRefresh) return;
nextRefresh = Time.unscaledTime + 0.5f;
Refresh();
}
void Refresh()
{
long now = MarchManager.NowMs;
var mine = new List<MarchData>();
foreach (var m in World.Marches)
if (m.ownerId == MarchManager.LocalPlayerId) mine.Add(m);
// Дома: свободные, гарнизон, слоты похода
if (GameServices.TryGet(out IArmy army))
summary.text = $"Дома свободно: {army.AvailableForMarch} • гарнизон: {army.GarrisonTotal} • " +
$"походов: {army.ActiveMarches} из {army.MarchSlots}";
// Состав списка поменялся -- строим заново, иначе только обновляем тексты
string key = string.Join(",", mine.ConvertAll(m => m.id + ":" + (int)m.state));
if (key != shownKey)
{
shownKey = key;
UIKit.Clear(list);
rows.Clear();
if (mine.Count == 0)
UIKit.Text(list, "Все войска дома. Отправьте войско: нажмите на цель на карте.", 26, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
foreach (var m in mine)
{
var row = UIKit.Row(list, 200, 16, UIKit.Theme.Card, 16); // крупно -- читается на телефоне
UIKit.Icon(row, StateColor(m), StateLetter(m), 96);
var text = UIKit.Text(row, "", 28, UIKit.Theme.Text);
UIKit.Size(text, -1, 188, 1);
rows.Add((m.id, text));
}
}
foreach (var (id, text) in rows)
{
MarchData m = mine.Find(x => x.id == id);
if (m != null) text.text = Describe(m, now);
}
}
// Четыре строки: куда и что делает, состав, груз, сколько ждать
string Describe(MarchData m, long now)
{
string target = World.TargetName(m);
int soldiers = m.state == MarchState.Returning ? m.survivors : m.soldiers;
int[] units = m.state == MarchState.Returning && m.survivorUnits != null ? m.survivorUnits : m.units;
string action;
string timer;
switch (m.state)
{
case MarchState.Outbound:
action = m.scout ? $"идёт в разведку: <b>{target}</b>"
: m.gather ? $"идёт на сбор: <b>{target}</b>" : $"идёт в бой: <b>{target}</b>";
timer = $"до цели {Clock(m.arriveMs - now)}, вернётся примерно через {Clock(ExpectedReturn(m) - now)}";
break;
case MarchState.Gathering:
action = $"собирает: <b>{target}</b>";
timer = $"сбор ещё {Clock(m.gatherEndMs - now)}, вернётся через {Clock(ExpectedReturn(m) - now)}";
break;
default:
action = (m.scout ? "возвращается из разведки: " : m.gather ? "возвращается со сбора: "
: m.victory ? "возвращается с победой: " : "возвращается: ") + $"<b>{target}</b>";
timer = $"дома через {Clock(m.returnMs - now)}";
break;
}
string cargo = m.state == MarchState.Outbound
? (m.gather ? "груз: будет известен на точке" : "груз: нет")
: "груз: " + MarchManager.FormatLoot(m.state == MarchState.Gathering ? CargoAsLoot(m) : m.loot);
return $"{action}\n" +
$"<color=#C8C8C8>{soldiers} воинов: {UnitsText(units)}</color>\n" +
$"<color=#C8C8C8>{cargo}</color>\n" +
$"<color=#E8C15A>{timer}</color>";
}
// Когда войско будет дома: после боя -- обратный путь, после сбора -- сбор и обратный путь
long ExpectedReturn(MarchData m)
{
if (m.state == MarchState.Returning) return m.returnMs;
MapNode home = World.GetNode(m.fromNodeId);
MapNode target = World.GetNode(m.targetNodeId);
if (home == null || target == null) return m.returnMs;
if (m.state == MarchState.Gathering) return m.gatherEndMs + World.TravelTimeMs(target, home, m.units);
long gather = 0;
if (m.gather)
{
World.PreviewGather(m.targetNodeId, m.units, m.arriveMs, out _, out gather);
}
return m.arriveMs + gather + World.TravelTimeMs(target, home, m.units); // после боя -- если все живы
}
List<RewardEntry> CargoAsLoot(MarchData m)
{
ResourcePoint p = World.GetPoint(m.targetNodeId);
var loot = new List<RewardEntry>();
if (p != null && m.cargo > 0) loot.Add(new RewardEntry { resource = ResourcePointRules.Resources[p.kind], amount = m.cargo });
return loot;
}
static string UnitsText(int[] u)
{
if (u == null || u.Length < TroopType.Count) return "состав неизвестен";
var parts = new List<string>();
if (u[TroopType.Infantry] > 0) parts.Add($"пех. {u[TroopType.Infantry]}");
if (u[TroopType.Archers] > 0) parts.Add($"луч. {u[TroopType.Archers]}");
if (u[TroopType.Cavalry] > 0) parts.Add($"кав. {u[TroopType.Cavalry]}");
if (u[TroopType.Siege] > 0) parts.Add($"осад. {u[TroopType.Siege]}");
return parts.Count > 0 ? string.Join(", ", parts) : "никого";
}
// Реальное время: «1 ч 05 мин», меньше часа -- «12:30»
static string Clock(long ms)
{
long sec = System.Math.Max(0, (ms + 999) / 1000);
if (sec >= 3600) return $"{sec / 3600} ч {sec / 60 % 60:00} мин";
return $"{sec / 60}:{sec % 60:00}";
}
static Color StateColor(MarchData m)
{
switch (m.state)
{
case MarchState.Outbound: return m.scout ? new Color(0.45f, 0.45f, 0.55f) : m.gather ? new Color(0.25f, 0.55f, 0.3f) : new Color(0.7f, 0.25f, 0.2f);
case MarchState.Gathering: return new Color(0.75f, 0.6f, 0.15f);
default: return new Color(0.25f, 0.45f, 0.75f);
}
}
static string StateLetter(MarchData m)
{
switch (m.state)
{
case MarchState.Outbound: return m.scout ? "Р" : m.gather ? "С" : "Б"; // разведка / на сбор / в бой
case MarchState.Gathering: return "Д"; // добыча
default: return "В"; // возвращается
}
}
}
}