Merge collection into deck builder and add card overlay
CI / test (push) Successful in 8m48s Details

This commit is contained in:
MpakobecEx 2026-10-21 19:14:03 +05:00
parent 14dce96218
commit 81af3aeb44
25 changed files with 427 additions and 1143 deletions

View File

@ -227,14 +227,15 @@
главный экран профиля. Подробности — `client/README.md` и `docs/HANDOFF.md`.
**Визуальное направление зафиксировано:** 3D-клиент на URP, отдельные
сцены-экраны **Login / Lobby / Collection / DeckBuilder / Battle**;
`Boot` — служебная сцена. Очередь и онбординг — оверлеи Lobby. Текущий
сцены-экраны **Login / Lobby / DeckBuilder / Battle**;
`DeckBuilder` совмещает коллекцию и редактор колоды. `Boot` — служебная
сцена. Очередь и онбординг — оверлеи Lobby. Текущий
единый Canvas — временный функциональный gray-box. Полное решение —
`docs/client-visual-direction.md`.
- [x] Unity 6 LTS, URP, Addressables, Input System, Localization.
DOTween — следующим шагом (пока UI без внешних твинов).
- [x] 3D-сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle`
- [x] 3D-сцены `Login`, `Lobby`, `DeckBuilder`, `Battle`
и `SceneFlow`; `Boot` — стартовая служебная сцена. 3D-окружение пока
из примитивов, UI — code-generated uGUI.
- [x] Composition root + DI (VContainer), без синглтон-свалок.

View File

@ -29,7 +29,6 @@ namespace Vanguard.Editor
Create<LoginSceneController>("Login");
Create<LobbySceneController>("Lobby");
Create<CollectionSceneController>("Collection");
Create<DeckBuilderSceneController>("DeckBuilder");
Create<BattleSceneController>("Battle");
@ -37,7 +36,6 @@ namespace Vanguard.Editor
Add(scenes, BootPath);
Add(scenes, SceneDir + "/Login.unity");
Add(scenes, SceneDir + "/Lobby.unity");
Add(scenes, SceneDir + "/Collection.unity");
Add(scenes, SceneDir + "/DeckBuilder.unity");
Add(scenes, SceneDir + "/Battle.unity");

View File

@ -170,6 +170,12 @@ namespace Vanguard.Api.Contracts
[JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty;
}
public sealed class CraftRequest
{
[JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty;
[JsonProperty("idempotencyKey")] public string IdempotencyKey { get; set; } = string.Empty;
}
public sealed class DeckDto
{
[JsonProperty("id")] public Guid Id { get; set; }
@ -277,12 +283,22 @@ namespace Vanguard.Api.Contracts
[JsonProperty("printingId")] public string PrintingId { get; set; } = string.Empty;
[JsonProperty("nameId")] public string NameId { get; set; } = string.Empty;
[JsonProperty("nameEn")] public string NameEn { get; set; } = string.Empty;
[JsonProperty("nameRu")] public string NameRu { get; set; }
[JsonProperty("rarity")] public string Rarity { get; set; } = string.Empty;
[JsonProperty("grade")] public int Grade { get; set; }
[JsonProperty("type")] public string Type { get; set; } = string.Empty;
[JsonProperty("power")] public int Power { get; set; }
[JsonProperty("shield")] public int Shield { get; set; }
[JsonProperty("critical")] public int Critical { get; set; }
[JsonProperty("trigger")] public string Trigger { get; set; } = "None";
[JsonProperty("clan")] public string Clan { get; set; } = string.Empty;
[JsonProperty("isSentinel")] public bool IsSentinel { get; set; }
[JsonProperty("isRestraint")] public bool IsRestraint { get; set; }
[JsonProperty("hasBoost")] public bool HasBoost { get; set; }
[JsonProperty("hasIntercept")] public bool HasIntercept { get; set; }
[JsonProperty("isParallel")] public bool IsParallel { get; set; }
[JsonProperty("basePrintingId")] public string BasePrintingId { get; set; }
[JsonProperty("textEn")] public string TextEn { get; set; }
[JsonProperty("textRu")] public string TextRu { get; set; }
}
}

View File

@ -1,29 +0,0 @@
using UnityEngine;
using Vanguard.Meta;
using Vanguard.State;
using Vanguard.UI;
namespace Vanguard.App
{
public sealed class CollectionSceneController : SceneControllerBase
{
private CollectionView _view;
private void Start()
{
SceneEnvironment.Build(SceneTheme.Collection);
var canvas = CreateCanvas("Collection Canvas");
_view = new CollectionView(
canvas.transform,
Resolve<MetaService>(),
Resolve<PlayerState>(),
() => _ = Flow.LoadLobbyAsync());
_view.Show();
}
private void OnDestroy()
{
_view?.Dispose();
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8ccb09cec5c89814493fae05cf2d6051

View File

@ -33,7 +33,6 @@ namespace Vanguard.App
_auth,
_player,
OnLogout,
() => _ = Flow.LoadCollectionAsync(),
() => _ = Flow.LoadDeckBuilderAsync(),
ShowStore,
ShowHistory,

View File

@ -6,7 +6,6 @@ namespace Vanguard.App
{
Login,
Lobby,
Collection,
DeckBuilder,
Battle,
}
@ -143,15 +142,6 @@ namespace Vanguard.App
Cube(root, "Lobby Light", new Vector3(0f, 5.2f, 4f), new Vector3(6f, 0.2f, 1f), palette.Accent, 1.8f);
break;
case SceneTheme.Collection:
for (var i = -2; i <= 2; i++)
{
Cube(root, "Shelf " + i, new Vector3(i * 3.2f, 1.6f, 6.5f), new Vector3(2.2f, 3.2f, 0.5f), palette.Structure);
Cube(root, "Shelf Glow " + i, new Vector3(i * 3.2f, 3.35f, 6.2f), new Vector3(2.2f, 0.1f, 0.5f), palette.Accent, 1.2f);
}
Cube(root, "Table", new Vector3(0f, 0.4f, 1.5f), new Vector3(7f, 0.8f, 4f), palette.Structure);
break;
case SceneTheme.DeckBuilder:
Cube(root, "Table", new Vector3(0f, 0.5f, 2f), new Vector3(11f, 1f, 6f), palette.Structure);
Cube(root, "Deck Stack", new Vector3(-3.8f, 1.3f, 3.2f), new Vector3(1.5f, 1.4f, 2.2f), palette.Accent, 0.8f);

View File

@ -16,7 +16,6 @@ namespace Vanguard.App
public const string Boot = "Boot";
public const string Login = "Login";
public const string Lobby = "Lobby";
public const string Collection = "Collection";
public const string DeckBuilder = "DeckBuilder";
public const string Battle = "Battle";
@ -32,8 +31,6 @@ namespace Vanguard.App
public Task LoadLobbyAsync() => LoadAsync(Lobby);
public Task LoadCollectionAsync() => LoadAsync(Collection);
public Task LoadDeckBuilderAsync() => LoadAsync(DeckBuilder);
public Task OpenLobbyWithQueueAsync(DeckDto deck)

View File

@ -61,6 +61,20 @@ namespace Vanguard.Meta
return balances;
}
public async Task<Balances> CraftAsync(string printingId)
{
var balances = await _api.PostAsync<Balances>(
"/api/collection/craft",
new CraftRequest
{
PrintingId = printingId,
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");

View File

@ -1,318 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.Api.Contracts;
using Vanguard.Meta;
using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Коллекция игрока и разбор карт в пыль.</summary>
public sealed class CollectionView : IDisposable
{
private readonly MetaService _meta;
private readonly PlayerState _player;
private readonly Action _onBack;
private bool _disposed;
private readonly RectTransform _root;
private readonly Text _balances;
private readonly Text _status;
private readonly RectTransform _confirm;
private Text _confirmText;
private RectTransform _content;
private CollectionItemDto _pending;
private bool _loading;
public CollectionView(
Transform parent,
MetaService meta,
PlayerState player,
Action onBack)
{
_meta = meta;
_player = player;
_onBack = onBack;
_root = UiKit.CreatePanel(parent, "CollectionScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
var top = UiKit.CreatePanel(_root, "TopBar", new Color(0.07f, 0.085f, 0.115f, 1f));
top.anchorMin = new Vector2(0f, 1f);
top.anchorMax = new Vector2(1f, 1f);
top.pivot = new Vector2(0.5f, 1f);
top.anchoredPosition = Vector2.zero;
top.sizeDelta = new Vector2(0f, 88f);
UiKit.AddHorizontalLayout(top, 22, 16);
var back = UiKit.CreateButton(top, "Back", "← Назад", () => _onBack(), 18);
UiKit.SetPreferredHeight(back, 44f, 0f);
var title = UiKit.CreateText(top, "Title", "Коллекция", 32, Color.white, TextAnchor.MiddleLeft);
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 46f);
_balances = UiKit.CreateText(
top, "Balances", string.Empty, 18,
new Color(0.70f, 0.76f, 0.90f), TextAnchor.MiddleRight);
UiKit.SetPreferredHeight(_balances, 36f);
var body = UiKit.CreatePanel(_root, "Body", new Color(0.035f, 0.045f, 0.065f, 0f));
body.anchorMin = Vector2.zero;
body.anchorMax = Vector2.one;
body.offsetMin = new Vector2(40f, 28f);
body.offsetMax = new Vector2(-40f, -110f);
UiKit.AddVerticalLayout(body, 8, 10);
var hint = UiKit.CreateText(
body,
"Hint",
"Разбор даёт пыль: C 5 · R 10 · RR 25 · RRR 100. SP не разбирается.",
17,
new Color(0.58f, 0.65f, 0.78f),
TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(hint, 26f);
var scroll = UiKit.CreateScrollView(body, "CollectionScroll", out _content, 8f);
var scrollElement = scroll.gameObject.AddComponent<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);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= OnPlayerChanged;
}
private RectTransform BuildConfirm(Transform parent)
{
var overlay = UiKit.CreatePanel(parent, "DismantleConfirm", new Color(0f, 0f, 0f, 0.72f));
var card = UiKit.CreateCenteredCard(
overlay, "ConfirmCard", new Vector2(620f, 300f),
new Color(0.08f, 0.10f, 0.14f, 1f));
UiKit.AddVerticalLayout(card, 30, 12);
var title = UiKit.CreateText(card, "Title", "Разобрать карту?", 28, Color.white);
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 42f);
_confirmText = UiKit.CreateText(
card, "Text", string.Empty, 19,
new Color(0.72f, 0.78f, 0.92f),
TextAnchor.MiddleCenter);
UiKit.SetPreferredHeight(_confirmText, 110f);
var yes = UiKit.CreateButton(card, "Yes", "Разобрать", OnConfirmDismantle, 20);
UiKit.SetPreferredHeight(yes, 48f);
var no = UiKit.CreateButton(card, "No", "Отмена", HideConfirm, 20);
UiKit.SetPreferredHeight(no, 44f);
overlay.gameObject.SetActive(false);
return overlay;
}
private async Task LoadAsync()
{
if (_loading)
return;
_loading = true;
SetStatus("Загружаем коллекцию…", new Color(0.62f, 0.68f, 0.82f));
try
{
var collection = await _meta.GetCollectionAsync();
if (_disposed)
return;
Rebuild(collection.Items ?? new List<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()
{
if (_disposed || _balances == null)
return;
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
+ " · Токены " + _player.Tokens;
}
private void SetStatus(string text, Color color)
{
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty;
_status.color = color;
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 117359b61c1e3454d8dcaebc08bfa85f

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.Api.Contracts;
@ -41,6 +42,20 @@ namespace Vanguard.UI
private Text _confirmText;
private DeckDto _pendingDelete;
private RectTransform _cardOverlay;
private Image _cardArt;
private Text _cardArtName;
private Text _cardTitle;
private Text _cardSubtitle;
private Text _cardStats;
private Text _cardAbilities;
private Text _cardOwned;
private Text _cardStatus;
private Button _dismantleButton;
private Button _craftButton;
private CollectionItemDto _selectedItem;
private CardCatalogItemDto _selectedMeta;
private readonly List<DeckDto> _decks = new List<DeckDto>();
private readonly List<CollectionItemDto> _collection = new List<CollectionItemDto>();
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
@ -75,11 +90,13 @@ namespace Vanguard.UI
_editorView = BuildEditorView();
_filterOverlay = BuildFilterOverlay();
_confirmOverlay = BuildDeleteConfirm();
_cardOverlay = BuildCardOverlay();
_listView.gameObject.SetActive(false);
_editorView.gameObject.SetActive(false);
_filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_root.gameObject.SetActive(false);
}
@ -94,6 +111,7 @@ namespace Vanguard.UI
{
_filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_root.gameObject.SetActive(false);
}
@ -105,7 +123,7 @@ namespace Vanguard.UI
var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 17);
SetFixedSize(back, 120f, 44f);
var title = UiKit.CreateText(top, "Title", "Колоды", 28, Color.white, TextAnchor.MiddleLeft);
var title = UiKit.CreateText(top, "Title", "Коллекция", 28, Color.white, TextAnchor.MiddleLeft);
title.fontStyle = FontStyle.Bold;
SetFixedSize(title, 150f, 44f);
@ -187,7 +205,7 @@ namespace Vanguard.UI
var view = UiKit.CreatePanel(_root, "DeckEditor", new Color(0.018f, 0.03f, 0.06f, 0.97f));
var top = BuildTopBar(view);
var back = UiKit.CreateButton(top, "Back", "← Колоды", BackToList, 17);
var back = UiKit.CreateButton(top, "Back", "← Коллекция", BackToList, 17);
SetFixedSize(back, 120f, 44f);
_nameInput = UiKit.CreateInput(top, "Name", "Название колоды");
@ -265,7 +283,7 @@ namespace Vanguard.UI
deckScrollElement.minHeight = 200f;
var hint = UiKit.CreateText(
deckArea, "Hint", "Нажмите на карту,\nчтобы убрать одну копию.", 14,
deckArea, "Hint", "ЛКМ — убрать копию,\nПКМ — посмотреть карту.", 14,
new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft);
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(hint, 46f);
@ -352,6 +370,265 @@ namespace Vanguard.UI
return overlay;
}
private RectTransform BuildCardOverlay()
{
var overlay = UiKit.CreatePanel(_root, "CardOverlay", new Color(0f, 0f, 0f, 0.82f));
var card = UiKit.CreateCenteredCard(
overlay, "CardView", new Vector2(1040f, 660f),
new Color(0.05f, 0.07f, 0.11f, 1f));
var body = UiKit.CreatePanel(card, "Body", new Color(0f, 0f, 0f, 0f));
body.anchorMin = Vector2.zero;
body.anchorMax = Vector2.one;
body.offsetMin = new Vector2(26f, 26f);
body.offsetMax = new Vector2(-26f, -26f);
UiKit.AddHorizontalLayout(body, 0, 22);
var art = UiKit.CreatePanel(body, "Art", new Color(0.2f, 0.3f, 0.5f, 1f));
SetFixedSize(art, 360f, 540f);
_cardArt = art.GetComponent<Image>();
_cardArtName = UiKit.CreateText(
art, "ArtName", string.Empty, 18,
new Color(1f, 1f, 1f, 0.92f), TextAnchor.MiddleCenter);
UiKit.Stretch(_cardArtName.rectTransform);
_cardArtName.rectTransform.offsetMin = new Vector2(14f, 16f);
_cardArtName.rectTransform.offsetMax = new Vector2(-14f, -16f);
_cardArtName.horizontalOverflow = HorizontalWrapMode.Wrap;
var info = UiKit.CreatePanel(body, "Info", new Color(0f, 0f, 0f, 0f));
var infoElement = info.gameObject.AddComponent<LayoutElement>();
infoElement.flexibleWidth = 1f;
infoElement.flexibleHeight = 1f;
UiKit.AddVerticalLayout(info, 4, 6);
_cardTitle = UiKit.CreateText(
info, "Title", string.Empty, 30, Color.white, TextAnchor.MiddleLeft);
_cardTitle.fontStyle = FontStyle.Bold;
_cardTitle.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardTitle, 44f);
_cardSubtitle = UiKit.CreateText(
info, "Subtitle", string.Empty, 16,
new Color(0.62f, 0.70f, 0.84f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(_cardSubtitle, 26f);
_cardStats = UiKit.CreateText(
info, "Stats", string.Empty, 15,
new Color(0.72f, 0.78f, 0.92f), TextAnchor.UpperLeft);
_cardStats.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardStats, 88f);
var abilitiesCaption = UiKit.CreateText(
info, "AbilitiesCaption", "Способности", 16,
new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(abilitiesCaption, 24f);
_cardAbilities = UiKit.CreateText(
info, "Abilities", string.Empty, 15,
new Color(0.82f, 0.86f, 0.96f), TextAnchor.UpperLeft);
_cardAbilities.horizontalOverflow = HorizontalWrapMode.Wrap;
var abilitiesElement = _cardAbilities.gameObject.AddComponent<LayoutElement>();
abilitiesElement.flexibleHeight = 1f;
abilitiesElement.minHeight = 100f;
_cardOwned = UiKit.CreateText(
info, "Owned", string.Empty, 15,
new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(_cardOwned, 24f);
_cardStatus = UiKit.CreateText(
info, "Status", string.Empty, 14,
new Color(0.62f, 0.68f, 0.82f), TextAnchor.UpperLeft);
_cardStatus.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardStatus, 38f);
var actions = UiKit.CreatePanel(info, "Actions", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(actions, 50f);
UiKit.AddHorizontalLayout(actions, 0, 8);
_dismantleButton = UiKit.CreateButton(
actions, "Dismantle", "Разобрать", DismantleSelected, 16);
SetFixedSize(_dismantleButton, 225f, 46f);
_craftButton = UiKit.CreateButton(
actions, "Craft", "Скрафтить", CraftSelected, 16);
SetFixedSize(_craftButton, 225f, 46f);
var close = UiKit.CreateButton(actions, "Close", "Закрыть", CloseCardOverlay, 16);
SetFixedSize(close, 110f, 46f);
return overlay;
}
private void AddRightClick(GameObject target, string printingId, CardCatalogItemDto meta)
{
var trigger = target.GetComponent<EventTrigger>()
?? target.AddComponent<EventTrigger>();
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerClick };
entry.callback.AddListener(data =>
{
if (data is PointerEventData pointer
&& pointer.button == PointerEventData.InputButton.Right)
{
ShowCardOverlay(printingId, meta);
}
});
trigger.triggers.Add(entry);
}
private void ShowCardOverlay(string printingId, CardCatalogItemDto meta)
{
if (meta == null)
return;
_selectedMeta = meta;
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
_cardStatus.text = string.Empty;
UpdateCardOverlay();
_cardOverlay.SetAsLastSibling();
_cardOverlay.gameObject.SetActive(true);
}
private void UpdateCardOverlay()
{
if (_selectedMeta == null)
return;
var meta = _selectedMeta;
_cardArt.color = ClanColor(meta.Clan);
_cardArtName.text = meta.NameEn;
_cardTitle.text = meta.NameEn;
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
+ meta.Clan + " · G" + meta.Grade;
_cardStats.text = "Тип: " + meta.Type + " · Редкость: " + meta.Rarity + "\n"
+ "Power " + meta.Power + " · Shield " + meta.Shield + " · Critical " + meta.Critical + "\n"
+ "Триггер: " + meta.Trigger
+ " · Sentinel: " + (meta.IsSentinel ? "да" : "нет")
+ " · Boost: " + (meta.HasBoost ? "да" : "нет")
+ " · Intercept: " + (meta.HasIntercept ? "да" : "нет") + "\n"
+ "Принт: " + meta.PrintingId;
_cardAbilities.text = string.IsNullOrWhiteSpace(meta.TextRu)
? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn)
: meta.TextRu;
_cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0)
+ " · в колоде: " + UsedByName(meta.NameId);
var canChange = CanChange(meta.Rarity);
_dismantleButton.interactable = canChange && !_busy
&& _selectedItem != null && _selectedItem.Count > 0;
_craftButton.interactable = canChange && !_busy;
SetButtonLabel(
_dismantleButton,
canChange ? "Разобрать (+" + DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
SetButtonLabel(
_craftButton,
canChange ? "Скрафтить (−" + CraftCostFor(meta.Rarity) + " пыли)" : "Нельзя скрафтить");
}
private static bool CanChange(string rarity) =>
rarity == "C" || rarity == "R" || rarity == "RR" || rarity == "RRR";
private async void DismantleSelected()
{
if (_busy || _selectedItem == null || _selectedItem.Count <= 0
|| _selectedMeta == null || !CanChange(_selectedMeta.Rarity))
return;
var printingId = _selectedItem.PrintingId;
_busy = true;
UpdateCardOverlay();
_cardStatus.text = "Разбираем…";
_cardStatus.color = new Color(0.62f, 0.68f, 0.82f);
try
{
await _auth.EnsureAccessAsync();
await _meta.DismantleAsync(printingId, 1);
await ReloadCollectionAsync(printingId);
SetCardStatus("Карта разобрана.", false);
}
catch (ApiException exception)
{
SetCardStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetCardStatus("Не удалось разобрать карту.", true);
}
finally
{
_busy = false;
UpdateCardOverlay();
}
}
private async void CraftSelected()
{
if (_busy || _selectedMeta == null || !CanChange(_selectedMeta.Rarity))
return;
var printingId = _selectedMeta.PrintingId;
_busy = true;
UpdateCardOverlay();
_cardStatus.text = "Крафтим…";
_cardStatus.color = new Color(0.62f, 0.68f, 0.82f);
try
{
await _auth.EnsureAccessAsync();
await _meta.CraftAsync(printingId);
await ReloadCollectionAsync(printingId);
SetCardStatus("Карта скрафчена.", false);
}
catch (ApiException exception)
{
SetCardStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetCardStatus("Не удалось скрафтить карту.", true);
}
finally
{
_busy = false;
UpdateCardOverlay();
}
}
private async Task ReloadCollectionAsync(string printingId)
{
var collection = await _meta.GetCollectionAsync();
_collection.Clear();
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
RebuildCollectionGrids();
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
if (_selectedItem == null)
{
_cardOverlay.gameObject.SetActive(false);
_listStatus.text = "Карт в коллекции: " + _collection.Count + ".";
}
else
{
UpdateCardOverlay();
}
}
private void SetCardStatus(string text, bool error)
{
_cardStatus.text = text;
_cardStatus.color = error
? new Color(1f, 0.55f, 0.45f)
: new Color(0.45f, 0.85f, 0.55f);
}
private void CloseCardOverlay()
{
_selectedItem = null;
_selectedMeta = null;
_cardOverlay.gameObject.SetActive(false);
}
private void AddFilterSection(Transform parent, string title, string[] labels, Action<string> onSelected)
{
var caption = UiKit.CreateText(
@ -411,8 +688,8 @@ namespace Vanguard.UI
BuildDeckList();
RebuildCollectionGrids();
_listStatus.text = _decks.Count == 0
? "Колод пока нет. Создайте первую."
: "Карт в коллекции: " + _collection.Count + ".";
? "Колод пока нет. Создайте первую. ПКМ по карте — подробности."
: "Карт в коллекции: " + _collection.Count + " · ПКМ по карте — подробности.";
}
catch (ApiException exception)
{
@ -491,6 +768,7 @@ namespace Vanguard.UI
_listView.gameObject.SetActive(false);
_editorView.gameObject.SetActive(true);
_filterOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_editorStatus.text = string.Empty;
RebuildEditor();
}
@ -498,6 +776,7 @@ namespace Vanguard.UI
private void BackToList()
{
_filterOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_editorView.gameObject.SetActive(false);
_listView.gameObject.SetActive(true);
RefreshAllCounts();
@ -508,6 +787,7 @@ namespace Vanguard.UI
_editorView.gameObject.SetActive(false);
_filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_listView.gameObject.SetActive(true);
}
@ -566,6 +846,7 @@ namespace Vanguard.UI
var cell = UiKit.CreatePanel(
content, "Owned_" + item.PrintingId, new Color(0.08f, 0.11f, 0.17f, 1f));
UiKit.AddVerticalLayout(cell, 6, 2);
AddRightClick(cell.gameObject, item.PrintingId, meta);
if (onAdd != null)
{
@ -651,6 +932,8 @@ namespace Vanguard.UI
var button = row.gameObject.AddComponent<Button>();
button.targetGraphic = row.GetComponent<Image>();
button.onClick.AddListener(() => RemoveCard(printingId));
if (meta != null)
AddRightClick(row.gameObject, printingId, meta);
var art = UiKit.CreatePanel(
row, "Art", ClanColor(meta != null ? meta.Clan : "None"));
@ -988,6 +1271,31 @@ namespace Vanguard.UI
_ => "—",
};
private static int DustFor(string rarity) => rarity switch
{
"C" => 5,
"R" => 10,
"RR" => 25,
"RRR" => 100,
_ => 0,
};
private static int CraftCostFor(string rarity) => rarity switch
{
"C" => 20,
"R" => 40,
"RR" => 100,
"RRR" => 400,
_ => 0,
};
private static void SetButtonLabel(Button button, string label)
{
var text = button.GetComponentInChildren<Text>();
if (text != null)
text.text = label;
}
private static Color ClanColor(string clan) => clan switch
{
"RoyalPaladin" => new Color(0.22f, 0.42f, 0.78f),

View File

@ -1,356 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.Api.Contracts;
using Vanguard.Meta;
using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Список колод и быстрый сборщик стартовой колоды из триал-деки.</summary>
public sealed class DecksView : IDisposable
{
private readonly MetaService _meta;
private readonly PlayerState _player;
private readonly Action _onBack;
private readonly Action<DeckDto> _onPlay;
private bool _disposed;
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,
Action<DeckDto> onPlay)
{
_meta = meta;
_player = player;
_onBack = onBack;
_onPlay = onPlay;
_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);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= OnPlayerChanged;
}
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();
if (_disposed)
return;
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)
{
_onPlay(deck);
}
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()
{
if (_disposed || _balances == null)
return;
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
+ " · Токены " + _player.Tokens;
}
private void SetStatus(string text, Color color)
{
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty;
_status.color = color;
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f00da70463bd90b45bdbf542ddb33bb1

View File

@ -14,7 +14,6 @@ namespace Vanguard.UI
private readonly AuthService _auth;
private readonly PlayerState _player;
private readonly Action _onLoggedOut;
private readonly Action _onCollection;
private readonly Action _onDecks;
private readonly Action _onStore;
private readonly Action _onHistory;
@ -41,7 +40,6 @@ namespace Vanguard.UI
AuthService auth,
PlayerState player,
Action onLoggedOut,
Action onCollection,
Action onDecks,
Action onStore,
Action onHistory,
@ -51,7 +49,6 @@ namespace Vanguard.UI
_auth = auth;
_player = player;
_onLoggedOut = onLoggedOut;
_onCollection = onCollection;
_onDecks = onDecks;
_onStore = onStore;
_onHistory = onHistory;
@ -112,8 +109,7 @@ namespace Vanguard.UI
UiKit.SetPreferredHeight(actions, 96f);
UiKit.AddHorizontalLayout(actions, 18, 14);
AddAction(actions, "Коллекция", _onCollection);
AddAction(actions, "Колоды", _onDecks);
AddAction(actions, "Коллекция", _onDecks);
AddAction(actions, "Магазин", _onStore);
AddAction(actions, "История", _onHistory);
AddAction(actions, "В бой", _onPlay);

View File

@ -1,352 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 2
m_PVRDenoiserTypeIndirect: 2
m_PVRDenoiserTypeAO: 2
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &490282527
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 490282529}
- component: {fileID: 490282528}
m_Layer: 0
m_Name: Collection Scene
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &490282528
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 490282527}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8ccb09cec5c89814493fae05cf2d6051, type: 3}
m_Name:
m_EditorClassIdentifier: Vanguard.Runtime::Vanguard.App.CollectionSceneController
--- !u!4 &490282529
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 490282527}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1697705437
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1697705439}
- component: {fileID: 1697705438}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &1697705438
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1697705437}
m_Enabled: 1
serializedVersion: 13
m_Type: 1
m_Color: {r: 0.82, g: 0.88, b: 1, a: 1}
m_Intensity: 0.9
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize2D: {x: 0.5, y: 0.5}
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShapeRadius: 0.025
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &1697705439
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1697705437}
serializedVersion: 2
m_LocalRotation: {x: 0.39098036, y: -0.25180724, z: 0.112111814, w: 0.87815624}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1894381784
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1894381786}
- component: {fileID: 1894381785}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &1894381785
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1894381784}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.02, g: 0.03, b: 0.055, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &1894381786
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1894381784}
serializedVersion: 2
m_LocalRotation: {x: 0.104528464, y: 0, z: 0, w: 0.9945219}
m_LocalPosition: {x: 0, y: 3.2, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 490282529}
- {fileID: 1894381786}
- {fileID: 1697705439}

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: cd8d654fe14bfb0419574bc6d2820591
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -33,11 +33,9 @@ namespace Vanguard.Tests
var lobby = new MetaView(
canvas.transform, auth, player, () => { },
() => { }, () => { }, () => { }, () => { }, () => { }, () => { });
() => { }, () => { }, () => { }, () => { }, () => { });
var onboarding = new OnboardingView(
canvas.transform, auth, meta, () => { }, () => { });
var collection = new CollectionView(
canvas.transform, meta, player, () => { });
var decks = new DeckBuilderView(
canvas.transform, meta, auth, () => { }, _ => { });
var queue = new QueueView(
@ -52,7 +50,6 @@ namespace Vanguard.Tests
Assert.IsNotNull(GameObject.Find("LoginScreen"));
Assert.IsTrue(Exists("MetaScreen"));
Assert.IsTrue(Exists("OnboardingScreen"));
Assert.IsTrue(Exists("CollectionScreen"));
Assert.IsTrue(Exists("DeckBuilderScreen"));
Assert.IsTrue(Exists("StoreScreen"));
Assert.IsTrue(Exists("HistoryScreen"));
@ -62,7 +59,6 @@ namespace Vanguard.Tests
// После смены сцены старые views не должны реагировать на PlayerState.
lobby.Dispose();
collection.Dispose();
store.Dispose();
player.Apply(new MeResponse
{

View File

@ -14,9 +14,6 @@ EditorBuildSettings:
- enabled: 1
path: Assets/Vanguard/Scenes/Lobby.unity
guid: 59a25f3bae0ec7c489d2a139d7eea285
- enabled: 1
path: Assets/Vanguard/Scenes/Collection.unity
guid: cd8d654fe14bfb0419574bc6d2820591
- enabled: 1
path: Assets/Vanguard/Scenes/DeckBuilder.unity
guid: b8567d3f2f60dcc4f85794bdbaf95b45

View File

@ -35,9 +35,9 @@ http://localhost:5080
## Визуальное направление
Клиент делаем в **3D** на URP. Основные экраны — отдельные сцены:
**Login**, **Lobby**, **Collection**, **DeckBuilder**, **Battle**.
`Boot` — служебная сцена с общими сервисами; очередь и онбординг —
оверлеи Lobby. Текущий единый Canvas — временный gray-box, его разносим
**Login**, **Lobby**, **DeckBuilder**, **Battle**.
`DeckBuilder` совмещает коллекцию и редактор колоды. `Boot` — служебная
сцена с общими сервисами; очередь и онбординг — оверлеи Lobby. Текущий единый Canvas — временный gray-box, его разносим
по сценам. Полное решение — `docs/client-visual-direction.md`.
## Что уже есть
@ -49,10 +49,10 @@ http://localhost:5080
- Логин, регистрация, refresh, `/me`, выход.
- Восстановление сессии по refresh-токену при старте.
- Онбординг: две триал-деки из `/api/store`, выбор одной, 50 карт и 5 токенов.
- Коллекция: список карт, балансы, разбор в пыль с подтверждением.
- Колоды: список справа, создание/редактирование в декбилдере;
сортировка коллекции клан→ранг→имя, фильтры, статистика по рангам,
добавление/удаление карт кликом.
- Коллекция и колоды: карточная сетка, сортировка клан→ранг→имя,
фильтры, статистика по рангам, добавление/удаление карт кликом.
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
крафтом за пыль.
- Магазин в Lobby: бустер за Gold/токен, показ 5 карт, покупка второй
триал-деки.
- История матчей: список результатов, награды, MMR; просмотр реплея по

View File

@ -47,7 +47,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
| Протокол и контракты | `docs/protocol.md`, `Vanguard.Shared` готовы |
| Сервер | сеть, матчмейкинг, PostgreSQL + Redis, Orleans, Docker |
| Мета-API | JWT, коллекция, колоды, магазин, экономика, история, реплеи |
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/Collection/DeckBuilder/Battle, логин/коллекция/колоды/очередь/gray-box бой |
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/DeckBuilder/Battle, логин/коллекция/колоды/очередь/gray-box бой |
| Лаунчер | не начат |
Блок «Игровое ядро» из `IMPLEMENTATION_CHECKLIST.md` закрыт по его критериям:
@ -526,8 +526,8 @@ CI подключён и проходит; серверный блок закр
- проект создан из URP-шаблона Unity 6.6; пакеты: URP, Input System,
Addressables, Localization, Newtonsoft.Json, VContainer (OpenUPM);
- визуальное направление зафиксировано: **3D**, отдельные сцены-экраны
Login/Lobby/Collection/DeckBuilder/Battle (`docs/client-visual-direction.md`);
сцены и `SceneFlow` уже созданы, UI пока code-generated gray-box;
Login/Lobby/DeckBuilder/Battle (`docs/client-visual-direction.md`);
`DeckBuilder` совмещает коллекцию и колоды; UI пока code-generated gray-box;
- сцена `Boot` + runtime-инициализатор, поэтому клиент стартует и в пустой
сцене;
- composition root на VContainer: `AppConfig`, `TokenStore`, `ApiClient`,
@ -541,9 +541,10 @@ CI подключён и проходит; серверный блок закр
переходами в разделы;
- онбординг: список триал-дек из `/api/store`, выбор одной, 50 карт + 5
токенов, обновление профиля;
- коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением;
- колоды: `/api/decks`, удаление и автосборка стартовой колоды из
выбранной триал-деки (50 карт);
- коллекция: `/api/collection`, балансы, разбор в пыль и крафт; ПКМ по
карте открывает оверлей с текстом способностей;
- колоды: `/api/decks`, список справа, редактор; карточная сетка
коллекции с сортировкой клан→ранг→имя;
- магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт,
покупка второй триал-деки;
- история матчей и реплей: `/api/matches`, `/api/replays/{id}`, просмотр
@ -555,7 +556,7 @@ CI подключён и проходит; серверный блок закр
сдача с подтверждением, результат;
- `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены
как плагины; `client/README.md` описывает запуск и ограничения;
- сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle` и
- сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` и
`SceneFlow`; `Boot` — стартовая служебная сцена, `VanguardApp` и сервисы
живут между сценами;
- 3D-фон на каждой сцене: камера, свет, пол и тематические примитивы;
@ -574,13 +575,13 @@ CI подключён и проходит; серверный блок закр
4. Локализация RU/EN, настройки, DOTween.
**Зафиксированное визуальное направление** — `docs/client-visual-direction.md`:
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection /
DeckBuilder / Battle; Boot и общие сервисы остаются персистентными.
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / DeckBuilder /
Battle; Boot и общие сервисы остаются персистентными.
Сцены и `SceneFlow` уже созданы; UI пока code-generated gray-box.
- Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
- VContainer, `ApiClient`, `GameConnection`, `SceneFlow`.
- Сцены: `Boot` (служебная), `Login`, `Lobby`, `Collection`, `DeckBuilder`,
- Сцены: `Boot` (служебная), `Login`, `Lobby`, `DeckBuilder`,
`Battle`; очередь/онбординг — оверлеи Lobby.
- История матчей и экран реплея: `GET /api/matches`, `GET /api/replays/{id}`,
проигрывание `frames` + `events`.
@ -720,10 +721,15 @@ DeckBuilder / Battle; Boot и общие сервисы остаются пер
- сортировка коллекции: клан → ранг → имя.
- Ограничения в UI: 50 карт, ≤4 по `nameId`, не больше owned-копий;
серверная валидация остаётся источником истины.
- `DeckBuilderSceneController` и `CollectionSceneController` теперь
вызывают `Show()`; раньше сцены создавали скрытый корень.
- `MetaService.GetCatalogAsync`, `UpdateDeckAsync`; `ApiClient.PutAsync`.
- Сцена **Collection** и отдельный `CollectionView` удалены; коллекция
теперь на экране **Коллекция** внутри `DeckBuilderView`.
- ПКМ по карте коллекции/колоды открывает оверлей: изображение-заглушка,
статы, текст способности, разбор в пыль и крафт (C/R/RR/RRR; SP и TD
нельзя).
- `MetaService.CraftAsync`; `GET /api/cards` отдаёт `power`, `shield`,
`critical`, `type`, `textEn/textRu` и флаги способностей.
- Проверки: серверные 131 + 44 на `ssh home`; Unity EditMode 2/2,
`SCENE_SMOKE success=True active=Login`; live `GET /api/cards` — 124
принта; live UI-проба: загрузка 50 карт, создание колоды,
добавление и удаление карты.
добавление/удаление, ПКМ-оверлей, текст способности и разбор карты;
live API-проба крафта: разбор 4 C → 20 пыли → крафт C.

View File

@ -29,8 +29,9 @@
- Пакеты: URP, Input System, Addressables, Localization, Newtonsoft.Json,
VContainer (OpenUPM).
- Визуальное направление: **3D**, отдельные сцены-экраны:
`Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle`.
`Boot` — стартовая служебная сцена; `VanguardApp` персистентный.
`Login`, `Lobby`, `DeckBuilder`, `Battle`.
`DeckBuilder` совмещает коллекцию и колоды; `Boot` — стартовая служебная
сцена; `VanguardApp` персистентный.
- Переходы — `SceneFlow`; очередь/онбординг — оверлеи **Lobby**.
- 3D-окружение сейчас из примитивов (`SceneEnvironment`): камера, свет,
пол, тематические кубы. UI — code-generated uGUI, пока gray-box.
@ -46,8 +47,13 @@
WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`),
таймер, результат.
- Декбилдер: `DeckBuilderView`; каталог метаданных — `GET /api/cards`
(grade, trigger, clan, rarity, printing), клиент объединяет с коллекцией.
(grade, trigger, clan, rarity, printing, power/shield/type, текст
способности), клиент объединяет с коллекцией.
Изображения карт пока заглушки-плашки по цвету клана.
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
крафтом; SP/TD не разбираются и не крафтятся.
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
- Не готово: арты/модели/анимации, локализация RU/EN, настройки, DOTween.
## Запуск
@ -93,17 +99,16 @@ curl http://127.0.0.1:18080/health
```
client/Assets/Vanguard/Runtime/App/
VanguardApp.cs composition root, персистентный
SceneFlow.cs переходы Login/Lobby/Collection/DeckBuilder/Battle
SceneFlow.cs переходы Login/Lobby/DeckBuilder/Battle
SceneEnvironment.cs 3D-фон сцен
SceneControllerBase.cs
LoginSceneController.cs
LobbySceneController.cs
CollectionSceneController.cs
DeckBuilderSceneController.cs
BattleSceneController.cs
client/Assets/Vanguard/Runtime/UI/
LoginView.cs MetaView.cs OnboardingView.cs
CollectionView.cs DeckBuilderView.cs QueueView.cs BattleView.cs
DeckBuilderView.cs QueueView.cs BattleView.cs
UiKit.cs
client/Assets/Vanguard/Runtime/Battle/GameConnection.cs
client/Assets/Vanguard/Runtime/Api/ApiClient.cs + ApiContracts.cs
@ -145,4 +150,4 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
- `UiKit.CreateCanvas` по умолчанию **не** персистентный: Canvas принадлежит
сцене; EventSystem остаётся персистентным.
- Сцена `Boot` должна быть первой в Build Settings; сейчас там
`Boot`, `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle`.
`Boot`, `Login`, `Lobby`, `DeckBuilder`, `Battle`.

