diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index d541fac..78992e6 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -227,14 +227,15 @@ главный экран профиля. Подробности — `client/README.md` и `docs/HANDOFF.md`. **Визуальное направление зафиксировано:** 3D-клиент на URP, отдельные -сцены-экраны **Login / Lobby / Collection / DeckBuilder / Battle**; -`Boot` — служебная сцена. Очередь и онбординг — оверлеи Lobby. Текущий +сцены-экраны **Login / Lobby / DeckBuilder / Battle**; +`DeckBuilder` совмещает коллекцию и редактор колоды. `Boot` — служебная +сцена. Очередь и онбординг — оверлеи Lobby. Текущий единый Canvas — временный функциональный gray-box. Полное решение — `docs/client-visual-direction.md`. - [x] Unity 6 LTS, URP, Addressables, Input System, Localization. DOTween — следующим шагом (пока UI без внешних твинов). -- [x] 3D-сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle` +- [x] 3D-сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` и `SceneFlow`; `Boot` — стартовая служебная сцена. 3D-окружение пока из примитивов, UI — code-generated uGUI. - [x] Composition root + DI (VContainer), без синглтон-свалок. diff --git a/client/Assets/Vanguard/Editor/SceneBuilder.cs b/client/Assets/Vanguard/Editor/SceneBuilder.cs index 325e9a8..47b2195 100644 --- a/client/Assets/Vanguard/Editor/SceneBuilder.cs +++ b/client/Assets/Vanguard/Editor/SceneBuilder.cs @@ -29,7 +29,6 @@ namespace Vanguard.Editor Create("Login"); Create("Lobby"); - Create("Collection"); Create("DeckBuilder"); Create("Battle"); @@ -37,7 +36,6 @@ namespace Vanguard.Editor Add(scenes, BootPath); Add(scenes, SceneDir + "/Login.unity"); Add(scenes, SceneDir + "/Lobby.unity"); - Add(scenes, SceneDir + "/Collection.unity"); Add(scenes, SceneDir + "/DeckBuilder.unity"); Add(scenes, SceneDir + "/Battle.unity"); diff --git a/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs b/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs index ba7d6ee..ce8e5b1 100644 --- a/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs +++ b/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs @@ -170,6 +170,12 @@ namespace Vanguard.Api.Contracts [JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty; } + public sealed class CraftRequest + { + [JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty; + [JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty; + } + public sealed class DeckDto { [JsonProperty("id")] public Guid Id { get; set; } @@ -277,12 +283,22 @@ namespace Vanguard.Api.Contracts [JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty; [JsonProperty("nameId")] public string NameId { get; set; } = string.Empty; [JsonProperty("nameEn")] public string NameEn { get; set; } = string.Empty; + [JsonProperty("nameRu")] public string NameRu { get; set; } [JsonProperty("rarity")] public string Rarity { get; set; } = string.Empty; [JsonProperty("grade")] public int Grade { get; set; } + [JsonProperty("type")] public string Type { get; set; } = string.Empty; + [JsonProperty("power")] public int Power { get; set; } + [JsonProperty("shield")] public int Shield { get; set; } + [JsonProperty("critical")] public int Critical { get; set; } [JsonProperty("trigger")] public string Trigger { get; set; } = "None"; [JsonProperty("clan")] public string Clan { get; set; } = string.Empty; [JsonProperty("isSentinel")] public bool IsSentinel { get; set; } + [JsonProperty("isRestraint")] public bool IsRestraint { get; set; } + [JsonProperty("hasBoost")] public bool HasBoost { get; set; } + [JsonProperty("hasIntercept")] public bool HasIntercept { get; set; } [JsonProperty("isParallel")] public bool IsParallel { get; set; } [JsonProperty("basePrintingId")] public string BasePrintingId { get; set; } + [JsonProperty("textEn")] public string TextEn { get; set; } + [JsonProperty("textRu")] public string TextRu { get; set; } } } diff --git a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs b/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs deleted file mode 100644 index 02eab81..0000000 --- a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using UnityEngine; -using Vanguard.Meta; -using Vanguard.State; -using Vanguard.UI; - -namespace Vanguard.App -{ - public sealed class CollectionSceneController : SceneControllerBase - { - private CollectionView _view; - - private void Start() - { - SceneEnvironment.Build(SceneTheme.Collection); - var canvas = CreateCanvas("Collection Canvas"); - _view = new CollectionView( - canvas.transform, - Resolve(), - Resolve(), - () => _ = Flow.LoadLobbyAsync()); - _view.Show(); - } - - private void OnDestroy() - { - _view?.Dispose(); - } - } -} diff --git a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs.meta b/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs.meta deleted file mode 100644 index 40f31ba..0000000 --- a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8ccb09cec5c89814493fae05cf2d6051 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs index a675d53..ae8fc70 100644 --- a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs @@ -33,7 +33,6 @@ namespace Vanguard.App _auth, _player, OnLogout, - () => _ = Flow.LoadCollectionAsync(), () => _ = Flow.LoadDeckBuilderAsync(), ShowStore, ShowHistory, diff --git a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs index d5df3b0..cd2fe2d 100644 --- a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs +++ b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs @@ -6,7 +6,6 @@ namespace Vanguard.App { Login, Lobby, - Collection, DeckBuilder, Battle, } @@ -143,15 +142,6 @@ namespace Vanguard.App Cube(root, "Lobby Light", new Vector3(0f, 5.2f, 4f), new Vector3(6f, 0.2f, 1f), palette.Accent, 1.8f); break; - case SceneTheme.Collection: - for (var i = -2; i <= 2; i++) - { - Cube(root, "Shelf " + i, new Vector3(i * 3.2f, 1.6f, 6.5f), new Vector3(2.2f, 3.2f, 0.5f), palette.Structure); - Cube(root, "Shelf Glow " + i, new Vector3(i * 3.2f, 3.35f, 6.2f), new Vector3(2.2f, 0.1f, 0.5f), palette.Accent, 1.2f); - } - Cube(root, "Table", new Vector3(0f, 0.4f, 1.5f), new Vector3(7f, 0.8f, 4f), palette.Structure); - break; - case SceneTheme.DeckBuilder: Cube(root, "Table", new Vector3(0f, 0.5f, 2f), new Vector3(11f, 1f, 6f), palette.Structure); Cube(root, "Deck Stack", new Vector3(-3.8f, 1.3f, 3.2f), new Vector3(1.5f, 1.4f, 2.2f), palette.Accent, 0.8f); diff --git a/client/Assets/Vanguard/Runtime/App/SceneFlow.cs b/client/Assets/Vanguard/Runtime/App/SceneFlow.cs index 25cb020..8db4d96 100644 --- a/client/Assets/Vanguard/Runtime/App/SceneFlow.cs +++ b/client/Assets/Vanguard/Runtime/App/SceneFlow.cs @@ -16,7 +16,6 @@ namespace Vanguard.App public const string Boot = "Boot"; public const string Login = "Login"; public const string Lobby = "Lobby"; - public const string Collection = "Collection"; public const string DeckBuilder = "DeckBuilder"; public const string Battle = "Battle"; @@ -32,8 +31,6 @@ namespace Vanguard.App public Task LoadLobbyAsync() => LoadAsync(Lobby); - public Task LoadCollectionAsync() => LoadAsync(Collection); - public Task LoadDeckBuilderAsync() => LoadAsync(DeckBuilder); public Task OpenLobbyWithQueueAsync(DeckDto deck) diff --git a/client/Assets/Vanguard/Runtime/Meta/MetaService.cs b/client/Assets/Vanguard/Runtime/Meta/MetaService.cs index da070ff..41b884d 100644 --- a/client/Assets/Vanguard/Runtime/Meta/MetaService.cs +++ b/client/Assets/Vanguard/Runtime/Meta/MetaService.cs @@ -61,6 +61,20 @@ namespace Vanguard.Meta return balances; } + public async Task CraftAsync(string printingId) + { + var balances = await _api.PostAsync( + "/api/collection/craft", + new CraftRequest + { + PrintingId = printingId, + IdempotencyKey = Guid.NewGuid().ToString("N"), + }); + + _player.ApplyBalances(balances); + return balances; + } + public async Task> GetDecksAsync() { var decks = await _api.GetAsync>("/api/decks"); diff --git a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs b/client/Assets/Vanguard/Runtime/UI/CollectionView.cs deleted file mode 100644 index 1220632..0000000 --- a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs +++ /dev/null @@ -1,318 +0,0 @@ -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.Meta; -using Vanguard.State; - -namespace Vanguard.UI -{ - /// Коллекция игрока и разбор карт в пыль. - public sealed class CollectionView : IDisposable - { - private readonly MetaService _meta; - private readonly PlayerState _player; - private readonly Action _onBack; - private bool _disposed; - - private readonly RectTransform _root; - private readonly Text _balances; - private readonly Text _status; - private readonly RectTransform _confirm; - private Text _confirmText; - private RectTransform _content; - private CollectionItemDto _pending; - private bool _loading; - - public CollectionView( - Transform parent, - MetaService meta, - PlayerState player, - Action onBack) - { - _meta = meta; - _player = player; - _onBack = onBack; - - _root = UiKit.CreatePanel(parent, "CollectionScreen", new Color(0.035f, 0.045f, 0.065f, 1f)); - - var top = UiKit.CreatePanel(_root, "TopBar", new Color(0.07f, 0.085f, 0.115f, 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.035f, 0.045f, 0.065f, 0f)); - body.anchorMin = Vector2.zero; - body.anchorMax = Vector2.one; - body.offsetMin = new Vector2(40f, 28f); - body.offsetMax = new Vector2(-40f, -110f); - UiKit.AddVerticalLayout(body, 8, 10); - - var hint = UiKit.CreateText( - body, - "Hint", - "Разбор даёт пыль: C 5 · R 10 · RR 25 · RRR 100. SP не разбирается.", - 17, - new Color(0.58f, 0.65f, 0.78f), - TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(hint, 26f); - - var scroll = UiKit.CreateScrollView(body, "CollectionScroll", out _content, 8f); - var scrollElement = scroll.gameObject.AddComponent(); - scrollElement.flexibleHeight = 1f; - scrollElement.minHeight = 200f; - - _status = UiKit.CreateText( - body, "Status", string.Empty, 18, - new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(_status, 34f); - - _confirm = BuildConfirm(parent); - _confirm.gameObject.SetActive(false); - - _root.gameObject.SetActive(false); - _player.Changed += OnPlayerChanged; - } - - public void Show() - { - UiKit.SetActive(_root, true); - _confirm.gameObject.SetActive(false); - OnPlayerChanged(); - _ = LoadAsync(); - } - - public void Hide() - { - UiKit.SetActive(_root, false); - _confirm.gameObject.SetActive(false); - } - - public void Dispose() - { - if (_disposed) - return; - - _disposed = true; - _player.Changed -= OnPlayerChanged; - } - - private RectTransform BuildConfirm(Transform parent) - { - var overlay = UiKit.CreatePanel(parent, "DismantleConfirm", new Color(0f, 0f, 0f, 0.72f)); - var card = UiKit.CreateCenteredCard( - overlay, "ConfirmCard", new Vector2(620f, 300f), - new Color(0.08f, 0.10f, 0.14f, 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), - TextAnchor.MiddleCenter); - UiKit.SetPreferredHeight(_confirmText, 110f); - - var yes = UiKit.CreateButton(card, "Yes", "Разобрать", OnConfirmDismantle, 20); - UiKit.SetPreferredHeight(yes, 48f); - - var no = UiKit.CreateButton(card, "No", "Отмена", HideConfirm, 20); - UiKit.SetPreferredHeight(no, 44f); - - overlay.gameObject.SetActive(false); - return overlay; - } - - private async Task LoadAsync() - { - if (_loading) - return; - - _loading = true; - SetStatus("Загружаем коллекцию…", new Color(0.62f, 0.68f, 0.82f)); - - try - { - var collection = await _meta.GetCollectionAsync(); - if (_disposed) - return; - - Rebuild(collection.Items ?? new List()); - SetStatus( - collection.Items == null || collection.Items.Count == 0 - ? "Коллекция пуста. Пройдите онбординг." - : "Карт в коллекции: " + collection.Items.Sum(item => item.Count) + ".", - 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; - } - } - - private void Rebuild(IReadOnlyList items) - { - for (var i = _content.childCount - 1; i >= 0; i--) - UnityEngine.Object.Destroy(_content.GetChild(i).gameObject); - - if (items.Count == 0) - return; - - foreach (var item in items) - AddRow(item); - } - - private void AddRow(CollectionItemDto item) - { - var row = UiKit.CreatePanel(_content, "Item_" + item.PrintingId, new Color(0.07f, 0.085f, 0.115f, 1f)); - var rowElement = row.gameObject.AddComponent(); - rowElement.preferredHeight = 64f; - UiKit.AddHorizontalLayout(row, 10, 10); - - var name = UiKit.CreateText( - row, "Name", item.NameEn, 19, Color.white, TextAnchor.MiddleLeft); - var nameElement = name.gameObject.AddComponent(); - nameElement.flexibleWidth = 1f; - nameElement.preferredHeight = 40f; - - var printing = UiKit.CreateText( - row, "Printing", item.PrintingId, 16, - new Color(0.58f, 0.65f, 0.78f), TextAnchor.MiddleLeft); - var printingElement = printing.gameObject.AddComponent(); - printingElement.preferredWidth = 190f; - printingElement.preferredHeight = 40f; - - var rarity = UiKit.CreateText( - row, "Rarity", item.Rarity, 17, - new Color(0.78f, 0.82f, 0.95f)); - var rarityElement = rarity.gameObject.AddComponent(); - rarityElement.preferredWidth = 64f; - rarityElement.preferredHeight = 40f; - - var count = UiKit.CreateText( - row, "Count", "×" + item.Count, 18, - new Color(0.78f, 0.82f, 0.95f)); - var countElement = count.gameObject.AddComponent(); - countElement.preferredWidth = 54f; - countElement.preferredHeight = 40f; - - var canDismantle = item.Rarity != "SP" && item.Count > 0; - var dismantle = UiKit.CreateButton( - row, - "Dismantle", - canDismantle ? "Разобрать 1" : "SP", - () => ShowConfirm(item), - 16); - dismantle.interactable = canDismantle; - var dismantleElement = dismantle.gameObject.AddComponent(); - dismantleElement.preferredWidth = 150f; - dismantleElement.preferredHeight = 44f; - dismantleElement.flexibleWidth = 0f; - } - - private void ShowConfirm(CollectionItemDto item) - { - _pending = item; - _confirmText.text = "«" + item.NameEn + "» (" + item.PrintingId + ")\n" - + "Вы получите " + DustFor(item.Rarity) + " пыли. Осталось: " + item.Count + "."; - _confirm.SetAsLastSibling(); - _confirm.gameObject.SetActive(true); - } - - private void HideConfirm() - { - _pending = null; - _confirm.gameObject.SetActive(false); - } - - private async void OnConfirmDismantle() - { - if (_pending == null) - return; - - var item = _pending; - _confirm.gameObject.SetActive(false); - SetStatus("Разбираем " + item.PrintingId + "…", new Color(0.62f, 0.68f, 0.82f)); - - try - { - await _meta.DismantleAsync(item.PrintingId, 1); - SetStatus("Разобрано: " + item.PrintingId + ".", new Color(0.45f, 0.85f, 0.55f)); - await LoadAsync(); - } - 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 - { - _pending = null; - } - } - - private static int DustFor(string rarity) - { - switch (rarity) - { - case "C": return 5; - case "R": return 10; - case "RR": return 25; - case "RRR": return 100; - default: return 0; - } - } - - private void OnPlayerChanged() - { - if (_disposed || _balances == null) - return; - - _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust - + " · Токены " + _player.Tokens; - } - - private void SetStatus(string text, Color color) - { - if (_disposed || _status == null) - return; - - _status.text = text ?? string.Empty; - _status.color = color; - } - } -} diff --git a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs.meta b/client/Assets/Vanguard/Runtime/UI/CollectionView.cs.meta deleted file mode 100644 index db8d2be..0000000 --- a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 117359b61c1e3454d8dcaebc08bfa85f \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs index 54a781c..c2ab602 100644 --- a/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs +++ b/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs @@ -3,6 +3,7 @@ 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; @@ -41,6 +42,20 @@ namespace Vanguard.UI 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 = @@ -75,11 +90,13 @@ namespace Vanguard.UI _editorView = BuildEditorView(); _filterOverlay = BuildFilterOverlay(); _confirmOverlay = BuildDeleteConfirm(); + _cardOverlay = BuildCardOverlay(); _listView.gameObject.SetActive(false); _editorView.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); + _cardOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } @@ -94,6 +111,7 @@ namespace Vanguard.UI { _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); + _cardOverlay.gameObject.SetActive(false); _root.gameObject.SetActive(false); } @@ -105,7 +123,7 @@ namespace Vanguard.UI var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 17); SetFixedSize(back, 120f, 44f); - var title = UiKit.CreateText(top, "Title", "Колоды", 28, Color.white, TextAnchor.MiddleLeft); + var title = UiKit.CreateText(top, "Title", "Коллекция", 28, Color.white, TextAnchor.MiddleLeft); title.fontStyle = FontStyle.Bold; SetFixedSize(title, 150f, 44f); @@ -187,7 +205,7 @@ 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); + var back = UiKit.CreateButton(top, "Back", "← Коллекция", BackToList, 17); SetFixedSize(back, 120f, 44f); _nameInput = UiKit.CreateInput(top, "Name", "Название колоды"); @@ -265,7 +283,7 @@ namespace Vanguard.UI deckScrollElement.minHeight = 200f; var hint = UiKit.CreateText( - deckArea, "Hint", "Нажмите на карту,\nчтобы убрать одну копию.", 14, + deckArea, "Hint", "ЛКМ — убрать копию,\nПКМ — посмотреть карту.", 14, new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft); hint.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(hint, 46f); @@ -352,6 +370,265 @@ namespace Vanguard.UI 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.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); + _cardTitle.fontStyle = FontStyle.Bold; + _cardTitle.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.SetPreferredHeight(_cardTitle, 44f); + + _cardSubtitle = UiKit.CreateText( + info, "Subtitle", string.Empty, 16, + new Color(0.62f, 0.70f, 0.84f), TextAnchor.MiddleLeft); + 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, + new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft); + UiKit.SetPreferredHeight(abilitiesCaption, 24f); + + _cardAbilities = UiKit.CreateText( + info, "Abilities", string.Empty, 15, + new Color(0.82f, 0.86f, 0.96f), TextAnchor.UpperLeft); + _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, + new Color(0.62f, 0.68f, 0.82f), 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 void AddRightClick(GameObject target, string printingId, CardCatalogItemDto meta) + { + var trigger = target.GetComponent() + ?? target.AddComponent(); + var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerClick }; + entry.callback.AddListener(data => + { + if (data is PointerEventData pointer + && pointer.button == PointerEventData.InputButton.Right) + { + ShowCardOverlay(printingId, meta); + } + }); + trigger.triggers.Add(entry); + } + + private void ShowCardOverlay(string printingId, CardCatalogItemDto meta) + { + if (meta == null) + return; + + _selectedMeta = meta; + _selectedItem = _collection.Find(item => item.PrintingId == printingId); + _cardStatus.text = string.Empty; + UpdateCardOverlay(); + _cardOverlay.SetAsLastSibling(); + _cardOverlay.gameObject.SetActive(true); + } + + private void UpdateCardOverlay() + { + if (_selectedMeta == null) + return; + + var meta = _selectedMeta; + _cardArt.color = ClanColor(meta.Clan); + _cardArtName.text = meta.NameEn; + _cardTitle.text = meta.NameEn; + _cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ") + + meta.Clan + " · G" + meta.Grade; + _cardStats.text = "Тип: " + meta.Type + " · Редкость: " + meta.Rarity + "\n" + + "Power " + meta.Power + " · Shield " + meta.Shield + " · Critical " + meta.Critical + "\n" + + "Триггер: " + meta.Trigger + + " · Sentinel: " + (meta.IsSentinel ? "да" : "нет") + + " · Boost: " + (meta.HasBoost ? "да" : "нет") + + " · Intercept: " + (meta.HasIntercept ? "да" : "нет") + "\n" + + "Принт: " + meta.PrintingId; + _cardAbilities.text = string.IsNullOrWhiteSpace(meta.TextRu) + ? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn) + : meta.TextRu; + _cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0) + + " · в колоде: " + UsedByName(meta.NameId); + + var canChange = CanChange(meta.Rarity); + _dismantleButton.interactable = canChange && !_busy + && _selectedItem != null && _selectedItem.Count > 0; + _craftButton.interactable = canChange && !_busy; + SetButtonLabel( + _dismantleButton, + canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать"); + SetButtonLabel( + _craftButton, + canChange ? "Скрафтить (−" + CraftCostFor(meta.Rarity) + " пыли)" : "Нельзя скрафтить"); + } + + private static bool CanChange(string rarity) => + rarity == "C" || rarity == "R" || rarity == "RR" || rarity == "RRR"; + + private async void DismantleSelected() + { + if (_busy || _selectedItem == null || _selectedItem.Count <= 0 + || _selectedMeta == null || !CanChange(_selectedMeta.Rarity)) + return; + + var printingId = _selectedItem.PrintingId; + _busy = true; + UpdateCardOverlay(); + _cardStatus.text = "Разбираем…"; + _cardStatus.color = new Color(0.62f, 0.68f, 0.82f); + try + { + await _auth.EnsureAccessAsync(); + await _meta.DismantleAsync(printingId, 1); + await ReloadCollectionAsync(printingId); + SetCardStatus("Карта разобрана.", false); + } + catch (ApiException exception) + { + SetCardStatus(exception.Message, true); + } + catch (Exception exception) + { + Debug.LogException(exception); + SetCardStatus("Не удалось разобрать карту.", true); + } + finally + { + _busy = false; + UpdateCardOverlay(); + } + } + + private async void CraftSelected() + { + if (_busy || _selectedMeta == null || !CanChange(_selectedMeta.Rarity)) + return; + + var printingId = _selectedMeta.PrintingId; + _busy = true; + UpdateCardOverlay(); + _cardStatus.text = "Крафтим…"; + _cardStatus.color = new Color(0.62f, 0.68f, 0.82f); + try + { + await _auth.EnsureAccessAsync(); + await _meta.CraftAsync(printingId); + await ReloadCollectionAsync(printingId); + SetCardStatus("Карта скрафчена.", false); + } + catch (ApiException exception) + { + SetCardStatus(exception.Message, true); + } + catch (Exception exception) + { + Debug.LogException(exception); + SetCardStatus("Не удалось скрафтить карту.", true); + } + finally + { + _busy = false; + UpdateCardOverlay(); + } + } + + private async Task ReloadCollectionAsync(string printingId) + { + var collection = await _meta.GetCollectionAsync(); + _collection.Clear(); + _collection.AddRange(collection.Items ?? new List()); + RebuildCollectionGrids(); + _selectedItem = _collection.Find(item => item.PrintingId == printingId); + if (_selectedItem == null) + { + _cardOverlay.gameObject.SetActive(false); + _listStatus.text = "Карт в коллекции: " + _collection.Count + "."; + } + else + { + UpdateCardOverlay(); + } + } + + private void SetCardStatus(string text, bool error) + { + _cardStatus.text = text; + _cardStatus.color = error + ? new Color(1f, 0.55f, 0.45f) + : new Color(0.45f, 0.85f, 0.55f); + } + + private void CloseCardOverlay() + { + _selectedItem = null; + _selectedMeta = null; + _cardOverlay.gameObject.SetActive(false); + } + private void AddFilterSection(Transform parent, string title, string[] labels, Action onSelected) { var caption = UiKit.CreateText( @@ -411,8 +688,8 @@ namespace Vanguard.UI BuildDeckList(); RebuildCollectionGrids(); _listStatus.text = _decks.Count == 0 - ? "Колод пока нет. Создайте первую." - : "Карт в коллекции: " + _collection.Count + "."; + ? "Колод пока нет. Создайте первую. ПКМ по карте — подробности." + : "Карт в коллекции: " + _collection.Count + " · ПКМ по карте — подробности."; } catch (ApiException exception) { @@ -491,6 +768,7 @@ namespace Vanguard.UI _listView.gameObject.SetActive(false); _editorView.gameObject.SetActive(true); _filterOverlay.gameObject.SetActive(false); + _cardOverlay.gameObject.SetActive(false); _editorStatus.text = string.Empty; RebuildEditor(); } @@ -498,6 +776,7 @@ namespace Vanguard.UI private void BackToList() { _filterOverlay.gameObject.SetActive(false); + _cardOverlay.gameObject.SetActive(false); _editorView.gameObject.SetActive(false); _listView.gameObject.SetActive(true); RefreshAllCounts(); @@ -508,6 +787,7 @@ namespace Vanguard.UI _editorView.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false); + _cardOverlay.gameObject.SetActive(false); _listView.gameObject.SetActive(true); } @@ -566,6 +846,7 @@ namespace Vanguard.UI var cell = UiKit.CreatePanel( content, "Owned_" + item.PrintingId, new Color(0.08f, 0.11f, 0.17f, 1f)); UiKit.AddVerticalLayout(cell, 6, 2); + AddRightClick(cell.gameObject, item.PrintingId, meta); if (onAdd != null) { @@ -651,6 +932,8 @@ namespace Vanguard.UI var button = row.gameObject.AddComponent