using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using Vanguard.Api; using Vanguard.Api.Contracts; using Vanguard.Auth; using Vanguard.Meta; using Vanguard.State; namespace Vanguard.UI { /// Магазин в Lobby: бустеры и вторая триал-дека. public sealed class StoreView { private readonly MetaService _meta; private readonly AuthService _auth; private readonly PlayerState _player; private readonly Action _onBack; private readonly RectTransform _root; private readonly Text _balances; private readonly Text _status; private readonly Button _goldButton; private readonly Button _tokenButton; private readonly RectTransform _trialContent; private readonly RectTransform _confirm; private Text _confirmText; private readonly RectTransform _result; private Text _resultTitle; private RectTransform _resultCards; private StoreResponse _store; private Func _confirmAction; private bool _busy; private bool _loading; public StoreView( Transform parent, MetaService meta, AuthService auth, PlayerState player, Action onBack) { _meta = meta; _auth = auth; _player = player; _onBack = onBack; _root = UiKit.CreatePanel(parent, "StoreScreen", new Color(0.018f, 0.03f, 0.06f, 0.94f)); var top = UiKit.CreatePanel(_root, "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, 22, 16); var back = UiKit.CreateButton(top, "Back", "← Назад", () => _onBack(), 18); UiKit.SetPreferredHeight(back, 44f, 0f); var title = UiKit.CreateText(top, "Title", "Магазин", 32, Color.white, TextAnchor.MiddleLeft); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 46f); _balances = UiKit.CreateText( top, "Balances", string.Empty, 18, new Color(0.70f, 0.76f, 0.90f), TextAnchor.MiddleRight); UiKit.SetPreferredHeight(_balances, 36f); var body = UiKit.CreatePanel(_root, "Body", new Color(0.02f, 0.03f, 0.06f, 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, 12); var booster = UiKit.CreatePanel(body, "Booster", new Color(0.07f, 0.10f, 0.17f, 1f)); UiKit.SetPreferredHeight(booster, 180f); UiKit.AddVerticalLayout(booster, 20, 8); var boosterTitle = UiKit.CreateText( booster, "Title", "Бустер BT01", 26, new Color(0.88f, 0.94f, 1f), TextAnchor.MiddleLeft); boosterTitle.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(boosterTitle, 34f); var boosterHint = UiKit.CreateText( booster, "Hint", "5 карт: 4 C + 1 R+ · duplicate protection · 100 Gold или 1 токен.", 18, new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(boosterHint, 28f); var buttons = UiKit.CreatePanel(booster, "Buttons", new Color(0f, 0f, 0f, 0f)); UiKit.SetPreferredHeight(buttons, 54f); UiKit.AddHorizontalLayout(buttons, 0, 12); _goldButton = UiKit.CreateButton( buttons, "BuyGold", "Купить за 100 Gold", () => AskBooster("gold"), 19); UiKit.SetPreferredHeight(_goldButton, 52f); _tokenButton = UiKit.CreateButton( buttons, "BuyToken", "Купить за 1 токен", () => AskBooster("token"), 19); UiKit.SetPreferredHeight(_tokenButton, 52f); var trialTitle = UiKit.CreateText( body, "TrialTitle", "Триал-деки", 24, Color.white, TextAnchor.MiddleLeft); trialTitle.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(trialTitle, 34f); var scroll = UiKit.CreateScrollView(body, "TrialScroll", out _trialContent, 8f); var scrollElement = scroll.gameObject.AddComponent(); scrollElement.flexibleHeight = 1f; scrollElement.minHeight = 160f; _status = UiKit.CreateText( body, "Status", string.Empty, 18, new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(_status, 34f); _confirm = BuildConfirm(_root); _result = BuildResult(_root); _confirm.gameObject.SetActive(false); _result.gameObject.SetActive(false); _root.gameObject.SetActive(false); _player.Changed += OnPlayerChanged; } public void Show() { UiKit.SetActive(_root, true); _confirm.gameObject.SetActive(false); _result.gameObject.SetActive(false); OnPlayerChanged(); _ = LoadAsync(); } public void Hide() { _confirm.gameObject.SetActive(false); _result.gameObject.SetActive(false); UiKit.SetActive(_root, false); } public void Tick() { } private async Task LoadAsync() { if (_loading) return; _loading = true; SetStatus("Загружаем магазин…", new Color(0.62f, 0.68f, 0.82f)); SetButtons(false); try { await _auth.EnsureAccessAsync(); _store = await _meta.GetStoreAsync(); RebuildTrials(_store.TrialDecks); _goldButton.GetComponentInChildren().text = "Купить за " + _store.BoosterPriceGold + " Gold"; SetStatus("Выберите покупку.", new Color(0.62f, 0.68f, 0.82f)); } catch (ApiException exception) { SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f)); } catch (Exception exception) { Debug.LogException(exception); SetStatus("Не удалось загрузить магазин.", new Color(1f, 0.55f, 0.45f)); } finally { _loading = false; SetButtons(true); } } private void RebuildTrials(IReadOnlyList decks) { ClearChildren(_trialContent); if (decks == null || decks.Count == 0) { var empty = UiKit.CreateText(_trialContent, "Empty", "Триал-деки недоступны.", 18, new Color(0.62f, 0.68f, 0.82f)); UiKit.SetPreferredHeight(empty, 80f); return; } foreach (var deck in decks) AddTrialRow(deck); } private void AddTrialRow(StoreTrialDeckDto deck) { var row = UiKit.CreatePanel(_trialContent, "Trial_" + deck.Id, new Color(0.07f, 0.09f, 0.14f, 1f)); var rowElement = row.gameObject.AddComponent(); rowElement.preferredHeight = 92f; UiKit.AddHorizontalLayout(row, 16, 14); var info = UiKit.CreatePanel(row, "Info", new Color(0f, 0f, 0f, 0f)); var infoElement = info.gameObject.AddComponent(); infoElement.flexibleWidth = 1f; infoElement.preferredHeight = 60f; UiKit.AddVerticalLayout(info, 0, 2); var name = UiKit.CreateText( info, "Name", deck.Name, 21, Color.white, TextAnchor.MiddleLeft); name.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(name, 28f); var details = UiKit.CreateText( info, "Details", (string.IsNullOrEmpty(deck.Clan) ? "—" : deck.Clan) + " · " + deck.CardCount + " карт · " + (deck.Owned ? "в коллекции" : "цена " + deck.PriceGold + " Gold"), 16, new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(details, 24f); if (deck.Owned) { var owned = UiKit.CreateText( row, "Owned", "Уже есть", 17, new Color(0.45f, 0.85f, 0.55f)); var ownedElement = owned.gameObject.AddComponent(); ownedElement.preferredWidth = 120f; ownedElement.preferredHeight = 40f; } else { var buy = UiKit.CreateButton( row, "Buy", "Купить за " + deck.PriceGold + " Gold", () => AskTrial(deck), 17); var buyElement = buy.gameObject.AddComponent(); buyElement.preferredWidth = 240f; buyElement.preferredHeight = 46f; buyElement.flexibleWidth = 0f; } } private void AskBooster(string payment) { if (_store == null || _busy) return; var price = payment == "gold" ? _store.BoosterPriceGold + " Gold" : "1 токен"; AskConfirm( "Открыть бустер BT01 за " + price + "?", () => BuyBoosterAsync(payment)); } private void AskTrial(StoreTrialDeckDto deck) { if (_busy) return; AskConfirm( "Купить «" + deck.Name + "» за " + deck.PriceGold + " Gold?", () => BuyTrialAsync(deck)); } private void AskConfirm(string text, Func action) { _confirmText.text = text; _confirmAction = action; _confirm.SetAsLastSibling(); _confirm.gameObject.SetActive(true); } private async void OnConfirmYes() { if (_confirmAction == null) return; var action = _confirmAction; _confirmAction = null; _confirm.gameObject.SetActive(false); _busy = true; SetButtons(false); SetStatus("Выполняем покупку…", new Color(0.62f, 0.68f, 0.82f)); try { await action(); } catch (ApiException exception) { SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f)); } catch (Exception exception) { Debug.LogException(exception); SetStatus("Покупка не удалась.", new Color(1f, 0.55f, 0.45f)); } finally { _busy = false; SetButtons(true); } } private async Task BuyBoosterAsync(string payment) { var result = await _meta.BuyBoosterAsync(payment); SetStatus("Бустер открыт!", new Color(0.45f, 0.85f, 0.55f)); ShowResult(result); } private async Task BuyTrialAsync(StoreTrialDeckDto deck) { await _meta.BuyTrialDeckAsync(deck.Id); SetStatus("«" + deck.Name + "» добавлена в коллекцию.", new Color(0.45f, 0.85f, 0.55f)); await LoadAsync(); } private void ShowResult(BoosterResultDto result) { ClearChildren(_resultCards); _resultTitle.text = "Бустер открыт · " + result.Cards.Count + " карт"; foreach (var card in result.Cards) { var panel = UiKit.CreatePanel(_resultCards, "Card_" + card.PrintingId, new Color(0.09f, 0.12f, 0.20f, 1f)); var element = panel.gameObject.AddComponent(); element.preferredWidth = 210f; element.preferredHeight = 280f; element.flexibleWidth = 0f; UiKit.AddVerticalLayout(panel, 14, 8); var printing = UiKit.CreateText( panel, "Printing", card.PrintingId, 18, Color.white); printing.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(printing, 50f); var name = UiKit.CreateText( panel, "Name", card.NameId, 16, new Color(0.70f, 0.78f, 0.94f)); name.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(name, 60f); var rarity = UiKit.CreateText( panel, "Rarity", card.Rarity, 20, new Color(0.45f, 0.80f, 1f)); UiKit.SetPreferredHeight(rarity, 30f); if (card.IsNew) { var isNew = UiKit.CreateText( panel, "New", "NEW", 18, new Color(1f, 0.78f, 0.32f)); isNew.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(isNew, 30f); } } _result.SetAsLastSibling(); _result.gameObject.SetActive(true); } private void OnResultClosed() { _result.gameObject.SetActive(false); _ = LoadAsync(); } private RectTransform BuildConfirm(Transform parent) { var overlay = UiKit.CreatePanel(parent, "StoreConfirm", new Color(0f, 0f, 0f, 0.75f)); var card = UiKit.CreateCenteredCard( overlay, "ConfirmCard", new Vector2(620f, 280f), new Color(0.08f, 0.10f, 0.16f, 1f)); UiKit.AddVerticalLayout(card, 30, 14); var title = UiKit.CreateText(card, "Title", "Подтвердите покупку", 27, Color.white); title.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(title, 40f); _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", "Купить", OnConfirmYes, 20); UiKit.SetPreferredHeight(yes, 48f); var no = UiKit.CreateButton( card, "No", "Отмена", () => { _confirmAction = null; _confirm.gameObject.SetActive(false); }, 20); UiKit.SetPreferredHeight(no, 44f); return overlay; } private RectTransform BuildResult(Transform parent) { var overlay = UiKit.CreatePanel(parent, "BoosterResult", new Color(0f, 0f, 0f, 0.82f)); var card = UiKit.CreateCenteredCard( overlay, "ResultCard", new Vector2(1400f, 520f), new Color(0.06f, 0.08f, 0.14f, 1f)); UiKit.AddVerticalLayout(card, 28, 14); _resultTitle = UiKit.CreateText( card, "Title", "Бустер открыт", 28, Color.white); _resultTitle.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(_resultTitle, 40f); var row = UiKit.CreatePanel(card, "Cards", new Color(0f, 0f, 0f, 0f)); var rowElement = row.gameObject.AddComponent(); rowElement.flexibleHeight = 1f; rowElement.minHeight = 300f; UiKit.AddHorizontalLayout(row, 0, 14); _resultCards = row; var close = UiKit.CreateButton(card, "Close", "Забрать", OnResultClosed, 21); UiKit.SetPreferredHeight(close, 52f); return overlay; } private void SetButtons(bool interactable) { if (_goldButton != null) _goldButton.interactable = interactable; if (_tokenButton != null) _tokenButton.interactable = interactable; } private void OnPlayerChanged() { _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust + " · Токены " + _player.Tokens; } private void SetStatus(string text, Color color) { _status.text = text ?? string.Empty; _status.color = color; } private static void ClearChildren(Transform parent) { for (var i = parent.childCount - 1; i >= 0; i--) UnityEngine.Object.Destroy(parent.GetChild(i).gameObject); } } }