View File

@ -10,9 +10,8 @@
камерой, светом и UI-слоем:
1. **Login** — вход/регистрация;
2. **Lobby** — лобби, профиль, выбор режима, очередь, магазин/история;
3. **Collection** — коллекция карт;
4. **DeckBuilder** — конструктор колоды;
5. **Battle** — боевой стол.
3. **DeckBuilder** — коллекция и конструктор колоды в одной сцене;
4. **Battle** — боевой стол.
- **Boot** — служебная сцена: поднимает `VanguardApp`, DI-контейнер,
сервисы, сеть, аудио и локализацию. Пользователю не показывается.
- Очередь, онбординг и настройки — **оверлеи внутри Lobby**, отдельными
@ -35,7 +34,7 @@
## Статус миграции
Сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle` созданы;
Сцены `Login`, `Lobby`, `DeckBuilder`, `Battle` созданы;
`VanguardApp` и общие сервисы персистентны, `SceneFlow` загружает сцены.
Сейчас 3D-окружение собрано из примитивов, UI — code-generated uGUI.
Дальше: арт/модели, prefab-экраны, анимации и наполнение сцен.
@ -44,8 +43,7 @@
- `LoginView` → сцена **Login** (3D-фон/стол, форма поверх);
- `MetaView`/`OnboardingView`/`QueueView` → сцена **Lobby**;
- `CollectionView` → сцена **Collection**;
- `DecksView` → сцена **DeckBuilder**;
- `DeckBuilderView` → сцена **DeckBuilder** (коллекция + редактор колоды);
- `BattleView` → сцена **Battle**.
Сетевые и доменные классы (`GameConnection`, `ApiClient`, `MetaService`)

View File

@ -188,19 +188,30 @@ GET /api/cards [Bearer]
"printingId": "BT01/034EN",
"nameId": "BT01-034",
"nameEn": "Dragon Monk, Ginkai",
"nameRu": null,
"rarity": "C",
"grade": 2,
"type": "NormalUnit",
"power": 7000,
"shield": 5000,
"critical": 1,
"trigger": "None",
"clan": "Tachikaze",
"isSentinel": false,
"isRestraint": false,
"hasBoost": false,
"hasIntercept": true,
"isParallel": false,
"basePrintingId": null
"basePrintingId": null,
"textEn": "[AUTO](VC/RC):…",
"textRu": null
}
]
```
`grade` — 0–3, `trigger` — `None/Critical/Draw/Stand/Heal`,
`clan` — имя клана, `rarity` — `C/R/RR/RRR/SP/TD`.
`clan` — имя клана, `rarity` — `C/R/RR/RRR/SP/TD`. `textEn/textRu` —
текст способностей для оверлея карты (RU может быть null).
---

View File

@ -30,13 +30,23 @@ public sealed record CardCatalogItemDto(
string PrintingId,
string NameId,
string NameEn,
string? NameRu,
string Rarity,
int Grade,
string Type,
int Power,
int Shield,
int Critical,
string Trigger,
string Clan,
bool IsSentinel,
bool IsRestraint,
bool HasBoost,
bool HasIntercept,
bool IsParallel,
string? BasePrintingId);
string? BasePrintingId,
string? TextEn,
string? TextRu);
public static class MetaEndpoints
{
@ -86,13 +96,23 @@ public static class MetaEndpoints
printing.PrintingId,
card.NameId,
card.NameEn,
card.NameRu,
printing.Rarity.ToString(),
card.Grade,
card.Type.ToString(),
card.Power,
card.Shield,
card.Critical,
card.Trigger.ToString(),
card.Clan.ToString(),
card.IsSentinel,
card.IsRestraint,
card.HasBoost,
card.HasIntercept,
printing.IsParallel,
printing.BasePrintingId);
printing.BasePrintingId,
card.TextEn,
card.TextRu);
})
.OrderBy(item => item.NameEn, StringComparer.Ordinal)
.ThenBy(item => item.PrintingId, StringComparer.Ordinal)