256 lines
14 KiB
C#
256 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
|
||
// ВОЙСКА: армия, очередь обучения (идёт и когда игра закрыта), найм воинов за ресурсы,
|
||
// ускорение за кристаллы. Найм -- карточки войск в ряд (листаются вбок): иконка, сила, цена,
|
||
// против кого силён и слаб (из правил боя). Новые типы войск открывают уровни Ратуши и Казарм.
|
||
public class TroopsPanel : BasePanel
|
||
{
|
||
readonly List<Image> queueBars = new List<Image>();
|
||
readonly List<TextMeshProUGUI> queueTimes = new List<TextMeshProUGUI>();
|
||
Button speedUp;
|
||
|
||
static readonly int[] Amounts = { 1, 10, 50 };
|
||
|
||
protected override void Build(RectTransform root)
|
||
{
|
||
var d = GP.Data;
|
||
queueBars.Clear(); queueTimes.Clear(); speedUp = null;
|
||
var list = ScrollList(root);
|
||
|
||
// --- Армия ---
|
||
var army = UIKit.Card(list, UIKit.Theme.CardDark);
|
||
ProfilePanel.Header(army, "Армия");
|
||
ProfilePanel.KV(army, "Всего воинов в городе", GP.TotalTroops.ToString("N0"));
|
||
ProfilePanel.KV(army, "В походе на карте мира", GameServices.Get<IArmy>().TroopsInMarches.ToString("N0"));
|
||
if (GameServices.TryGet(out IPopulation population))
|
||
{
|
||
// Воины берутся из свободных жителей (модуль Населения), резерв не нанять
|
||
ProfilePanel.KV(army, "Обучаются в Казармах", population.InTraining.ToString("N0"));
|
||
ProfilePanel.KV(army, "Можно нанять из жителей", population.Recruitable.ToString("N0") +
|
||
" (резерв " + population.CivilianReserve.ToString("N0") + ")");
|
||
}
|
||
// Раненые (решения 31.10, п. 7): лечатся в госпитале или ждут места; не воюют, но считаются людьми
|
||
IArmy hurt = GameServices.Get<IArmy>();
|
||
if (hurt.WoundedTotal > 0)
|
||
{
|
||
int waiting = 0, healingNow = 0;
|
||
long readyMs = long.MaxValue;
|
||
for (int t = 0; t < hurt.TypeCount; t++) waiting += hurt.GetWoundedWaiting(t);
|
||
foreach (var h in hurt.Healing) { healingNow += h.Count; readyMs = System.Math.Min(readyMs, h.EndMs); }
|
||
string next = readyMs != long.MaxValue
|
||
? ", ближайшие вылечатся через " + GameData.FormatTime(System.Math.Max(0, readyMs - GameClock.UnixNowMs) / 1000.0)
|
||
: "";
|
||
ProfilePanel.KV(army, "Раненые", hurt.WoundedTotal.ToString("N0") + " (лечатся " + healingNow + ", ждут места " + waiting + ")");
|
||
ProfilePanel.KV(army, "Госпиталь", "мест " + hurt.HospitalBeds + next);
|
||
}
|
||
ProfilePanel.KV(army, "Сила войск", GP.TroopsPower.ToString("N0"));
|
||
ProfilePanel.KV(army, "Сила армии с героем", GP.ArmyPower.ToString("N0"));
|
||
UIKit.Button(army, "Отправить армию на карте мира", UIKit.Theme.Green, () => OpenPanel("World"), -1, 80, 26);
|
||
|
||
// --- Гарнизон: часть воинов охраняет город и в походы не уходит ---
|
||
IArmy troopsArmy = GameServices.Get<IArmy>();
|
||
var garrison = UIKit.Card(list, UIKit.Theme.CardDark);
|
||
ProfilePanel.Header(garrison, "Гарнизон города");
|
||
ProfilePanel.KV(garrison, "В гарнизоне", troopsArmy.GarrisonTotal.ToString("N0") + " из " + troopsArmy.SoldiersAtHome.ToString("N0"));
|
||
ProfilePanel.KV(garrison, "Свободны для походов", troopsArmy.AvailableForMarch.ToString("N0"));
|
||
UIKit.Text(garrison, "Гарнизон защищает город и не уходит в походы. Воины в нём не пропадают: всего воинов столько же.", 22, UIKit.Theme.TextDim);
|
||
var gRow = UIKit.Row(garrison, 80, 10);
|
||
GarrisonButton(gRow, troopsArmy, "0", 0);
|
||
GarrisonButton(gRow, troopsArmy, "-10", troopsArmy.GarrisonTotal - 10);
|
||
GarrisonButton(gRow, troopsArmy, "-1", troopsArmy.GarrisonTotal - 1);
|
||
GarrisonButton(gRow, troopsArmy, "+1", troopsArmy.GarrisonTotal + 1);
|
||
GarrisonButton(gRow, troopsArmy, "+10", troopsArmy.GarrisonTotal + 10);
|
||
GarrisonButton(gRow, troopsArmy, "Все", troopsArmy.SoldiersAtHome);
|
||
|
||
// --- Нет казарм -- нанимать негде ---
|
||
if (Barracks.Count == 0)
|
||
{
|
||
var warn = UIKit.Card(list, new Color(0.35f, 0.18f, 0.12f));
|
||
UIKit.Text(warn, "Нанимать войска можно только в Казармах. Постройте их на свободном участке.", 28, UIKit.Theme.Text);
|
||
UIKit.Button(warn, "ПОСТРОИТЬ КАЗАРМЫ", UIKit.Theme.Green, () =>
|
||
{
|
||
if (BuildModeController.Instance != null) BuildModeController.Instance.SetMode(true);
|
||
else UIKit.Toast("Нажмите кнопку СТРОЙКА");
|
||
}, -1, 96, 32);
|
||
}
|
||
|
||
// --- Очередь обучения ---
|
||
var queue = UIKit.Card(list);
|
||
var training = GP.Training;
|
||
ProfilePanel.Header(queue, "Обучение (" + training.Count + " / " + Barracks.QueueSize + ") • казарм: " + Barracks.Count);
|
||
if (training.Count == 0)
|
||
{
|
||
UIKit.Text(queue, "Никто не обучается. Выберите войска ниже.", 26, UIKit.Theme.TextDim);
|
||
}
|
||
else
|
||
{
|
||
foreach (var o in training)
|
||
{
|
||
var def = GameData.Troops[o.Type];
|
||
var r = UIKit.Row(queue, 44, 10);
|
||
var n = UIKit.Text(r, def.name + " × " + o.Count, 28);
|
||
UIKit.Size(n, -1, -1, 1);
|
||
var t = UIKit.Text(r, "", 26, UIKit.Theme.Gold, TextAlignmentOptions.Right);
|
||
UIKit.Size(t, 180);
|
||
queueTimes.Add(t);
|
||
queueBars.Add(UIKit.Bar(queue, 0, def.color, 20));
|
||
}
|
||
speedUp = UIKit.Button(queue, "", UIKit.Theme.Blue, () => GP.SpeedUpTraining(), -1, 80);
|
||
}
|
||
Tick();
|
||
|
||
// --- Типы войск: карточки в ряд, листаются вбок ---
|
||
var troopsCard = UIKit.Card(list, UIKit.Theme.CardDark);
|
||
ProfilePanel.Header(troopsCard, "Найм войск");
|
||
UIKit.Text(troopsCard, "Листайте карточки вбок →", 22, UIKit.Theme.TextDim);
|
||
var strip = HorizontalStrip(troopsCard, CardHeight);
|
||
for (int i = 0; i < GameData.Troops.Count; i++) TroopCard(strip, i);
|
||
}
|
||
|
||
// ---------------- Карточка войска ----------------
|
||
// Место под иконку: пока квадрат цвета войска с первой буквой. Картинку поставим в TroopIcon, когда будет арт.
|
||
// "Силён / слаб против" берётся из правил боя (контры в модуле Боя и BattleConfig), а не пишется руками:
|
||
// поменяют контры -- поменяется и текст.
|
||
|
||
const float CardWidth = 340f;
|
||
const float CardHeight = 640f;
|
||
|
||
void TroopCard(Transform strip, int i)
|
||
{
|
||
int idx = i;
|
||
var def = GameData.Troops[i];
|
||
bool unlocked = GP.TroopUnlocked(i);
|
||
|
||
var card = UIKit.Card(strip, unlocked ? UIKit.Theme.Card : UIKit.Theme.CardDark, 16, 8);
|
||
UIKit.Size(card, CardWidth, -1);
|
||
|
||
var iconRow = UIKit.Row(card, 130, 0);
|
||
var lpad = UIKit.Make("Pad", iconRow); UIKit.Size(lpad, -1, -1, 1);
|
||
TroopIcon(iconRow, i, unlocked);
|
||
var rpad = UIKit.Make("Pad", iconRow); UIKit.Size(rpad, -1, -1, 1);
|
||
|
||
UIKit.Text(card, def.name, 32, UIKit.Theme.Text, TextAlignmentOptions.Center).fontStyle = FontStyles.Bold;
|
||
UIKit.Text(card, "в армии: " + GP.TroopCount(i).ToString("N0"), 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||
UIKit.Text(card, "Сила " + def.power + " • " + def.secondsPerUnit + " сек/воин", 22, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||
UIKit.Text(card, GameData.RewardsText(def.cost), 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||
|
||
string strong, weak;
|
||
CounterText(i, out strong, out weak);
|
||
if (strong.Length > 0) UIKit.Text(card, "<color=#9be59b>Силён против:</color> " + strong, 22, UIKit.Theme.Text);
|
||
if (weak.Length > 0) UIKit.Text(card, "<color=#ff8a80>Слаб против:</color> " + weak, 22, UIKit.Theme.Text);
|
||
UIKit.Text(card, def.description, 20, UIKit.Theme.TextDim);
|
||
|
||
var fill = UIKit.Make("Fill", card); UIKit.Size(fill, -1, -1).flexibleHeight = 1;
|
||
|
||
if (!unlocked)
|
||
{
|
||
UIKit.Text(card, "Откроется:\n" + GP.TroopLockText(i), 24, UIKit.Theme.Gold, TextAlignmentOptions.Center);
|
||
return;
|
||
}
|
||
|
||
var btns = UIKit.Row(card, 70, 8);
|
||
foreach (int amount in Amounts)
|
||
{
|
||
int a = amount;
|
||
bool can = Barracks.Count > 0 && GP.CanAfford(def.cost, a) && GP.Training.Count < Barracks.QueueSize && GP.Recruitable >= a;
|
||
var b = UIKit.Button(btns, "+" + a, can ? UIKit.Theme.Green : UIKit.Theme.Gray, () => GP.Train(idx, a), -1, 70, 26);
|
||
UIKit.Size(b, -1, 70, 1);
|
||
}
|
||
}
|
||
|
||
// Иконка войска. Сейчас -- заглушка (цветной квадрат с буквой); сюда же потом подставим спрайт.
|
||
static void TroopIcon(Transform parent, int i, bool unlocked)
|
||
{
|
||
var def = GameData.Troops[i];
|
||
UIKit.Icon(parent, unlocked ? def.color : UIKit.Theme.Gray, def.name.Substring(0, 1), 120);
|
||
}
|
||
|
||
static readonly string[] TroopNamesGenitive = { "пехоты", "лучников", "кавалерии", "осадных" };
|
||
|
||
// Контры из правил боя: против кого этот тип сильнее и кто сильнее против него
|
||
static void CounterText(int type, out string strong, out string weak)
|
||
{
|
||
var s = new List<string>();
|
||
var w = new List<string>();
|
||
var battle = GameServices.TryGet(out IBattleSystem b) ? b as BattleSystem : null;
|
||
if (battle != null)
|
||
for (int j = 0; j < TroopType.Count && j < GameData.Troops.Count; j++)
|
||
{
|
||
if (j == type) continue;
|
||
int mine = battle.Counter(type, j), theirs = battle.Counter(j, type);
|
||
if (mine > 0) s.Add(Genitive(j) + " (+" + mine + "% силы)");
|
||
if (theirs > 0) w.Add(Genitive(j));
|
||
}
|
||
if (type == TroopType.Siege)
|
||
{
|
||
var cfg = BattleConfig.Load();
|
||
if (cfg.siegeVsWallsPercent > 0) s.Add("стен при штурме (+" + cfg.siegeVsWallsPercent + "%)");
|
||
if (cfg.siegeInFieldPercent < 0) w.Add("всех в поле и в обороне (" + cfg.siegeInFieldPercent + "%)");
|
||
}
|
||
strong = string.Join(", ", s.ToArray());
|
||
weak = string.Join(", ", w.ToArray());
|
||
}
|
||
|
||
static string Genitive(int type)
|
||
{
|
||
return type >= 0 && type < TroopNamesGenitive.Length ? TroopNamesGenitive[type] : GameData.Troops[type].name;
|
||
}
|
||
|
||
// Горизонтальная лента с прокруткой вбок внутри общего вертикального списка.
|
||
// Вертикальный свайп по ленте прокручивает весь список (NestedScrollRect).
|
||
static RectTransform HorizontalStrip(Transform parent, float height)
|
||
{
|
||
var root = UIKit.Make("TroopStrip", parent);
|
||
UIKit.Size(root, -1, height);
|
||
root.gameObject.AddComponent<Image>().color = new Color(0, 0, 0, 0);
|
||
var scroll = root.gameObject.AddComponent<NestedScrollRect>();
|
||
scroll.horizontal = true;
|
||
scroll.vertical = false;
|
||
scroll.movementType = ScrollRect.MovementType.Elastic;
|
||
scroll.scrollSensitivity = 40f;
|
||
|
||
var viewport = UIKit.Stretch(UIKit.Make("Viewport", root));
|
||
viewport.gameObject.AddComponent<RectMask2D>();
|
||
|
||
var content = UIKit.Make("Content", viewport);
|
||
content.anchorMin = new Vector2(0, 0); content.anchorMax = new Vector2(0, 1);
|
||
content.pivot = new Vector2(0, 0.5f);
|
||
content.offsetMin = Vector2.zero; content.offsetMax = Vector2.zero;
|
||
var h = content.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
h.spacing = 14;
|
||
h.padding = new RectOffset(4, 4, 4, 4);
|
||
h.childControlWidth = true; h.childControlHeight = true;
|
||
h.childForceExpandWidth = false; h.childForceExpandHeight = true;
|
||
content.gameObject.AddComponent<ContentSizeFitter>().horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||
|
||
scroll.viewport = viewport;
|
||
scroll.content = content;
|
||
return content;
|
||
}
|
||
|
||
// Окно перестраивается само: Армия публикует TroopsChangedEvent, GameProgress сообщает об изменении
|
||
static void GarrisonButton(Transform row, IArmy army, string label, int target)
|
||
{
|
||
target = Mathf.Clamp(target, 0, army.SoldiersAtHome);
|
||
bool can = target != army.GarrisonTotal;
|
||
var b = UIKit.Button(row, label, can ? UIKit.Theme.Blue : UIKit.Theme.Gray, () => { if (can) army.SetGarrison(target); }, -1, 80, 28);
|
||
UIKit.Size(b, -1, 80, 1);
|
||
}
|
||
|
||
protected override void Tick()
|
||
{
|
||
var training = GP.Training;
|
||
for (int i = 0; i < queueBars.Count && i < training.Count; i++)
|
||
{
|
||
var o = training[i];
|
||
UIKit.SetBar(queueBars[i], GP.TrainingProgress(o));
|
||
queueTimes[i].text = GameData.FormatTime(GP.TrainingSecondsLeft(o));
|
||
}
|
||
if (speedUp != null && training.Count > 0)
|
||
UIKit.SetButton(speedUp, "Ускорить всё за " + GP.SpeedUpCost + " кристаллов", true);
|
||
}
|
||
}
|