client: DeckBuilder по референсу — топбар Lobby, крупные карты
CI / test (push) Successful in 3m48s Details

- PlayerTopBar вынесен из LobbyView в общий компонент; DeckBuilder
  использует его, Rating/Community неактивны, «В лобби» — в меню.
- Коллекция: крупная сетка 6 в ряд со скроллом, ячейка 216×242 —
  грейд, иллюстрация, имя поверх, количество, камень редкости.
- Просмотр: ЛКМ не добавляет, ПКМ открывает карточку; режим колоды
  включает добавление и показывает «в колоде ×N · есть ×M».
- Список колод: + Новый набор, эмблема клана, N/50 · T N/16, кебаб-меню
  «Открыть/Удалить».
- Фильтры в оверлее: поиск по названию, редкость C/R/RR/RRR/SP, клан,
  грейд, триггер.
- Тесты: DeckFilter редкость/поиск, PlayerTopBar, панели DeckBuilder.
- Проверки: EditMode 69/69, SCENE_SMOKE success=True, PLAYMAT_SMOKE
  success=True, standalone собран.
This commit is contained in:
MpakobecEx 2026-11-01 02:40:10 +05:00
parent f22a24e5ad
commit f9aed0cae8
13 changed files with 814 additions and 365 deletions

View File

@ -2,6 +2,7 @@ using UnityEngine;
using Vanguard.Api.Contracts; using Vanguard.Api.Contracts;
using Vanguard.Auth; using Vanguard.Auth;
using Vanguard.Meta; using Vanguard.Meta;
using Vanguard.State;
using Vanguard.UI; using Vanguard.UI;
namespace Vanguard.App namespace Vanguard.App
@ -9,6 +10,7 @@ namespace Vanguard.App
public sealed class DeckBuilderSceneController : SceneControllerBase public sealed class DeckBuilderSceneController : SceneControllerBase
{ {
private DeckBuilderView _view; private DeckBuilderView _view;
private PlayerTopBar _topBar;
private void Start() private void Start()
{ {
@ -21,12 +23,24 @@ namespace Vanguard.App
() => _ = Flow.LoadLobbyAsync(), () => _ = Flow.LoadLobbyAsync(),
deck => _ = Flow.OpenLobbyWithQueueAsync(deck), deck => _ = Flow.OpenLobbyWithQueueAsync(deck),
info => _ = Flow.LoadBattleAsync(info)); info => _ = Flow.LoadBattleAsync(info));
_topBar = new PlayerTopBar(
canvas.transform,
Resolve<AppConfig>(),
Resolve<PlayerState>(),
() => _ = Flow.LoadLoginAsync(),
null,
null,
null,
() => _ = Flow.LoadLobbyAsync());
_view.Show(); _view.Show();
} }
private void OnDestroy() private void OnDestroy()
{ {
_view?.Hide(); _view?.Hide();
_topBar?.Dispose();
} }
} }
} }

View File

