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.Auth;
|
||||
using Vanguard.Meta;
|
||||
using Vanguard.State;
|
||||
using Vanguard.UI;
|
||||
|
||||
namespace Vanguard.App
|
||||
|
|
@ -9,6 +10,7 @@ namespace Vanguard.App
|
|||
public sealed class DeckBuilderSceneController : SceneControllerBase
|
||||
{
|
||||
private DeckBuilderView _view;
|
||||
private PlayerTopBar _topBar;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
|
|
@ -21,12 +23,24 @@ namespace Vanguard.App
|
|||
() => _ = Flow.LoadLobbyAsync(),
|
||||
deck => _ = Flow.OpenLobbyWithQueueAsync(deck),
|
||||
info => _ = Flow.LoadBattleAsync(info));
|
||||
|
||||
_topBar = new PlayerTopBar(
|
||||
canvas.transform,
|
||||
Resolve<AppConfig>(),
|
||||
Resolve<PlayerState>(),
|
||||
() => _ = Flow.LoadLoginAsync(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
() => _ = Flow.LoadLobbyAsync());
|
||||
|
||||
_view.Show();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_view?.Hide();
|
||||
_topBar?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ namespace Vanguard.UI
|
|||
|
||||
private readonly MetaService _meta;
|
||||
private readonly AuthService _auth;
|
||||
private readonly Action _onBack;
|
||||
private readonly Action<DeckDto> _onPlay;
|
||||
private readonly Action<MatchConnectionInfo> _onPracticeStarted;
|
||||
|
||||
|
|
@ -44,7 +43,6 @@ namespace Vanguard.UI
|
|||
private RectTransform _deckListContent;
|
||||
private RectTransform _deckCardContent;
|
||||
private InputField _nameInput;
|
||||
private Text _title;
|
||||
private Text _status;
|
||||
private Text _filterSummary;
|
||||
private Text _stats;
|
||||
|
|
@ -53,12 +51,17 @@ namespace Vanguard.UI
|
|||
private Button _botButton;
|
||||
private Button _duelButton;
|
||||
private Button _deleteButton;
|
||||
private RectTransform _deckMenuOverlay;
|
||||
private Text _deckMenuTitle;
|
||||
private DeckDto _deckMenuDeck;
|
||||
|
||||
private RectTransform _filterOverlay;
|
||||
private RectTransform _rarityChips;
|
||||
private RectTransform _gradeChips;
|
||||
private RectTransform _triggerChips;
|
||||
private RectTransform _clanChips;
|
||||
private RectTransform _confirmOverlay;
|
||||
private InputField _searchInput;
|
||||
private Text _confirmText;
|
||||
private DeckDto _pendingDelete;
|
||||
|
||||
|
|
@ -90,6 +93,7 @@ namespace Vanguard.UI
|
|||
private DeckDto _editing;
|
||||
private int _dust;
|
||||
private bool _dirty;
|
||||
private bool _deckOpen;
|
||||
private bool _loading;
|
||||
private bool _busy;
|
||||
|
||||
|
|
@ -103,19 +107,19 @@ namespace Vanguard.UI
|
|||
{
|
||||
_meta = meta;
|
||||
_auth = auth;
|
||||
_onBack = onBack;
|
||||
_onPlay = onPlay;
|
||||
_onPracticeStarted = onPracticeStarted;
|
||||
|
||||
_root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f));
|
||||
BuildTopBar();
|
||||
BuildBody();
|
||||
_filterOverlay = BuildFilterOverlay();
|
||||
_confirmOverlay = BuildDeleteConfirm();
|
||||
_deckMenuOverlay = BuildDeckMenu();
|
||||
_cardOverlay = BuildCardOverlay();
|
||||
|
||||
_filterOverlay.gameObject.SetActive(false);
|
||||
_confirmOverlay.gameObject.SetActive(false);
|
||||
_deckMenuOverlay.gameObject.SetActive(false);
|
||||
_cardOverlay.gameObject.SetActive(false);
|
||||
_root.gameObject.SetActive(false);
|
||||
}
|
||||
|
|
@ -131,49 +135,20 @@ namespace Vanguard.UI
|
|||
{
|
||||
_filterOverlay.gameObject.SetActive(false);
|
||||
_confirmOverlay.gameObject.SetActive(false);
|
||||
_deckMenuOverlay.gameObject.SetActive(false);
|
||||
_cardOverlay.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()
|
||||
{
|
||||
var body = UiKit.CreatePanel(_root, "Body", new Color(0f, 0f, 0f, 0f));
|
||||
body.anchorMin = Vector2.zero;
|
||||
body.anchorMax = Vector2.one;
|
||||
body.offsetMin = new Vector2(34f, 28f);
|
||||
body.offsetMax = new Vector2(-34f, -108f);
|
||||
body.offsetMin = new Vector2(34f, 24f);
|
||||
body.offsetMax = new Vector2(-34f, -120f);
|
||||
UiKit.AddHorizontalLayout(body, 0, 14);
|
||||
|
||||
var collectionArea = UiKit.CreatePanel(
|
||||
|
|
@ -184,16 +159,39 @@ namespace Vanguard.UI
|
|||
collectionElement.minHeight = 300f;
|
||||
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(
|
||||
collectionArea,
|
||||
"CollectionGrid",
|
||||
new Vector2(174f, 238f),
|
||||
new Vector2(10f, 10f),
|
||||
new Vector2(216f, 242f),
|
||||
new Vector2(14f, 14f),
|
||||
out _collectionContent);
|
||||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||||
scrollElement.flexibleHeight = 1f;
|
||||
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(
|
||||
collectionArea, "Status", string.Empty, 15,
|
||||
TextMuted, TextAnchor.MiddleLeft);
|
||||
|
|
@ -221,17 +219,9 @@ namespace Vanguard.UI
|
|||
UiKit.Stretch(panel);
|
||||
UiKit.AddVerticalLayout(panel, 10, 8);
|
||||
|
||||
var header = UiKit.CreatePanel(panel, "Header", new Color(0f, 0f, 0f, 0f));
|
||||
UiKit.SetPreferredHeight(header, 38f);
|
||||
UiKit.AddHorizontalLayout(header, 0, 8);
|
||||
|
||||
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 create = UiKit.CreateButton(panel, "Create", "+ Новый набор", NewDeck, 17);
|
||||
UiKit.SetPreferredHeight(create, 52f);
|
||||
SetButtonColor(create, new Color(0.10f, 0.28f, 0.58f, 0.95f));
|
||||
|
||||
var scroll = UiKit.CreateScrollView(panel, "DeckListScroll", out _deckListContent, 8f);
|
||||
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 card = UiKit.CreateCenteredCard(
|
||||
overlay, "FilterCard", new Vector2(900f, 860f), PanelFill);
|
||||
overlay, "FilterCard", new Vector2(900f, 940f), PanelFill);
|
||||
UiKit.AddVerticalLayout(card, 24, 10);
|
||||
|
||||
var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary);
|
||||
title.fontStyle = FontStyle.Bold;
|
||||
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);
|
||||
_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));
|
||||
UiKit.SetPreferredHeight(footer, 48f);
|
||||
|
|
@ -376,6 +371,33 @@ namespace Vanguard.UI
|
|||
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()
|
||||
{
|
||||
var overlay = UiKit.CreatePanel(
|
||||
|
|
@ -575,11 +597,35 @@ namespace Vanguard.UI
|
|||
row.GetComponent<Image>().color = isOpen
|
||||
? new Color(0.10f, 0.18f, 0.32f, 1f)
|
||||
: 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));
|
||||
UiKit.Stretch(content);
|
||||
content.offsetMin = new Vector2(10f, 8f);
|
||||
content.offsetMax = new Vector2(-96f, -8f);
|
||||
var contentElement = content.gameObject.AddComponent<LayoutElement>();
|
||||
contentElement.flexibleWidth = 1f;
|
||||
contentElement.preferredHeight = 64f;
|
||||
UiKit.AddVerticalLayout(content, 0, 2);
|
||||
|
||||
var name = UiKit.CreateText(
|
||||
|
|
@ -593,54 +639,76 @@ namespace Vanguard.UI
|
|||
var info = UiKit.CreateText(
|
||||
content,
|
||||
"Info",
|
||||
deck.Cards.Count + "/50 · T " + validation.Triggers + "/16 · "
|
||||
+ FormatName(deck.Format),
|
||||
deck.Cards.Count + "/50 · T " + validation.Triggers + "/16",
|
||||
12,
|
||||
TextMuted,
|
||||
validation.IsValid
|
||||
? new Color(0.55f, 0.9f, 0.62f)
|
||||
: new Color(1f, 0.66f, 0.5f),
|
||||
TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(info);
|
||||
UiKit.SetPreferredHeight(info, 18f);
|
||||
|
||||
var grades = UiKit.CreateText(
|
||||
content, "Grades", GradeLine(deck.Cards), 11, TextDim, TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(grades);
|
||||
UiKit.SetPreferredHeight(grades, 16f);
|
||||
|
||||
var badge = UiKit.CreatePanel(
|
||||
row,
|
||||
"Validity",
|
||||
validation.IsValid
|
||||
? new Color(0.12f, 0.34f, 0.22f, 0.95f)
|
||||
: new Color(0.34f, 0.12f, 0.14f, 0.95f));
|
||||
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);
|
||||
var kebab = UiKit.CreateButton(row, "Menu", string.Empty, () => OpenDeckMenu(deck), 14);
|
||||
SetFixedSize(kebab, 34f, 34f);
|
||||
SetButtonColor(kebab, new Color(0.06f, 0.11f, 0.22f, 0.9f));
|
||||
for (var i = -1; i <= 1; i++)
|
||||
{
|
||||
NeonUi.AddImage(
|
||||
kebab.transform,
|
||||
"Dot" + i,
|
||||
NeonUi.Circle(4, TextMuted),
|
||||
new Vector2(4f, 4f),
|
||||
new Vector2(0f, i * 7f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ редактор колоды
|
||||
|
||||
|
|
@ -708,30 +776,35 @@ namespace Vanguard.UI
|
|||
|
||||
private void ShowDeckList()
|
||||
{
|
||||
_title.text = "Колоды";
|
||||
_deckOpen = false;
|
||||
_deckListPanel.gameObject.SetActive(true);
|
||||
_deckPanel.gameObject.SetActive(false);
|
||||
_filterOverlay.gameObject.SetActive(false);
|
||||
_cardOverlay.gameObject.SetActive(false);
|
||||
_deckMenuOverlay.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ShowDeckPanel()
|
||||
{
|
||||
_title.text = "Колода";
|
||||
_deckOpen = true;
|
||||
_deckListPanel.gameObject.SetActive(false);
|
||||
_deckPanel.gameObject.SetActive(true);
|
||||
_filterOverlay.gameObject.SetActive(false);
|
||||
_cardOverlay.gameObject.SetActive(false);
|
||||
_deckMenuOverlay.gameObject.SetActive(false);
|
||||
RebuildDeckPanel();
|
||||
RebuildCollection();
|
||||
}
|
||||
|
||||
private void BackToDeckList()
|
||||
{
|
||||
_dirty = false;
|
||||
_editing = null;
|
||||
_cards.Clear();
|
||||
_deckListPanel.gameObject.SetActive(true);
|
||||
_deckPanel.gameObject.SetActive(false);
|
||||
BuildDeckList();
|
||||
RefreshCounts();
|
||||
RebuildCollection();
|
||||
ShowDeckList();
|
||||
}
|
||||
|
||||
|
|
@ -790,60 +863,101 @@ namespace Vanguard.UI
|
|||
{
|
||||
var cell = UiKit.CreatePanel(
|
||||
_collectionContent, "Owned_" + item.PrintingId, RowFill);
|
||||
UiKit.AddVerticalLayout(cell, 6, 2);
|
||||
UiKit.AddVerticalLayout(cell, 0, 0);
|
||||
AddRightClick(cell.gameObject, item.PrintingId, meta);
|
||||
|
||||
var frame = cell.gameObject.AddComponent<Outline>();
|
||||
frame.effectColor = CardArt.ClanColor(meta.Clan);
|
||||
frame.effectDistance = new Vector2(2f, -2f);
|
||||
|
||||
if (_deckOpen)
|
||||
{
|
||||
var button = cell.gameObject.AddComponent<Button>();
|
||||
button.targetGraphic = cell.GetComponent<Image>();
|
||||
button.onClick.AddListener(() => AddCard(item));
|
||||
}
|
||||
|
||||
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
||||
UiKit.SetPreferredHeight(art, 144f);
|
||||
UiKit.SetPreferredHeight(art, 182f);
|
||||
CardImageService.ApplyIllustration(
|
||||
art.GetComponent<Image>(),
|
||||
meta.ImageKey,
|
||||
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||
|
||||
var grade = UiKit.CreateText(
|
||||
art, "Grade", "G" + meta.Grade, 14, Color.white, TextAnchor.MiddleCenter);
|
||||
grade.fontStyle = FontStyle.Bold;
|
||||
var gradeRect = grade.rectTransform;
|
||||
gradeRect.anchorMin = new Vector2(1f, 1f);
|
||||
gradeRect.anchorMax = new Vector2(1f, 1f);
|
||||
gradeRect.pivot = new Vector2(1f, 1f);
|
||||
gradeRect.anchoredPosition = new Vector2(-5f, -5f);
|
||||
gradeRect.sizeDelta = new Vector2(34f, 22f);
|
||||
var badge = UiKit.CreatePanel(art, "GradeBadge", new Color(0f, 0f, 0f, 0f));
|
||||
var badgeRect = (RectTransform)badge.transform;
|
||||
badgeRect.anchorMin = new Vector2(0f, 1f);
|
||||
badgeRect.anchorMax = new Vector2(0f, 1f);
|
||||
badgeRect.pivot = new Vector2(0f, 1f);
|
||||
badgeRect.sizeDelta = new Vector2(42f, 36f);
|
||||
badgeRect.anchoredPosition = new Vector2(8f, -8f);
|
||||
var badgeImage = badge.GetComponent<Image>();
|
||||
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(
|
||||
art, "Trigger", TriggerShort(meta.Trigger), 13, Color.white, TextAnchor.MiddleCenter);
|
||||
var triggerRect = trigger.rectTransform;
|
||||
triggerRect.anchorMin = new Vector2(0f, 0f);
|
||||
triggerRect.anchorMax = new Vector2(0f, 0f);
|
||||
triggerRect.pivot = new Vector2(0f, 0f);
|
||||
triggerRect.anchoredPosition = new Vector2(5f, 5f);
|
||||
triggerRect.sizeDelta = new Vector2(34f, 20f);
|
||||
var grade = UiKit.CreateText(
|
||||
badge, "Grade", meta.Grade.ToString(), 17, Color.white);
|
||||
grade.fontStyle = FontStyle.Bold;
|
||||
UiKit.Stretch(grade.rectTransform);
|
||||
|
||||
var band = UiKit.CreatePanel(art, "NameBand", new Color(0.01f, 0.02f, 0.06f, 0.78f));
|
||||
var bandRect = (RectTransform)band.transform;
|
||||
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(
|
||||
cell, "Name", Loc.CardName(meta), 13, TextPrimary, TextAnchor.UpperLeft);
|
||||
band, "Name", Loc.CardName(meta), 14, TextPrimary, TextAnchor.MiddleCenter);
|
||||
UiKit.MakeLiteral(name);
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
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(
|
||||
cell, "Meta", meta.Clan + " · " + meta.Rarity, 11, TextMuted, TextAnchor.MiddleLeft);
|
||||
UiKit.SetPreferredHeight(metaLine, 16f);
|
||||
var qty = UiKit.CreatePanel(cell, "Qty", new Color(0.02f, 0.05f, 0.12f, 0.95f));
|
||||
UiKit.SetPreferredHeight(qty, 58f);
|
||||
|
||||
var counts = UiKit.CreateText(
|
||||
cell, "Counts", CountText(item), 13, new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft);
|
||||
UiKit.SetPreferredHeight(counts, 20f);
|
||||
qty,
|
||||
"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;
|
||||
|
||||
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()
|
||||
{
|
||||
var triggerCap = TriggerCount() >= 16;
|
||||
|
|
@ -855,16 +969,19 @@ namespace Vanguard.UI
|
|||
|
||||
var meta = MetaOf(item);
|
||||
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.color = atCap
|
||||
? 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) =>
|
||||
"×" + item.Count + " · в колоде " + UsedByName(item.NameId);
|
||||
_deckOpen
|
||||
? "в колоде ×" + UsedByName(item.NameId) + " · есть ×" + item.Count
|
||||
: "есть ×" + item.Count;
|
||||
|
||||
private void AddCard(CollectionItemDto item)
|
||||
{
|
||||
|
|
@ -918,6 +1035,7 @@ namespace Vanguard.UI
|
|||
private void OpenFilters()
|
||||
{
|
||||
_draftFilter.CopyFrom(_filter);
|
||||
_searchInput.SetTextWithoutNotify(_draftFilter.Search ?? string.Empty);
|
||||
RebuildFilterChips();
|
||||
_filterOverlay.SetAsLastSibling();
|
||||
_filterOverlay.gameObject.SetActive(true);
|
||||
|
|
@ -939,6 +1057,7 @@ namespace Vanguard.UI
|
|||
private void ResetDraftFilters()
|
||||
{
|
||||
_draftFilter.Clear();
|
||||
_searchInput.SetTextWithoutNotify(string.Empty);
|
||||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
|
|
@ -951,6 +1070,18 @@ namespace Vanguard.UI
|
|||
|
||||
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);
|
||||
for (var grade = 0; grade <= 3; grade++)
|
||||
{
|
||||
|
|
@ -1021,6 +1152,13 @@ namespace Vanguard.UI
|
|||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
private void ToggleRarity(string rarity)
|
||||
{
|
||||
if (!_draftFilter.Rarities.Add(rarity))
|
||||
_draftFilter.Rarities.Remove(rarity);
|
||||
RebuildFilterChips();
|
||||
}
|
||||
|
||||
private static Button Chip(
|
||||
Transform parent,
|
||||
string label,
|
||||
|
|
@ -1055,12 +1193,16 @@ namespace Vanguard.UI
|
|||
}
|
||||
|
||||
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)
|
||||
parts.Add("Клан: " + string.Join(", ", _filter.Clans.OrderBy(item => item, StringComparer.Ordinal)));
|
||||
if (_filter.Grades.Count > 0)
|
||||
parts.Add("Ранг: " + string.Join(", ", _filter.Grades.OrderBy(item => item)));
|
||||
if (_filter.Triggers.Count > 0)
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@ namespace Vanguard.UI
|
|||
public HashSet<string> Clans { get; } = new HashSet<string>(StringComparer.Ordinal);
|
||||
public HashSet<int> Grades { get; } = new HashSet<int>();
|
||||
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)
|
||||
{
|
||||
|
|
@ -31,6 +34,16 @@ namespace Vanguard.UI
|
|||
if (Triggers.Count > 0 && !Triggers.Contains(meta.Trigger ?? "None"))
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +52,8 @@ namespace Vanguard.UI
|
|||
Clans.Clear();
|
||||
Grades.Clear();
|
||||
Triggers.Clear();
|
||||
Rarities.Clear();
|
||||
Search = string.Empty;
|
||||
}
|
||||
|
||||
public void CopyFrom(DeckFilter other)
|
||||
|
|
@ -50,6 +65,8 @@ namespace Vanguard.UI
|
|||
Clans.UnionWith(other.Clans);
|
||||
Grades.UnionWith(other.Grades);
|
||||
Triggers.UnionWith(other.Triggers);
|
||||
Rarities.UnionWith(other.Rarities);
|
||||
Search = other.Search ?? string.Empty;
|
||||
}
|
||||
|
||||
public DeckFilter Clone()
|
||||
|
|
|
|||
|
|
@ -16,16 +16,19 @@ namespace Vanguard.UI
|
|||
private readonly RectTransform _panel;
|
||||
private readonly Action _onLogout;
|
||||
private readonly Action _onHistory;
|
||||
private readonly Action _onLobby;
|
||||
|
||||
public HamburgerMenu(
|
||||
Transform parent,
|
||||
AppConfig config,
|
||||
Action onLogout = null,
|
||||
Action onHistory = null,
|
||||
Action onLobby = null,
|
||||
string title = "Меню")
|
||||
{
|
||||
_onLogout = onLogout;
|
||||
_onHistory = onHistory;
|
||||
_onLobby = onLobby;
|
||||
|
||||
_blocker = new GameObject(
|
||||
"MenuBlocker",
|
||||
|
|
@ -91,7 +94,8 @@ namespace Vanguard.UI
|
|||
|
||||
var height = 300f
|
||||
+ (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));
|
||||
_panel = (RectTransform)panelGo.transform;
|
||||
_panel.SetParent(parent, false);
|
||||
|
|
@ -206,6 +210,24 @@ namespace Vanguard.UI
|
|||
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)
|
||||
{
|
||||
var history = UiKit.CreateButton(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ namespace Vanguard.UI
|
|||
public const string RootName = "LobbyScreen";
|
||||
|
||||
private const float Margin = 24f;
|
||||
private const float TopHeight = 96f;
|
||||
private const float LeftWidth = 560f;
|
||||
private const float RightWidth = 336f;
|
||||
private const float Gutter = 16f;
|
||||
|
|
@ -39,13 +38,10 @@ namespace Vanguard.UI
|
|||
private readonly PlayerState _player;
|
||||
private readonly Action _onLoggedOut;
|
||||
private readonly Action _onStore;
|
||||
private readonly HamburgerMenu _menu;
|
||||
private readonly PlayerTopBar _topBar;
|
||||
private readonly StatusTicker _ticker;
|
||||
|
||||
private readonly RectTransform _root;
|
||||
private Text _avatar;
|
||||
private Text _playerName;
|
||||
private Text _mmr;
|
||||
private Text _gold;
|
||||
private Text _dust;
|
||||
private Text _tokens;
|
||||
|
|
@ -74,9 +70,14 @@ namespace Vanguard.UI
|
|||
_root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0f));
|
||||
_root.GetComponent<Image>().raycastTarget = false;
|
||||
|
||||
var top = TopBar(out _avatar, out _playerName, out _mmr);
|
||||
AddTopAction(top, "Rating", "Рейтинг", -312f, 180f, () => onRating(), LobbyIcon.Chart);
|
||||
AddTopAction(top, "Community", "Сообщество", -96f, 200f, () => onCommunity(), LobbyIcon.Friends);
|
||||
_topBar = new PlayerTopBar(
|
||||
_root,
|
||||
config,
|
||||
player,
|
||||
onLoggedOut,
|
||||
onHistory,
|
||||
onRating,
|
||||
onCommunity);
|
||||
|
||||
BuildActions(onBattle, onDecks, onStore);
|
||||
BuildMissions(out _status);
|
||||
|
|
@ -84,7 +85,6 @@ namespace Vanguard.UI
|
|||
BuildChat();
|
||||
BuildFriends();
|
||||
|
||||
_menu = new HamburgerMenu(_root, config, onLoggedOut, onHistory);
|
||||
_ticker = _root.gameObject.AddComponent<StatusTicker>();
|
||||
_ticker.Hide = () => SetStatus(string.Empty);
|
||||
|
||||
|
|
@ -94,12 +94,12 @@ namespace Vanguard.UI
|
|||
_root.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public bool IsMenuOpen => _menu != null && _menu.IsOpen;
|
||||
public bool IsMenuOpen => _topBar != null && _topBar.IsMenuOpen;
|
||||
|
||||
public void CloseMenu()
|
||||
{
|
||||
if (_menu != null)
|
||||
_menu.Close();
|
||||
if (_topBar != null)
|
||||
_topBar.CloseMenu();
|
||||
}
|
||||
|
||||
public void Show()
|
||||
|
|
@ -119,129 +119,7 @@ namespace Vanguard.UI
|
|||
_disposed = true;
|
||||
_player.Changed -= UpdateTexts;
|
||||
Loc.LanguageChanged -= UpdateTexts;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ топбар
|
||||
|
||||
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);
|
||||
_topBar?.Dispose();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ левая колонка
|
||||
|
|
@ -758,27 +636,11 @@ namespace Vanguard.UI
|
|||
if (_disposed)
|
||||
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();
|
||||
_dust.text = _player.Dust.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)
|
||||
{
|
||||
if (_status == null)
|
||||
|
|
|
|||
|
|
@ -433,6 +433,13 @@ namespace Vanguard.UI
|
|||
"Clans will appear after the collection loads.",
|
||||
["Нужно ровно 50 карт, 16 триггеров и не больше 4 копий."] =
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
[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]
|
||||
public void Community_Tabs_Switch_Content()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,6 +59,56 @@ namespace Vanguard.Tests
|
|||
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]
|
||||
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-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-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, крупные карты, фильтры
|
||||
|
||||
- **Статус:** `[ ]`
|
||||
- **Статус:** `[!]`
|
||||
- **Создано:** 2026-11-01
|
||||
- **Анализ:** DeepSeek выполнен (референс DeckBuilder от 2026-11-01)
|
||||
- **Реализация:** не начата
|
||||
- **Проверка:** не выполнена
|
||||
- **Реализация:** DeepSeek: C1–C10 выполнены
|
||||
- **Проверка:** EditMode 69/69; `SCENE_SMOKE`, `PLAYMAT_SMOKE`; ручная приёмка — за пользователем
|
||||
- **Затронутые системы:** `LobbyView`, новый `PlayerTopBar`,
|
||||
`HamburgerMenu`, `DeckBuilderView`, `DeckBuilderSceneController`,
|
||||
`CardImageService`, `DeckListFilter`, `Localization`, EditMode-тесты.
|
||||
|
|
@ -2654,87 +2654,75 @@ HUD с номером хода и фазовой шкалой, компактн
|
|||
|
||||
#### C. Чек-лист изменений
|
||||
|
||||
- [ ] **C1. `CREATE` `Runtime/UI/PlayerTopBar.cs`; `MODIFY`
|
||||
- [x] **C1. `CREATE` `Runtime/UI/PlayerTopBar.cs`; `MODIFY`
|
||||
`LobbyView.cs`.**
|
||||
- Вынести из `LobbyView` топбар: аватар, ник, MMR, сезон, две кнопки,
|
||||
гамбургер; подписка на `PlayerState`; меню `HamburgerMenu`.
|
||||
- Конструктор принимает `onRating`, `onCommunity`, `onHistory`,
|
||||
`onLobby`, `onLogout`; `null` для действия = кнопка disabled.
|
||||
- `LobbyView` использует компонент без визуальных изменений.
|
||||
- `HamburgerMenu`: пункт «В лобби» при `onLobby != null`.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- Топбар вынесен: аватар, ник, MMR, сезон, две кнопки, гамбургер;
|
||||
подписка на `PlayerState`; `HamburgerMenu` с «В лобби».
|
||||
- `null`-действие отключает кнопку; `LobbyView` использует компонент.
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69, `SCENE_SMOKE`.
|
||||
- Зависит: нет.
|
||||
|
||||
- [ ] **C2. `MODIFY` `Runtime/App/DeckBuilderSceneController.cs`.**
|
||||
- Резолвить `PlayerState`/`AppConfig`; создать `PlayerTopBar` с
|
||||
`onRating: null`, `onCommunity: null`, «В лобби» → Lobby.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C2. `MODIFY` `Runtime/App/DeckBuilderSceneController.cs`.**
|
||||
- Резолвит `PlayerState`/`AppConfig`; создаёт `PlayerTopBar` с
|
||||
неактивными Rating/Community и «В лобби» → Lobby; освобождает его.
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69.
|
||||
- Зависит: C1.
|
||||
|
||||
- [ ] **C3. `MODIFY` `DeckBuilderView` — каркас.**
|
||||
- Убрать собственный топбар; тело начинается под `PlayerTopBar`.
|
||||
- Сохранить панели `DeckListPanel`/`DeckPanel`, статус под коллекцией.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C3. `MODIFY` `DeckBuilderView` — каркас.**
|
||||
- Собственный топбар убран; тело под `PlayerTopBar` (верх -120).
|
||||
- Панели `DeckListPanel`/`DeckPanel`, статус под коллекцией, отдельная
|
||||
строка фильтров со сводкой.
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69.
|
||||
- Зависит: C1, C2.
|
||||
|
||||
- [ ] **C4. `MODIFY` сетка коллекции.**
|
||||
- `GridLayoutGroup` 6 колонок, ячейка ≈216×286, вертикальный скролл без
|
||||
пагинации.
|
||||
- Ячейка: иллюстрация `ApplyIllustration` (216×182), номер грейда в
|
||||
углу, плашка имени поверх низа иллюстрации, строка количества,
|
||||
камень редкости.
|
||||
- ЛКМ в просмотре ничего не делает; ПКМ — карточка; в режиме колоды
|
||||
ЛКМ добавляет, ПКМ — карточка.
|
||||
- [x] **C4. `MODIFY` сетка коллекции.**
|
||||
- 6 колонок (`FixedColumnCount`), ячейка 216×242, вертикальный скролл.
|
||||
- Ячейка: иллюстрация 216×182, номер грейда в углу, плашка имени поверх
|
||||
низа, строка количества, камень редкости.
|
||||
- ЛКМ в просмотре не добавляет; ПКМ — карточка; в режиме колоды ЛКМ
|
||||
добавляет.
|
||||
- Количество: «есть ×N» / «в колоде ×N · есть ×M».
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
|
||||
- Зависит: C3, C5.
|
||||
|
||||
- [ ] **C5. `MODIFY` режимы просмотра и колоды.**
|
||||
- `_editing == null` — просмотр: панель колод, добавление выключено;
|
||||
нажатие на колоду открывает состав и включает добавление.
|
||||
- «К списку колод» возвращает просмотр (несохранённые изменения
|
||||
отбрасываются; предупреждение — отдельный пункт при необходимости).
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C5. `MODIFY` режимы просмотра и колоды.**
|
||||
- Режим определяется `_deckOpen`: просмотр — панель колод, добавление
|
||||
выключено; открытие/создание колоды включает добавление.
|
||||
- «К списку колод» сбрасывает `_editing`, состав и возвращает просмотр
|
||||
(несохранённые изменения отбрасываются).
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
|
||||
- Зависит: C3.
|
||||
|
||||
- [ ] **C6. `MODIFY` список колод по референсу.**
|
||||
- Кнопка `+ Новый набор` (полная ширина).
|
||||
- Строка: эмблема клана (процедурная, цвет клана), имя, `N/50 ·
|
||||
T N/16`, бейдж валидности точкой/цветом; кебаб-меню «Открыть /
|
||||
Удалить».
|
||||
- Подсветка открытой колоды.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C6. `MODIFY` список колод по референсу.**
|
||||
- `+ Новый набор` на всю ширину.
|
||||
- Строка: эмблема клана (процедурная), имя, `N/50 · T N/16` (цвет —
|
||||
валидность), кебаб с меню «Открыть / Удалить / Отмена»; открытая
|
||||
колода подсвечена.
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69, ручная — позже.
|
||||
- Зависит: C3.
|
||||
|
||||
- [ ] **C7. `MODIFY` фильтры.**
|
||||
- `DeckFilter`: добавить `Rarities` (C/R/RR/RRR/SP) и `Search`.
|
||||
- Оверлей: поиск, чипы редкости, клан/грейд/триггер; «Сбросить»,
|
||||
«Отмена», «Применить»; сводка включает редкость и поиск.
|
||||
- `DeckListModel`/`DeckFilter` остаются чистыми и тестируемыми.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C7. `MODIFY` фильтры.**
|
||||
- `DeckFilter`: `Rarities` (C/R/RR/RRR/SP) и `Search`.
|
||||
- Оверлей: поиск, редкость, клан, грейд, триггер; «Сбросить», «Отмена»,
|
||||
«Применить»; сводка включает редкость и поиск.
|
||||
- Реализация: выполнена. Проверка: `Rarity_And_Search_Filter_Are_Applied`.
|
||||
- Зависит: C3.
|
||||
|
||||
- [ ] **C8. `MODIFY` `Localization.cs`** — «В лобби», «Новый набор»,
|
||||
«Поиск по названию», «Редкость», «есть», «в колоде», «Открыть»,
|
||||
«Ещё», названия редкостей.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C8. `MODIFY` `Localization.cs`** — «В лобби», «Новый набор»,
|
||||
«Поиск по названию», «Редкость», «есть», «в колоде», «Открыть».
|
||||
- Реализация: выполнена. Проверка: дубликатов ключей нет.
|
||||
- Зависит: C4–C7.
|
||||
|
||||
- [ ] **C9. `TEST` EditMode.**
|
||||
- `DeckFilter`: редкость и поиск (регистр, локализованное имя).
|
||||
- `PlayerTopBar`: создаётся, ник/MMR обновляются, disabled-кнопки при
|
||||
`null`-действиях.
|
||||
- UI: `DeckBuilderScreen`, панели, отсутствие добавления в просмотре
|
||||
(seam), переключение в режим колоды.
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C9. `TEST` EditMode.**
|
||||
- `DeckBuilderTests`: редкость и поиск; `ClientSmokeTests`:
|
||||
`PlayerTopBar` с неактивными кнопками, `DeckListPanel`/`DeckPanel`/
|
||||
`FilterOverlay`.
|
||||
- Реализация: выполнена. Проверка: EditMode **69/69**.
|
||||
- Зависит: C1–C8.
|
||||
|
||||
- [ ] **C10. `VERIFY` EditMode, smoke, standalone.**
|
||||
- Копия без `Library`; EditMode; `SCENE_SMOKE`; `PLAYMAT_SMOKE`;
|
||||
standalone.
|
||||
- Ручная проверка: топбар и меню, крупные карты, скролл, фильтры и
|
||||
поиск, режимы, добавление/удаление, сохранение, «В бой»/«С ботом».
|
||||
- Реализация: не выполнена. Проверка: не выполнена.
|
||||
- [x] **C10. `VERIFY` EditMode, smoke, standalone.**
|
||||
- Реализация: выполнена. Проверка: EditMode 69/69, `SCENE_SMOKE
|
||||
success=True`, `PLAYMAT_SMOKE success=True`; ручная — пользователь.
|
||||
- Зависит: C9.
|
||||
|
||||
#### D. Последовательность выполнения
|
||||
|
|
@ -2786,11 +2774,24 @@ HUD с номером хода и фазовой шкалой, компактн
|
|||
|
||||
#### G. Результат
|
||||
|
||||
- **Реализовано:** —
|
||||
- **Проверки:** —
|
||||
- **Не завершено/заблокировано:** —
|
||||
- **Skill обновлён:** —
|
||||
- **Git:** —
|
||||
- **Реализовано:** C1–C10. Общий `PlayerTopBar` для Lobby и DeckBuilder
|
||||
(Rating/Community неактивны, «В лобби» в меню). Крупная сетка
|
||||
коллекции 6×N со скроллом: грейд, иллюстрация, имя поверх, количество,
|
||||
камень редкости. Просмотр без добавления (только ПКМ-карточка), режим
|
||||
колоды включает добавление. Список колод с `+ Новый набор`, эмблемами,
|
||||
счётчиками, кебаб-меню. Фильтры в оверлее: клан, грейд, триггер,
|
||||
редкость, поиск.
|
||||
- **Проверки:** 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
|
||||
|
||||
- Один экран вместо двух: слева коллекция, справа панель «Мои колоды» ⇄
|
||||
|
|
|
|||
|
|
@ -61,17 +61,18 @@
|
|||
- Сервисы: `ApiClient`, `AuthService`, `MetaService`, `TokenStore`,
|
||||
`PlayerState`, `GameConnection`.
|
||||
- Работает: логин/регистрация/refresh, онбординг, коллекция с разбором,
|
||||
декбилдер (слева карточная сетка коллекции с мультифильтрами, справа
|
||||
панель «Мои колоды» ⇄ «Колода»; список карт колоды — грейд→имя,
|
||||
превью-иллюстрации), магазин (бустер/триал-дека), история матчей и
|
||||
реплей, очередь, WebSocket-матч (snapshot, интенты, маллиган, сдача,
|
||||
`GameFinished`), таймер, результат.
|
||||
- Декбилдер: `DeckBuilderView` (один экран), `DeckListFilter`
|
||||
декбилдер (топбар Lobby, крупная сетка карт 6 в ряд со скроллом,
|
||||
мультифильтры в оверлее, справа список колод ⇄ состав), магазин
|
||||
(бустер/триал-дека), история матчей и реплей, очередь,
|
||||
WebSocket-матч (snapshot, интенты, маллиган, сдача, `GameFinished`),
|
||||
таймер, результат.
|
||||
- Декбилдер: `DeckBuilderView` + общий `PlayerTopBar`, `DeckListFilter`
|
||||
(`DeckFilter`/`DeckListModel`); каталог метаданных — `GET /api/cards`
|
||||
(grade, trigger, clan, rarity, printing, power/shield/type, текст
|
||||
способности), клиент объединяет с коллекцией.
|
||||
Превью карт — иллюстрация (верхние 59% арта) без искажений; полный арт
|
||||
остаётся в карточке и 3D-поле.
|
||||
остаётся в карточке и 3D-поле. В просмотре карта открывается по ПКМ,
|
||||
добавление — только когда открыта колода.
|
||||
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
|
||||
крафтом; SP/TD не разбираются и не крафтятся.
|
||||
- Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и
|
||||
|
|
@ -285,7 +286,7 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **67/67** EditMode (включая новые тесты TASK-2026-001:
|
||||
Клиентские проверки: **69/69** EditMode (включая новые тесты TASK-2026-001:
|
||||
повторный render скрытых карт, открытие урона, source-позиция нового юнита,
|
||||
coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод
|
||||
строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила
|
||||
|
|
@ -376,11 +377,11 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
|||
**TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной
|
||||
приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone
|
||||
собраны и запущены. **TASK-2026-023 — DeckBuilder (один экран,
|
||||
мультифильтры, список колоды): реализован C1–C11, ждёт ручной
|
||||
приёмки.** EditMode 67/67, `SCENE_SMOKE`, `PLAYMAT_SMOKE`; standalone.
|
||||
мультифильтры, список колоды): реализован C1–C11.** EditMode 67/67.
|
||||
**TASK-2026-024 — DeckBuilder по референсу (топбар Lobby, крупные
|
||||
карты, фильтры в оверлее): план готов**, реализация — следующим
|
||||
проходом. После приёмки — **TASK-2026-019** (загрузочная сцена, fade,
|
||||
карты, фильтры в оверлее): реализован C1–C10, ждёт ручной приёмки.**
|
||||
EditMode 69/69, `SCENE_SMOKE`, `PLAYMAT_SMOKE`; standalone.
|
||||
После приёмки — **TASK-2026-019** (загрузочная сцена, fade,
|
||||
кадр без покадровых аллокаций).
|
||||
2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride,
|
||||
move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого
|
||||
|
|
|
|||
Loading…
Reference in New Issue