diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index 2f17f17..b814dad 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -248,7 +248,8 @@ валидация в реальном времени. - [x] Базовая коллекция: список, балансы, разбор в пыль с подтверждением; арты и «сколько в колодах» — дальше. -- [ ] Магазин: бустер (анимация открытия), триал-деки, обмен токенов. +- [x] Магазин в Lobby: бустер за Gold/токен с показом 5 карт, вторая + триал-дека; анимация открытия — дальше. - [ ] Локализация RU/EN, переключение на лету (пакет подключён). - [ ] Настройки: язык, звук, FPS, мьют. diff --git a/README.md b/README.md index 18ca541..270428a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ | CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` | | EF-миграции | готово (`InitialCreate`) | | Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` | -| Клиент | Unity 6.6: 3D-сцены Login/Lobby/Collection/DeckBuilder/Battle + SceneFlow; UI gray-box | +| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, колоды, магазин, очередь, gray-box бой | | Лаунчер | не начато | ## Требования diff --git a/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs b/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs index de3b55e..250c60f 100644 --- a/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs +++ b/client/Assets/Vanguard/Runtime/Api/ApiContracts.cs @@ -196,4 +196,30 @@ namespace Vanguard.Api.Contracts [JsonProperty("code")] public string Code { get; set; } = string.Empty; [JsonProperty("message")] public string Message { get; set; } = string.Empty; } + + public sealed class BoosterBuyRequest + { + [JsonProperty("payment")] public string Payment { get; set; } = "gold"; + [JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty; + } + + public sealed class TrialDeckBuyRequest + { + [JsonProperty("deckId")] public string DeckId { get; set; } = string.Empty; + [JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty; + } + + public sealed class BoosterResultDto + { + [JsonProperty("cards")] public List Cards { get; set; } = new List(); + [JsonProperty("balances")] public Balances Balances { get; set; } = new Balances(); + } + + public sealed class BoosterCardDto + { + [JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty; + [JsonProperty("nameId")] public string NameId { get; set; } = string.Empty; + [JsonProperty("rarity")] public string Rarity { get; set; } = string.Empty; + [JsonProperty("isNew")] public bool IsNew { get; set; } + } } diff --git a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs index e4f94a7..3491516 100644 --- a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs @@ -16,6 +16,7 @@ namespace Vanguard.App private MetaView _metaView; private OnboardingView _onboarding; private QueueView _queue; + private StoreView _store; private void Start() { @@ -33,12 +34,15 @@ namespace Vanguard.App OnLogout, () => _ = Flow.LoadCollectionAsync(), () => _ = Flow.LoadDeckBuilderAsync(), + ShowStore, () => _ = Flow.LoadDeckBuilderAsync(), ShowOnboarding); _onboarding = new OnboardingView( canvas.transform, _auth, _meta, OnOnboarded, OnLogout); _queue = new QueueView( canvas.transform, _meta, _auth, OnQueueBack, OnMatched); + _store = new StoreView( + canvas.transform, _meta, _auth, _player, OnStoreBack); ShowDefault(); } @@ -66,6 +70,18 @@ namespace Vanguard.App _onboarding.Show(); } + private void ShowStore() + { + HideAll(); + _store.Show(); + } + + private void OnStoreBack() + { + _store.Hide(); + _metaView.Show(); + } + private void OnOnboarded() { HideAll(); @@ -93,6 +109,7 @@ namespace Vanguard.App _metaView.Hide(); _onboarding.Hide(); _queue.Hide(); + _store.Hide(); } } } diff --git a/client/Assets/Vanguard/Runtime/Meta/MetaService.cs b/client/Assets/Vanguard/Runtime/Meta/MetaService.cs index 8aa668c..b858df5 100644 --- a/client/Assets/Vanguard/Runtime/Meta/MetaService.cs +++ b/client/Assets/Vanguard/Runtime/Meta/MetaService.cs @@ -96,5 +96,33 @@ namespace Vanguard.Meta await _api.PostAsync( "/api/game/tickets", new TicketRequest { MatchId = matchId }); + + public async Task BuyBoosterAsync(string payment) + { + var result = await _api.PostAsync( + "/api/store/buy/booster", + new BoosterBuyRequest + { + Payment = payment, + IdempotencyKey = Guid.NewGuid().ToString("N"), + }); + + _player.ApplyBalances(result.Balances); + return result; + } + + public async Task BuyTrialDeckAsync(string deckId) + { + var balances = await _api.PostAsync( + "/api/store/buy/trialdeck", + new TrialDeckBuyRequest + { + DeckId = deckId, + IdempotencyKey = Guid.NewGuid().ToString("N"), + }); + + _player.ApplyBalances(balances); + return balances; + } } } diff --git a/client/Assets/Vanguard/Runtime/UI/MetaView.cs b/client/Assets/Vanguard/Runtime/UI/MetaView.cs index 3b6ea72..0107cf8 100644 --- a/client/Assets/Vanguard/Runtime/UI/MetaView.cs +++ b/client/Assets/Vanguard/Runtime/UI/MetaView.cs @@ -16,6 +16,7 @@ namespace Vanguard.UI private readonly Action _onLoggedOut; private readonly Action _onCollection; private readonly Action _onDecks; + private readonly Action _onStore; private readonly Action _onPlay; private readonly Action _onOnboarding; @@ -40,6 +41,7 @@ namespace Vanguard.UI Action onLoggedOut, Action onCollection, Action onDecks, + Action onStore, Action onPlay, Action onOnboarding) { @@ -48,6 +50,7 @@ namespace Vanguard.UI _onLoggedOut = onLoggedOut; _onCollection = onCollection; _onDecks = onDecks; + _onStore = onStore; _onPlay = onPlay; _onOnboarding = onOnboarding; @@ -107,7 +110,7 @@ namespace Vanguard.UI AddAction(actions, "Коллекция", _onCollection); AddAction(actions, "Колоды", _onDecks); - AddAction(actions, "Магазин", OnStoreClicked); + AddAction(actions, "Магазин", _onStore); AddAction(actions, "В бой", _onPlay); var lower = UiKit.CreatePanel(content, "Lower", new Color(0.07f, 0.085f, 0.115f, 1f)); @@ -225,9 +228,6 @@ namespace Vanguard.UI _status.color = color; } - private void OnStoreClicked() => - SetStatus("Магазин и открытие бустеров — следующий экран.", new Color(0.70f, 0.76f, 0.90f)); - private async void OnLogoutClicked() { _logout.interactable = false; diff --git a/client/Assets/Vanguard/Runtime/UI/StoreView.cs b/client/Assets/Vanguard/Runtime/UI/StoreView.cs new file mode 100644 index 0000000..05a445b --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/StoreView.cs @@ -0,0 +1,464 @@ +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); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/StoreView.cs.meta b/client/Assets/Vanguard/Runtime/UI/StoreView.cs.meta new file mode 100644 index 0000000..bd87cbe --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/StoreView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 32a84a2b50f421b418e26b59dcb18dcb \ No newline at end of file diff --git a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs index 0369a27..98adad3 100644 --- a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs @@ -32,7 +32,7 @@ namespace Vanguard.Tests var lobby = new MetaView( canvas.transform, auth, player, () => { }, - () => { }, () => { }, () => { }, () => { }); + () => { }, () => { }, () => { }, () => { }, () => { }); var onboarding = new OnboardingView( canvas.transform, auth, meta, () => { }, () => { }); var collection = new CollectionView( @@ -41,6 +41,8 @@ namespace Vanguard.Tests canvas.transform, meta, player, () => { }, _ => { }); var queue = new QueueView( canvas.transform, meta, auth, () => { }, _ => { }); + var store = new StoreView( + canvas.transform, meta, auth, player, () => { }); var battle = new BattleView( canvas.transform, meta, () => { }); @@ -49,6 +51,7 @@ namespace Vanguard.Tests Assert.IsTrue(Exists("OnboardingScreen")); Assert.IsTrue(Exists("CollectionScreen")); Assert.IsTrue(Exists("DecksScreen")); + Assert.IsTrue(Exists("StoreScreen")); Assert.IsTrue(Exists("QueueScreen")); Assert.IsTrue(Exists("BattleScreen")); diff --git a/client/README.md b/client/README.md index d315b47..b0b6fd9 100644 --- a/client/README.md +++ b/client/README.md @@ -52,6 +52,8 @@ http://localhost:5080 - Коллекция: список карт, балансы, разбор в пыль с подтверждением. - Колоды: список, удаление, автосборка стартовой колоды из выбранной триал-деки. +- Магазин в Lobby: бустер за Gold/токен, показ 5 карт, покупка второй + триал-деки. - Очередь: classic-ranked, опрос статуса, отмена, тикет и переход в бой. - `GameConnection`: WebSocket + MessagePack, handshake, snapshot/events, интенты, ответы на ping, закрытие и переподключение. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 1bc007f..77e28f7 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -544,6 +544,8 @@ CI подключён и проходит; серверный блок закр - коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением; - колоды: `/api/decks`, удаление и автосборка стартовой колоды из выбранной триал-деки (50 карт); +- магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт, + покупка второй триал-деки; - очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`; - `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`, snapshot/events, интенты с ответом сервера, ping/pong, закрытие; @@ -564,14 +566,11 @@ CI подключён и проходит; серверный блок закр Дальше по клиенту: -1. Наполнить 3D-сцены артом и перевести Login/Lobby на полноценные - prefab/UI; сейчас это 3D-каркас с code-generated uGUI. -2. Довести боевой UI: арты, зоны, анимации, скип/ускорение. -3. Реконнект на всех шагах и аккуратная обработка таймаутов. -4. Полный декбилдер с фильтрами и счётчиками. -5. Магазин и открытие бустера. -6. История матчей и реплей. -7. Локализация RU/EN на лету, настройки, DOTween-анимации. +1. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`. +2. Полный декбилдер: фильтры, счётчики, валидация. +3. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты. +4. 3D-наполнение сцен: модели/арт, prefab-экраны, камера. +5. Локализация RU/EN, настройки, DOTween. **Зафиксированное визуальное направление** — `docs/client-visual-direction.md`: 3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection / diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 7920b17..6e09cf9 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -39,10 +39,11 @@ - Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`, `PlayerState`, `GameConnection`. - Работает: логин/регистрация/refresh, онбординг, коллекция с разбором, - список/удаление/автосборка колоды, очередь, WebSocket-матч (snapshot, - интенты, маллиган, сдача, `GameFinished`), таймер, результат. -- Не готово: полноценный декбилдер, магазин/бустеры, история/реплей в UI, - арты/модели/анимации, локализация RU/EN, настройки, DOTween. + список/удаление/автосборка колоды, магазин (бустер/триал-дека), очередь, + WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`), + таймер, результат. +- Не готово: полноценный декбилдер, история/реплей в UI, арты/модели/ + анимации, локализация RU/EN, настройки, DOTween. ## Запуск @@ -121,11 +122,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack ## Следующие шаги -1. Магазин в Lobby: `GET /api/store`, бустер за Gold/токен, показ 5 карт, - покупка второй триал-деки; балансы через `MetaService`. -2. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`. +1. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`. +2. Боевой UI: арты, зоны, анимации, скип; реконнект и таймауты. 3. Полный декбилдер: фильтры, счётчики, валидация. -4. 3D-наполнение: модели/арт, prefab-экраны, камера/анимации боя. +4. 3D-наполнение: модели/арт, prefab-экраны, камера. 5. Локализация RU/EN, настройки, DOTween. 6. Лаунчер WPF.