@ -33,7 +33,6 @@ namespace Vanguard.UI
private readonly MetaService _meta; private readonly MetaService _meta;
private readonly AuthService _auth; private readonly AuthService _auth;
private readonly Action _onBack;
private readonly Action<DeckDto> _onPlay; private readonly Action<DeckDto> _onPlay;
private readonly Action<MatchConnectionInfo> _onPracticeStarted; private readonly Action<MatchConnectionInfo> _onPracticeStarted;
@ -44,7 +43,6 @@ namespace Vanguard.UI
private RectTransform _deckListContent; private RectTransform _deckListContent;
private RectTransform _deckCardContent; private RectTransform _deckCardContent;
private InputField _nameInput; private InputField _nameInput;
private Text _title;
private Text _status; private Text _status;
private Text _filterSummary; private Text _filterSummary;
private Text _stats; private Text _stats;
@ -53,12 +51,17 @@ namespace Vanguard.UI
private Button _botButton; private Button _botButton;
private Button _duelButton; private Button _duelButton;
private Button _deleteButton; private Button _deleteButton;
private RectTransform _deckMenuOverlay;
private Text _deckMenuTitle;
private DeckDto _deckMenuDeck;
private RectTransform _filterOverlay; private RectTransform _filterOverlay;
private RectTransform _rarityChips;
private RectTransform _gradeChips; private RectTransform _gradeChips;
private RectTransform _triggerChips; private RectTransform _triggerChips;
private RectTransform _clanChips; private RectTransform _clanChips;
private RectTransform _confirmOverlay; private RectTransform _confirmOverlay;
private InputField _searchInput;
private Text _confirmText; private Text _confirmText;
private DeckDto _pendingDelete; private DeckDto _pendingDelete;
@ -90,6 +93,7 @@ namespace Vanguard.UI
private DeckDto _editing; private DeckDto _editing;
private int _dust; private int _dust;
private bool _dirty; private bool _dirty;
private bool _deckOpen;
private bool _loading; private bool _loading;
private bool _busy; private bool _busy;
@ -103,19 +107,19 @@ namespace Vanguard.UI
{ {
_meta = meta; _meta = meta;
_auth = auth; _auth = auth;
_onBack = onBack;
_onPlay = onPlay; _onPlay = onPlay;
_onPracticeStarted = onPracticeStarted; _onPracticeStarted = onPracticeStarted;
_root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f)); _root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f));
BuildTopBar();
BuildBody(); BuildBody();
_filterOverlay = BuildFilterOverlay(); _filterOverlay = BuildFilterOverlay();
_confirmOverlay = BuildDeleteConfirm(); _confirmOverlay = BuildDeleteConfirm();
_deckMenuOverlay = BuildDeckMenu();
_cardOverlay = BuildCardOverlay(); _cardOverlay = BuildCardOverlay();
_filterOverlay.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false);
_deckMenuOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false);
_root.gameObject.SetActive(false); _root.gameObject.SetActive(false);
} }
@ -131,49 +135,20 @@ namespace Vanguard.UI
{ {
_filterOverlay.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false); _confirmOverlay.gameObject.SetActive(false);
_deckMenuOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false);
_root.gameObject.SetActive(false); _root.gameObject.SetActive(false);
} }
// ------------------------------------------------------------ каркас // ------------------------------------------------------------ каркас
private void BuildTopBar()
{
var top = UiKit.CreatePanel(_root, "TopBar", new Color(0.06f, 0.08f, 0.13f, 1f));
var topRect = (RectTransform)top.transform;
topRect.anchorMin = new Vector2(0f, 1f);
topRect.anchorMax = new Vector2(1f, 1f);
topRect.pivot = new Vector2(0.5f, 1f);
topRect.anchoredPosition = Vector2.zero;
topRect.sizeDelta = new Vector2(0f, 88f);
UiKit.AddHorizontalLayout(top, 18, 12);
var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 17);
SetFixedSize(back, 130f, 44f);
_title = UiKit.CreateText(top, "Title", "Колоды", 28, TextPrimary, TextAnchor.MiddleLeft);
_title.fontStyle = FontStyle.Bold;
SetFixedSize(_title, 200f, 44f);
_filterSummary = UiKit.CreateText(
top, "FilterSummary", "Все карты", 15, TextMuted, TextAnchor.MiddleLeft);
_filterSummary.horizontalOverflow = HorizontalWrapMode.Wrap;
SetFlexible(_filterSummary, 1f, 44f);
var filters = UiKit.CreateButton(top, "Filters", "Фильтры", OpenFilters, 17);
SetFixedSize(filters, 110f, 44f);
var reset = UiKit.CreateButton(top, "ResetFilters", "Сброс", ResetFilters, 17);
SetFixedSize(reset, 90f, 44f);
}
private void BuildBody() private void BuildBody()
{ {
var body = UiKit.CreatePanel(_root, "Body", new Color(0f, 0f, 0f, 0f)); var body = UiKit.CreatePanel(_root, "Body", new Color(0f, 0f, 0f, 0f));
body.anchorMin = Vector2.zero; body.anchorMin = Vector2.zero;
body.anchorMax = Vector2.one; body.anchorMax = Vector2.one;
body.offsetMin = new Vector2(34f, 28f); body.offsetMin = new Vector2(34f, 24f);
body.offsetMax = new Vector2(-34f, -108f); body.offsetMax = new Vector2(-34f, -120f);
UiKit.AddHorizontalLayout(body, 0, 14); UiKit.AddHorizontalLayout(body, 0, 14);
var collectionArea = UiKit.CreatePanel( var collectionArea = UiKit.CreatePanel(
@ -184,16 +159,39 @@ namespace Vanguard.UI
collectionElement.minHeight = 300f; collectionElement.minHeight = 300f;
UiKit.AddVerticalLayout(collectionArea, 8, 6); UiKit.AddVerticalLayout(collectionArea, 8, 6);
var filterBar = UiKit.CreatePanel(collectionArea, "FilterBar", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(filterBar, 44f);
UiKit.AddHorizontalLayout(filterBar, 0, 8);
_filterSummary = UiKit.CreateText(
filterBar, "FilterSummary", "Все карты", 15, TextMuted, TextAnchor.MiddleLeft);
_filterSummary.horizontalOverflow = HorizontalWrapMode.Wrap;
SetFlexible(_filterSummary, 1f, 44f);
var filters = UiKit.CreateButton(filterBar, "Filters", "Фильтры", OpenFilters, 16);
SetFixedSize(filters, 110f, 38f);
var reset = UiKit.CreateButton(filterBar, "ResetFilters", "Сброс", ResetFilters, 16);
SetFixedSize(reset, 90f, 38f);
var scroll = UiKit.CreateGridScrollView( var scroll = UiKit.CreateGridScrollView(
collectionArea, collectionArea,
"CollectionGrid", "CollectionGrid",
new Vector2(174f, 238f), new Vector2(216f, 242f),
new Vector2(10f, 10f), new Vector2(14f, 14f),
out _collectionContent); out _collectionContent);
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>(); var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
scrollElement.flexibleHeight = 1f; scrollElement.flexibleHeight = 1f;
scrollElement.minHeight = 220f; scrollElement.minHeight = 220f;
var grid = _collectionContent.GetComponent<GridLayoutGroup>();
if (grid != null)
{
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = 6;
grid.childAlignment = TextAnchor.UpperLeft;
}
_status = UiKit.CreateText( _status = UiKit.CreateText(
collectionArea, "Status", string.Empty, 15, collectionArea, "Status", string.Empty, 15,
TextMuted, TextAnchor.MiddleLeft); TextMuted, TextAnchor.MiddleLeft);
@ -221,17 +219,9 @@ namespace Vanguard.UI
UiKit.Stretch(panel); UiKit.Stretch(panel);
UiKit.AddVerticalLayout(panel, 10, 8); UiKit.AddVerticalLayout(panel, 10, 8);
var header = UiKit.CreatePanel(panel, "Header", new Color(0f, 0f, 0f, 0f)); var create = UiKit.CreateButton(panel, "Create", "+ Новый набор", NewDeck, 17);
UiKit.SetPreferredHeight(header, 38f); UiKit.SetPreferredHeight(create, 52f);
UiKit.AddHorizontalLayout(header, 0, 8); SetButtonColor(create, new Color(0.10f, 0.28f, 0.58f, 0.95f));
var title = UiKit.CreateText(
header, "Title", "Мои колоды", 20, TextPrimary, TextAnchor.MiddleLeft);
title.fontStyle = FontStyle.Bold;
SetFlexible(title, 1f, 34f);
var create = UiKit.CreateButton(header, "Create", "Создать", NewDeck, 15);
SetFixedSize(create, 120f, 36f);
var scroll = UiKit.CreateScrollView(panel, "DeckListScroll", out _deckListContent, 8f); var scroll = UiKit.CreateScrollView(panel, "DeckListScroll", out _deckListContent, 8f);
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>(); var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
@ -315,16 +305,21 @@ namespace Vanguard.UI
var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f)); var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f));
var card = UiKit.CreateCenteredCard( var card = UiKit.CreateCenteredCard(
overlay, "FilterCard", new Vector2(900f, 860f), PanelFill); overlay, "FilterCard", new Vector2(900f, 940f), PanelFill);
UiKit.AddVerticalLayout(card, 24, 10); UiKit.AddVerticalLayout(card, 24, 10);
var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary); var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary);
title.fontStyle = FontStyle.Bold; title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 40f); UiKit.SetPreferredHeight(title, 40f);
_searchInput = UiKit.CreateInput(card, "Search", "Поиск по названию");
UiKit.SetPreferredHeight(_searchInput, 44f);
_searchInput.onValueChanged.AddListener(value => _draftFilter.Search = value);
_rarityChips = BuildFilterSection(card, "Редкость", 44f);
_gradeChips = BuildFilterSection(card, "Ранг", 44f); _gradeChips = BuildFilterSection(card, "Ранг", 44f);
_triggerChips = BuildFilterSection(card, "Триггер", 44f); _triggerChips = BuildFilterSection(card, "Триггер", 44f);
_clanChips = BuildFilterSection(card, "Клан", 300f, flexible: true); _clanChips = BuildFilterSection(card, "Клан", 260f, flexible: true);
var footer = UiKit.CreatePanel(card, "Footer", new Color(0f, 0f, 0f, 0f)); var footer = UiKit.CreatePanel(card, "Footer", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(footer, 48f); UiKit.SetPreferredHeight(footer, 48f);
@ -376,6 +371,33 @@ namespace Vanguard.UI
return container; return container;
} }
private RectTransform BuildDeckMenu()
{
var overlay = UiKit.CreatePanel(_root, "DeckMenuOverlay", new Color(0f, 0f, 0f, 0.78f));
var card = UiKit.CreateCenteredCard(
overlay, "MenuCard", new Vector2(520f, 320f), PanelFill);
UiKit.AddVerticalLayout(card, 28, 10);
_deckMenuTitle = UiKit.CreateText(
card, "Title", string.Empty, 24, TextPrimary);
_deckMenuTitle.fontStyle = FontStyle.Bold;
UiKit.MakeLiteral(_deckMenuTitle);
UiKit.SetPreferredHeight(_deckMenuTitle, 40f);
var open = UiKit.CreateButton(card, "Open", "Открыть", OpenDeckFromMenu, 17);
UiKit.SetPreferredHeight(open, 48f);
var delete = UiKit.CreateButton(card, "Delete", "Удалить", DeleteDeckFromMenu, 17);
UiKit.SetPreferredHeight(delete, 48f);
SetButtonColor(delete, new Color(0.30f, 0.08f, 0.11f, 0.95f));
var cancel = UiKit.CreateButton(card, "Cancel", "Отмена", CloseDeckMenu, 17);
UiKit.SetPreferredHeight(cancel, 48f);
return overlay;
}
private RectTransform BuildDeleteConfirm() private RectTransform BuildDeleteConfirm()
{ {
var overlay = UiKit.CreatePanel( var overlay = UiKit.CreatePanel(
@ -575,11 +597,35 @@ namespace Vanguard.UI
row.GetComponent<Image>().color = isOpen row.GetComponent<Image>().color = isOpen
? new Color(0.10f, 0.18f, 0.32f, 1f) ? new Color(0.10f, 0.18f, 0.32f, 1f)
: RowFill; : RowFill;
UiKit.AddHorizontalLayout(row, 8, 8);
var emblem = UiKit.CreatePanel(row, "Emblem", new Color(0f, 0f, 0f, 0f));
SetFixedSize(emblem, 48f, 48f);
var clan = DeckClan(deck);
var color = CardArt.ClanColor(clan ?? "None");
NeonUi.AddImage(
emblem,
"Crest",
NeonUi.BodySliced(
12,
new Color(color.r, color.g, color.b, 0.35f),
new Color(color.r, color.g, color.b, 0.95f),
2),
new Vector2(48f, 48f),
Vector2.zero,
true);
var gem = NeonUi.AddImage(
emblem,
"Gem",
NeonUi.Body(20, 20, 4, color, Color.white, 1),
new Vector2(20f, 20f),
Vector2.zero);
gem.rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
var content = UiKit.CreatePanel(row, "Content", new Color(0f, 0f, 0f, 0f)); var content = UiKit.CreatePanel(row, "Content", new Color(0f, 0f, 0f, 0f));
UiKit.Stretch(content); var contentElement = content.gameObject.AddComponent<LayoutElement>();
content.offsetMin = new Vector2(10f, 8f); contentElement.flexibleWidth = 1f;
content.offsetMax = new Vector2(-96f, -8f); contentElement.preferredHeight = 64f;
UiKit.AddVerticalLayout(content, 0, 2); UiKit.AddVerticalLayout(content, 0, 2);
var name = UiKit.CreateText( var name = UiKit.CreateText(
@ -593,55 +639,77 @@ namespace Vanguard.UI
var info = UiKit.CreateText( var info = UiKit.CreateText(
content, content,
"Info", "Info",
deck.Cards.Count + "/50 · T " + validation.Triggers + "/16 · " deck.Cards.Count + "/50 · T " + validation.Triggers + "/16",
+ FormatName(deck.Format),
12, 12,
TextMuted, validation.IsValid
? new Color(0.55f, 0.9f, 0.62f)
: new Color(1f, 0.66f, 0.5f),
TextAnchor.MiddleLeft); TextAnchor.MiddleLeft);
UiKit.MakeLiteral(info); UiKit.MakeLiteral(info);
UiKit.SetPreferredHeight(info, 18f); UiKit.SetPreferredHeight(info, 18f);
var grades = UiKit.CreateText( var kebab = UiKit.CreateButton(row, "Menu", string.Empty, () => OpenDeckMenu(deck), 14);
content, "Grades", GradeLine(deck.Cards), 11, TextDim, TextAnchor.MiddleLeft); SetFixedSize(kebab, 34f, 34f);
UiKit.MakeLiteral(grades); SetButtonColor(kebab, new Color(0.06f, 0.11f, 0.22f, 0.9f));
UiKit.SetPreferredHeight(grades, 16f); for (var i = -1; i <= 1; i++)
{
var badge = UiKit.CreatePanel( NeonUi.AddImage(
row, kebab.transform,
"Validity", "Dot" + i,
validation.IsValid NeonUi.Circle(4, TextMuted),
? new Color(0.12f, 0.34f, 0.22f, 0.95f) new Vector2(4f, 4f),
: new Color(0.34f, 0.12f, 0.14f, 0.95f)); new Vector2(0f, i * 7f));
var badgeRect = (RectTransform)badge.transform; }
badgeRect.anchorMin = new Vector2(1f, 1f);
badgeRect.anchorMax = new Vector2(1f, 1f);
badgeRect.pivot = new Vector2(1f, 1f);
badgeRect.sizeDelta = new Vector2(84f, 22f);
badgeRect.anchoredPosition = new Vector2(-8f, -8f);
badge.GetComponent<Image>().sprite = NeonUi.BodySliced(
11,
validation.IsValid
? new Color(0.12f, 0.34f, 0.22f, 0.95f)
: new Color(0.34f, 0.12f, 0.14f, 0.95f),
validation.IsValid
? new Color(0.35f, 0.8f, 0.5f, 0.8f)
: new Color(0.9f, 0.4f, 0.4f, 0.8f),
1);
badge.GetComponent<Image>().type = Image.Type.Sliced;
badge.GetComponent<Image>().color = Color.white;
var badgeText = UiKit.CreateText(
badge,
"Label",
validation.IsValid ? "Готова" : "Не готова",
11,
validation.IsValid
? new Color(0.65f, 0.95f, 0.75f)
: new Color(1f, 0.62f, 0.55f));
UiKit.Stretch(badgeText.rectTransform);
} }
} }
private void OpenDeckMenu(DeckDto deck)
{
_deckMenuDeck = deck;
_deckMenuTitle.text = deck.Name;
_deckMenuOverlay.SetAsLastSibling();
_deckMenuOverlay.gameObject.SetActive(true);
}
private void CloseDeckMenu()
{
_deckMenuDeck = null;
_deckMenuOverlay.gameObject.SetActive(false);
}
private void OpenDeckFromMenu()
{
var deck = _deckMenuDeck;
CloseDeckMenu();
if (deck != null)
OpenDeck(deck);
}
private void DeleteDeckFromMenu()
{
var deck = _deckMenuDeck;
CloseDeckMenu();
if (deck != null)
RequestDelete(deck);
}
private string DeckClan(DeckDto deck)
{
if (deck == null || deck.Cards == null)
return null;
foreach (var printingId in deck.Cards)
{
if (_catalog.TryGetValue(printingId, out var meta)
&& !string.IsNullOrEmpty(meta.Clan))
{
return meta.Clan;
}
}
return null;
}
// ------------------------------------------------------------ редактор колоды // ------------------------------------------------------------ редактор колоды
private void RebuildDeckCards() private void RebuildDeckCards()
@ -708,30 +776,35 @@ namespace Vanguard.UI
private void ShowDeckList() private void ShowDeckList()
{ {
_title.text = "Колоды"; _deckOpen = false;
_deckListPanel.gameObject.SetActive(true); _deckListPanel.gameObject.SetActive(true);
_deckPanel.gameObject.SetActive(false); _deckPanel.gameObject.SetActive(false);
_filterOverlay.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false);
_deckMenuOverlay.gameObject.SetActive(false);
} }
private void ShowDeckPanel() private void ShowDeckPanel()
{ {
_title.text = "Колода"; _deckOpen = true;
_deckListPanel.gameObject.SetActive(false); _deckListPanel.gameObject.SetActive(false);
_deckPanel.gameObject.SetActive(true); _deckPanel.gameObject.SetActive(true);
_filterOverlay.gameObject.SetActive(false); _filterOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false); _cardOverlay.gameObject.SetActive(false);
_deckMenuOverlay.gameObject.SetActive(false);
RebuildDeckPanel(); RebuildDeckPanel();
RebuildCollection();
} }
private void BackToDeckList() private void BackToDeckList()
{ {
_dirty = false; _dirty = false;
_editing = null;
_cards.Clear();
_deckListPanel.gameObject.SetActive(true); _deckListPanel.gameObject.SetActive(true);
_deckPanel.gameObject.SetActive(false); _deckPanel.gameObject.SetActive(false);
BuildDeckList(); BuildDeckList();
RefreshCounts(); RebuildCollection();
ShowDeckList(); ShowDeckList();
} }
@ -790,60 +863,101 @@ namespace Vanguard.UI
{ {
var cell = UiKit.CreatePanel( var cell = UiKit.CreatePanel(
_collectionContent, "Owned_" + item.PrintingId, RowFill); _collectionContent, "Owned_" + item.PrintingId, RowFill);
UiKit.AddVerticalLayout(cell, 6, 2); UiKit.AddVerticalLayout(cell, 0, 0);
AddRightClick(cell.gameObject, item.PrintingId, meta); AddRightClick(cell.gameObject, item.PrintingId, meta);
var frame = cell.gameObject.AddComponent<Outline>(); var frame = cell.gameObject.AddComponent<Outline>();
frame.effectColor = CardArt.ClanColor(meta.Clan); frame.effectColor = CardArt.ClanColor(meta.Clan);
frame.effectDistance = new Vector2(2f, -2f); frame.effectDistance = new Vector2(2f, -2f);
var button = cell.gameObject.AddComponent<Button>(); if (_deckOpen)
button.targetGraphic = cell.GetComponent<Image>(); {
button.onClick.AddListener(() => AddCard(item)); var button = cell.gameObject.AddComponent<Button>();
button.targetGraphic = cell.GetComponent<Image>();
button.onClick.AddListener(() => AddCard(item));
}
var art = UiKit.CreatePanel(cell, "Art", Color.white); var art = UiKit.CreatePanel(cell, "Art", Color.white);
UiKit.SetPreferredHeight(art, 144f); UiKit.SetPreferredHeight(art, 182f);
CardImageService.ApplyIllustration( CardImageService.ApplyIllustration(
art.GetComponent<Image>(), art.GetComponent<Image>(),
meta.ImageKey, meta.ImageKey,
CardArt.ForCard(meta.NameId, meta.Clan)); CardArt.ForCard(meta.NameId, meta.Clan));
var grade = UiKit.CreateText( var badge = UiKit.CreatePanel(art, "GradeBadge", new Color(0f, 0f, 0f, 0f));
art, "Grade", "G" + meta.Grade, 14, Color.white, TextAnchor.MiddleCenter); var badgeRect = (RectTransform)badge.transform;
grade.fontStyle = FontStyle.Bold; badgeRect.anchorMin = new Vector2(0f, 1f);
var gradeRect = grade.rectTransform; badgeRect.anchorMax = new Vector2(0f, 1f);
gradeRect.anchorMin = new Vector2(1f, 1f); badgeRect.pivot = new Vector2(0f, 1f);
gradeRect.anchorMax = new Vector2(1f, 1f); badgeRect.sizeDelta = new Vector2(42f, 36f);
gradeRect.pivot = new Vector2(1f, 1f); badgeRect.anchoredPosition = new Vector2(8f, -8f);
gradeRect.anchoredPosition = new Vector2(-5f, -5f); var badgeImage = badge.GetComponent<Image>();
gradeRect.sizeDelta = new Vector2(34f, 22f); badgeImage.sprite = NeonUi.BodySliced(
10,
new Color(0.05f, 0.12f, 0.28f, 0.95f),
new Color(0.5f, 0.8f, 1f, 0.85f),
2);
badgeImage.type = Image.Type.Sliced;
badgeImage.color = Color.white;
badgeImage.raycastTarget = false;
var trigger = UiKit.CreateText( var grade = UiKit.CreateText(
art, "Trigger", TriggerShort(meta.Trigger), 13, Color.white, TextAnchor.MiddleCenter); badge, "Grade", meta.Grade.ToString(), 17, Color.white);
var triggerRect = trigger.rectTransform; grade.fontStyle = FontStyle.Bold;
triggerRect.anchorMin = new Vector2(0f, 0f); UiKit.Stretch(grade.rectTransform);
triggerRect.anchorMax = new Vector2(0f, 0f);
triggerRect.pivot = new Vector2(0f, 0f); var band = UiKit.CreatePanel(art, "NameBand", new Color(0.01f, 0.02f, 0.06f, 0.78f));
triggerRect.anchoredPosition = new Vector2(5f, 5f); var bandRect = (RectTransform)band.transform;
triggerRect.sizeDelta = new Vector2(34f, 20f); bandRect.anchorMin = new Vector2(0f, 0f);
bandRect.anchorMax = new Vector2(1f, 0f);
bandRect.pivot = new Vector2(0.5f, 0f);
bandRect.offsetMin = new Vector2(0f, 0f);
bandRect.offsetMax = new Vector2(0f, 36f);
band.GetComponent<Image>().raycastTarget = false;
var name = UiKit.CreateText( var name = UiKit.CreateText(
cell, "Name", Loc.CardName(meta), 13, TextPrimary, TextAnchor.UpperLeft); band, "Name", Loc.CardName(meta), 14, TextPrimary, TextAnchor.MiddleCenter);
UiKit.MakeLiteral(name); UiKit.MakeLiteral(name);
name.fontStyle = FontStyle.Bold; name.fontStyle = FontStyle.Bold;
name.horizontalOverflow = HorizontalWrapMode.Wrap; name.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(name, 32f); UiKit.Stretch(name.rectTransform);
name.rectTransform.offsetMin = new Vector2(6f, 2f);
name.rectTransform.offsetMax = new Vector2(-6f, -2f);
var metaLine = UiKit.CreateText( var qty = UiKit.CreatePanel(cell, "Qty", new Color(0.02f, 0.05f, 0.12f, 0.95f));
cell, "Meta", meta.Clan + " · " + meta.Rarity, 11, TextMuted, TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(qty, 58f);
UiKit.SetPreferredHeight(metaLine, 16f);
var counts = UiKit.CreateText( var counts = UiKit.CreateText(
cell, "Counts", CountText(item), 13, new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft); qty,
UiKit.SetPreferredHeight(counts, 20f); "Counts",
CountText(item),
12,
new Color(0.90f, 0.86f, 0.70f),
TextAnchor.MiddleCenter);
UiKit.Stretch(counts.rectTransform);
counts.rectTransform.offsetMin = new Vector2(8f, 0f);
counts.rectTransform.offsetMax = new Vector2(-24f, 0f);
_countTexts[item.PrintingId] = counts; _countTexts[item.PrintingId] = counts;
var gem = NeonUi.AddImage(
qty,
"Rarity",
NeonUi.Body(12, 12, 2, RarityColor(meta.Rarity), Color.white, 1),
new Vector2(12f, 12f),
new Vector2(84f, 0f));
gem.rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
} }
private static Color RarityColor(string rarity) => rarity switch
{
"C" => new Color(0.62f, 0.68f, 0.78f),
"R" => new Color(0.35f, 0.62f, 1f),
"RR" => new Color(0.72f, 0.45f, 1f),
"RRR" => new Color(1f, 0.78f, 0.32f),
"SP" => new Color(1f, 0.45f, 0.75f),
_ => new Color(0.5f, 0.55f, 0.65f),
};
private void RefreshCounts() private void RefreshCounts()
{ {
var triggerCap = TriggerCount() >= 16; var triggerCap = TriggerCount() >= 16;
@ -855,16 +969,19 @@ namespace Vanguard.UI
var meta = MetaOf(item); var meta = MetaOf(item);
var used = UsedByName(item.NameId); var used = UsedByName(item.NameId);
var atCap = used >= 4 || (meta != null && IsTrigger(meta.Trigger) && triggerCap); var atCap = _deckOpen
&& (used >= 4 || (meta != null && IsTrigger(meta.Trigger) && triggerCap));
pair.Value.text = CountText(item); pair.Value.text = CountText(item);
pair.Value.color = atCap pair.Value.color = atCap
? new Color(1f, 0.55f, 0.45f) ? new Color(1f, 0.55f, 0.45f)
: new Color(0.98f, 0.82f, 0.42f); : new Color(0.90f, 0.86f, 0.70f);
} }
} }
private string CountText(CollectionItemDto item) => private string CountText(CollectionItemDto item) =>
"×" + item.Count + " · в колоде " + UsedByName(item.NameId); _deckOpen
? "в колоде ×" + UsedByName(item.NameId) + " · есть ×" + item.Count
: "есть ×" + item.Count;
private void AddCard(CollectionItemDto item) private void AddCard(CollectionItemDto item)
{ {
@ -918,6 +1035,7 @@ namespace Vanguard.UI
private void OpenFilters() private void OpenFilters()
{ {
_draftFilter.CopyFrom(_filter); _draftFilter.CopyFrom(_filter);
_searchInput.SetTextWithoutNotify(_draftFilter.Search ?? string.Empty);
RebuildFilterChips(); RebuildFilterChips();
_filterOverlay.SetAsLastSibling(); _filterOverlay.SetAsLastSibling();
_filterOverlay.gameObject.SetActive(true); _filterOverlay.gameObject.SetActive(true);
@ -939,6 +1057,7 @@ namespace Vanguard.UI
private void ResetDraftFilters() private void ResetDraftFilters()
{ {
_draftFilter.Clear(); _draftFilter.Clear();
_searchInput.SetTextWithoutNotify(string.Empty);
RebuildFilterChips(); RebuildFilterChips();
} }
@ -951,6 +1070,18 @@ namespace Vanguard.UI
private void RebuildFilterChips() private void RebuildFilterChips()
{ {
ClearChildren(_rarityChips);
foreach (var rarity in new[] { "C", "R", "RR", "RRR", "SP" })
{
var value = rarity;
Chip(
_rarityChips,
value,
_draftFilter.Rarities.Contains(value),
() => ToggleRarity(value),
120f);
}
ClearChildren(_gradeChips); ClearChildren(_gradeChips);
for (var grade = 0; grade <= 3; grade++) for (var grade = 0; grade <= 3; grade++)
{ {
@ -1021,6 +1152,13 @@ namespace Vanguard.UI
RebuildFilterChips(); RebuildFilterChips();
} }
private void ToggleRarity(string rarity)
{
if (!_draftFilter.Rarities.Add(rarity))
_draftFilter.Rarities.Remove(rarity);
RebuildFilterChips();
}
private static Button Chip( private static Button Chip(
Transform parent, Transform parent,
string label, string label,
@ -1055,12 +1193,16 @@ namespace Vanguard.UI
} }
var parts = new List<string>(); var parts = new List<string>();
if (_filter.Rarities.Count > 0)
parts.Add("Редкость: " + string.Join(", ", _filter.Rarities.OrderBy(item => item, StringComparer.Ordinal)));
if (_filter.Clans.Count > 0) if (_filter.Clans.Count > 0)
parts.Add("Клан: " + string.Join(", ", _filter.Clans.OrderBy(item => item, StringComparer.Ordinal))); parts.Add("Клан: " + string.Join(", ", _filter.Clans.OrderBy(item => item, StringComparer.Ordinal)));
if (_filter.Grades.Count > 0) if (_filter.Grades.Count > 0)
parts.Add("Ранг: " + string.Join(", ", _filter.Grades.OrderBy(item => item))); parts.Add("Ранг: " + string.Join(", ", _filter.Grades.OrderBy(item => item)));
if (_filter.Triggers.Count > 0) if (_filter.Triggers.Count > 0)
parts.Add("Триггер: " + string.Join(", ", _filter.Triggers.OrderBy(item => item, StringComparer.Ordinal))); parts.Add("Триггер: " + string.Join(", ", _filter.Triggers.OrderBy(item => item, StringComparer.Ordinal)));
if (!string.IsNullOrWhiteSpace(_filter.Search))
parts.Add("Поиск: " + _filter.Search.Trim());
_filterSummary.text = string.Join(" · ", parts); _filterSummary.text = string.Join(" · ", parts);
} }

View File

@ -14,8 +14,11 @@ namespace Vanguard.UI
public HashSet<string> Clans { get; } = new HashSet<string>(StringComparer.Ordinal); public HashSet<string> Clans { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<int> Grades { get; } = new HashSet<int>(); public HashSet<int> Grades { get; } = new HashSet<int>();
public HashSet<string> Triggers { get; } = new HashSet<string>(StringComparer.Ordinal); public HashSet<string> Triggers { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Rarities { get; } = new HashSet<string>(StringComparer.Ordinal);
public string Search { get; set; } = string.Empty;
public bool IsEmpty => Clans.Count == 0 && Grades.Count == 0 && Triggers.Count == 0; public bool IsEmpty => Clans.Count == 0 && Grades.Count == 0 && Triggers.Count == 0
&& Rarities.Count == 0 && string.IsNullOrWhiteSpace(Search);
public bool Matches(CardCatalogItemDto meta) public bool Matches(CardCatalogItemDto meta)
{ {
@ -31,6 +34,16 @@ namespace Vanguard.UI
if (Triggers.Count > 0 && !Triggers.Contains(meta.Trigger ?? "None")) if (Triggers.Count > 0 && !Triggers.Contains(meta.Trigger ?? "None"))
return false; return false;
if (Rarities.Count > 0 && !Rarities.Contains(meta.Rarity))
return false;
if (!string.IsNullOrWhiteSpace(Search))
{
var name = Loc.CardName(meta) ?? string.Empty;
if (name.IndexOf(Search.Trim(), StringComparison.OrdinalIgnoreCase) < 0)
return false;
}
return true; return true;
} }
@ -39,6 +52,8 @@ namespace Vanguard.UI
Clans.Clear(); Clans.Clear();
Grades.Clear(); Grades.Clear();
Triggers.Clear(); Triggers.Clear();
Rarities.Clear();
Search = string.Empty;
} }
public void CopyFrom(DeckFilter other) public void CopyFrom(DeckFilter other)
@ -50,6 +65,8 @@ namespace Vanguard.UI
Clans.UnionWith(other.Clans); Clans.UnionWith(other.Clans);
Grades.UnionWith(other.Grades); Grades.UnionWith(other.Grades);
Triggers.UnionWith(other.Triggers); Triggers.UnionWith(other.Triggers);
Rarities.UnionWith(other.Rarities);
Search = other.Search ?? string.Empty;
} }
public DeckFilter Clone() public DeckFilter Clone()

View File

@ -16,16 +16,19 @@ namespace Vanguard.UI
private readonly RectTransform _panel; private readonly RectTransform _panel;
private readonly Action _onLogout; private readonly Action _onLogout;
private readonly Action _onHistory; private readonly Action _onHistory;
private readonly Action _onLobby;
public HamburgerMenu( public HamburgerMenu(
Transform parent, Transform parent,
AppConfig config, AppConfig config,
Action onLogout = null, Action onLogout = null,
Action onHistory = null, Action onHistory = null,
Action onLobby = null,
string title = "Меню") string title = "Меню")
{ {
_onLogout = onLogout; _onLogout = onLogout;
_onHistory = onHistory; _onHistory = onHistory;
_onLobby = onLobby;
_blocker = new GameObject( _blocker = new GameObject(
"MenuBlocker", "MenuBlocker",
@ -91,7 +94,8 @@ namespace Vanguard.UI
var height = 300f var height = 300f
+ (onLogout != null ? 56f : 0f) + (onLogout != null ? 56f : 0f)
+ (onHistory != null ? 46f : 0f); + (onHistory != null ? 46f : 0f)
+ (onLobby != null ? 46f : 0f);
var panelGo = new GameObject("MenuPanel", typeof(RectTransform), typeof(Image)); var panelGo = new GameObject("MenuPanel", typeof(RectTransform), typeof(Image));
_panel = (RectTransform)panelGo.transform; _panel = (RectTransform)panelGo.transform;
_panel.SetParent(parent, false); _panel.SetParent(parent, false);
@ -206,6 +210,24 @@ namespace Vanguard.UI
server.text = config.ServerBaseUrl; server.text = config.ServerBaseUrl;
}); });
if (_onLobby != null)
{
var lobby = UiKit.CreateButton(
_panel,
"Lobby",
"В лобби",
() =>
{
Close();
_onLobby();
},
15);
var lobbyImage = lobby.GetComponent<Image>();
if (lobbyImage != null)
lobbyImage.color = new Color(0.06f, 0.12f, 0.24f, 0.95f);
UiKit.SetPreferredHeight(lobby, 36f);
}
if (_onHistory != null) if (_onHistory != null)
{ {
var history = UiKit.CreateButton( var history = UiKit.CreateButton(

View File

@ -18,7 +18,6 @@ namespace Vanguard.UI
public const string RootName = "LobbyScreen"; public const string RootName = "LobbyScreen";
private const float Margin = 24f; private const float Margin = 24f;
private const float TopHeight = 96f;
private const float LeftWidth = 560f; private const float LeftWidth = 560f;
private const float RightWidth = 336f; private const float RightWidth = 336f;
private const float Gutter = 16f; private const float Gutter = 16f;
@ -39,13 +38,10 @@ namespace Vanguard.UI
private readonly PlayerState _player; private readonly PlayerState _player;
private readonly Action _onLoggedOut; private readonly Action _onLoggedOut;
private readonly Action _onStore; private readonly Action _onStore;
private readonly HamburgerMenu _menu; private readonly PlayerTopBar _topBar;
private readonly StatusTicker _ticker; private readonly StatusTicker _ticker;
private readonly RectTransform _root; private readonly RectTransform _root;
private Text _avatar;
private Text _playerName;
private Text _mmr;
private Text _gold; private Text _gold;
private Text _dust; private Text _dust;
private Text _tokens; private Text _tokens;
@ -74,9 +70,14 @@ namespace Vanguard.UI
_root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0f)); _root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0f));
_root.GetComponent<Image>().raycastTarget = false; _root.GetComponent<Image>().raycastTarget = false;
var top = TopBar(out _avatar, out _playerName, out _mmr); _topBar = new PlayerTopBar(
AddTopAction(top, "Rating", "Рейтинг", -312f, 180f, () => onRating(), LobbyIcon.Chart); _root,
AddTopAction(top, "Community", "Сообщество", -96f, 200f, () => onCommunity(), LobbyIcon.Friends); config,
player,
onLoggedOut,
onHistory,
onRating,
onCommunity);
BuildActions(onBattle, onDecks, onStore); BuildActions(onBattle, onDecks, onStore);
BuildMissions(out _status); BuildMissions(out _status);
@ -84,7 +85,6 @@ namespace Vanguard.UI
BuildChat(); BuildChat();
BuildFriends(); BuildFriends();
_menu = new HamburgerMenu(_root, config, onLoggedOut, onHistory);
_ticker = _root.gameObject.AddComponent<StatusTicker>(); _ticker = _root.gameObject.AddComponent<StatusTicker>();
_ticker.Hide = () => SetStatus(string.Empty); _ticker.Hide = () => SetStatus(string.Empty);
@ -94,12 +94,12 @@ namespace Vanguard.UI
_root.gameObject.SetActive(false); _root.gameObject.SetActive(false);
} }
public bool IsMenuOpen => _menu != null && _menu.IsOpen; public bool IsMenuOpen => _topBar != null && _topBar.IsMenuOpen;
public void CloseMenu() public void CloseMenu()
{ {
if (_menu != null) if (_topBar != null)
_menu.Close(); _topBar.CloseMenu();
} }
public void Show() public void Show()
@ -119,129 +119,7 @@ namespace Vanguard.UI
_disposed = true; _disposed = true;
_player.Changed -= UpdateTexts; _player.Changed -= UpdateTexts;
Loc.LanguageChanged -= UpdateTexts; Loc.LanguageChanged -= UpdateTexts;
} _topBar?.Dispose();
// ------------------------------------------------------------ топбар
private RectTransform TopBar(out Text avatar, out Text playerName, out Text mmr)
{
var top = Surface(_root, "TopBar", new Color(0.012f, 0.03f, 0.08f, 0.94f), Border, 1, 0);
top.anchorMin = new Vector2(0f, 1f);
top.anchorMax = new Vector2(1f, 1f);
top.pivot = new Vector2(0.5f, 1f);
top.offsetMin = new Vector2(Margin, -TopHeight);
top.offsetMax = new Vector2(-Margin, 0f);
var avatarRect = Surface(
top,
"Avatar",
new Color(0.05f, 0.13f, 0.3f, 0.95f),
BorderStrong,
2,
14);
avatarRect.anchorMin = new Vector2(0f, 0.5f);
avatarRect.anchorMax = new Vector2(0f, 0.5f);
avatarRect.pivot = new Vector2(0f, 0.5f);
avatarRect.sizeDelta = new Vector2(64f, 64f);
avatarRect.anchoredPosition = new Vector2(20f, 0f);
avatar = UiKit.CreateText(avatarRect, "Initials", "?", 26, TextPrimary);
avatar.fontStyle = FontStyle.Bold;
UiKit.Stretch(avatar.rectTransform);
UiKit.MakeLiteral(avatar);
playerName = UiKit.CreateText(
top,
"PlayerName",
"…",
22,
TextPrimary,
TextAnchor.MiddleLeft);
playerName.fontStyle = FontStyle.Bold;
var nameRect = playerName.rectTransform;
nameRect.anchorMin = new Vector2(0f, 0.5f);
nameRect.anchorMax = new Vector2(0f, 0.5f);
nameRect.pivot = new Vector2(0f, 0.5f);
nameRect.sizeDelta = new Vector2(420f, 30f);
nameRect.anchoredPosition = new Vector2(104f, 14f);
UiKit.MakeLiteral(playerName);
mmr = UiKit.CreateText(
top,
"Mmr",
string.Empty,
14,
TextMuted,
TextAnchor.MiddleLeft);
var mmrRect = mmr.rectTransform;
mmrRect.anchorMin = new Vector2(0f, 0.5f);
mmrRect.anchorMax = new Vector2(0f, 0.5f);
mmrRect.pivot = new Vector2(0f, 0.5f);
mmrRect.sizeDelta = new Vector2(460f, 22f);
mmrRect.anchoredPosition = new Vector2(104f, -16f);
UiKit.MakeLiteral(mmr);
var season = UiKit.CreateText(
top,
"Season",
"Сезон — скоро",
14,
TextDim,
TextAnchor.MiddleCenter);
var seasonRect = season.rectTransform;
seasonRect.anchorMin = new Vector2(0.5f, 0.5f);
seasonRect.anchorMax = new Vector2(0.5f, 0.5f);
seasonRect.sizeDelta = new Vector2(280f, 30f);
seasonRect.anchoredPosition = Vector2.zero;
return top;
}
private void AddTopAction(
RectTransform parent,
string name,
string label,
float rightOffset,
float width,
Action onClick,
Action<RectTransform, Color> icon)
{
var buttonRect = Surface(
parent,
name,
new Color(0.035f, 0.09f, 0.21f, 0.85f),
Border,
1,
12);
buttonRect.anchorMin = new Vector2(1f, 0.5f);
buttonRect.anchorMax = new Vector2(1f, 0.5f);
buttonRect.pivot = new Vector2(1f, 0.5f);
buttonRect.sizeDelta = new Vector2(width, 52f);
buttonRect.anchoredPosition = new Vector2(rightOffset, 0f);
var image = buttonRect.GetComponent<Image>();
image.raycastTarget = true;
var button = buttonRect.gameObject.AddComponent<Button>();
button.targetGraphic = image;
button.transition = Selectable.Transition.ColorTint;
button.colors = HoverColors();
button.onClick.AddListener(() => onClick());
var iconRect = Icon(buttonRect, new Vector2(30f, 30f), new Vector2(-width * 0.5f + 30f, 0f), 0.6f);
icon(iconRect, Accent);
var text = UiKit.CreateText(
buttonRect,
"Label",
label,
17,
TextPrimary,
TextAnchor.MiddleLeft);
var textRect = text.rectTransform;
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(1f, 1f);
textRect.offsetMin = new Vector2(52f, 0f);
textRect.offsetMax = new Vector2(-14f, 0f);
} }
// ------------------------------------------------------------ левая колонка // ------------------------------------------------------------ левая колонка
@ -758,27 +636,11 @@ namespace Vanguard.UI
if (_disposed) if (_disposed)
return; return;
var name = string.IsNullOrEmpty(_player.UserName) ? "…" : _player.UserName;
_playerName.text = name;
_avatar.text = name.Length > 0 ? name.Substring(0, 1).ToUpperInvariant() : "?";
_mmr.text = "MMR " + _player.RankedMmr
+ " · " + Loc.Translate("матчей") + " " + _player.Matches
+ " · " + Loc.Translate("побед") + " " + WinsPercent();
_gold.text = _player.Gold.ToString(); _gold.text = _player.Gold.ToString();
_dust.text = _player.Dust.ToString(); _dust.text = _player.Dust.ToString();
_tokens.text = _player.Tokens.ToString(); _tokens.text = _player.Tokens.ToString();
} }
private string WinsPercent()
{
if (_player.Matches <= 0)
return "0%";
return Mathf.RoundToInt(_player.Wins * 100f / _player.Matches) + "%";
}
private void SetStatus(string text) private void SetStatus(string text)
{ {
if (_status == null) if (_status == null)

View File

@ -433,6 +433,13 @@ namespace Vanguard.UI
"Clans will appear after the collection loads.", "Clans will appear after the collection loads.",
["Нужно ровно 50 карт, 16 триггеров и не больше 4 копий."] = ["Нужно ровно 50 карт, 16 триггеров и не больше 4 копий."] =
"Exactly 50 cards, 16 triggers and no more than 4 copies are required.", "Exactly 50 cards, 16 triggers and no more than 4 copies are required.",
["В лобби"] = "To lobby",
["Новый набор"] = "New deck",
["Поиск по названию"] = "Search by name",
["Редкость"] = "Rarity",
["есть"] = "owned",
["в колоде"] = "in deck",
["Открыть"] = "Open",
}; };
private static readonly KeyValuePair<string, string>[] Phrases = Exact private static readonly KeyValuePair<string, string>[] Phrases = Exact

View File

@ -0,0 +1,274 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.App;
using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>
/// Верхняя панель игрока, общая для Lobby и DeckBuilder: аватар-инициалы,
/// ник, MMR, сезон, Rating/Community и гамбургер-меню.
/// </summary>
public sealed class PlayerTopBar : IDisposable
{
public const string RootName = "PlayerTopBar";
private const float Margin = 24f;
private const float Height = 96f;
private static readonly Color Border = new Color(0.18f, 0.49f, 1f, 0.55f);
private static readonly Color BorderStrong = new Color(0.34f, 0.72f, 1f, 0.9f);
private static readonly Color TextPrimary = new Color(0.88f, 0.93f, 1f);
private static readonly Color TextMuted = new Color(0.58f, 0.69f, 0.88f);
private static readonly Color TextDim = new Color(0.44f, 0.55f, 0.75f);
private static readonly Color Accent = new Color(0.37f, 0.69f, 1f);
private readonly PlayerState _player;
private readonly HamburgerMenu _menu;
private Text _avatar;
private Text _playerName;
private Text _mmr;
private bool _disposed;
public PlayerTopBar(
Transform parent,
AppConfig config,
PlayerState player,
Action onLoggedOut,
Action onHistory,
Action onRating,
Action onCommunity,
Action onLobby = null)
{
_player = player;
var top = Surface(parent, RootName, new Color(0.012f, 0.03f, 0.08f, 0.94f), 0);
top.anchorMin = new Vector2(0f, 1f);
top.anchorMax = new Vector2(1f, 1f);
top.pivot = new Vector2(0.5f, 1f);
top.offsetMin = new Vector2(Margin, -Height);
top.offsetMax = new Vector2(-Margin, 0f);
var avatarRect = Surface(
top,
"Avatar",
new Color(0.05f, 0.13f, 0.3f, 0.95f),
14);
avatarRect.anchorMin = new Vector2(0f, 0.5f);
avatarRect.anchorMax = new Vector2(0f, 0.5f);
avatarRect.pivot = new Vector2(0f, 0.5f);
avatarRect.sizeDelta = new Vector2(64f, 64f);
avatarRect.anchoredPosition = new Vector2(20f, 0f);
_avatar = UiKit.CreateText(avatarRect, "Initials", "?", 26, TextPrimary);
_avatar.fontStyle = FontStyle.Bold;
UiKit.Stretch(_avatar.rectTransform);
UiKit.MakeLiteral(_avatar);
_playerName = UiKit.CreateText(
top,
"PlayerName",
"…",
22,
TextPrimary,
TextAnchor.MiddleLeft);
_playerName.fontStyle = FontStyle.Bold;
var nameRect = _playerName.rectTransform;
nameRect.anchorMin = new Vector2(0f, 0.5f);
nameRect.anchorMax = new Vector2(0f, 0.5f);
nameRect.pivot = new Vector2(0f, 0.5f);
nameRect.sizeDelta = new Vector2(420f, 30f);
nameRect.anchoredPosition = new Vector2(104f, 14f);
UiKit.MakeLiteral(_playerName);
_mmr = UiKit.CreateText(
top,
"Mmr",
string.Empty,
14,
TextMuted,
TextAnchor.MiddleLeft);
var mmrRect = _mmr.rectTransform;
mmrRect.anchorMin = new Vector2(0f, 0.5f);
mmrRect.anchorMax = new Vector2(0f, 0.5f);
mmrRect.pivot = new Vector2(0f, 0.5f);
mmrRect.sizeDelta = new Vector2(460f, 22f);
mmrRect.anchoredPosition = new Vector2(104f, -16f);
UiKit.MakeLiteral(_mmr);
var season = UiKit.CreateText(
top,
"Season",
"Сезон — скоро",
14,
TextDim,
TextAnchor.MiddleCenter);
var seasonRect = season.rectTransform;
seasonRect.anchorMin = new Vector2(0.5f, 0.5f);
seasonRect.anchorMax = new Vector2(0.5f, 0.5f);
seasonRect.sizeDelta = new Vector2(280f, 30f);
seasonRect.anchoredPosition = Vector2.zero;
AddAction(top, "Rating", "Рейтинг", -312f, 180f, onRating, LobbyIcon.Chart);
AddAction(top, "Community", "Сообщество", -96f, 200f, onCommunity, LobbyIcon.Friends);
_menu = new HamburgerMenu(parent, config, onLoggedOut, onHistory, onLobby);
_player.Changed += UpdateTexts;
Loc.LanguageChanged += UpdateTexts;
UpdateTexts();
}
public bool IsMenuOpen => _menu != null && _menu.IsOpen;
public void CloseMenu()
{
if (_menu != null)
_menu.Close();
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= UpdateTexts;
Loc.LanguageChanged -= UpdateTexts;
}
private void UpdateTexts()
{
if (_disposed)
return;
var name = string.IsNullOrEmpty(_player.UserName) ? "…" : _player.UserName;
_playerName.text = name;
_avatar.text = Initial(name);
_mmr.text = "MMR " + _player.RankedMmr
+ " · " + Loc.Translate("матчей") + " " + _player.Matches
+ " · " + Loc.Translate("побед") + " " + WinsPercent();
}
private string WinsPercent()
{
if (_player.Matches <= 0)
return "0%";
return Mathf.RoundToInt(_player.Wins * 100f / _player.Matches) + "%";
}
private static string Initial(string name)
{
foreach (var character in name)
{
if (char.IsLetterOrDigit(character))
return char.ToUpperInvariant(character).ToString();
}
return "?";
}
private static void AddAction(
RectTransform parent,
string name,
string label,
float rightOffset,
float width,
Action onClick,
Action<RectTransform, Color> icon)
{
var buttonRect = Surface(parent, name, new Color(0.035f, 0.09f, 0.21f, 0.85f), 12);
buttonRect.anchorMin = new Vector2(1f, 0.5f);
buttonRect.anchorMax = new Vector2(1f, 0.5f);
buttonRect.pivot = new Vector2(1f, 0.5f);
buttonRect.sizeDelta = new Vector2(width, 52f);
buttonRect.anchoredPosition = new Vector2(rightOffset, 0f);
var image = buttonRect.GetComponent<Image>();
image.raycastTarget = true;
var button = buttonRect.gameObject.AddComponent<Button>();
button.targetGraphic = image;
button.transition = Selectable.Transition.ColorTint;
button.colors = HoverColors();
if (onClick != null)
button.onClick.AddListener(() => onClick());
else
button.interactable = false;
var iconColor = onClick != null
? Accent
: new Color(Accent.r, Accent.g, Accent.b, 0.35f);
var iconRect = Icon(buttonRect, new Vector2(30f, 30f), new Vector2(-width * 0.5f + 30f, 0f), 0.6f);
icon(iconRect, iconColor);
var text = UiKit.CreateText(
buttonRect,
"Label",
label,
17,
onClick != null ? TextPrimary : new Color(TextPrimary.r, TextPrimary.g, TextPrimary.b, 0.4f),
TextAnchor.MiddleLeft);
var textRect = text.rectTransform;
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(1f, 1f);
textRect.offsetMin = new Vector2(52f, 0f);
textRect.offsetMax = new Vector2(-14f, 0f);
}
private static RectTransform Surface(
Transform parent,
string name,
Color fill,
int radius)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
var rect = (RectTransform)go.transform;
rect.SetParent(parent, false);
var image = go.GetComponent<Image>();
image.sprite = NeonUi.BodySliced(
radius,
fill,
radius > 0 ? Border : new Color(Border.r, Border.g, Border.b, 0.35f),
1);
image.type = Image.Type.Sliced;
image.color = Color.white;
image.raycastTarget = false;
return rect;
}
private static RectTransform Icon(
Transform parent,
Vector2 size,
Vector2 position,
float scale)
{
var go = new GameObject("Icon", typeof(RectTransform));
var rect = (RectTransform)go.transform;
rect.SetParent(parent, false);
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = size;
rect.anchoredPosition = position;
rect.localScale = Vector3.one * scale;
return rect;
}
private static ColorBlock HoverColors()
{
return new ColorBlock
{
normalColor = Color.white,
highlightedColor = new Color(1.16f, 1.16f, 1.16f, 1f),
pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f),
selectedColor = Color.white,
disabledColor = new Color(0.55f, 0.55f, 0.55f, 0.6f),
colorMultiplier = 1f,
fadeDuration = 0.08f,
};
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 005df6bb93202ee4d904bd16eebbffa0

