Add onboarding, collection and deck screens to the Unity client
CI / test (push) Successful in 1m20s
Details
CI / test (push) Successful in 1m20s
Details
This commit is contained in:
parent
32d9c31f4a
commit
1aca632958
|
|
@ -233,12 +233,12 @@
|
||||||
- [x] `ApiClient` (HTTP/JSON), `PlayerState`, таймауты и разбор `ApiError`.
|
- [x] `ApiClient` (HTTP/JSON), `PlayerState`, таймауты и разбор `ApiError`.
|
||||||
- [ ] `GameConnection` (WebSocket + MessagePack), очередь событий,
|
- [ ] `GameConnection` (WebSocket + MessagePack), очередь событий,
|
||||||
`ClientGameState`.
|
`ClientGameState`.
|
||||||
- [ ] Экраны: логин/регистрация — готово; главное меню — каркас;
|
- [x] Экраны: логин/регистрация, главное меню, онбординг, коллекция,
|
||||||
профиль, магазин, коллекция, декбилдер, очередь, настройки — дальше.
|
список колод и автосборка стартовой; магазин/полный декбилдер — дальше.
|
||||||
- [ ] Декбилдер: фильтры (клан, грейд, редкость, тип, арт), счётчики,
|
- [ ] Декбилдер: фильтры (клан, грейд, редкость, тип, арт), счётчики,
|
||||||
валидация в реальном времени.
|
валидация в реальном времени.
|
||||||
- [ ] Коллекция: арты раздельно, «сколько в колодах», разбор в пыль
|
- [x] Базовая коллекция: список, балансы, разбор в пыль с подтверждением;
|
||||||
с подтверждением.
|
арты и «сколько в колодах» — дальше.
|
||||||
- [ ] Магазин: бустер (анимация открытия), триал-деки, обмен токенов.
|
- [ ] Магазин: бустер (анимация открытия), триал-деки, обмен токенов.
|
||||||
- [ ] Локализация RU/EN, переключение на лету (пакет подключён).
|
- [ ] Локализация RU/EN, переключение на лету (пакет подключён).
|
||||||
- [ ] Настройки: язык, звук, FPS, мьют.
|
- [ ] Настройки: язык, звук, FPS, мьют.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,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, логин/регистрация, профиль, VContainer |
|
| Клиент | каркас: Unity 6.6, логин, онбординг, коллекция, колоды |
|
||||||
| Лаунчер | не начато |
|
| Лаунчер | не начато |
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ namespace Vanguard.Api
|
||||||
public Task PostAsync(string path, object body, bool authenticated = true) =>
|
public Task PostAsync(string path, object body, bool authenticated = true) =>
|
||||||
SendAsync("POST", path, body, authenticated);
|
SendAsync("POST", path, body, authenticated);
|
||||||
|
|
||||||
|
public Task DeleteAsync(string path, bool authenticated = true) =>
|
||||||
|
SendAsync("DELETE", path, null, authenticated);
|
||||||
|
|
||||||
private async Task<string> SendAsync(
|
private async Task<string> SendAsync(
|
||||||
string method,
|
string method,
|
||||||
string path,
|
string path,
|
||||||
|
|
|
||||||
|
|
@ -103,4 +103,97 @@ namespace Vanguard.Api.Contracts
|
||||||
[JsonProperty("wsUrl")] public string WsUrl { get; set; } = string.Empty;
|
[JsonProperty("wsUrl")] public string WsUrl { get; set; } = string.Empty;
|
||||||
[JsonProperty("expiresAt")] public DateTimeOffset ExpiresAt { get; set; }
|
[JsonProperty("expiresAt")] public DateTimeOffset ExpiresAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class Balances
|
||||||
|
{
|
||||||
|
[JsonProperty("gold")] public int Gold { get; set; }
|
||||||
|
[JsonProperty("dust")] public int Dust { get; set; }
|
||||||
|
[JsonProperty("tokens")] public int Tokens { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OnboardingResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("balances")] public Balances Balances { get; set; } = new Balances();
|
||||||
|
[JsonProperty("deckId")] public string DeckId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OnboardingRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("deckId")] public string DeckId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoreResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("balances")] public Balances Balances { get; set; } = new Balances();
|
||||||
|
[JsonProperty("boosterPriceGold")] public int BoosterPriceGold { get; set; }
|
||||||
|
[JsonProperty("trialDecks")] public List<StoreTrialDeckDto> TrialDecks { get; set; } = new List<StoreTrialDeckDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoreTrialDeckDto
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public string Id { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("name")] public string Name { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("clan")] public string Clan { get; set; }
|
||||||
|
[JsonProperty("cardCount")] public int CardCount { get; set; }
|
||||||
|
[JsonProperty("owned")] public bool Owned { get; set; }
|
||||||
|
[JsonProperty("priceGold")] public int PriceGold { get; set; }
|
||||||
|
[JsonProperty("cards")] public List<TrialDeckCardDto> Cards { get; set; } = new List<TrialDeckCardDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TrialDeckCardDto
|
||||||
|
{
|
||||||
|
[JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("count")] public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CollectionResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("items")] public List<CollectionItemDto> Items { get; set; } = new List<CollectionItemDto>();
|
||||||
|
[JsonProperty("balances")] public Balances Balances { get; set; } = new Balances();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CollectionItemDto
|
||||||
|
{
|
||||||
|
[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("rarity")] public string Rarity { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("count")] public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DismantleRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("count")] public int Count { get; set; }
|
||||||
|
[JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeckDto
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public Guid Id { get; set; }
|
||||||
|
[JsonProperty("name")] public string Name { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("format")] public string Format { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("cards")] public List<string> Cards { get; set; } = new List<string>();
|
||||||
|
[JsonProperty("createdAt")] public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
[JsonProperty("updatedAt")] public DateTimeOffset UpdatedAt { get; set; }
|
||||||
|
[JsonProperty("shareCode")] public string ShareCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeckRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("name")] public string Name { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("cards")] public List<string> Cards { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeckValidationResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("valid")] public bool Valid { get; set; }
|
||||||
|
[JsonProperty("violations")] public List<DeckViolationDto> Violations { get; set; } = new List<DeckViolationDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeckViolationDto
|
||||||
|
{
|
||||||
|
[JsonProperty("code")] public string Code { get; set; } = string.Empty;
|
||||||
|
[JsonProperty("message")] public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using UnityEngine;
|
||||||
using VContainer;
|
using VContainer;
|
||||||
using Vanguard.Api;
|
using Vanguard.Api;
|
||||||
using Vanguard.Auth;
|
using Vanguard.Auth;
|
||||||
|
using Vanguard.Meta;
|
||||||
using Vanguard.State;
|
using Vanguard.State;
|
||||||
using Vanguard.UI;
|
using Vanguard.UI;
|
||||||
|
|
||||||
|
|
@ -15,10 +16,14 @@ namespace Vanguard.App
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class VanguardApp : MonoBehaviour
|
public sealed class VanguardApp : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
public static string LastInitializationError { get; private set; }
|
||||||
|
|
||||||
private IObjectResolver _container;
|
private IObjectResolver _container;
|
||||||
private AppRootView _root;
|
private AppRootView _root;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
DontDestroyOnLoad(gameObject);
|
DontDestroyOnLoad(gameObject);
|
||||||
Application.targetFrameRate = 60;
|
Application.targetFrameRate = 60;
|
||||||
|
|
@ -32,6 +37,7 @@ namespace Vanguard.App
|
||||||
builder.Register<PlayerState>(Lifetime.Singleton);
|
builder.Register<PlayerState>(Lifetime.Singleton);
|
||||||
builder.Register<ApiClient>(Lifetime.Singleton);
|
builder.Register<ApiClient>(Lifetime.Singleton);
|
||||||
builder.Register<AuthService>(Lifetime.Singleton);
|
builder.Register<AuthService>(Lifetime.Singleton);
|
||||||
|
builder.Register<MetaService>(Lifetime.Singleton);
|
||||||
builder.Register<AppRootView>(Lifetime.Singleton);
|
builder.Register<AppRootView>(Lifetime.Singleton);
|
||||||
|
|
||||||
_container = builder.Build();
|
_container = builder.Build();
|
||||||
|
|
@ -40,6 +46,12 @@ namespace Vanguard.App
|
||||||
|
|
||||||
_ = RestoreSessionAsync();
|
_ = RestoreSessionAsync();
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
LastInitializationError = exception.ToString();
|
||||||
|
Debug.LogException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RestoreSessionAsync()
|
private async Task RestoreSessionAsync()
|
||||||
{
|
{
|
||||||
|
|
@ -49,7 +61,7 @@ namespace Vanguard.App
|
||||||
{
|
{
|
||||||
if (await auth.TryRestoreAsync())
|
if (await auth.TryRestoreAsync())
|
||||||
{
|
{
|
||||||
_root.ShowMeta();
|
_root.ShowAfterLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f0c456d495296174e8f2184b20fc35fe
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Vanguard.Api;
|
||||||
|
using Vanguard.Api.Contracts;
|
||||||
|
using Vanguard.State;
|
||||||
|
|
||||||
|
namespace Vanguard.Meta
|
||||||
|
{
|
||||||
|
/// <summary>Обёртка над мета-эндпоинтами: магазин, коллекция, колоды.</summary>
|
||||||
|
public sealed class MetaService
|
||||||
|
{
|
||||||
|
private readonly ApiClient _api;
|
||||||
|
private readonly PlayerState _player;
|
||||||
|
|
||||||
|
public MetaService(ApiClient api, PlayerState player)
|
||||||
|
{
|
||||||
|
_api = api;
|
||||||
|
_player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoreResponse> GetStoreAsync()
|
||||||
|
{
|
||||||
|
var store = await _api.GetAsync<StoreResponse>("/api/store");
|
||||||
|
_player.ApplyBalances(store.Balances);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OnboardingResponse> OnboardAsync(string deckId)
|
||||||
|
{
|
||||||
|
var response = await _api.PostAsync<OnboardingResponse>(
|
||||||
|
"/api/meta/onboarding",
|
||||||
|
new OnboardingRequest { DeckId = deckId });
|
||||||
|
|
||||||
|
_player.ApplyBalances(response.Balances);
|
||||||
|
_player.MarkOnboarded(response.DeckId);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CollectionResponse> GetCollectionAsync()
|
||||||
|
{
|
||||||
|
var collection = await _api.GetAsync<CollectionResponse>("/api/collection");
|
||||||
|
_player.ApplyBalances(collection.Balances);
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Balances> DismantleAsync(string printingId, int count)
|
||||||
|
{
|
||||||
|
var balances = await _api.PostAsync<Balances>(
|
||||||
|
"/api/collection/dismantle",
|
||||||
|
new DismantleRequest
|
||||||
|
{
|
||||||
|
PrintingId = printingId,
|
||||||
|
Count = count,
|
||||||
|
IdempotencyKey = Guid.NewGuid().ToString("N"),
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.ApplyBalances(balances);
|
||||||
|
return balances;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DeckDto>> GetDecksAsync()
|
||||||
|
{
|
||||||
|
var decks = await _api.GetAsync<List<DeckDto>>("/api/decks");
|
||||||
|
return decks ?? new List<DeckDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeckDto> CreateDeckAsync(string name, List<string> cards) =>
|
||||||
|
await _api.PostAsync<DeckDto>(
|
||||||
|
"/api/decks",
|
||||||
|
new DeckRequest { Name = name, Cards = cards });
|
||||||
|
|
||||||
|
public Task DeleteDeckAsync(Guid deckId) =>
|
||||||
|
_api.DeleteAsync("/api/decks/" + deckId.ToString("D"));
|
||||||
|
|
||||||
|
public async Task<DeckValidationResponse> ValidateDeckAsync(Guid deckId, List<string> cards) =>
|
||||||
|
await _api.PostAsync<DeckValidationResponse>(
|
||||||
|
"/api/decks/" + deckId.ToString("D") + "/validate",
|
||||||
|
new DeckRequest { Cards = cards });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3391a8289838a8841b8e71f1ac76e6f6
|
||||||
|
|
@ -63,6 +63,24 @@ namespace Vanguard.State
|
||||||
NotifyChanged();
|
NotifyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MarkOnboarded(string deckId)
|
||||||
|
{
|
||||||
|
OnboardingCompleted = true;
|
||||||
|
SelectedTrialDeckId = deckId;
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyBalances(Balances balances)
|
||||||
|
{
|
||||||
|
if (balances == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Gold = balances.Gold;
|
||||||
|
Dust = balances.Dust;
|
||||||
|
Tokens = balances.Tokens;
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void NotifyChanged()
|
private void NotifyChanged()
|
||||||
{
|
{
|
||||||
var handler = Changed;
|
var handler = Changed;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Vanguard.App;
|
using Vanguard.App;
|
||||||
using Vanguard.Auth;
|
using Vanguard.Auth;
|
||||||
|
using Vanguard.Meta;
|
||||||
using Vanguard.State;
|
using Vanguard.State;
|
||||||
|
|
||||||
namespace Vanguard.UI
|
namespace Vanguard.UI
|
||||||
|
|
@ -11,40 +12,109 @@ namespace Vanguard.UI
|
||||||
private readonly AppConfig _config;
|
private readonly AppConfig _config;
|
||||||
private readonly AuthService _auth;
|
private readonly AuthService _auth;
|
||||||
private readonly PlayerState _player;
|
private readonly PlayerState _player;
|
||||||
|
private readonly MetaService _metaService;
|
||||||
|
|
||||||
private LoginView _login;
|
private LoginView _login;
|
||||||
private MetaView _meta;
|
private MetaView _metaView;
|
||||||
|
private OnboardingView _onboarding;
|
||||||
|
private CollectionView _collection;
|
||||||
|
private DecksView _decks;
|
||||||
|
|
||||||
public AppRootView(AppConfig config, AuthService auth, PlayerState player)
|
public AppRootView(
|
||||||
|
AppConfig config,
|
||||||
|
AuthService auth,
|
||||||
|
PlayerState player,
|
||||||
|
MetaService metaService)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_player = player;
|
_player = player;
|
||||||
|
_metaService = metaService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
var canvas = UiKit.CreateCanvas("Vanguard Canvas");
|
var canvas = UiKit.CreateCanvas("Vanguard Canvas");
|
||||||
|
|
||||||
_login = new LoginView(canvas.transform, _config, _auth, OnAuthenticated);
|
_login = new LoginView(canvas.transform, _config, _auth, OnAuthenticated);
|
||||||
_meta = new MetaView(canvas.transform, _auth, _player, OnLoggedOut);
|
_metaView = new MetaView(
|
||||||
|
canvas.transform,
|
||||||
|
_auth,
|
||||||
|
_player,
|
||||||
|
OnLoggedOut,
|
||||||
|
ShowCollection,
|
||||||
|
ShowDecks,
|
||||||
|
ShowDecks,
|
||||||
|
ShowOnboarding);
|
||||||
|
_onboarding = new OnboardingView(
|
||||||
|
canvas.transform, _auth, _metaService, OnOnboarded, OnLoggedOut);
|
||||||
|
_collection = new CollectionView(
|
||||||
|
canvas.transform, _metaService, _player, ShowMeta);
|
||||||
|
_decks = new DecksView(
|
||||||
|
canvas.transform, _metaService, _player, ShowMeta);
|
||||||
|
|
||||||
ShowLogin("Проверяем сессию…");
|
ShowLogin("Проверяем сессию…");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowLogin(string status = null)
|
public void ShowLogin(string status = null)
|
||||||
{
|
{
|
||||||
if (_meta != null)
|
_metaView.Hide();
|
||||||
_meta.Hide();
|
_onboarding.Hide();
|
||||||
|
_collection.Hide();
|
||||||
|
_decks.Hide();
|
||||||
_login.Show(status);
|
_login.Show(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowAfterLogin()
|
||||||
|
{
|
||||||
|
if (_player.OnboardingCompleted)
|
||||||
|
ShowMeta();
|
||||||
|
else
|
||||||
|
ShowOnboarding();
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowMeta()
|
public void ShowMeta()
|
||||||
{
|
{
|
||||||
_login.Hide();
|
_login.Hide();
|
||||||
_meta.Show();
|
_onboarding.Hide();
|
||||||
|
_collection.Hide();
|
||||||
|
_decks.Hide();
|
||||||
|
_metaView.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowOnboarding()
|
||||||
|
{
|
||||||
|
_login.Hide();
|
||||||
|
_metaView.Hide();
|
||||||
|
_collection.Hide();
|
||||||
|
_decks.Hide();
|
||||||
|
_onboarding.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowCollection()
|
||||||
|
{
|
||||||
|
_login.Hide();
|
||||||
|
_metaView.Hide();
|
||||||
|
_onboarding.Hide();
|
||||||
|
_decks.Hide();
|
||||||
|
_collection.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowDecks()
|
||||||
|
{
|
||||||
|
_login.Hide();
|
||||||
|
_metaView.Hide();
|
||||||
|
_onboarding.Hide();
|
||||||
|
_collection.Hide();
|
||||||
|
_decks.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAuthenticated()
|
private void OnAuthenticated()
|
||||||
|
{
|
||||||
|
ShowAfterLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOnboarded()
|
||||||
{
|
{
|
||||||
ShowMeta();
|
ShowMeta();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>Коллекция игрока и разбор карт в пыль.</summary>
|
||||||
|
public sealed class CollectionView
|
||||||
|
{
|
||||||
|
private readonly MetaService _meta;
|
||||||
|
private readonly PlayerState _player;
|
||||||
|
private readonly Action _onBack;
|
||||||
|
|
||||||
|
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<LayoutElement>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
Rebuild(collection.Items ?? new List<CollectionItemDto>());
|
||||||
|
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<CollectionItemDto> 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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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<LayoutElement>();
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
|
||||||
|
+ " · Токены " + _player.Tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStatus(string text, Color color)
|
||||||
|
{
|
||||||
|
_status.text = text ?? string.Empty;
|
||||||
|
_status.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 117359b61c1e3454d8dcaebc08bfa85f
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>Список колод и быстрый сборщик стартовой колоды из триал-деки.</summary>
|
||||||
|
public sealed class DecksView
|
||||||
|
{
|
||||||
|
private readonly MetaService _meta;
|
||||||
|
private readonly PlayerState _player;
|
||||||
|
private readonly Action _onBack;
|
||||||
|
|
||||||
|
private readonly RectTransform _root;
|
||||||
|
private RectTransform _content;
|
||||||
|
private readonly Text _balances;
|
||||||
|
private readonly Text _status;
|
||||||
|
private readonly Button _createButton;
|
||||||
|
private readonly RectTransform _confirm;
|
||||||
|
private Text _confirmText;
|
||||||
|
private DeckDto _pendingDelete;
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
public DecksView(
|
||||||
|
Transform parent,
|
||||||
|
MetaService meta,
|
||||||
|
PlayerState player,
|
||||||
|
Action onBack)
|
||||||
|
{
|
||||||
|
_meta = meta;
|
||||||
|
_player = player;
|
||||||
|
_onBack = onBack;
|
||||||
|
|
||||||
|
_root = UiKit.CreatePanel(parent, "DecksScreen", 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",
|
||||||
|
"Стартовая колода собирается из выбранной триал-деки: 50 карт, состав уже проверен сервером.",
|
||||||
|
17,
|
||||||
|
new Color(0.58f, 0.65f, 0.78f),
|
||||||
|
TextAnchor.MiddleLeft);
|
||||||
|
UiKit.SetPreferredHeight(hint, 26f);
|
||||||
|
|
||||||
|
var scroll = UiKit.CreateScrollView(body, "DecksScroll", out _content, 8f);
|
||||||
|
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||||||
|
scrollElement.flexibleHeight = 1f;
|
||||||
|
scrollElement.minHeight = 180f;
|
||||||
|
|
||||||
|
var actions = UiKit.CreatePanel(body, "Actions", new Color(0.05f, 0.06f, 0.085f, 0f));
|
||||||
|
UiKit.SetPreferredHeight(actions, 56f);
|
||||||
|
UiKit.AddHorizontalLayout(actions, 0, 12);
|
||||||
|
|
||||||
|
_createButton = UiKit.CreateButton(
|
||||||
|
actions,
|
||||||
|
"CreateFromTrial",
|
||||||
|
"Собрать из триал-деки",
|
||||||
|
OnCreateFromTrialClicked,
|
||||||
|
19);
|
||||||
|
UiKit.SetPreferredHeight(_createButton, 52f);
|
||||||
|
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RectTransform BuildConfirm(Transform parent)
|
||||||
|
{
|
||||||
|
var overlay = UiKit.CreatePanel(parent, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.72f));
|
||||||
|
var card = UiKit.CreateCenteredCard(
|
||||||
|
overlay, "ConfirmCard", new Vector2(620f, 280f),
|
||||||
|
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, 80f);
|
||||||
|
|
||||||
|
var yes = UiKit.CreateButton(card, "Yes", "Удалить", OnConfirmDelete, 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 decks = await _meta.GetDecksAsync();
|
||||||
|
Rebuild(decks);
|
||||||
|
SetStatus(
|
||||||
|
decks.Count == 0 ? "Колод пока нет." : "Колод: " + decks.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<DeckDto> decks)
|
||||||
|
{
|
||||||
|
for (var i = _content.childCount - 1; i >= 0; i--)
|
||||||
|
UnityEngine.Object.Destroy(_content.GetChild(i).gameObject);
|
||||||
|
|
||||||
|
foreach (var deck in decks)
|
||||||
|
AddRow(deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRow(DeckDto deck)
|
||||||
|
{
|
||||||
|
var row = UiKit.CreatePanel(_content, "Deck_" + deck.Id, new Color(0.07f, 0.085f, 0.115f, 1f));
|
||||||
|
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||||||
|
rowElement.preferredHeight = 72f;
|
||||||
|
UiKit.AddHorizontalLayout(row, 12, 12);
|
||||||
|
|
||||||
|
var name = UiKit.CreateText(
|
||||||
|
row, "Name", deck.Name, 21, Color.white, TextAnchor.MiddleLeft);
|
||||||
|
name.fontStyle = FontStyle.Bold;
|
||||||
|
var nameElement = name.gameObject.AddComponent<LayoutElement>();
|
||||||
|
nameElement.flexibleWidth = 1f;
|
||||||
|
nameElement.preferredHeight = 44f;
|
||||||
|
|
||||||
|
var info = UiKit.CreateText(
|
||||||
|
row,
|
||||||
|
"Info",
|
||||||
|
deck.Cards.Count + " карт · " + deck.Format,
|
||||||
|
16,
|
||||||
|
new Color(0.58f, 0.65f, 0.78f),
|
||||||
|
TextAnchor.MiddleLeft);
|
||||||
|
var infoElement = info.gameObject.AddComponent<LayoutElement>();
|
||||||
|
infoElement.preferredWidth = 230f;
|
||||||
|
infoElement.preferredHeight = 40f;
|
||||||
|
|
||||||
|
var play = UiKit.CreateButton(row, "Play", "В бой", () => OnPlayClicked(deck), 17);
|
||||||
|
var playElement = play.gameObject.AddComponent<LayoutElement>();
|
||||||
|
playElement.preferredWidth = 120f;
|
||||||
|
playElement.preferredHeight = 46f;
|
||||||
|
playElement.flexibleWidth = 0f;
|
||||||
|
|
||||||
|
var delete = UiKit.CreateButton(row, "Delete", "Удалить", () => ShowConfirm(deck), 16);
|
||||||
|
var deleteElement = delete.gameObject.AddComponent<LayoutElement>();
|
||||||
|
deleteElement.preferredWidth = 110f;
|
||||||
|
deleteElement.preferredHeight = 46f;
|
||||||
|
deleteElement.flexibleWidth = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnCreateFromTrialClicked()
|
||||||
|
{
|
||||||
|
if (_loading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_loading = true;
|
||||||
|
_createButton.interactable = false;
|
||||||
|
SetStatus("Ищем выбранную триал-деку…", new Color(0.62f, 0.68f, 0.82f));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var store = await _meta.GetStoreAsync();
|
||||||
|
var trial = store.TrialDecks.FirstOrDefault(deck => deck.Owned);
|
||||||
|
|
||||||
|
if (trial == null)
|
||||||
|
{
|
||||||
|
SetStatus("Сначала завершите онбординг и выберите триал-деку.", new Color(1f, 0.60f, 0.45f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cards = trial.Cards
|
||||||
|
.SelectMany(card => Enumerable.Repeat(card.PrintingId, card.Count))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var created = await _meta.CreateDeckAsync(trial.Name + " (старт)", cards);
|
||||||
|
SetStatus("Колода «" + created.Name + "» собрана: " + created.Cards.Count + " карт.", new Color(0.45f, 0.85f, 0.55f));
|
||||||
|
_loading = false;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
_createButton.interactable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayClicked(DeckDto deck)
|
||||||
|
{
|
||||||
|
SetStatus("Очередь и бой подключим следующим шагом. Колода: " + deck.Name + ".", new Color(1f, 0.76f, 0.38f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowConfirm(DeckDto deck)
|
||||||
|
{
|
||||||
|
_pendingDelete = deck;
|
||||||
|
_confirmText.text = "«" + deck.Name + "» будет удалена без возможности восстановления.";
|
||||||
|
_confirm.SetAsLastSibling();
|
||||||
|
_confirm.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideConfirm()
|
||||||
|
{
|
||||||
|
_pendingDelete = null;
|
||||||
|
_confirm.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnConfirmDelete()
|
||||||
|
{
|
||||||
|
if (_pendingDelete == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var deck = _pendingDelete;
|
||||||
|
_confirm.gameObject.SetActive(false);
|
||||||
|
SetStatus("Удаляем «" + deck.Name + "»…", new Color(0.62f, 0.68f, 0.82f));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _meta.DeleteDeckAsync(deck.Id);
|
||||||
|
SetStatus("Колода удалена.", 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
|
||||||
|
{
|
||||||
|
_pendingDelete = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f00da70463bd90b45bdbf542ddb33bb1
|
||||||
|
|
@ -14,6 +14,10 @@ namespace Vanguard.UI
|
||||||
private readonly AuthService _auth;
|
private readonly AuthService _auth;
|
||||||
private readonly PlayerState _player;
|
private readonly PlayerState _player;
|
||||||
private readonly Action _onLoggedOut;
|
private readonly Action _onLoggedOut;
|
||||||
|
private readonly Action _onCollection;
|
||||||
|
private readonly Action _onDecks;
|
||||||
|
private readonly Action _onPlay;
|
||||||
|
private readonly Action _onOnboarding;
|
||||||
|
|
||||||
private readonly RectTransform _root;
|
private readonly RectTransform _root;
|
||||||
private readonly Text _playerName;
|
private readonly Text _playerName;
|
||||||
|
|
@ -33,11 +37,19 @@ namespace Vanguard.UI
|
||||||
Transform parent,
|
Transform parent,
|
||||||
AuthService auth,
|
AuthService auth,
|
||||||
PlayerState player,
|
PlayerState player,
|
||||||
Action onLoggedOut)
|
Action onLoggedOut,
|
||||||
|
Action onCollection,
|
||||||
|
Action onDecks,
|
||||||
|
Action onPlay,
|
||||||
|
Action onOnboarding)
|
||||||
{
|
{
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_player = player;
|
_player = player;
|
||||||
_onLoggedOut = onLoggedOut;
|
_onLoggedOut = onLoggedOut;
|
||||||
|
_onCollection = onCollection;
|
||||||
|
_onDecks = onDecks;
|
||||||
|
_onPlay = onPlay;
|
||||||
|
_onOnboarding = onOnboarding;
|
||||||
|
|
||||||
_root = UiKit.CreatePanel(parent, "MetaScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
|
_root = UiKit.CreatePanel(parent, "MetaScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
|
||||||
|
|
||||||
|
|
@ -93,10 +105,10 @@ namespace Vanguard.UI
|
||||||
UiKit.SetPreferredHeight(actions, 96f);
|
UiKit.SetPreferredHeight(actions, 96f);
|
||||||
UiKit.AddHorizontalLayout(actions, 18, 14);
|
UiKit.AddHorizontalLayout(actions, 18, 14);
|
||||||
|
|
||||||
AddAction(actions, "Коллекция", OnCollectionClicked);
|
AddAction(actions, "Коллекция", _onCollection);
|
||||||
AddAction(actions, "Колоды", OnDecksClicked);
|
AddAction(actions, "Колоды", _onDecks);
|
||||||
AddAction(actions, "Магазин", OnStoreClicked);
|
AddAction(actions, "Магазин", OnStoreClicked);
|
||||||
AddAction(actions, "В бой", OnPlayClicked);
|
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));
|
||||||
UiKit.SetPreferredHeight(lower, 220f, 1f);
|
UiKit.SetPreferredHeight(lower, 220f, 1f);
|
||||||
|
|
@ -113,7 +125,7 @@ namespace Vanguard.UI
|
||||||
UiKit.SetPreferredHeight(_status, 70f);
|
UiKit.SetPreferredHeight(_status, 70f);
|
||||||
|
|
||||||
_onboardingButton = UiKit.CreateButton(
|
_onboardingButton = UiKit.CreateButton(
|
||||||
lower, "OnboardingButton", "Выбрать триал-деку", OnOnboardingClicked, 20);
|
lower, "OnboardingButton", "Выбрать триал-деку", () => _onOnboarding(), 20);
|
||||||
UiKit.SetPreferredHeight(_onboardingButton, 46f);
|
UiKit.SetPreferredHeight(_onboardingButton, 46f);
|
||||||
|
|
||||||
_player.Changed += UpdateTexts;
|
_player.Changed += UpdateTexts;
|
||||||
|
|
@ -213,27 +225,9 @@ namespace Vanguard.UI
|
||||||
_status.color = color;
|
_status.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCollectionClicked() =>
|
|
||||||
SetStatus("Коллекция и разбор карт — следующий экран.", new Color(0.70f, 0.76f, 0.90f));
|
|
||||||
|
|
||||||
private void OnDecksClicked() =>
|
|
||||||
SetStatus("Декбилдер и список колод — следующий экран.", new Color(0.70f, 0.76f, 0.90f));
|
|
||||||
|
|
||||||
private void OnStoreClicked() =>
|
private void OnStoreClicked() =>
|
||||||
SetStatus("Магазин и открытие бустеров — следующий экран.", new Color(0.70f, 0.76f, 0.90f));
|
SetStatus("Магазин и открытие бустеров — следующий экран.", new Color(0.70f, 0.76f, 0.90f));
|
||||||
|
|
||||||
private void OnPlayClicked()
|
|
||||||
{
|
|
||||||
SetStatus(
|
|
||||||
_player.OnboardingCompleted
|
|
||||||
? "Очередь подключим после выбора колоды."
|
|
||||||
: "Сначала завершите онбординг.",
|
|
||||||
new Color(1f, 0.76f, 0.38f));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOnboardingClicked() =>
|
|
||||||
SetStatus("Выбор триал-деки подключим следующим шагом.", new Color(1f, 0.76f, 0.38f));
|
|
||||||
|
|
||||||
private async void OnLogoutClicked()
|
private async void OnLogoutClicked()
|
||||||
{
|
{
|
||||||
_logout.interactable = false;
|
_logout.interactable = false;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using Vanguard.Api;
|
||||||
|
using Vanguard.Api.Contracts;
|
||||||
|
using Vanguard.Auth;
|
||||||
|
using Vanguard.Meta;
|
||||||
|
|
||||||
|
namespace Vanguard.UI
|
||||||
|
{
|
||||||
|
/// <summary>Первый вход: выбор одной из двух бесплатных триал-дек.</summary>
|
||||||
|
public sealed class OnboardingView
|
||||||
|
{
|
||||||
|
private readonly AuthService _auth;
|
||||||
|
private readonly MetaService _meta;
|
||||||
|
private readonly Action _onCompleted;
|
||||||
|
private readonly Action _onLoggedOut;
|
||||||
|
|
||||||
|
private readonly RectTransform _root;
|
||||||
|
private readonly RectTransform _deckRow;
|
||||||
|
private readonly Text _status;
|
||||||
|
private readonly Button _retry;
|
||||||
|
private readonly Button _logout;
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
public OnboardingView(
|
||||||
|
Transform parent,
|
||||||
|
AuthService auth,
|
||||||
|
MetaService meta,
|
||||||
|
Action onCompleted,
|
||||||
|
Action onLoggedOut)
|
||||||
|
{
|
||||||
|
_auth = auth;
|
||||||
|
_meta = meta;
|
||||||
|
_onCompleted = onCompleted;
|
||||||
|
_onLoggedOut = onLoggedOut;
|
||||||
|
|
||||||
|
_root = UiKit.CreatePanel(parent, "OnboardingScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
|
||||||
|
var card = UiKit.CreateCenteredCard(
|
||||||
|
_root,
|
||||||
|
"OnboardingCard",
|
||||||
|
new Vector2(1320f, 900f),
|
||||||
|
new Color(0.07f, 0.085f, 0.115f, 1f));
|
||||||
|
UiKit.AddVerticalLayout(card, 34, 12);
|
||||||
|
|
||||||
|
var title = UiKit.CreateText(
|
||||||
|
card, "Title", "Стартовый набор", 46, Color.white);
|
||||||
|
title.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(title, 58f);
|
||||||
|
|
||||||
|
var subtitle = UiKit.CreateText(
|
||||||
|
card,
|
||||||
|
"Subtitle",
|
||||||
|
"Выберите одну триал-деку. 50 карт попадут в коллекцию, плюс 5 токенов на бустеры.",
|
||||||
|
20,
|
||||||
|
new Color(0.68f, 0.74f, 0.88f));
|
||||||
|
UiKit.SetPreferredHeight(subtitle, 54f);
|
||||||
|
|
||||||
|
_deckRow = UiKit.CreatePanel(
|
||||||
|
card,
|
||||||
|
"DeckRow",
|
||||||
|
new Color(0.05f, 0.06f, 0.085f, 1f));
|
||||||
|
UiKit.SetPreferredHeight(_deckRow, 500f);
|
||||||
|
UiKit.AddHorizontalLayout(_deckRow, 16, 18);
|
||||||
|
|
||||||
|
_status = UiKit.CreateText(
|
||||||
|
card, "Status", string.Empty, 18,
|
||||||
|
new Color(0.68f, 0.74f, 0.88f),
|
||||||
|
TextAnchor.MiddleCenter);
|
||||||
|
UiKit.SetPreferredHeight(_status, 38f);
|
||||||
|
|
||||||
|
_retry = UiKit.CreateButton(card, "Retry", "Повторить загрузку", OnRetryClicked, 18);
|
||||||
|
UiKit.SetPreferredHeight(_retry, 42f);
|
||||||
|
_retry.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
_logout = UiKit.CreateButton(card, "Logout", "Выйти из аккаунта", OnLogoutClicked, 18);
|
||||||
|
UiKit.SetPreferredHeight(_logout, 42f);
|
||||||
|
|
||||||
|
_root.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Show()
|
||||||
|
{
|
||||||
|
UiKit.SetActive(_root, true);
|
||||||
|
_ = LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide() => UiKit.SetActive(_root, false);
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
if (_loading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_loading = true;
|
||||||
|
_retry.gameObject.SetActive(false);
|
||||||
|
_logout.interactable = false;
|
||||||
|
ClearDeckRow();
|
||||||
|
SetStatus("Загружаем триал-деки…", new Color(0.68f, 0.74f, 0.88f));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var store = await _meta.GetStoreAsync();
|
||||||
|
BuildDecks(store);
|
||||||
|
SetStatus("Выберите деку — выбор делается один раз.", new Color(0.68f, 0.74f, 0.88f));
|
||||||
|
}
|
||||||
|
catch (ApiException exception)
|
||||||
|
{
|
||||||
|
SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f));
|
||||||
|
_retry.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Debug.LogException(exception);
|
||||||
|
SetStatus("Не удалось загрузить триал-деки. Сервер недоступен?", new Color(1f, 0.55f, 0.45f));
|
||||||
|
_retry.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
_logout.interactable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildDecks(StoreResponse store)
|
||||||
|
{
|
||||||
|
if (store.TrialDecks == null || store.TrialDecks.Count == 0)
|
||||||
|
{
|
||||||
|
SetStatus("Сервер не вернул ни одной триал-деки.", new Color(1f, 0.55f, 0.45f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var deck in store.TrialDecks)
|
||||||
|
BuildDeck(deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildDeck(StoreTrialDeckDto deck)
|
||||||
|
{
|
||||||
|
var card = UiKit.CreatePanel(
|
||||||
|
_deckRow,
|
||||||
|
"Trial_" + deck.Id,
|
||||||
|
new Color(0.09f, 0.11f, 0.15f, 1f));
|
||||||
|
var element = card.gameObject.AddComponent<LayoutElement>();
|
||||||
|
element.flexibleWidth = 1f;
|
||||||
|
element.preferredHeight = 460f;
|
||||||
|
UiKit.AddVerticalLayout(card, 24, 10);
|
||||||
|
|
||||||
|
var name = UiKit.CreateText(card, "Name", deck.Name, 30, Color.white, TextAnchor.MiddleLeft);
|
||||||
|
name.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(name, 42f);
|
||||||
|
|
||||||
|
var clan = UiKit.CreateText(
|
||||||
|
card, "Clan", "Клан: " + (string.IsNullOrEmpty(deck.Clan) ? "—" : deck.Clan),
|
||||||
|
20, new Color(0.68f, 0.74f, 0.88f), TextAnchor.MiddleLeft);
|
||||||
|
UiKit.SetPreferredHeight(clan, 30f);
|
||||||
|
|
||||||
|
var count = UiKit.CreateText(
|
||||||
|
card, "Count", deck.CardCount + " карт · " + deck.Cards.Count + " уникальных",
|
||||||
|
18, new Color(0.60f, 0.67f, 0.80f), TextAnchor.MiddleLeft);
|
||||||
|
UiKit.SetPreferredHeight(count, 28f);
|
||||||
|
|
||||||
|
var preview = UiKit.CreateText(
|
||||||
|
card,
|
||||||
|
"Preview",
|
||||||
|
BuildPreview(deck),
|
||||||
|
16,
|
||||||
|
new Color(0.52f, 0.60f, 0.74f),
|
||||||
|
TextAnchor.UpperLeft);
|
||||||
|
preview.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
|
UiKit.SetPreferredHeight(preview, 150f, 1f);
|
||||||
|
|
||||||
|
if (deck.Owned)
|
||||||
|
{
|
||||||
|
var owned = UiKit.CreateText(
|
||||||
|
card, "Owned", "Уже в коллекции", 18,
|
||||||
|
new Color(0.45f, 0.85f, 0.55f));
|
||||||
|
UiKit.SetPreferredHeight(owned, 28f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var choose = UiKit.CreateButton(
|
||||||
|
card,
|
||||||
|
"Choose",
|
||||||
|
deck.Owned ? "Дека уже получена" : "Выбрать бесплатно",
|
||||||
|
() => OnChooseClicked(deck),
|
||||||
|
21);
|
||||||
|
choose.interactable = !deck.Owned;
|
||||||
|
UiKit.SetPreferredHeight(choose, 54f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPreview(StoreTrialDeckDto deck)
|
||||||
|
{
|
||||||
|
if (deck.Cards == null || deck.Cards.Count == 0)
|
||||||
|
return "Состав появится после выбора.";
|
||||||
|
|
||||||
|
var lines = deck.Cards
|
||||||
|
.Take(8)
|
||||||
|
.Select(card => "• " + card.PrintingId + " ×" + card.Count);
|
||||||
|
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnChooseClicked(StoreTrialDeckDto deck)
|
||||||
|
{
|
||||||
|
if (_loading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_loading = true;
|
||||||
|
_retry.gameObject.SetActive(false);
|
||||||
|
SetStatus("Активируем " + deck.Name + "…", new Color(0.68f, 0.74f, 0.88f));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _meta.OnboardAsync(deck.Id);
|
||||||
|
_onCompleted();
|
||||||
|
}
|
||||||
|
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 OnRetryClicked()
|
||||||
|
{
|
||||||
|
_ = LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnLogoutClicked()
|
||||||
|
{
|
||||||
|
_logout.interactable = false;
|
||||||
|
await _auth.LogoutAsync();
|
||||||
|
_onLoggedOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearDeckRow()
|
||||||
|
{
|
||||||
|
for (var i = _deckRow.childCount - 1; i >= 0; i--)
|
||||||
|
UnityEngine.Object.Destroy(_deckRow.GetChild(i).gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStatus(string text, Color color)
|
||||||
|
{
|
||||||
|
_status.text = text ?? string.Empty;
|
||||||
|
_status.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eeb467af94c7f9a428bd9199668ca2c7
|
||||||
|
|
@ -235,6 +235,72 @@ namespace Vanguard.UI
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RectTransform CreateScrollView(
|
||||||
|
Transform parent,
|
||||||
|
string name,
|
||||||
|
out RectTransform content,
|
||||||
|
float spacing = 8f)
|
||||||
|
{
|
||||||
|
var root = new GameObject(
|
||||||
|
name,
|
||||||
|
typeof(RectTransform),
|
||||||
|
typeof(Image),
|
||||||
|
typeof(ScrollRect));
|
||||||
|
var rootRect = (RectTransform)root.transform;
|
||||||
|
rootRect.SetParent(parent, false);
|
||||||
|
Stretch(rootRect);
|
||||||
|
|
||||||
|
var background = root.GetComponent<Image>();
|
||||||
|
background.color = new Color(0f, 0f, 0f, 0.01f);
|
||||||
|
background.raycastTarget = true;
|
||||||
|
|
||||||
|
var viewportGo = new GameObject(
|
||||||
|
"Viewport",
|
||||||
|
typeof(RectTransform),
|
||||||
|
typeof(RectMask2D));
|
||||||
|
var viewport = (RectTransform)viewportGo.transform;
|
||||||
|
viewport.SetParent(rootRect, false);
|
||||||
|
Stretch(viewport);
|
||||||
|
|
||||||
|
var contentGo = new GameObject(
|
||||||
|
"Content",
|
||||||
|
typeof(RectTransform),
|
||||||
|
typeof(VerticalLayoutGroup),
|
||||||
|
typeof(ContentSizeFitter));
|
||||||
|
content = (RectTransform)contentGo.transform;
|
||||||
|
content.SetParent(viewport, false);
|
||||||
|
content.anchorMin = new Vector2(0f, 1f);
|
||||||
|
content.anchorMax = new Vector2(1f, 1f);
|
||||||
|
content.pivot = new Vector2(0.5f, 1f);
|
||||||
|
content.anchoredPosition = Vector2.zero;
|
||||||
|
content.sizeDelta = new Vector2(0f, 0f);
|
||||||
|
|
||||||
|
var layout = contentGo.GetComponent<VerticalLayoutGroup>();
|
||||||
|
layout.padding = new RectOffset(4, 4, 4, 4);
|
||||||
|
layout.spacing = spacing;
|
||||||
|
layout.childAlignment = TextAnchor.UpperCenter;
|
||||||
|
layout.childControlWidth = true;
|
||||||
|
layout.childControlHeight = true;
|
||||||
|
layout.childForceExpandWidth = true;
|
||||||
|
layout.childForceExpandHeight = false;
|
||||||
|
|
||||||
|
var fitter = contentGo.GetComponent<ContentSizeFitter>();
|
||||||
|
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||||
|
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||||
|
|
||||||
|
var scroll = root.GetComponent<ScrollRect>();
|
||||||
|
scroll.viewport = viewport;
|
||||||
|
scroll.content = content;
|
||||||
|
scroll.horizontal = false;
|
||||||
|
scroll.vertical = true;
|
||||||
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
||||||
|
scroll.scrollSensitivity = 30f;
|
||||||
|
scroll.inertia = true;
|
||||||
|
scroll.decelerationRate = 0.135f;
|
||||||
|
|
||||||
|
return rootRect;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Stretch(RectTransform rect)
|
public static void Stretch(RectTransform rect)
|
||||||
{
|
{
|
||||||
rect.anchorMin = Vector2.zero;
|
rect.anchorMin = Vector2.zero;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using UnityEngine.EventSystems;
|
||||||
using Vanguard.Api;
|
using Vanguard.Api;
|
||||||
using Vanguard.App;
|
using Vanguard.App;
|
||||||
using Vanguard.Auth;
|
using Vanguard.Auth;
|
||||||
|
using Vanguard.Meta;
|
||||||
using Vanguard.State;
|
using Vanguard.State;
|
||||||
using Vanguard.UI;
|
using Vanguard.UI;
|
||||||
|
|
||||||
|
|
@ -22,7 +23,8 @@ namespace Vanguard.Tests
|
||||||
var api = new ApiClient(config, tokens);
|
var api = new ApiClient(config, tokens);
|
||||||
var player = new PlayerState();
|
var player = new PlayerState();
|
||||||
var auth = new AuthService(api, tokens, player);
|
var auth = new AuthService(api, tokens, player);
|
||||||
var root = new AppRootView(config, auth, player);
|
var meta = new MetaService(api, player);
|
||||||
|
var root = new AppRootView(config, auth, player, meta);
|
||||||
|
|
||||||
root.Initialize();
|
root.Initialize();
|
||||||
root.ShowLogin("smoke");
|
root.ShowLogin("smoke");
|
||||||
|
|
@ -30,6 +32,9 @@ namespace Vanguard.Tests
|
||||||
Assert.IsNotNull(GameObject.Find("Vanguard Canvas"));
|
Assert.IsNotNull(GameObject.Find("Vanguard Canvas"));
|
||||||
Assert.IsNotNull(GameObject.Find("LoginScreen"));
|
Assert.IsNotNull(GameObject.Find("LoginScreen"));
|
||||||
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == "MetaScreen"));
|
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == "MetaScreen"));
|
||||||
|
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == "OnboardingScreen"));
|
||||||
|
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == "CollectionScreen"));
|
||||||
|
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == "DecksScreen"));
|
||||||
|
|
||||||
Cleanup();
|
Cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
|
|
@ -10,7 +11,7 @@ namespace Vanguard.Tests
|
||||||
public sealed class AppBootstrapPlayModeTests
|
public sealed class AppBootstrapPlayModeTests
|
||||||
{
|
{
|
||||||
[UnityTest]
|
[UnityTest]
|
||||||
public IEnumerator App_Bootstraps_And_Shows_Login()
|
public IEnumerator App_Bootstraps_And_Shows_A_Start_Screen()
|
||||||
{
|
{
|
||||||
if (Object.FindAnyObjectByType<VanguardApp>() == null)
|
if (Object.FindAnyObjectByType<VanguardApp>() == null)
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +24,24 @@ namespace Vanguard.Tests
|
||||||
|
|
||||||
Assert.IsNotNull(Object.FindAnyObjectByType<VanguardApp>());
|
Assert.IsNotNull(Object.FindAnyObjectByType<VanguardApp>());
|
||||||
Assert.IsNotNull(GameObject.Find("Vanguard Canvas"));
|
Assert.IsNotNull(GameObject.Find("Vanguard Canvas"));
|
||||||
Assert.IsNotNull(GameObject.Find("LoginScreen"));
|
|
||||||
|
var login = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.FirstOrDefault(go => go.name == "LoginScreen");
|
||||||
|
var onboarding = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.FirstOrDefault(go => go.name == "OnboardingScreen");
|
||||||
|
var hierarchy = string.Join(
|
||||||
|
"\n",
|
||||||
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.Where(go => go.name.EndsWith("Screen") || go.name == "Vanguard Canvas")
|
||||||
|
.Select(go => go.name + ": activeSelf=" + go.activeSelf + ", activeInHierarchy=" + go.activeInHierarchy));
|
||||||
|
|
||||||
|
var hasStartScreen = (login != null && login.activeInHierarchy)
|
||||||
|
|| (onboarding != null && onboarding.activeInHierarchy);
|
||||||
|
|
||||||
|
Assert.IsTrue(
|
||||||
|
hasStartScreen,
|
||||||
|
"Ни LoginScreen, ни OnboardingScreen не активны.\nInitError: "
|
||||||
|
+ VanguardApp.LastInitializationError + "\n" + hierarchy);
|
||||||
|
|
||||||
Cleanup();
|
Cleanup();
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,26 @@ MonoBehaviour:
|
||||||
m_Compliance:
|
m_Compliance:
|
||||||
m_Status: 0
|
m_Status: 0
|
||||||
m_Violations: []
|
m_Violations: []
|
||||||
m_ScopedRegistries: []
|
m_ScopedRegistries:
|
||||||
m_UserSelectedRegistryName:
|
- m_Id: scoped:project:package.openupm.com
|
||||||
|
m_Name: package.openupm.com
|
||||||
|
m_Url: https://package.openupm.com
|
||||||
|
m_Scopes:
|
||||||
|
- jp.hadashikick.vcontainer
|
||||||
|
m_IsDefault: 0
|
||||||
|
m_IsUnityRegistry: 0
|
||||||
|
m_Capabilities: 0
|
||||||
|
m_ConfigSource: 4
|
||||||
|
m_Compliance:
|
||||||
|
m_Status: 0
|
||||||
|
m_Violations: []
|
||||||
|
m_UserSelectedRegistryName: package.openupm.com
|
||||||
m_UserAddingNewScopedRegistry: 0
|
m_UserAddingNewScopedRegistry: 0
|
||||||
m_RegistryInfoDraft:
|
m_RegistryInfoDraft:
|
||||||
m_Modified: 0
|
m_Modified: 0
|
||||||
m_ErrorMessage:
|
m_ErrorMessage:
|
||||||
m_UserModificationsEntityId:
|
m_UserModificationsEntityId:
|
||||||
m_rawData: 568105584918792958
|
m_rawData: 1099511629233
|
||||||
m_OriginalEntityId:
|
m_OriginalEntityId:
|
||||||
m_rawData: 568105584918792960
|
m_rawData: 1099511629234
|
||||||
m_LoadAssets: 0
|
m_LoadAssets: 0
|
||||||
|
|
|
||||||
|
|
@ -36,22 +36,24 @@ http://localhost:5080
|
||||||
|
|
||||||
- `ClientBootstrap` — запуск без привязки к сцене.
|
- `ClientBootstrap` — запуск без привязки к сцене.
|
||||||
- DI на VContainer: `AppConfig`, `TokenStore`, `ApiClient`, `AuthService`,
|
- DI на VContainer: `AppConfig`, `TokenStore`, `ApiClient`, `AuthService`,
|
||||||
`PlayerState`, `AppRootView`.
|
`MetaService`, `PlayerState`, `AppRootView`.
|
||||||
- HTTP-клиент на `UnityWebRequest` + Newtonsoft.Json, разбор `ApiError`.
|
- HTTP-клиент на `UnityWebRequest` + Newtonsoft.Json, разбор `ApiError`.
|
||||||
- Логин, регистрация, refresh, `/me`, выход.
|
- Логин, регистрация, refresh, `/me`, выход.
|
||||||
- Восстановление сессии по refresh-токену при старте.
|
- Восстановление сессии по refresh-токену при старте.
|
||||||
- Экран входа и главный экран с балансами, MMR и заглушками разделов.
|
- Онбординг: две триал-деки из `/api/store`, выбор одной, 50 карт и 5 токенов.
|
||||||
|
- Коллекция: список карт, балансы, разбор в пыль с подтверждением.
|
||||||
|
- Колоды: список, удаление, автосборка стартовой колоды из выбранной
|
||||||
|
триал-деки.
|
||||||
|
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
||||||
|
|
||||||
## Что дальше
|
## Что дальше
|
||||||
|
|
||||||
1. Онбординг: выбор триал-деки (`POST /api/meta/onboarding`).
|
1. Очередь и экран ожидания → тикет → WebSocket (`GameConnection`).
|
||||||
2. Коллекция, разбор/крафт.
|
2. Боевая сцена: `MatchView`, проекция событий, анимации.
|
||||||
3. Декбилдер и список колод.
|
3. Полный декбилдер с фильтрами и счётчиками.
|
||||||
4. Магазин и открытие бустера.
|
4. Магазин и открытие бустера.
|
||||||
5. Очередь и экран ожидания → тикет → WebSocket.
|
5. История матчей и реплей.
|
||||||
6. Боевая сцена: `MatchView`, `GameConnection`, проекция событий.
|
6. Локализация RU/EN, настройки, DOTween-анимации.
|
||||||
7. История матчей и реплей.
|
|
||||||
8. Локализация RU/EN, настройки, DOTween-анимации.
|
|
||||||
|
|
||||||
## Плагины
|
## Плагины
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -518,21 +518,27 @@ CI подключён и проходит; серверный блок закр
|
||||||
сохранение токенов в `PlayerPrefs` (dev);
|
сохранение токенов в `PlayerPrefs` (dev);
|
||||||
- экран входа с выбором адреса сервера (стенд по умолчанию
|
- экран входа с выбором адреса сервера (стенд по умолчанию
|
||||||
`http://192.168.31.158:18080`) и главный экран с балансами, MMR и
|
`http://192.168.31.158:18080`) и главный экран с балансами, MMR и
|
||||||
заглушками разделов;
|
переходами в разделы;
|
||||||
|
- онбординг: список триал-дек из `/api/store`, выбор одной, 50 карт + 5
|
||||||
|
токенов, обновление профиля;
|
||||||
|
- коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением;
|
||||||
|
- колоды: `/api/decks`, удаление и автосборка стартовой колоды из
|
||||||
|
выбранной триал-деки (50 карт);
|
||||||
- `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены
|
- `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены
|
||||||
как плагины; `client/README.md` описывает запуск и ограничения;
|
как плагины; `client/README.md` описывает запуск и ограничения;
|
||||||
- EditMode и PlayMode smoke-тесты `Vanguard.Tests.Editor` /
|
- EditMode и PlayMode smoke-тесты `Vanguard.Tests.Editor` /
|
||||||
`Vanguard.Tests.PlayMode` собирают оба экрана и проверяют запуск приложения.
|
`Vanguard.Tests.PlayMode` собирают все экраны и проверяют запуск
|
||||||
|
приложения; live-probe (не входит в набор) проверил онбординг,
|
||||||
|
коллекцию, создание и удаление колоды против стенда.
|
||||||
|
|
||||||
Дальше по клиенту:
|
Дальше по клиенту:
|
||||||
|
|
||||||
1. Онбординг: `POST /api/meta/onboarding`, выбор триал-деки.
|
1. Очередь → тикет → `GameConnection` (WebSocket + MessagePack).
|
||||||
2. Коллекция, разбор/крафт.
|
2. Боевая сцена: `MatchView`, проекция событий, анимации.
|
||||||
3. Декбилдер, список колод, валидация.
|
3. Полный декбилдер с фильтрами и счётчиками.
|
||||||
4. Магазин и открытие бустера.
|
4. Магазин и открытие бустера.
|
||||||
5. Очередь → тикет → `GameConnection` (WebSocket + MessagePack).
|
5. История матчей и реплей.
|
||||||
6. Боевая сцена и реплей.
|
6. Локализация RU/EN на лету, настройки, DOTween-анимации.
|
||||||
7. Локализация RU/EN на лету, настройки, DOTween-анимации.
|
|
||||||
|
|
||||||
Старый план экранов остаётся в силе:
|
Старый план экранов остаётся в силе:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue