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(); 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(); 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 ? $"идёт в разведку: {target}" : m.gather ? $"идёт на сбор: {target}" : $"идёт в бой: {target}"; timer = $"до цели {Clock(m.arriveMs - now)}, вернётся примерно через {Clock(ExpectedReturn(m) - now)}"; break; case MarchState.Gathering: action = m.scout ? $"наблюдает за: {target} (разведка {(m.reconLevel == 3 ? "III" : "II")})" : $"собирает: {target}"; timer = $"сбор ещё {Clock(m.gatherEndMs - now)}, вернётся через {Clock(ExpectedReturn(m) - now)}"; break; default: action = (m.scout ? "возвращается из разведки: " : m.gather ? "возвращается со сбора: " : m.victory ? "возвращается с победой: " : "возвращается: ") + $"{target}"; 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" + $"{soldiers} воинов: {UnitsText(units)}\n" + $"{cargo}\n" + $"{timer}"; } // Когда войско будет дома: после боя -- обратный путь, после сбора -- сбор и обратный путь 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 CargoAsLoot(MarchData m) { ResourcePoint p = World.GetPoint(m.targetNodeId); var loot = new List(); 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(); 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 "В"; // возвращается } } } }