Add store with booster and trial deck purchases
CI / test (push) Has been cancelled
Details
CI / test (push) Has been cancelled
Details
This commit is contained in:
parent
42445eb075
commit
2fca766ba1
|
|
@ -248,7 +248,8 @@
|
||||||
валидация в реальном времени.
|
валидация в реальном времени.
|
||||||
- [x] Базовая коллекция: список, балансы, разбор в пыль с подтверждением;
|
- [x] Базовая коллекция: список, балансы, разбор в пыль с подтверждением;
|
||||||
арты и «сколько в колодах» — дальше.
|
арты и «сколько в колодах» — дальше.
|
||||||
- [ ] Магазин: бустер (анимация открытия), триал-деки, обмен токенов.
|
- [x] Магазин в Lobby: бустер за Gold/токен с показом 5 карт, вторая
|
||||||
|
триал-дека; анимация открытия — дальше.
|
||||||
- [ ] Локализация RU/EN, переключение на лету (пакет подключён).
|
- [ ] Локализация RU/EN, переключение на лету (пакет подключён).
|
||||||
- [ ] Настройки: язык, звук, FPS, мьют.
|
- [ ] Настройки: язык, звук, FPS, мьют.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
| CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` |
|
| CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` |
|
||||||
| EF-миграции | готово (`InitialCreate`) |
|
| EF-миграции | готово (`InitialCreate`) |
|
||||||
| Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` |
|
| Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` |
|
||||||
| Клиент | Unity 6.6: 3D-сцены Login/Lobby/Collection/DeckBuilder/Battle + SceneFlow; UI gray-box |
|
| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, колоды, магазин, очередь, gray-box бой |
|
||||||
| Лаунчер | не начато |
|
| Лаунчер | не начато |
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
|
||||||
|
|
@ -196,4 +196,30 @@ namespace Vanguard.Api.Contracts
|
||||||
[JsonProperty("code")] public string Code { get; set; } = string.Empty;
|
[JsonProperty("code")] public string Code { get; set; } = string.Empty;
|
||||||
[JsonProperty("message")] public string Message { 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<BoosterCardDto> Cards { get; set; } = new List<BoosterCardDto>();
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ namespace Vanguard.App
|
||||||
private MetaView _metaView;
|
private MetaView _metaView;
|
||||||
private OnboardingView _onboarding;
|
private OnboardingView _onboarding;
|
||||||
private QueueView _queue;
|
private QueueView _queue;
|
||||||
|
private StoreView _store;
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
|
|
@ -33,12 +34,15 @@ namespace Vanguard.App
|
||||||
OnLogout,
|
OnLogout,
|
||||||
() => _ = Flow.LoadCollectionAsync(),
|
() => _ = Flow.LoadCollectionAsync(),
|
||||||
() => _ = Flow.LoadDeckBuilderAsync(),
|
() => _ = Flow.LoadDeckBuilderAsync(),
|
||||||
|
ShowStore,
|
||||||
() => _ = Flow.LoadDeckBuilderAsync(),
|
() => _ = Flow.LoadDeckBuilderAsync(),
|
||||||
ShowOnboarding);
|
ShowOnboarding);
|
||||||
_onboarding = new OnboardingView(
|
_onboarding = new OnboardingView(
|
||||||
canvas.transform, _auth, _meta, OnOnboarded, OnLogout);
|
canvas.transform, _auth, _meta, OnOnboarded, OnLogout);
|
||||||
_queue = new QueueView(
|
_queue = new QueueView(
|
||||||
canvas.transform, _meta, _auth, OnQueueBack, OnMatched);
|
canvas.transform, _meta, _auth, OnQueueBack, OnMatched);
|
||||||
|
_store = new StoreView(
|
||||||
|
canvas.transform, _meta, _auth, _player, OnStoreBack);
|
||||||
|
|
||||||
ShowDefault();
|
ShowDefault();
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +70,18 @@ namespace Vanguard.App
|
||||||
_onboarding.Show();
|
_onboarding.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowStore()
|
||||||
|
{
|
||||||
|
HideAll();
|
||||||
|
_store.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStoreBack()
|
||||||
|
{
|
||||||
|
_store.Hide();
|
||||||
|
_metaView.Show();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnOnboarded()
|
private void OnOnboarded()
|
||||||
{
|
{
|
||||||
HideAll();
|
HideAll();
|
||||||
|
|
@ -93,6 +109,7 @@ namespace Vanguard.App
|
||||||
_metaView.Hide();
|
_metaView.Hide();
|
||||||
_onboarding.Hide();
|
_onboarding.Hide();
|
||||||
_queue.Hide();
|
_queue.Hide();
|
||||||
|
_store.Hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,5 +96,33 @@ namespace Vanguard.Meta
|
||||||
await _api.PostAsync<TicketResponse>(
|
await _api.PostAsync<TicketResponse>(
|
||||||
"/api/game/tickets",
|
"/api/game/tickets",
|
||||||
new TicketRequest { MatchId = matchId });
|
new TicketRequest { MatchId = matchId });
|
||||||
|
|
||||||
|
public async Task<BoosterResultDto> BuyBoosterAsync(string payment)
|
||||||
|
{
|
||||||
|
var result = await _api.PostAsync<BoosterResultDto>(
|
||||||
|
"/api/store/buy/booster",
|
||||||
|
new BoosterBuyRequest
|
||||||
|
{
|
||||||
|
Payment = payment,
|
||||||
|
IdempotencyKey = Guid.NewGuid().ToString("N"),
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.ApplyBalances(result.Balances);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Balances> BuyTrialDeckAsync(string deckId)
|
||||||
|
{
|
||||||
|
var balances = await _api.PostAsync<Balances>(
|
||||||
|
"/api/store/buy/trialdeck",
|
||||||
|
new TrialDeckBuyRequest
|
||||||
|
{
|
||||||
|
DeckId = deckId,
|
||||||
|
IdempotencyKey = Guid.NewGuid().ToString("N"),
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.ApplyBalances(balances);
|
||||||
|
return balances;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ namespace Vanguard.UI
|
||||||
private readonly Action _onLoggedOut;
|
private readonly Action _onLoggedOut;
|
||||||
private readonly Action _onCollection;
|
private readonly Action _onCollection;
|
||||||
private readonly Action _onDecks;
|
private readonly Action _onDecks;
|
||||||
|
private readonly Action _onStore;
|
||||||
private readonly Action _onPlay;
|
private readonly Action _onPlay;
|
||||||
private readonly Action _onOnboarding;
|
private readonly Action _onOnboarding;
|
||||||
|
|
||||||
|
|
@ -40,6 +41,7 @@ namespace Vanguard.UI
|
||||||
Action onLoggedOut,
|
Action onLoggedOut,
|
||||||
Action onCollection,
|
Action onCollection,
|
||||||
Action onDecks,
|
Action onDecks,
|
||||||
|
Action onStore,
|
||||||
Action onPlay,
|
Action onPlay,
|
||||||
Action onOnboarding)
|
Action onOnboarding)
|
||||||
{
|
{
|
||||||
|
|
@ -48,6 +50,7 @@ namespace Vanguard.UI
|
||||||
_onLoggedOut = onLoggedOut;
|
_onLoggedOut = onLoggedOut;
|
||||||
_onCollection = onCollection;
|
_onCollection = onCollection;
|
||||||
_onDecks = onDecks;
|
_onDecks = onDecks;
|
||||||
|
_onStore = onStore;
|
||||||
_onPlay = onPlay;
|
_onPlay = onPlay;
|
||||||
_onOnboarding = onOnboarding;
|
_onOnboarding = onOnboarding;
|
||||||
|
|
||||||
|
|
@ -107,7 +110,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
AddAction(actions, "Коллекция", _onCollection);
|
AddAction(actions, "Коллекция", _onCollection);
|
||||||
AddAction(actions, "Колоды", _onDecks);
|
AddAction(actions, "Колоды", _onDecks);
|
||||||
AddAction(actions, "Магазин", OnStoreClicked);
|
AddAction(actions, "Магазин", _onStore);
|
||||||
AddAction(actions, "В бой", _onPlay);
|
AddAction(actions, "В бой", _onPlay);
|
||||||
|
|
||||||
var lower = UiKit.CreatePanel(content, "Lower", new Color(0.07f, 0.085f, 0.115f, 1f));
|
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;
|
_status.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnStoreClicked() =>
|
|
||||||
SetStatus("Магазин и открытие бустеров — следующий экран.", new Color(0.70f, 0.76f, 0.90f));
|
|
||||||
|
|
||||||
private async void OnLogoutClicked()
|
private async void OnLogoutClicked()
|
||||||
{
|
{
|
||||||
_logout.interactable = false;
|
_logout.interactable = false;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>Магазин в Lobby: бустеры и вторая триал-дека.</summary>
|
||||||
|
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<Task> _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<LayoutElement>();
|
||||||
|
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>().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<StoreTrialDeckDto> 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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
ownedElement.preferredWidth = 120f;
|
||||||
|
ownedElement.preferredHeight = 40f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var buy = UiKit.CreateButton(
|
||||||
|
row,
|
||||||
|
"Buy",
|
||||||
|
"Купить за " + deck.PriceGold + " Gold",
|
||||||
|
() => AskTrial(deck),
|
||||||
|
17);
|
||||||
|
var buyElement = buy.gameObject.AddComponent<LayoutElement>();
|
||||||
|
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<Task> 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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 32a84a2b50f421b418e26b59dcb18dcb
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Vanguard.Tests
|
||||||
|
|
||||||
var lobby = new MetaView(
|
var lobby = new MetaView(
|
||||||
canvas.transform, auth, player, () => { },
|
canvas.transform, auth, player, () => { },
|
||||||
() => { }, () => { }, () => { }, () => { });
|
() => { }, () => { }, () => { }, () => { }, () => { });
|
||||||
var onboarding = new OnboardingView(
|
var onboarding = new OnboardingView(
|
||||||
canvas.transform, auth, meta, () => { }, () => { });
|
canvas.transform, auth, meta, () => { }, () => { });
|
||||||
var collection = new CollectionView(
|
var collection = new CollectionView(
|
||||||
|
|
@ -41,6 +41,8 @@ namespace Vanguard.Tests
|
||||||
canvas.transform, meta, player, () => { }, _ => { });
|
canvas.transform, meta, player, () => { }, _ => { });
|
||||||
var queue = new QueueView(
|
var queue = new QueueView(
|
||||||
canvas.transform, meta, auth, () => { }, _ => { });
|
canvas.transform, meta, auth, () => { }, _ => { });
|
||||||
|
var store = new StoreView(
|
||||||
|
canvas.transform, meta, auth, player, () => { });
|
||||||
var battle = new BattleView(
|
var battle = new BattleView(
|
||||||
canvas.transform, meta, () => { });
|
canvas.transform, meta, () => { });
|
||||||
|
|
||||||
|
|
@ -49,6 +51,7 @@ namespace Vanguard.Tests
|
||||||
Assert.IsTrue(Exists("OnboardingScreen"));
|
Assert.IsTrue(Exists("OnboardingScreen"));
|
||||||
Assert.IsTrue(Exists("CollectionScreen"));
|
Assert.IsTrue(Exists("CollectionScreen"));
|
||||||
Assert.IsTrue(Exists("DecksScreen"));
|
Assert.IsTrue(Exists("DecksScreen"));
|
||||||
|
Assert.IsTrue(Exists("StoreScreen"));
|
||||||
Assert.IsTrue(Exists("QueueScreen"));
|
Assert.IsTrue(Exists("QueueScreen"));
|
||||||
Assert.IsTrue(Exists("BattleScreen"));
|
Assert.IsTrue(Exists("BattleScreen"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ http://localhost:5080
|
||||||
- Коллекция: список карт, балансы, разбор в пыль с подтверждением.
|
- Коллекция: список карт, балансы, разбор в пыль с подтверждением.
|
||||||
- Колоды: список, удаление, автосборка стартовой колоды из выбранной
|
- Колоды: список, удаление, автосборка стартовой колоды из выбранной
|
||||||
триал-деки.
|
триал-деки.
|
||||||
|
- Магазин в Lobby: бустер за Gold/токен, показ 5 карт, покупка второй
|
||||||
|
триал-деки.
|
||||||
- Очередь: classic-ranked, опрос статуса, отмена, тикет и переход в бой.
|
- Очередь: classic-ranked, опрос статуса, отмена, тикет и переход в бой.
|
||||||
- `GameConnection`: WebSocket + MessagePack, handshake, snapshot/events,
|
- `GameConnection`: WebSocket + MessagePack, handshake, snapshot/events,
|
||||||
интенты, ответы на ping, закрытие и переподключение.
|
интенты, ответы на ping, закрытие и переподключение.
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,8 @@ CI подключён и проходит; серверный блок закр
|
||||||
- коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением;
|
- коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением;
|
||||||
- колоды: `/api/decks`, удаление и автосборка стартовой колоды из
|
- колоды: `/api/decks`, удаление и автосборка стартовой колоды из
|
||||||
выбранной триал-деки (50 карт);
|
выбранной триал-деки (50 карт);
|
||||||
|
- магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт,
|
||||||
|
покупка второй триал-деки;
|
||||||
- очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`;
|
- очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`;
|
||||||
- `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`,
|
- `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`,
|
||||||
snapshot/events, интенты с ответом сервера, ping/pong, закрытие;
|
snapshot/events, интенты с ответом сервера, ping/pong, закрытие;
|
||||||
|
|
@ -564,14 +566,11 @@ CI подключён и проходит; серверный блок закр
|
||||||
|
|
||||||
Дальше по клиенту:
|
Дальше по клиенту:
|
||||||
|
|
||||||
1. Наполнить 3D-сцены артом и перевести Login/Lobby на полноценные
|
1. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`.
|
||||||
prefab/UI; сейчас это 3D-каркас с code-generated uGUI.
|
2. Полный декбилдер: фильтры, счётчики, валидация.
|
||||||
2. Довести боевой UI: арты, зоны, анимации, скип/ускорение.
|
3. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты.
|
||||||
3. Реконнект на всех шагах и аккуратная обработка таймаутов.
|
4. 3D-наполнение сцен: модели/арт, prefab-экраны, камера.
|
||||||
4. Полный декбилдер с фильтрами и счётчиками.
|
5. Локализация RU/EN, настройки, DOTween.
|
||||||
5. Магазин и открытие бустера.
|
|
||||||
6. История матчей и реплей.
|
|
||||||
7. Локализация RU/EN на лету, настройки, DOTween-анимации.
|
|
||||||
|
|
||||||
**Зафиксированное визуальное направление** — `docs/client-visual-direction.md`:
|
**Зафиксированное визуальное направление** — `docs/client-visual-direction.md`:
|
||||||
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection /
|
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection /
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,11 @@
|
||||||
- Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`,
|
- Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`,
|
||||||
`PlayerState`, `GameConnection`.
|
`PlayerState`, `GameConnection`.
|
||||||
- Работает: логин/регистрация/refresh, онбординг, коллекция с разбором,
|
- Работает: логин/регистрация/refresh, онбординг, коллекция с разбором,
|
||||||
список/удаление/автосборка колоды, очередь, WebSocket-матч (snapshot,
|
список/удаление/автосборка колоды, магазин (бустер/триал-дека), очередь,
|
||||||
интенты, маллиган, сдача, `GameFinished`), таймер, результат.
|
WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`),
|
||||||
- Не готово: полноценный декбилдер, магазин/бустеры, история/реплей в UI,
|
таймер, результат.
|
||||||
арты/модели/анимации, локализация RU/EN, настройки, DOTween.
|
- Не готово: полноценный декбилдер, история/реплей в UI, арты/модели/
|
||||||
|
анимации, локализация RU/EN, настройки, DOTween.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
@ -121,11 +122,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
||||||
|
|
||||||
## Следующие шаги
|
## Следующие шаги
|
||||||
|
|
||||||
1. Магазин в Lobby: `GET /api/store`, бустер за Gold/токен, показ 5 карт,
|
1. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`.
|
||||||
покупка второй триал-деки; балансы через `MetaService`.
|
2. Боевой UI: арты, зоны, анимации, скип; реконнект и таймауты.
|
||||||
2. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`.
|
|
||||||
3. Полный декбилдер: фильтры, счётчики, валидация.
|
3. Полный декбилдер: фильтры, счётчики, валидация.
|
||||||
4. 3D-наполнение: модели/арт, prefab-экраны, камера/анимации боя.
|
4. 3D-наполнение: модели/арт, prefab-экраны, камера.
|
||||||
5. Локализация RU/EN, настройки, DOTween.
|
5. Локализация RU/EN, настройки, DOTween.
|
||||||
6. Лаунчер WPF.
|
6. Лаунчер WPF.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue