client: DeckBuilder по референсу — топбар Lobby, крупные карты
CI / test (push) Successful in 3m48s
Details
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:
parent
f22a24e5ad
commit
f9aed0cae8
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 005df6bb93202ee4d904bd16eebbffa0
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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:** не коммитилось.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
- Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄
|
- Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄
|
||||||
|
|
|
||||||
|
|
@ -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, открытие скрытого
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue