using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; // ВОЙСКА: армия, очередь обучения (идёт и когда игра закрыта), найм воинов за ресурсы, // ускорение за кристаллы. Найм -- карточки войск в ряд (листаются вбок): иконка, сила, цена, // против кого силён и слаб (из правил боя). Новые типы войск открывают уровни Ратуши и Казарм. public class TroopsPanel : BasePanel { readonly List queueBars = new List(); readonly List queueTimes = new List(); 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().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(); 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(); 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, "Силён против: " + strong, 22, UIKit.Theme.Text); if (weak.Length > 0) UIKit.Text(card, "Слаб против: " + 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(); var w = new List(); 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().color = new Color(0, 0, 0, 0); var scroll = root.gameObject.AddComponent(); 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(); 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(); 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().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); } }