Files
MVP1/Assets/Scripts/WorldMap/ArmiesWindow.cs
T
AlexanderandClaude Opus 5.5 33db41002e Поход «Разведка» (решения 31.10, п. 2): до цели и обратно без боя, сбора и потерь, обычный слот
IWorld.TryStartScout, MarchData.scout, WorldEventType.ScoutArrived; кнопка «Разведка» в окне атаки;
разведка не разбивает лагерь и не занимает точку.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:11:55 +01:00

205 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "В"; // возвращается
}
}
}
}