diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index 6eaafe7..d541fac 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -245,8 +245,9 @@ список колод; магазин и декбилдер подключены. - [x] Очередь: classic-ranked, опрос, отмена, тикет, переход в матч. - [x] Простой декбилдер: фильтры (клан, грейд, триггер) в оверлее, - сброс, статистика триггеров/грейдов, коллекция сеткой, текущая колода, - создание/редактирование/сохранение, лимиты 50/4/owned. + сброс, сортировка клан→ранг→имя, карточная сетка с количеством, + узкий список колод справа, редактор с вертикальным списком карт, + статистика по рангам и общему числу, добавление/удаление кликом. - [ ] Декбилдер: drag&drop, live-валидация, редкость/тип/арт, пресеты. - [x] Базовая коллекция: список, балансы, разбор в пыль с подтверждением; арты и «сколько в колодах» — дальше. diff --git a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs index f8e33ab..54a781c 100644 --- a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs +++ b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs @@ -12,8 +12,8 @@ using Vanguard.Meta; namespace Vanguard.UI { /// - /// Простой декбилдер: коллекция слева, текущая колода справа, - /// фильтры в оверлее, статистика по триггерам и грейдам. + /// Декбилдер в духе HS: коллекция в центре, узкая колонка колод/карт справа. + /// Список колод и редактор используют один и тот же блок коллекции. /// public sealed class DeckBuilderView { @@ -21,13 +21,14 @@ namespace Vanguard.UI private readonly AuthService _auth; private readonly Action _onBack; private readonly Action _onPlay; - private readonly RectTransform _root; + private readonly RectTransform _listView; private readonly RectTransform _editorView; - private RectTransform _listContent; - private RectTransform _collectionContent; - private RectTransform _deckContent; + private RectTransform _deckListContent; + private RectTransform _listCollectionContent; + private RectTransform _editorCollectionContent; + private RectTransform _deckCardContent; private InputField _nameInput; private Text _listStatus; private Text _editorStatus; @@ -44,10 +45,12 @@ namespace Vanguard.UI private readonly List _collection = new List(); private readonly Dictionary _catalog = new Dictionary(StringComparer.Ordinal); - private readonly List _cards = new List(); - private readonly Dictionary _collectionCountTexts = + private readonly Dictionary _listCountTexts = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _editorCountTexts = + new Dictionary(StringComparer.Ordinal); + private DeckDto _editing; private string _filterClan = "Все"; private string _filterTrigger = "Все"; @@ -68,11 +71,11 @@ namespace Vanguard.UI _onPlay = onPlay; _root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f)); - _listView = BuildListView(); _editorView = BuildEditorView(); _filterOverlay = BuildFilterOverlay(); _confirmOverlay = BuildDeleteConfirm(); + _listView.gameObject.SetActive(false); _editorView.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false); @@ -98,35 +101,83 @@ namespace Vanguard.UI { var view = UiKit.CreatePanel(_root, "DeckList", new Color(0.018f, 0.03f, 0.06f, 0.97f)); - var top = BuildTopBar(view, "Колоды", out _); - var create = UiKit.CreateButton(top, "Create", "Создать колоду", NewDeck, 19); - UiKit.SetPreferredHeight(create, 46f, 0f); + var top = BuildTopBar(view); + var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 17); + SetFixedSize(back, 120f, 44f); + + var title = UiKit.CreateText(top, "Title", "Колоды", 28, Color.white, TextAnchor.MiddleLeft); + title.fontStyle = FontStyle.Bold; + SetFixedSize(title, 150f, 44f); + + _filterSummary = UiKit.CreateText( + top, "FilterSummary", "Все карты", 16, + new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); + SetFlexible(_filterSummary, 1f, 44f); + + var filters = UiKit.CreateButton(top, "Filters", "Фильтры", OpenFilters, 17); + SetFixedSize(filters, 110f, 44f); + + var reset = UiKit.CreateButton(top, "ResetFilters", "Сброс", ResetFilters, 17); + SetFixedSize(reset, 90f, 44f); + + var create = UiKit.CreateButton(top, "Create", "Создать колоду", NewDeck, 17); + SetFixedSize(create, 180f, 44f); var body = UiKit.CreatePanel(view, "Body", new Color(0f, 0f, 0f, 0f)); body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one; - body.offsetMin = new Vector2(44f, 30f); - body.offsetMax = new Vector2(-44f, -110f); - UiKit.AddVerticalLayout(body, 8, 10); + body.offsetMin = new Vector2(34f, 28f); + body.offsetMax = new Vector2(-34f, -108f); + UiKit.AddHorizontalLayout(body, 0, 14); - var hint = UiKit.CreateText( - body, - "Hint", - "Откройте колоду для редактирования или создайте новую. «В бой» ставит колоду в очередь.", - 17, - new Color(0.60f, 0.68f, 0.84f), - TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(hint, 26f); + var collectionArea = UiKit.CreatePanel( + body, "CollectionArea", new Color(0.04f, 0.06f, 0.10f, 0.55f)); + var collectionElement = collectionArea.gameObject.AddComponent(); + collectionElement.flexibleWidth = 1f; + collectionElement.flexibleHeight = 1f; + collectionElement.minHeight = 300f; + UiKit.AddVerticalLayout(collectionArea, 8, 6); - var scroll = UiKit.CreateScrollView(body, "DeckListScroll", out _listContent, 8f); - var element = scroll.gameObject.AddComponent(); - element.flexibleHeight = 1f; - element.minHeight = 200f; + var scroll = UiKit.CreateGridScrollView( + collectionArea, + "CollectionGrid", + new Vector2(174f, 238f), + new Vector2(10f, 10f), + out _listCollectionContent); + var scrollElement = scroll.gameObject.AddComponent(); + scrollElement.flexibleHeight = 1f; + scrollElement.minHeight = 220f; _listStatus = UiKit.CreateText( - body, "Status", string.Empty, 18, + collectionArea, "Status", string.Empty, 16, new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(_listStatus, 34f); + UiKit.SetPreferredHeight(_listStatus, 32f); + + var deckArea = UiKit.CreatePanel( + body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f)); + var deckElement = deckArea.gameObject.AddComponent(); + deckElement.preferredWidth = 300f; + deckElement.minWidth = 280f; + deckElement.flexibleWidth = 0f; + deckElement.flexibleHeight = 1f; + deckElement.minHeight = 300f; + UiKit.AddVerticalLayout(deckArea, 10, 6); + + var deckTitle = UiKit.CreateText( + deckArea, "Title", "Мои колоды", 20, Color.white, TextAnchor.MiddleLeft); + deckTitle.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(deckTitle, 30f); + + var deckScroll = UiKit.CreateScrollView(deckArea, "DeckListScroll", out _deckListContent, 8f); + var deckScrollElement = deckScroll.gameObject.AddComponent(); + deckScrollElement.flexibleHeight = 1f; + deckScrollElement.minHeight = 200f; + + var hint = UiKit.CreateText( + deckArea, "Hint", "Нажмите на колоду,\nчтобы открыть редактор.", 14, + new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft); + hint.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.SetPreferredHeight(hint, 46f); return view; } @@ -135,6 +186,95 @@ namespace Vanguard.UI { var view = UiKit.CreatePanel(_root, "DeckEditor", new Color(0.018f, 0.03f, 0.06f, 0.97f)); + var top = BuildTopBar(view); + var back = UiKit.CreateButton(top, "Back", "← Колоды", BackToList, 17); + SetFixedSize(back, 120f, 44f); + + _nameInput = UiKit.CreateInput(top, "Name", "Название колоды"); + SetFlexible(_nameInput, 0.7f, 46f); + + var filters = UiKit.CreateButton(top, "Filters", "Фильтры", OpenFilters, 17); + SetFixedSize(filters, 110f, 44f); + + var reset = UiKit.CreateButton(top, "ResetFilters", "Сброс", ResetFilters, 17); + SetFixedSize(reset, 90f, 44f); + + _stats = UiKit.CreateText( + top, "Stats", string.Empty, 15, + new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft); + _stats.horizontalOverflow = HorizontalWrapMode.Wrap; + SetFlexible(_stats, 1.4f, 50f); + + var save = UiKit.CreateButton(top, "Save", "Сохранить", SaveDeck, 17); + SetFixedSize(save, 120f, 44f); + + var duel = UiKit.CreateButton(top, "Duel", "В бой", PlayDeck, 17); + SetFixedSize(duel, 90f, 44f); + + var delete = UiKit.CreateButton(top, "Delete", "Удалить", RequestDeleteCurrent, 17); + SetFixedSize(delete, 100f, 44f); + + var body = UiKit.CreatePanel(view, "Body", new Color(0f, 0f, 0f, 0f)); + body.anchorMin = Vector2.zero; + body.anchorMax = Vector2.one; + body.offsetMin = new Vector2(34f, 28f); + body.offsetMax = new Vector2(-34f, -108f); + UiKit.AddHorizontalLayout(body, 0, 14); + + var collectionArea = UiKit.CreatePanel( + body, "CollectionArea", new Color(0.04f, 0.06f, 0.10f, 0.55f)); + var collectionElement = collectionArea.gameObject.AddComponent(); + collectionElement.flexibleWidth = 1f; + collectionElement.flexibleHeight = 1f; + collectionElement.minHeight = 300f; + UiKit.AddVerticalLayout(collectionArea, 8, 6); + + var scroll = UiKit.CreateGridScrollView( + collectionArea, + "CollectionGrid", + new Vector2(174f, 238f), + new Vector2(10f, 10f), + out _editorCollectionContent); + var scrollElement = scroll.gameObject.AddComponent(); + scrollElement.flexibleHeight = 1f; + scrollElement.minHeight = 220f; + + _editorStatus = UiKit.CreateText( + collectionArea, "Status", string.Empty, 16, + new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft); + UiKit.SetPreferredHeight(_editorStatus, 32f); + + var deckArea = UiKit.CreatePanel( + body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f)); + var deckElement = deckArea.gameObject.AddComponent(); + deckElement.preferredWidth = 300f; + deckElement.minWidth = 280f; + deckElement.flexibleWidth = 0f; + deckElement.flexibleHeight = 1f; + deckElement.minHeight = 300f; + UiKit.AddVerticalLayout(deckArea, 10, 6); + + _deckCount = UiKit.CreateText( + deckArea, "DeckCount", "Колода: 0/50", 19, Color.white, TextAnchor.MiddleLeft); + _deckCount.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(_deckCount, 28f); + + var deckScroll = UiKit.CreateScrollView(deckArea, "DeckCardScroll", out _deckCardContent, 6f); + var deckScrollElement = deckScroll.gameObject.AddComponent(); + deckScrollElement.flexibleHeight = 1f; + deckScrollElement.minHeight = 200f; + + var hint = UiKit.CreateText( + deckArea, "Hint", "Нажмите на карту,\nчтобы убрать одну копию.", 14, + new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft); + hint.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.SetPreferredHeight(hint, 46f); + + return view; + } + + private RectTransform BuildTopBar(RectTransform view) + { var top = UiKit.CreatePanel(view, "TopBar", new Color(0.06f, 0.08f, 0.13f, 1f)); top.anchorMin = new Vector2(0f, 1f); top.anchorMax = new Vector2(1f, 1f); @@ -142,104 +282,12 @@ namespace Vanguard.UI top.anchoredPosition = Vector2.zero; top.sizeDelta = new Vector2(0f, 88f); UiKit.AddHorizontalLayout(top, 18, 12); - - var back = UiKit.CreateButton(top, "Back", "← Колоды", BackToCards, 18); - UiKit.SetPreferredHeight(back, 44f, 0f); - - var title = UiKit.CreateText(top, "Title", "Конструктор", 28, Color.white, TextAnchor.MiddleLeft); - title.fontStyle = FontStyle.Bold; - UiKit.SetPreferredHeight(title, 44f, 0f); - - _nameInput = UiKit.CreateInput(top, "Name", "Название колоды"); - UiKit.SetPreferredHeight(_nameInput, 46f); - - var save = UiKit.CreateButton(top, "Save", "Сохранить", SaveDeck, 18); - UiKit.SetPreferredHeight(save, 44f, 0f); - - var body = UiKit.CreatePanel(view, "Body", new Color(0f, 0f, 0f, 0f)); - body.anchorMin = Vector2.zero; - body.anchorMax = Vector2.one; - body.offsetMin = new Vector2(34f, 30f); - body.offsetMax = new Vector2(-34f, -106f); - UiKit.AddHorizontalLayout(body, 0, 14); - - var left = UiKit.CreatePanel(body, "CollectionArea", new Color(0.04f, 0.06f, 0.10f, 0.7f)); - var leftElement = left.gameObject.AddComponent(); - leftElement.flexibleWidth = 1f; - leftElement.flexibleHeight = 1f; - leftElement.minHeight = 300f; - UiKit.AddVerticalLayout(left, 10, 6); - - var filterRow = UiKit.CreatePanel(left, "FilterRow", new Color(0f, 0f, 0f, 0f)); - UiKit.SetPreferredHeight(filterRow, 48f); - UiKit.AddHorizontalLayout(filterRow, 0, 10); - - var filters = UiKit.CreateButton(filterRow, "Filters", "Фильтры", OpenFilters, 18); - UiKit.SetPreferredHeight(filters, 44f, 0f); - - var reset = UiKit.CreateButton(filterRow, "ResetFilters", "Сбросить фильтры", ResetFilters, 17); - UiKit.SetPreferredHeight(reset, 44f, 0f); - - _filterSummary = UiKit.CreateText( - filterRow, "FilterSummary", "Фильтры: все", 16, - new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(_filterSummary, 44f); - - var statsPanel = UiKit.CreatePanel(left, "Stats", new Color(0.06f, 0.08f, 0.13f, 1f)); - UiKit.SetPreferredHeight(statsPanel, 76f); - UiKit.AddVerticalLayout(statsPanel, 8, 2); - - _stats = UiKit.CreateText( - statsPanel, "Text", string.Empty, 16, - new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft); - _stats.horizontalOverflow = HorizontalWrapMode.Wrap; - UiKit.SetPreferredHeight(_stats, 56f); - - var gridScroll = UiKit.CreateGridScrollView( - left, - "CollectionGrid", - new Vector2(166f, 186f), - new Vector2(8f, 8f), - out _collectionContent); - var gridElement = gridScroll.gameObject.AddComponent(); - gridElement.flexibleHeight = 1f; - gridElement.minHeight = 220f; - - var right = UiKit.CreatePanel(body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f)); - var rightElement = right.gameObject.AddComponent(); - rightElement.preferredWidth = 430f; - rightElement.minWidth = 360f; - rightElement.flexibleWidth = 0f; - rightElement.flexibleHeight = 1f; - rightElement.minHeight = 300f; - UiKit.AddVerticalLayout(right, 10, 6); - - _deckCount = UiKit.CreateText( - right, "DeckCount", "Колода: 0/50", 20, Color.white, TextAnchor.MiddleLeft); - _deckCount.fontStyle = FontStyle.Bold; - UiKit.SetPreferredHeight(_deckCount, 30f); - - var deckScroll = UiKit.CreateHorizontalScrollView( - right, "DeckScroll", out _deckContent, 6f); - var deckElement = deckScroll.gameObject.AddComponent(); - deckElement.flexibleHeight = 1f; - deckElement.minHeight = 220f; - - var editorStatusPanel = UiKit.CreatePanel(right, "StatusPanel", new Color(0f, 0f, 0f, 0f)); - UiKit.SetPreferredHeight(editorStatusPanel, 54f); - _editorStatus = UiKit.CreateText( - editorStatusPanel, "Status", string.Empty, 16, - new Color(0.62f, 0.68f, 0.82f), TextAnchor.UpperLeft); - _editorStatus.horizontalOverflow = HorizontalWrapMode.Wrap; - UiKit.SetPreferredHeight(_editorStatus, 50f); - - return view; + return top; } private RectTransform BuildFilterOverlay() { - var overlay = UiKit.CreatePanel( - _editorView, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f)); + var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f)); var card = UiKit.CreateCenteredCard( overlay, "FilterCard", new Vector2(860f, 820f), @@ -304,29 +352,6 @@ namespace Vanguard.UI return overlay; } - private void RequestDelete(DeckDto deck) - { - _pendingDelete = deck; - _confirmText.text = deck.Name + "\nКолода будет удалена без восстановления."; - _confirmOverlay.SetAsLastSibling(); - _confirmOverlay.gameObject.SetActive(true); - } - - private void CancelDelete() - { - _pendingDelete = null; - _confirmOverlay.gameObject.SetActive(false); - } - - private void ConfirmDelete() - { - var deck = _pendingDelete; - _confirmOverlay.gameObject.SetActive(false); - _pendingDelete = null; - if (deck != null) - DeleteDeck(deck); - } - private void AddFilterSection(Transform parent, string title, string[] labels, Action onSelected) { var caption = UiKit.CreateText( @@ -355,25 +380,6 @@ namespace Vanguard.UI UiKit.SetPreferredHeight(button, 40f); } - private RectTransform BuildTopBar(RectTransform view, string titleText, out Text title) - { - var top = UiKit.CreatePanel(view, "TopBar", new Color(0.06f, 0.08f, 0.13f, 1f)); - top.anchorMin = new Vector2(0f, 1f); - top.anchorMax = new Vector2(1f, 1f); - top.pivot = new Vector2(0.5f, 1f); - top.anchoredPosition = Vector2.zero; - top.sizeDelta = new Vector2(0f, 88f); - UiKit.AddHorizontalLayout(top, 18, 12); - - var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 18); - UiKit.SetPreferredHeight(back, 44f, 0f); - - title = UiKit.CreateText(top, "Title", titleText, 30, Color.white, TextAnchor.MiddleLeft); - title.fontStyle = FontStyle.Bold; - UiKit.SetPreferredHeight(title, 46f); - return top; - } - private async Task LoadAsync() { if (_loading) @@ -382,7 +388,7 @@ namespace Vanguard.UI _loading = true; _listStatus.text = "Загружаем коллекцию и колоды…"; _listStatus.color = new Color(0.62f, 0.68f, 0.82f); - ClearChildren(_listContent); + ClearChildren(_deckListContent); try { @@ -399,13 +405,14 @@ namespace Vanguard.UI foreach (var item in catalog) _catalog[item.PrintingId] = item; RebuildClanFilters(); + _decks.Clear(); _decks.AddRange(decks); - BuildDeckList(); + RebuildCollectionGrids(); _listStatus.text = _decks.Count == 0 ? "Колод пока нет. Создайте первую." - : "Колод: " + _decks.Count + "."; + : "Карт в коллекции: " + _collection.Count + "."; } catch (ApiException exception) { @@ -426,50 +433,39 @@ namespace Vanguard.UI private void BuildDeckList() { - ClearChildren(_listContent); + ClearChildren(_deckListContent); + if (_decks.Count == 0) + { + var empty = UiKit.CreateText( + _deckListContent, "Empty", "Нет колод", 16, + new Color(0.58f, 0.66f, 0.80f)); + UiKit.SetPreferredHeight(empty, 90f); return; + } foreach (var deck in _decks) { var row = UiKit.CreatePanel( - _listContent, "Deck_" + deck.Id, new Color(0.07f, 0.09f, 0.14f, 1f)); + _deckListContent, "DeckRow_" + deck.Id, new Color(0.08f, 0.11f, 0.17f, 1f)); var rowElement = row.gameObject.AddComponent(); - rowElement.preferredHeight = 82f; - UiKit.AddHorizontalLayout(row, 14, 10); + rowElement.preferredHeight = 76f; + UiKit.AddVerticalLayout(row, 10, 2); + + var button = row.gameObject.AddComponent