using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using Vanguard.Api; using Vanguard.Api.Contracts; using Vanguard.Auth; using Vanguard.Battle; using Vanguard.Meta; namespace Vanguard.UI { /// /// Декбилдер: слева коллекция с мультифильтрами, справа панель, которая /// показывает либо список колод, либо карты открытой колоды. /// public sealed class DeckBuilderView { private const float PanelWidth = 380f; private const float RowHeight = 92f; private static readonly Color TextPrimary = new Color(0.88f, 0.93f, 1f); private static readonly Color TextMuted = new Color(0.62f, 0.70f, 0.84f); private static readonly Color TextDim = new Color(0.50f, 0.58f, 0.72f); private static readonly Color Accent = new Color(0.30f, 0.65f, 1f); private static readonly Color PanelFill = new Color(0.06f, 0.08f, 0.14f, 0.96f); private static readonly Color RowFill = new Color(0.08f, 0.11f, 0.18f, 1f); private static readonly Color ChipOn = new Color(0.16f, 0.40f, 0.80f, 0.95f); private static readonly Color ChipOff = new Color(0.05f, 0.10f, 0.22f, 0.9f); private readonly MetaService _meta; private readonly AuthService _auth; private readonly Action _onPlay; private readonly Action _onPracticeStarted; private readonly RectTransform _root; private RectTransform _collectionContent; private RectTransform _deckListPanel; private RectTransform _deckPanel; private RectTransform _deckListContent; private RectTransform _deckCardContent; private InputField _nameInput; private Text _status; private Text _filterSummary; private Text _stats; private Text _deckCount; private Button _saveButton; private Button _botButton; private Button _duelButton; private Button _deleteButton; private RectTransform _deckMenuOverlay; private Text _deckMenuTitle; private DeckDto _deckMenuDeck; private RectTransform _filterOverlay; private RectTransform _rarityChips; private RectTransform _gradeChips; private RectTransform _triggerChips; private RectTransform _clanChips; private RectTransform _confirmOverlay; private InputField _searchInput; private Text _confirmText; private DeckDto _pendingDelete; private RectTransform _cardOverlay; private Image _cardArt; private Text _cardArtName; private Text _cardTitle; private Text _cardSubtitle; private Text _cardStats; private Text _cardAbilities; private Text _cardOwned; private Text _cardStatus; private Button _dismantleButton; private Button _craftButton; private CollectionItemDto _selectedItem; private CardCatalogItemDto _selectedMeta; private readonly List _decks = new List(); private readonly List _collection = new List(); private readonly Dictionary _catalog = new Dictionary(StringComparer.Ordinal); private readonly List _cards = new List(); private readonly Dictionary _countTexts = new Dictionary(StringComparer.Ordinal); private readonly DeckFilter _filter = new DeckFilter(); private readonly DeckFilter _draftFilter = new DeckFilter(); private DeckDto _editing; private int _dust; private bool _dirty; private bool _deckOpen; private bool _loading; private bool _busy; public DeckBuilderView( Transform parent, MetaService meta, AuthService auth, Action onBack, Action onPlay, Action onPracticeStarted) { _meta = meta; _auth = auth; _onPlay = onPlay; _onPracticeStarted = onPracticeStarted; _root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f)); BuildBody(); _filterOverlay = BuildFilterOverlay(); _confirmOverlay = BuildDeleteConfirm(); _deckMenuOverlay = BuildDeckMenu(); _cardOverlay = BuildCardOverlay(); _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); _deckMenuOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } public RectTransform Root => _root; /// Esc: закрывает верхний оверлей, если он открыт. public void CloseTopmost() { if (_cardOverlay.gameObject.activeSelf) { CloseCardOverlay(); return; } if (_confirmOverlay.gameObject.activeSelf) { CancelDelete(); return; } if (_deckMenuOverlay.gameObject.activeSelf) { CloseDeckMenu(); return; } if (_filterOverlay.gameObject.activeSelf) CloseFilters(); } public void Show() { UiKit.SetActive(_root, true); ShowDeckList(); _ = LoadAsync(); } public void Hide() { _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); _deckMenuOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } // ------------------------------------------------------------ каркас private void BuildBody() { var body = UiKit.CreatePanel(_root, "Body", new Color(0f, 0f, 0f, 0f)); body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one; body.offsetMin = new Vector2(34f, 24f); body.offsetMax = new Vector2(-34f, -120f); 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 filterBar = UiKit.CreatePanel(collectionArea, "FilterBar", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(filterBar, 44f); UiKit.AddHorizontalLayout(filterBar, 0, 8); _filterSummary = UiKit.CreateText( filterBar, "FilterSummary", "Все карты", 15, TextMuted, TextAnchor.MiddleLeft); _filterSummary.horizontalOverflow = HorizontalWrapMode.Wrap; SetFlexible(_filterSummary, 1f, 44f); var filters = UiKit.CreateButton(filterBar, "Filters", "Фильтры", OpenFilters, 16); SetFixedSize(filters, 110f, 38f); var reset = UiKit.CreateButton(filterBar, "ResetFilters", "Сброс", ResetFilters, 16); SetFixedSize(reset, 90f, 38f); var scroll = UiKit.CreateGridScrollView( collectionArea, "CollectionGrid", new Vector2(216f, 242f), new Vector2(14f, 14f), out _collectionContent); var scrollElement = scroll.gameObject.AddComponent(); scrollElement.flexibleHeight = 1f; scrollElement.minHeight = 220f; var grid = _collectionContent.GetComponent(); if (grid != null) { grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = 6; grid.childAlignment = TextAnchor.UpperLeft; } _status = UiKit.CreateText( collectionArea, "Status", string.Empty, 15, TextMuted, TextAnchor.MiddleLeft); _status.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(_status, 30f); var right = UiKit.CreatePanel( body, "RightPanel", new Color(0.04f, 0.06f, 0.10f, 0.85f)); var rightElement = right.gameObject.AddComponent(); rightElement.preferredWidth = PanelWidth; rightElement.minWidth = PanelWidth; rightElement.flexibleWidth = 0f; rightElement.flexibleHeight = 1f; rightElement.minHeight = 300f; _deckListPanel = BuildDeckListPanel(right); _deckPanel = BuildDeckPanel(right); _deckListPanel.gameObject.SetActive(true); _deckPanel.gameObject.SetActive(false); } private RectTransform BuildDeckListPanel(RectTransform parent) { var panel = UiKit.CreatePanel(parent, "DeckListPanel", new Color(0f, 0f, 0f, 0f)); UiKit.Stretch(panel); UiKit.AddVerticalLayout(panel, 10, 8); var create = UiKit.CreateButton(panel, "Create", "+ Новый набор", NewDeck, 17); UiKit.SetPreferredHeight(create, 52f); SetButtonColor(create, new Color(0.10f, 0.28f, 0.58f, 0.95f)); var scroll = UiKit.CreateScrollView(panel, "DeckListScroll", out _deckListContent, 8f); var scrollElement = scroll.gameObject.AddComponent(); scrollElement.flexibleHeight = 1f; scrollElement.minHeight = 200f; var hint = UiKit.CreateText( panel, "Hint", "Нажмите на колоду,\nчтобы открыть редактор.", 13, TextDim, TextAnchor.MiddleLeft); hint.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(hint, 42f); return panel; } private RectTransform BuildDeckPanel(RectTransform parent) { var panel = UiKit.CreatePanel(parent, "DeckPanel", new Color(0f, 0f, 0f, 0f)); UiKit.Stretch(panel); UiKit.AddVerticalLayout(panel, 10, 6); var header = UiKit.CreatePanel(panel, "Header", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(header, 36f); UiKit.AddHorizontalLayout(header, 0, 8); var back = UiKit.CreateButton(header, "BackToList", "← К списку колод", BackToDeckList, 14); SetFixedSize(back, 180f, 34f); _deckCount = UiKit.CreateText( header, "DeckCount", "0/50", 16, TextPrimary, TextAnchor.MiddleRight); _deckCount.fontStyle = FontStyle.Bold; SetFlexible(_deckCount, 1f, 34f); _nameInput = UiKit.CreateInput(panel, "Name", "Название колоды"); UiKit.SetPreferredHeight(_nameInput, 44f); _nameInput.onValueChanged.AddListener(_ => { _dirty = true; UpdateStats(); }); _stats = UiKit.CreateText( panel, "Stats", string.Empty, 13, TextMuted, TextAnchor.UpperLeft); _stats.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(_stats, 44f); var scroll = UiKit.CreateScrollView(panel, "DeckCardScroll", out _deckCardContent, 6f); var scrollElement = scroll.gameObject.AddComponent(); scrollElement.flexibleHeight = 1f; scrollElement.minHeight = 200f; _saveButton = UiKit.CreateButton(panel, "Save", "Сохранить", SaveDeck, 16); UiKit.SetPreferredHeight(_saveButton, 42f); var row = UiKit.CreatePanel(panel, "Actions", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(row, 40f); UiKit.AddHorizontalLayout(row, 0, 8); _botButton = UiKit.CreateButton(row, "Bot", "С ботом", PlayBot, 15); _duelButton = UiKit.CreateButton(row, "Duel", "В бой", PlayDeck, 15); SetFlexible(_botButton, 1f, 38f); SetFlexible(_duelButton, 1f, 38f); _deleteButton = UiKit.CreateButton(panel, "Delete", "Удалить колоду", RequestDeleteCurrent, 14); UiKit.SetPreferredHeight(_deleteButton, 34f); SetButtonColor(_deleteButton, new Color(0.30f, 0.08f, 0.11f, 0.95f)); var hint = UiKit.CreateText( panel, "Hint", "ЛКМ — убрать копию,\nПКМ — посмотреть карту.", 13, TextDim, TextAnchor.MiddleLeft); hint.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(hint, 36f); return panel; } // ------------------------------------------------------------ оверлеи private RectTransform BuildFilterOverlay() { var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f)); var card = UiKit.CreateCenteredCard( overlay, "FilterCard", new Vector2(900f, 940f), PanelFill); UiKit.AddVerticalLayout(card, 24, 10); var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 40f); _searchInput = UiKit.CreateInput(card, "Search", "Поиск по названию"); UiKit.SetPreferredHeight(_searchInput, 44f); _searchInput.onValueChanged.AddListener(value => _draftFilter.Search = value); _rarityChips = BuildFilterSection(card, "Редкость", 44f); _gradeChips = BuildFilterSection(card, "Ранг", 44f); _triggerChips = BuildFilterSection(card, "Триггер", 44f); _clanChips = BuildFilterSection(card, "Клан", 260f, flexible: true); var footer = UiKit.CreatePanel(card, "Footer", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(footer, 48f); UiKit.AddHorizontalLayout(footer, 0, 8); var reset = UiKit.CreateButton(footer, "Reset", "Сбросить", ResetDraftFilters, 16); SetFixedSize(reset, 200f, 44f); var cancel = UiKit.CreateButton(footer, "Cancel", "Отмена", CloseFilters, 16); SetFixedSize(cancel, 180f, 44f); var apply = UiKit.CreateButton(footer, "Apply", "Применить", ApplyFilters, 16); SetFixedSize(apply, 220f, 44f); return overlay; } private static RectTransform BuildFilterSection( RectTransform parent, string title, float height, bool flexible = false) { var caption = UiKit.CreateText( parent, "Caption_" + title, title, 17, TextMuted, TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(caption, 24f); var container = UiKit.CreatePanel(parent, "Chips_" + title, new Color(0f, 0f, 0f, 0f)); var element = container.gameObject.AddComponent(); element.preferredHeight = height; if (flexible) { element.flexibleHeight = 1f; element.minHeight = 140f; } else { element.flexibleHeight = 0f; } if (flexible) { var scroll = UiKit.CreateScrollView(container, "Scroll", out var content, 4f); UiKit.Stretch(scroll); return content; } UiKit.AddHorizontalLayout(container, 0, 6); return container; } private RectTransform BuildDeckMenu() { var overlay = UiKit.CreatePanel(_root, "DeckMenuOverlay", new Color(0f, 0f, 0f, 0.78f)); var card = UiKit.CreateCenteredCard( overlay, "MenuCard", new Vector2(520f, 320f), PanelFill); UiKit.AddVerticalLayout(card, 28, 10); _deckMenuTitle = UiKit.CreateText( card, "Title", string.Empty, 24, TextPrimary); _deckMenuTitle.fontStyle = FontStyle.Bold; UiKit.MakeLiteral(_deckMenuTitle); UiKit.SetPreferredHeight(_deckMenuTitle, 40f); var open = UiKit.CreateButton(card, "Open", "Открыть", OpenDeckFromMenu, 17); UiKit.SetPreferredHeight(open, 48f); var delete = UiKit.CreateButton(card, "Delete", "Удалить", DeleteDeckFromMenu, 17); UiKit.SetPreferredHeight(delete, 48f); SetButtonColor(delete, new Color(0.30f, 0.08f, 0.11f, 0.95f)); var cancel = UiKit.CreateButton(card, "Cancel", "Отмена", CloseDeckMenu, 17); UiKit.SetPreferredHeight(cancel, 48f); return overlay; } private RectTransform BuildDeleteConfirm() { var overlay = UiKit.CreatePanel( _root, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.78f)); var card = UiKit.CreateCenteredCard( overlay, "ConfirmCard", new Vector2(640f, 300f), PanelFill); UiKit.AddVerticalLayout(card, 30, 12); var title = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, TextPrimary); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 42f); _confirmText = UiKit.CreateText( card, "Text", string.Empty, 19, TextMuted); UiKit.SetPreferredHeight(_confirmText, 70f); var yes = UiKit.CreateButton(card, "Yes", "Удалить", ConfirmDelete, 20); UiKit.SetPreferredHeight(yes, 48f); var no = UiKit.CreateButton(card, "No", "Отмена", CancelDelete, 20); UiKit.SetPreferredHeight(no, 44f); return overlay; } private RectTransform BuildCardOverlay() { var overlay = UiKit.CreatePanel(_root, "CardOverlay", new Color(0f, 0f, 0f, 0.82f)); var card = UiKit.CreateCenteredCard( overlay, "CardView", new Vector2(1040f, 660f), new Color(0.05f, 0.07f, 0.11f, 1f)); var body = UiKit.CreatePanel(card, "Body", new Color(0f, 0f, 0f, 0f)); body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one; body.offsetMin = new Vector2(26f, 26f); body.offsetMax = new Vector2(-26f, -26f); UiKit.AddHorizontalLayout(body, 0, 22); var art = UiKit.CreatePanel(body, "Art", new Color(0.2f, 0.3f, 0.5f, 1f)); SetFixedSize(art, 360f, 540f); _cardArt = art.GetComponent(); _cardArtName = UiKit.CreateText( art, "ArtName", string.Empty, 18, new Color(1f, 1f, 1f, 0.92f), TextAnchor.MiddleCenter); UiKit.MakeLiteral(_cardArtName); UiKit.Stretch(_cardArtName.rectTransform); _cardArtName.rectTransform.offsetMin = new Vector2(14f, 16f); _cardArtName.rectTransform.offsetMax = new Vector2(-14f, -16f); _cardArtName.horizontalOverflow = HorizontalWrapMode.Wrap; var info = UiKit.CreatePanel(body, "Info", new Color(0f, 0f, 0f, 0f)); var infoElement = info.gameObject.AddComponent(); infoElement.flexibleWidth = 1f; infoElement.flexibleHeight = 1f; UiKit.AddVerticalLayout(info, 4, 6); _cardTitle = UiKit.CreateText( info, "Title", string.Empty, 30, Color.white, TextAnchor.MiddleLeft); UiKit.MakeLiteral(_cardTitle); _cardTitle.fontStyle = FontStyle.Bold; _cardTitle.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(_cardTitle, 44f); _cardSubtitle = UiKit.CreateText( info, "Subtitle", string.Empty, 16, TextMuted, TextAnchor.MiddleLeft); UiKit.MakeLiteral(_cardSubtitle); UiKit.SetPreferredHeight(_cardSubtitle, 26f); _cardStats = UiKit.CreateText( info, "Stats", string.Empty, 15, new Color(0.72f, 0.78f, 0.92f), TextAnchor.UpperLeft); _cardStats.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(_cardStats, 88f); var abilitiesCaption = UiKit.CreateText( info, "AbilitiesCaption", "Способности", 16, TextMuted, TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(abilitiesCaption, 24f); _cardAbilities = UiKit.CreateText( info, "Abilities", string.Empty, 15, new Color(0.82f, 0.86f, 0.96f), TextAnchor.UpperLeft); UiKit.MakeLiteral(_cardAbilities); _cardAbilities.horizontalOverflow = HorizontalWrapMode.Wrap; var abilitiesElement = _cardAbilities.gameObject.AddComponent(); abilitiesElement.flexibleHeight = 1f; abilitiesElement.minHeight = 100f; _cardOwned = UiKit.CreateText( info, "Owned", string.Empty, 15, new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(_cardOwned, 24f); _cardStatus = UiKit.CreateText( info, "Status", string.Empty, 14, TextDim, TextAnchor.UpperLeft); _cardStatus.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(_cardStatus, 38f); var actions = UiKit.CreatePanel(info, "Actions", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(actions, 50f); UiKit.AddHorizontalLayout(actions, 0, 8); _dismantleButton = UiKit.CreateButton( actions, "Dismantle", "Разобрать", DismantleSelected, 16); SetFixedSize(_dismantleButton, 225f, 46f); _craftButton = UiKit.CreateButton( actions, "Craft", "Скрафтить", CraftSelected, 16); SetFixedSize(_craftButton, 225f, 46f); var close = UiKit.CreateButton(actions, "Close", "Закрыть", CloseCardOverlay, 16); SetFixedSize(close, 110f, 46f); return overlay; } // ------------------------------------------------------------ загрузка private async Task LoadAsync() { if (_loading) return; _loading = true; SetStatus("Загружаем коллекцию и колоды…", false); ClearChildren(_deckListContent); try { await _auth.EnsureAccessAsync(); var collection = await _meta.GetCollectionAsync(); var catalog = await _meta.GetCatalogAsync(); var decks = await _meta.GetDecksAsync(); if (!_root.gameObject.activeSelf) return; _collection.Clear(); _collection.AddRange(collection.Items ?? new List()); _dust = collection.Balances != null ? collection.Balances.Dust : 0; _catalog.Clear(); foreach (var item in catalog) _catalog[item.PrintingId] = item; _decks.Clear(); _decks.AddRange(decks); BuildDeckList(); RebuildCollection(); SetStatus( _decks.Count == 0 ? "Колод пока нет. Пыль: " + _dust + ". ПКМ по карте — подробности." : "Карт в коллекции: " + _collection.Count + " · пыль " + _dust + " · ПКМ по карте — подробности.", false); } catch (ApiException exception) { SetStatus(exception.Message, true); } catch (Exception exception) { Debug.LogException(exception); SetStatus("Не удалось загрузить декбилдер.", true); } finally { _loading = false; } } // ------------------------------------------------------------ список колод private void BuildDeckList() { ClearChildren(_deckListContent); if (_decks.Count == 0) { var empty = UiKit.CreateText( _deckListContent, "Empty", "Нет колод", 15, TextDim); UiKit.SetPreferredHeight(empty, 90f); return; } foreach (var deck in _decks) { var row = UiKit.CreatePanel( _deckListContent, "DeckRow_" + deck.Id, RowFill); var rowElement = row.gameObject.AddComponent(); rowElement.preferredHeight = RowHeight; var button = row.gameObject.AddComponent