diff --git a/Assets/Scripts/Meta/Panels/TroopsPanel.cs b/Assets/Scripts/Meta/Panels/TroopsPanel.cs index 73f64d4..b0b7f14 100644 --- a/Assets/Scripts/Meta/Panels/TroopsPanel.cs +++ b/Assets/Scripts/Meta/Panels/TroopsPanel.cs @@ -4,7 +4,8 @@ using UnityEngine.UI; using TMPro; // ВОЙСКА: армия, очередь обучения (идёт и когда игра закрыта), найм воинов за ресурсы, -// ускорение за кристаллы. Новые типы войск открываются уровнем Ратуши. +// ускорение за кристаллы. Найм -- карточки войск в ряд (листаются вбок): иконка, сила, цена, +// против кого силён и слаб (из правил боя). Новые типы войск открывают уровни Ратуши и Казарм. public class TroopsPanel : BasePanel { readonly List queueBars = new List(); @@ -101,37 +102,133 @@ public class TroopsPanel : BasePanel } Tick(); - // --- Типы войск --- - for (int i = 0; i < GameData.Troops.Count; i++) + // --- Типы войск: карточки в ряд, листаются вбок --- + 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) { - int idx = i; - var def = GameData.Troops[i]; - bool unlocked = GP.TroopUnlocked(i); - - var card = UIKit.Card(list, unlocked ? UIKit.Theme.Card : UIKit.Theme.CardDark); - var top = UIKit.Row(card, 120, 16); - UIKit.Icon(top, unlocked ? def.color : UIKit.Theme.Gray, def.name.Substring(0, 1), 110); - var info = UIKit.Card(top, new Color(0, 0, 0, 0), 0, 2); - UIKit.Size(info, -1, -1, 1); - UIKit.Text(info, def.name + " в армии: " + GP.TroopCount(i).ToString("N0") + "", 32).fontStyle = FontStyles.Bold; - UIKit.Text(info, def.description, 22, UIKit.Theme.TextDim); - UIKit.Text(info, "Сила " + def.power + " • " + def.secondsPerUnit + " сек/воин • " + GameData.RewardsText(def.cost), 22, UIKit.Theme.TextDim); - - if (!unlocked) - { - UIKit.Text(card, "Откроется: " + GP.TroopLockText(i), 26, UIKit.Theme.Gold, TextAlignmentOptions.Center); - continue; - } - - var btns = UIKit.Row(card, 80, 10); - 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, 80, 30); - UIKit.Size(b, -1, 80, 1); - } + 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 сообщает об изменении diff --git a/Assets/Scripts/Meta/UI/NestedScrollRect.cs b/Assets/Scripts/Meta/UI/NestedScrollRect.cs new file mode 100644 index 0000000..ef9f4da --- /dev/null +++ b/Assets/Scripts/Meta/UI/NestedScrollRect.cs @@ -0,0 +1,56 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +// Прокрутка внутри прокрутки (например, лента карточек вбок внутри вертикального списка). +// Свайп в сторону своей оси листает эту ленту, свайп поперёк и колесо мыши -- родительский список. +// Без этого палец на ленте "застревает": общий список не прокручивается. +public class NestedScrollRect : ScrollRect +{ + ScrollRect parentScroll; + bool routeToParent; + + protected override void Start() + { + base.Start(); + if (transform.parent != null) parentScroll = transform.parent.GetComponentInParent(); + } + + bool CrossAxis(PointerEventData e) + { + float dx = Mathf.Abs(e.delta.x), dy = Mathf.Abs(e.delta.y); + return horizontal && !vertical ? dy > dx : vertical && !horizontal ? dx > dy : false; + } + + public override void OnInitializePotentialDrag(PointerEventData e) + { + if (parentScroll != null) parentScroll.OnInitializePotentialDrag(e); + base.OnInitializePotentialDrag(e); + } + + public override void OnBeginDrag(PointerEventData e) + { + routeToParent = parentScroll != null && CrossAxis(e); + if (routeToParent) parentScroll.OnBeginDrag(e); + else base.OnBeginDrag(e); + } + + public override void OnDrag(PointerEventData e) + { + if (routeToParent) parentScroll.OnDrag(e); + else base.OnDrag(e); + } + + public override void OnEndDrag(PointerEventData e) + { + if (routeToParent) parentScroll.OnEndDrag(e); + else base.OnEndDrag(e); + routeToParent = false; + } + + public override void OnScroll(PointerEventData e) + { + if (parentScroll != null) parentScroll.OnScroll(e); + else base.OnScroll(e); + } +} diff --git a/Assets/Scripts/Meta/UI/NestedScrollRect.cs.meta b/Assets/Scripts/Meta/UI/NestedScrollRect.cs.meta new file mode 100644 index 0000000..06a637f --- /dev/null +++ b/Assets/Scripts/Meta/UI/NestedScrollRect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 59a54e4603712204d81f167a1c02873f \ No newline at end of file