View File

@ -873,6 +873,43 @@ namespace Vanguard.Tests
Cleanup(); Cleanup();
} }
[Test]
public void PlayerTopBar_Disables_Unavailable_Actions()
{
Loc.SetLanguage(UiLanguage.Ru);
var config = new AppConfig();
config.Load();
var player = new PlayerState();
player.Apply(new MeResponse
{
UserName = "Nick",
RankedMmr = 1240,
Matches = 10,
Wins = 6,
});
var canvas = UiKit.CreateCanvas("Test Canvas");
var bar = new PlayerTopBar(
canvas.transform, config, player, null, null, null, null);
var root = GameObject.Find(PlayerTopBar.RootName);
Assert.IsNotNull(root, "PlayerTopBar не создан");
var name = root.transform.Find("PlayerName")?.GetComponent<Text>();
Assert.IsNotNull(name);
Assert.AreEqual("Nick", name.text);
var rating = root.transform.Find("Rating")?.GetComponent<Button>();
var community = root.transform.Find("Community")?.GetComponent<Button>();
Assert.IsNotNull(rating);
Assert.IsNotNull(community);
Assert.IsFalse(rating.interactable, "Rating без обработчика должен быть неактивен");
Assert.IsFalse(community.interactable, "Community без обработчика должен быть неактивен");
bar.Dispose();
Cleanup();
}
[Test] [Test]
public void Community_Tabs_Switch_Content() public void Community_Tabs_Switch_Content()
{ {

View File

@ -59,6 +59,56 @@ namespace Vanguard.Tests
Assert.IsFalse(filter.IsEmpty); Assert.IsFalse(filter.IsEmpty);
} }
[Test]
public void Rarity_And_Search_Filter_Are_Applied()
{
Loc.SetLanguage(UiLanguage.Ru);
var filter = new DeckFilter();
filter.Rarities.Add("RR");
filter.Search = "alpha";
var match = new CardCatalogItemDto
{
PrintingId = "p1",
NameId = "alpha",
NameRu = "Alpha Knight",
NameEn = "Alpha Knight",
Clan = "Kagero",
Rarity = "RR",
Grade = 2,
Trigger = "None",
};
var wrongRarity = new CardCatalogItemDto
{
PrintingId = "p2",
NameId = "alpha2",
NameRu = "Alpha Guard",
NameEn = "Alpha Guard",
Clan = "Kagero",
Rarity = "R",
Grade = 1,
Trigger = "None",
};
var wrongName = new CardCatalogItemDto
{
PrintingId = "p3",
NameId = "beta",
NameRu = "Beta Knight",
NameEn = "Beta Knight",
Clan = "Kagero",
Rarity = "RR",
Grade = 2,
Trigger = "None",
};
Assert.IsTrue(filter.Matches(match));
Assert.IsFalse(filter.Matches(wrongRarity));
Assert.IsFalse(filter.Matches(wrongName));
filter.Search = "ALPHA";
Assert.IsTrue(filter.Matches(match), "Поиск не зависит от регистра");
}
[Test] [Test]
public void GroupDeck_Sums_Printings_And_Sorts_By_Grade_Then_Name() public void GroupDeck_Sums_Printings_And_Sorts_By_Grade_Then_Name()
{ {

View File

@ -44,7 +44,7 @@
| TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь | | TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь |
| TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь | | TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь |
| TASK-2026-023 | `[!]` | DeckBuilder: один экран, мультифильтры и список колоды | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 67/67; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная — пользователь | | TASK-2026-023 | `[!]` | DeckBuilder: один экран, мультифильтры и список колоды | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 67/67; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная — пользователь |
| TASK-2026-024 | `[ ]` | DeckBuilder по референсу: топбар Lobby, крупные карты, фильтры | DeepSeek: анализ и план готовы; реализация ожидает | не выполнена | | TASK-2026-024 | `[!]` | DeckBuilder по референсу: топбар Lobby, крупные карты, фильтры | DeepSeek: C1–C10 реализованы; ждёт ручной приёмки | EditMode 69/69; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная — пользователь |
Новые задачи добавляются в конец файла отдельными разделами. Не используй один Новые задачи добавляются в конец файла отдельными разделами. Не используй один
раздел для нескольких независимых задач. раздел для нескольких независимых задач.
@ -2574,11 +2574,11 @@ HUD с номером хода и фазовой шкалой, компактн
### TASK-2026-024 — DeckBuilder по референсу: топбар Lobby, крупные карты, фильтры ### TASK-2026-024 — DeckBuilder по референсу: топбар Lobby, крупные карты, фильтры
- **Статус:** `[ ]` - **Статус:** `[!]`
- **Создано:** 2026-11-01 - **Создано:** 2026-11-01
- **Анализ:** DeepSeek выполнен (референс DeckBuilder от 2026-11-01) - **Анализ:** DeepSeek выполнен (референс DeckBuilder от 2026-11-01)
- **Реализация:** не начата - **Реализация:** DeepSeek: C1–C10 выполнены
- **Проверка:** не выполнена - **Проверка:** EditMode 69/69; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная приёмка — за пользователем
- **Затронутые системы:** `LobbyView`, новый `PlayerTopBar`, - **Затронутые системы:** `LobbyView`, новый `PlayerTopBar`,
`HamburgerMenu`, `DeckBuilderView`, `DeckBuilderSceneController`, `HamburgerMenu`, `DeckBuilderView`, `DeckBuilderSceneController`,
`CardImageService`, `DeckListFilter`, `Localization`, EditMode-тесты. `CardImageService`, `DeckListFilter`, `Localization`, EditMode-тесты.
@ -2654,87 +2654,75 @@ HUD с номером хода и фазовой шкалой, компактн
#### C. Чек-лист изменений #### C. Чек-лист изменений
- [ ] **C1. `CREATE` `Runtime/UI/PlayerTopBar.cs`; `MODIFY` - [x] **C1. `CREATE` `Runtime/UI/PlayerTopBar.cs`; `MODIFY`
`LobbyView.cs`.** `LobbyView.cs`.**
- Вынести из `LobbyView` топбар: аватар, ник, MMR, сезон, две кнопки, - Топбар вынесен: аватар, ник, MMR, сезон, две кнопки, гамбургер;
гамбургер; подписка на `PlayerState`; меню `HamburgerMenu`. подписка на `PlayerState`; `HamburgerMenu` с «В лобби».
- Конструктор принимает `onRating`, `onCommunity`, `onHistory`, - `null`-действие отключает кнопку; `LobbyView` использует компонент.
`onLobby`, `onLogout`; `null` для действия = кнопка disabled. - Реализация: выполнена. Проверка: EditMode 69/69, `SCENE_SMOKE`.
- `LobbyView` использует компонент без визуальных изменений.
- `HamburgerMenu`: пункт «В лобби» при `onLobby != null`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: нет. - Зависит: нет.
- [ ] **C2. `MODIFY` `Runtime/App/DeckBuilderSceneController.cs`.** - [x] **C2. `MODIFY` `Runtime/App/DeckBuilderSceneController.cs`.**
- Резолвить `PlayerState`/`AppConfig`; создать `PlayerTopBar` с - Резолвит `PlayerState`/`AppConfig`; создаёт `PlayerTopBar` с
`onRating: null`, `onCommunity: null`, «В лобби» → Lobby. неактивными Rating/Community и «В лобби» → Lobby; освобождает его.
- Реализация: не выполнена. Проверка: не выполнена. - Реализация: выполнена. Проверка: EditMode 69/69.
- Зависит: C1. - Зависит: C1.
- [ ] **C3. `MODIFY` `DeckBuilderView` — каркас.** - [x] **C3. `MODIFY` `DeckBuilderView` — каркас.**
- Убрать собственный топбар; тело начинается под `PlayerTopBar`. - Собственный топбар убран; тело под `PlayerTopBar` (верх -120).
- Сохранить панели `DeckListPanel`/`DeckPanel`, статус под коллекцией. - Панели `DeckListPanel`/`DeckPanel`, статус под коллекцией, отдельная
- Реализация: не выполнена. Проверка: не выполнена. строка фильтров со сводкой.
- Реализация: выполнена. Проверка: EditMode 69/69.
- Зависит: C1, C2. - Зависит: C1, C2.
- [ ] **C4. `MODIFY` сетка коллекции.** - [x] **C4. `MODIFY` сетка коллекции.**
- `GridLayoutGroup` 6 колонок, ячейка ≈216×286, вертикальный скролл без - 6 колонок (`FixedColumnCount`), ячейка 216×242, вертикальный скролл.
пагинации. - Ячейка: иллюстрация 216×182, номер грейда в углу, плашка имени поверх
- Ячейка: иллюстрация `ApplyIllustration` (216×182), номер грейда в низа, строка количества, камень редкости.
углу, плашка имени поверх низа иллюстрации, строка количества, - ЛКМ в просмотре не добавляет; ПКМ — карточка; в режиме колоды ЛКМ
камень редкости. добавляет.
- ЛКМ в просмотре ничего не делает; ПКМ — карточка; в режиме колоды
ЛКМ добавляет, ПКМ — карточка.
- Количество: «есть ×N» / «в колоде ×N · есть ×M». - Количество: «есть ×N» / «в колоде ×N · есть ×M».
- Реализация: не выполнена. Проверка: не выполнена. - Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
- Зависит: C3, C5. - Зависит: C3, C5.
- [ ] **C5. `MODIFY` режимы просмотра и колоды.** - [x] **C5. `MODIFY` режимы просмотра и колоды.**
- `_editing == null` — просмотр: панель колод, добавление выключено; - Режим определяется `_deckOpen`: просмотр — панель колод, добавление
нажатие на колоду открывает состав и включает добавление. выключено; открытие/создание колоды включает добавление.
- «К списку колод» возвращает просмотр (несохранённые изменения - «К списку колод» сбрасывает `_editing`, состав и возвращает просмотр
отбрасываются; предупреждение — отдельный пункт при необходимости). (несохранённые изменения отбрасываются).
- Реализация: не выполнена. Проверка: не выполнена. - Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
- Зависит: C3. - Зависит: C3.
- [ ] **C6. `MODIFY` список колод по референсу.** - [x] **C6. `MODIFY` список колод по референсу.**
- Кнопка `+ Новый набор` (полная ширина). - `+ Новый набор` на всю ширину.
- Строка: эмблема клана (процедурная, цвет клана), имя, `N/50 · - Строка: эмблема клана (процедурная), имя, `N/50 · T N/16` (цвет —
T N/16`, бейдж валидности точкой/цветом; кебаб-меню «Открыть / валидность), кебаб с меню «Открыть / Удалить / Отмена»; открытая
Удалить». колода подсвечена.
- Подсветка открытой колоды. - Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C3. - Зависит: C3.
- [ ] **C7. `MODIFY` фильтры.** - [x] **C7. `MODIFY` фильтры.**
- `DeckFilter`: добавить `Rarities` (C/R/RR/RRR/SP) и `Search`. - `DeckFilter`: `Rarities` (C/R/RR/RRR/SP) и `Search`.
- Оверлей: поиск, чипы редкости, клан/грейд/триггер; «Сбросить», - Оверлей: поиск, редкость, клан, грейд, триггер; «Сбросить», «Отмена»,
«Отмена», «Применить»; сводка включает редкость и поиск. «Применить»; сводка включает редкость и поиск.
- `DeckListModel`/`DeckFilter` остаются чистыми и тестируемыми. - Реализация: выполнена. Проверка: `Rarity_And_Search_Filter_Are_Applied`.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C3. - Зависит: C3.
- [ ] **C8. `MODIFY` `Localization.cs`** — «В лобби», «Новый набор», - [x] **C8. `MODIFY` `Localization.cs`** — «В лобби», «Новый набор»,
«Поиск по названию», «Редкость», «есть», «в колоде», «Открыть», «Поиск по названию», «Редкость», «есть», «в колоде», «Открыть».
«Ещё», названия редкостей. - Реализация: выполнена. Проверка: дубликатов ключей нет.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C4–C7. - Зависит: C4–C7.
- [ ] **C9. `TEST` EditMode.** - [x] **C9. `TEST` EditMode.**
- `DeckFilter`: редкость и поиск (регистр, локализованное имя). - `DeckBuilderTests`: редкость и поиск; `ClientSmokeTests`:
- `PlayerTopBar`: создаётся, ник/MMR обновляются, disabled-кнопки при `PlayerTopBar` с неактивными кнопками, `DeckListPanel`/`DeckPanel`/
`null`-действиях. `FilterOverlay`.
- UI: `DeckBuilderScreen`, панели, отсутствие добавления в просмотре - Реализация: выполнена. Проверка: EditMode **69/69**.
(seam), переключение в режим колоды.
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C1–C8. - Зависит: C1–C8.
- [ ] **C10. `VERIFY` EditMode, smoke, standalone.** - [x] **C10. `VERIFY` EditMode, smoke, standalone.**
- Копия без `Library`; EditMode; `SCENE_SMOKE`; `PLAYMAT_SMOKE`; - Реализация: выполнена. Проверка: EditMode 69/69, `SCENE_SMOKE
standalone. success=True`, `PLAYMAT_SMOKE success=True`; ручная — пользователь.
- Ручная проверка: топбар и меню, крупные карты, скролл, фильтры и
поиск, режимы, добавление/удаление, сохранение, «В бой»/«С ботом».
- Реализация: не выполнена. Проверка: не выполнена.
- Зависит: C9. - Зависит: C9.
#### D. Последовательность выполнения #### D. Последовательность выполнения
@ -2786,11 +2774,24 @@ HUD с номером хода и фазовой шкалой, компактн
#### G. Результат #### G. Результат
- **Реализовано:** — - **Реализовано:** C1–C10. Общий `PlayerTopBar` для Lobby и DeckBuilder
- **Проверки:** — (Rating/Community неактивны, «В лобби» в меню). Крупная сетка
- **Не завершено/заблокировано:** — коллекции 6×N со скроллом: грейд, иллюстрация, имя поверх, количество,
- **Skill обновлён:** — камень редкости. Просмотр без добавления (только ПКМ-карточка), режим
- **Git:** — колоды включает добавление. Список колод с `+ Новый набор`, эмблемами,
счётчиками, кебаб-меню. Фильтры в оверлее: клан, грейд, триггер,
редкость, поиск.
- **Проверки:** EditMode **69/69** в копии без `Library`;
`SCENE_SMOKE success=True`; `PLAYMAT_SMOKE success=True`.
- **Не завершено:** ручная проверка топбара/меню, крупной сетки, скролла,
фильтров и поиска, режимов, добавления/удаления, кебаб-меню,
сохранения и боя.
- **Замечания:** в просмотре карта открывается только по ПКМ — нужна
подсказка в статусе. Несохранённые изменения при «К списку колод»
отбрасываются. Ячейка 216×242 (план допускал 216×286 — уменьшено под
пропорцию иллюстрации).
- **Skill обновлён:** не требуется.
- **Git:** не коммитилось.

View File

@ -6,6 +6,26 @@
--- ---
## 2026-11-01 — TASK-2026-024: DeckBuilder по референсу
- Топбар Lobby вынесен в общий `PlayerTopBar`; используется в Lobby и
DeckBuilder. В DeckBuilder Rating/Community неактивны, «В лобби» — в
меню.
- Коллекция — крупная сетка 6 в ряд со скроллом: номер грейда,
иллюстрация, имя поверх, количество, камень редкости. Ячейка 216×242.
- Режим просмотра: ЛКМ не добавляет, ПКМ открывает карточку (крафт/
разбор). Режим колоды: справа состав, ЛКМ добавляет, строка состава
убирает; «К списку колод» возвращает просмотр.
- Список колод: `+ Новый набор`, эмблема клана, имя, `N/50 · T N/16`
(цвет — валидность), кебаб-меню «Открыть / Удалить».
- Фильтры в оверлее: поиск по названию, редкость C/R/RR/RRR/SP, клан,
грейд, триггер; черновик + «Применить».
- Проверки: EditMode **69/69**, `SCENE_SMOKE success=True`,
`PLAYMAT_SMOKE success=True`, standalone.
- Ручная проверка — за пользователем.
---
## 2026-10-31 — TASK-2026-023: переделка DeckBuilder ## 2026-10-31 — TASK-2026-023: переделка DeckBuilder
- Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄ - Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄

View File

@ -61,17 +61,18 @@
- Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`, - Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`,
`PlayerState`, `GameConnection`. `PlayerState`, `GameConnection`.
- Работает: логин/регистрация/refresh, онбординг, коллекция с разбором, - Работает: логин/регистрация/refresh, онбординг, коллекция с разбором,
декбилдер (слева карточная сетка коллекции с мультифильтрами, справа декбилдер (топбар Lobby, крупная сетка карт 6 в ряд со скроллом,
панель «Мои колоды» ⇄ «Колода»; список карт колоды — грейд→имя, мультифильтры в оверлее, справа список колод ⇄ состав), магазин
превью-иллюстрации), магазин (бустер/триал-дека), история матчей и (бустер/триал-дека), история матчей и реплей, очередь,
реплей, очередь, WebSocket-матч (snapshot, интенты, маллиган, сдача, WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`),
`GameFinished`), таймер, результат. таймер, результат.
- Декбилдер: `DeckBuilderView` (один экран), `DeckListFilter` - Декбилдер: `DeckBuilderView` + общий `PlayerTopBar`, `DeckListFilter`
(`DeckFilter`/`DeckListModel`); каталог метаданных — `GET /api/cards` (`DeckFilter`/`DeckListModel`); каталог метаданных — `GET /api/cards`
(grade, trigger, clan, rarity, printing, power/shield/type, текст (grade, trigger, clan, rarity, printing, power/shield/type, текст
способности), клиент объединяет с коллекцией. способности), клиент объединяет с коллекцией.
Превью карт — иллюстрация (верхние 59% арта) без искажений; полный арт Превью карт — иллюстрация (верхние 59% арта) без искажений; полный арт
остаётся в карточке и 3D-поле. остаётся в карточке и 3D-поле. В просмотре карта открывается по ПКМ,
добавление — только когда открыта колода.
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и - ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
крафтом; SP/TD не разбираются и не крафтятся. крафтом; SP/TD не разбираются и не крафтятся.
- Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и - Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и
@ -285,7 +286,7 @@ Standalone Windows:
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
``` ```
Клиентские проверки: **67/67** EditMode (включая новые тесты TASK-2026-001: Клиентские проверки: **69/69** EditMode (включая новые тесты TASK-2026-001:
повторный render скрытых карт, открытие урона, source-позиция нового юнита, повторный render скрытых карт, открытие урона, source-позиция нового юнита,
coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод
строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила
@ -376,11 +377,11 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
**TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной **TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной
приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone
собраны и запущены. **TASK-2026-023 — DeckBuilder (один экран, собраны и запущены. **TASK-2026-023 — DeckBuilder (один экран,
мультифильтры, список колоды): реализован C1–C11, ждёт ручной мультифильтры, список колоды): реализован C1–C11.** EditMode 67/67.
приёмки.** EditMode 67/67, `SCENE_SMOKE`, `PLAYMAT_SMOKE`; standalone.
**TASK-2026-024 — DeckBuilder по референсу (топбар Lobby, крупные **TASK-2026-024 — DeckBuilder по референсу (топбар Lobby, крупные
карты, фильтры в оверлее): план готов**, реализация — следующим карты, фильтры в оверлее): реализован C1–C10, ждёт ручной приёмки.**
проходом. После приёмки — **TASK-2026-019** (загрузочная сцена, fade, EditMode 69/69, `SCENE_SMOKE`, `PLAYMAT_SMOKE`; standalone.
После приёмки — **TASK-2026-019** (загрузочная сцена, fade,
кадр без покадровых аллокаций). кадр без покадровых аллокаций).
2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride, 2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride,
move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого