IWorld.TryStartScout, MarchData.scout, WorldEventType.ScoutArrived; кнопка «Разведка» в окне атаки; разведка не разбивает лагерь и не занимает точку. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
9.6 KiB
C#
205 lines
9.6 KiB
C#
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 "В"; // возвращается
|
||
}
|
||
}
|
||
}
|
||
}
|