using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using Vanguard.Api; using Vanguard.Api.Contracts; using Vanguard.Auth; using Vanguard.Meta; namespace Vanguard.UI { /// /// Простой декбилдер: коллекция слева, текущая колода справа, /// фильтры в оверлее, статистика по триггерам и грейдам. /// public sealed class DeckBuilderView { private readonly MetaService _meta; 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 InputField _nameInput; private Text _listStatus; private Text _editorStatus; private Text _stats; private Text _deckCount; private Text _filterSummary; private RectTransform _filterOverlay; private RectTransform _clanContent; private RectTransform _confirmOverlay; private Text _confirmText; private DeckDto _pendingDelete; 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 _collectionCountTexts = new Dictionary(StringComparer.Ordinal); private DeckDto _editing; private string _filterClan = "Все"; private string _filterTrigger = "Все"; private int _filterGrade = -1; private bool _loading; private bool _busy; public DeckBuilderView( Transform parent, MetaService meta, AuthService auth, Action onBack, Action onPlay) { _meta = meta; _auth = auth; _onBack = onBack; _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); _confirmOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } public void Show() { UiKit.SetActive(_root, true); ShowList(); _ = LoadAsync(); } public void Hide() { _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } private RectTransform BuildListView() { 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 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); var hint = UiKit.CreateText( body, "Hint", "Откройте колоду для редактирования или создайте новую. «В бой» ставит колоду в очередь.", 17, new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(hint, 26f); var scroll = UiKit.CreateScrollView(body, "DeckListScroll", out _listContent, 8f); var element = scroll.gameObject.AddComponent(); element.flexibleHeight = 1f; element.minHeight = 200f; _listStatus = UiKit.CreateText( body, "Status", string.Empty, 18, new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(_listStatus, 34f); return view; } private RectTransform BuildEditorView() { var view = UiKit.CreatePanel(_root, "DeckEditor", new Color(0.018f, 0.03f, 0.06f, 0.97f)); 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", "← Колоды", 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; } private RectTransform BuildFilterOverlay() { var overlay = UiKit.CreatePanel( _editorView, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f)); var card = UiKit.CreateCenteredCard( overlay, "FilterCard", new Vector2(860f, 820f), new Color(0.06f, 0.08f, 0.13f, 1f)); UiKit.AddVerticalLayout(card, 26, 10); var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, Color.white); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 40f); AddFilterSection(card, "Ранг", new[] { "Все", "0", "1", "2", "3" }, OnGradeFilter); AddFilterSection(card, "Триггер", new[] { "Все", "None", "Critical", "Draw", "Stand", "Heal" }, OnTriggerFilter); var clanTitle = UiKit.CreateText( card, "ClanTitle", "Клан", 17, new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(clanTitle, 24f); var clanScroll = UiKit.CreateScrollView(card, "ClanScroll", out _clanContent, 4f); var clanElement = clanScroll.gameObject.AddComponent(); clanElement.flexibleHeight = 1f; clanElement.minHeight = 150f; var close = UiKit.CreateButton(card, "Close", "Готово", CloseFilters, 20); UiKit.SetPreferredHeight(close, 48f); return overlay; } private void RebuildClanFilters() { ClearChildren(_clanContent); AddClanButton(_clanContent, "Все"); foreach (var clan in _catalog.Values.Select(item => item.Clan).Distinct().OrderBy(item => item)) AddClanButton(_clanContent, clan); } 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), new Color(0.06f, 0.08f, 0.13f, 1f)); UiKit.AddVerticalLayout(card, 30, 12); var title = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, Color.white); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 42f); _confirmText = UiKit.CreateText( card, "Text", string.Empty, 19, new Color(0.72f, 0.78f, 0.92f)); 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 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( parent, "Caption_" + title, title, 17, new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(caption, 24f); var row = UiKit.CreatePanel(parent, "Row_" + title, new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(row, 44f); UiKit.AddHorizontalLayout(row, 0, 6); foreach (var label in labels) { var captured = label; var button = UiKit.CreateButton( row, "Filter_" + label, label, () => onSelected(captured), 16); UiKit.SetPreferredHeight(button, 40f); } } private void AddClanButton(Transform parent, string clan) { var captured = clan; var button = UiKit.CreateButton( parent, "Clan_" + clan, clan, () => OnClanFilter(captured), 16); 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) return; _loading = true; _listStatus.text = "Загружаем коллекцию и колоды…"; _listStatus.color = new Color(0.62f, 0.68f, 0.82f); ClearChildren(_listContent); 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()); _catalog.Clear(); foreach (var item in catalog) _catalog[item.PrintingId] = item; RebuildClanFilters(); _decks.Clear(); _decks.AddRange(decks); BuildDeckList(); _listStatus.text = _decks.Count == 0 ? "Колод пока нет. Создайте первую." : "Колод: " + _decks.Count + "."; } catch (ApiException exception) { _listStatus.text = exception.Message; _listStatus.color = new Color(1f, 0.55f, 0.45f); } catch (Exception exception) { Debug.LogException(exception); _listStatus.text = "Не удалось загрузить декбилдер."; _listStatus.color = new Color(1f, 0.55f, 0.45f); } finally { _loading = false; } } private void BuildDeckList() { ClearChildren(_listContent); if (_decks.Count == 0) return; foreach (var deck in _decks) { var row = UiKit.CreatePanel( _listContent, "Deck_" + deck.Id, new Color(0.07f, 0.09f, 0.14f, 1f)); var rowElement = row.gameObject.AddComponent(); rowElement.preferredHeight = 82f; UiKit.AddHorizontalLayout(row, 14, 10); var name = UiKit.CreateText( row, "Name", deck.Name, 21, Color.white, TextAnchor.MiddleLeft); name.fontStyle = FontStyle.Bold; var nameElement = name.gameObject.AddComponent(); nameElement.flexibleWidth = 1f; nameElement.preferredHeight = 40f; var info = UiKit.CreateText( row, "Info", deck.Cards.Count + " карт · " + deck.Format, 16, new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); var infoElement = info.gameObject.AddComponent(); infoElement.preferredWidth = 190f; infoElement.preferredHeight = 36f; infoElement.flexibleWidth = 0f; var edit = UiKit.CreateButton(row, "Edit", "Открыть", () => OpenDeck(deck), 17); var editElement = edit.gameObject.AddComponent(); editElement.preferredWidth = 120f; editElement.preferredHeight = 44f; editElement.flexibleWidth = 0f; var play = UiKit.CreateButton(row, "Play", "В бой", () => _onPlay(deck), 17); var playElement = play.gameObject.AddComponent(); playElement.preferredWidth = 100f; playElement.preferredHeight = 44f; playElement.flexibleWidth = 0f; var delete = UiKit.CreateButton(row, "Delete", "Удалить", () => RequestDelete(deck), 16); var deleteElement = delete.gameObject.AddComponent(); deleteElement.preferredWidth = 105f; deleteElement.preferredHeight = 44f; deleteElement.flexibleWidth = 0f; } } private void NewDeck() { _editing = null; _cards.Clear(); _nameInput.text = "Новая колода"; ShowEditor(); } private void OpenDeck(DeckDto deck) { _editing = deck; _cards.Clear(); _cards.AddRange(deck.Cards); _nameInput.text = deck.Name; ShowEditor(); } private void ShowEditor() { _listView.gameObject.SetActive(false); _editorView.gameObject.SetActive(true); _nameInput.text = _editing?.Name ?? "Новая колода"; _deckCount.text = "Колода: " + _cards.Count + "/50"; _editorStatus.text = string.Empty; RebuildEditor(); } private void BackToCards() { _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); _editorView.gameObject.SetActive(false); _listView.gameObject.SetActive(true); BuildDeckList(); } private void RebuildEditor() { UpdateStats(); RebuildDeck(); RebuildCollection(); _deckCount.text = "Колода: " + _cards.Count + "/50"; } private void RebuildCollection() { ClearChildren(_collectionContent); _collectionCountTexts.Clear(); if (_catalog.Count == 0) return; foreach (var item in _collection.OrderBy(item => GradeOf(item)).ThenBy(item => item.NameEn, StringComparer.Ordinal)) { if (!_catalog.TryGetValue(item.PrintingId, out var meta)) continue; if (_filterGrade >= 0 && meta.Grade != _filterGrade) continue; if (_filterTrigger != "Все" && meta.Trigger != _filterTrigger) continue; if (_filterClan != "Все" && meta.Clan != _filterClan) continue; AddCollectionCell(item, meta); } } private void AddCollectionCell(CollectionItemDto item, CardCatalogItemDto meta) { var cell = UiKit.CreatePanel( _collectionContent, "Owned_" + item.PrintingId, new Color(0.08f, 0.11f, 0.17f, 1f)); var button = cell.gameObject.AddComponent