VanguardReMaster/client/Assets/Vanguard/Runtime/UI/DeckBuilderView.cs

1810 lines
68 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.Api.Contracts;
using Vanguard.Auth;
using Vanguard.Battle;
using Vanguard.Meta;
namespace Vanguard.UI
{
/// <summary>
/// Декбилдер: слева коллекция с мультифильтрами, справа панель, которая
/// показывает либо список колод, либо карты открытой колоды.
/// </summary>
public sealed class DeckBuilderView
{
private const float PanelWidth = 420f;
private const float RowHeight = 92f;
private static readonly Color TextPrimary = new Color(0.88f, 0.93f, 1f);
private static readonly Color TextMuted = new Color(0.62f, 0.70f, 0.84f);
private static readonly Color TextDim = new Color(0.50f, 0.58f, 0.72f);
private static readonly Color Accent = new Color(0.30f, 0.65f, 1f);
private static readonly Color PanelFill = new Color(0.06f, 0.08f, 0.14f, 0.96f);
private static readonly Color RowFill = new Color(0.08f, 0.11f, 0.18f, 1f);
private static readonly Color ChipOn = new Color(0.16f, 0.40f, 0.80f, 0.95f);
private static readonly Color ChipOff = new Color(0.05f, 0.10f, 0.22f, 0.9f);
private readonly MetaService _meta;
private readonly AuthService _auth;
private readonly Action<DeckDto> _onPlay;
private readonly Action<MatchConnectionInfo> _onPracticeStarted;
private readonly RectTransform _root;
private RectTransform _collectionContent;
private RectTransform _deckListPanel;
private RectTransform _deckPanel;
private RectTransform _deckListContent;
private RectTransform _deckCardContent;
private InputField _nameInput;
private Text _status;
private Text _filterSummary;
private Text _stats;
private Text _deckCount;
private Button _saveButton;
private Button _botButton;
private Button _duelButton;
private Button _deleteButton;
private Button _dismantleAllButton;
private RectTransform _unownedChips;
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 _confirmTitle;
private Text _confirmText;
private Action _confirmAction;
private RectTransform _cardOverlay;
private Image _cardArt;
private Text _cardArtName;
private Text _cardTitle;
private Text _cardSubtitle;
private Text _cardStats;
private Text _cardAbilities;
private Text _cardOwned;
private Text _cardStatus;
private Button _dismantleButton;
private Button _craftButton;
private CollectionItemDto _selectedItem;
private CardCatalogItemDto _selectedMeta;
private readonly List<DeckDto> _decks = new List<DeckDto>();
private readonly List<CollectionItemDto> _collection = new List<CollectionItemDto>();
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal);
private readonly List<string> _cards = new List<string>();
private readonly Dictionary<string, Text> _countTexts =
new Dictionary<string, Text>(StringComparer.Ordinal);
private readonly DeckFilter _filter = new DeckFilter();
private readonly DeckFilter _draftFilter = new DeckFilter();
private DeckDto _editing;
private int _dust;
private bool _dirty;
private bool _deckOpen;
private bool _loading;
private bool _busy;
public DeckBuilderView(
Transform parent,
MetaService meta,
AuthService auth,
Action onBack,
Action<DeckDto> onPlay,
Action<MatchConnectionInfo> onPracticeStarted)
{
_meta = meta;
_auth = auth;
_onPlay = onPlay;
_onPracticeStarted = onPracticeStarted;
_root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f));
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);
}
public RectTransform Root => _root;
/// <summary>Esc: закрывает верхний оверлей, если он открыт.</summary>
public void CloseTopmost()
{
if (_cardOverlay.gameObject.activeSelf)
{
CloseCardOverlay();
return;
}
if (_confirmOverlay.gameObject.activeSelf)
{
CancelDelete();
return;
}
if (_deckMenuOverlay.gameObject.activeSelf)
{
CloseDeckMenu();
return;
}
if (_filterOverlay.gameObject.activeSelf)
CloseFilters();
}
public void Show()
{
UiKit.SetActive(_root, true);
ShowDeckList();
_ = LoadAsync();
}
public void Hide()
{
_filterOverlay.gameObject.SetActive(false);
_confirmOverlay.gameObject.SetActive(false);
_deckMenuOverlay.gameObject.SetActive(false);
_cardOverlay.gameObject.SetActive(false);
_root.gameObject.SetActive(false);
}
// ------------------------------------------------------------ каркас
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, 24f);
body.offsetMax = new Vector2(-34f, -120f);
UiKit.AddHorizontalLayout(body, 0, 14);
var bodyLayout = body.GetComponent<HorizontalLayoutGroup>();
if (bodyLayout != null)
bodyLayout.childForceExpandWidth = false;
var collectionArea = UiKit.CreatePanel(
body, "CollectionArea", new Color(0.04f, 0.06f, 0.10f, 0.55f));
var collectionElement = collectionArea.gameObject.AddComponent<LayoutElement>();
collectionElement.flexibleWidth = 1f;
collectionElement.flexibleHeight = 1f;
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);
_dismantleAllButton = UiKit.CreateButton(
filterBar,
"DismantleAll",
"Распылить лишнее",
ConfirmDismantleExtras,
15);
SetFixedSize(_dismantleAllButton, 200f, 38f);
SetButtonColor(_dismantleAllButton, new Color(0.30f, 0.16f, 0.08f, 0.95f));
_dismantleAllButton.gameObject.SetActive(false);
var scroll = UiKit.CreateGridScrollView(
collectionArea,
"CollectionGrid",
new Vector2(160f, 280f),
new Vector2(14f, 14f),
out _collectionContent);
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
scrollElement.flexibleHeight = 1f;
scrollElement.minHeight = 220f;
var smooth = scroll.GetComponent<SmoothScrollRect>();
if (smooth != null)
smooth.WheelStep = 294f;
var grid = _collectionContent.GetComponent<GridLayoutGroup>();
if (grid != null)
grid.childAlignment = TextAnchor.UpperLeft;
_status = UiKit.CreateText(
collectionArea, "Status", string.Empty, 15,
TextMuted, TextAnchor.MiddleLeft);
_status.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_status, 30f);
var right = UiKit.CreatePanel(
body, "RightPanel", new Color(0.04f, 0.06f, 0.10f, 0.85f));
var rightElement = right.gameObject.AddComponent<LayoutElement>();
rightElement.preferredWidth = PanelWidth;
rightElement.minWidth = PanelWidth;
rightElement.flexibleWidth = 0f;
rightElement.flexibleHeight = 1f;
rightElement.minHeight = 300f;
_deckListPanel = BuildDeckListPanel(right);
_deckPanel = BuildDeckPanel(right);
_deckListPanel.gameObject.SetActive(true);
_deckPanel.gameObject.SetActive(false);
}
private RectTransform BuildDeckListPanel(RectTransform parent)
{
var panel = UiKit.CreatePanel(parent, "DeckListPanel", new Color(0f, 0f, 0f, 0f));
UiKit.Stretch(panel);
UiKit.AddVerticalLayout(panel, 10, 8);
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>();
scrollElement.flexibleHeight = 1f;
scrollElement.minHeight = 200f;
var hint = UiKit.CreateText(
panel, "Hint", "Нажмите на колоду,\nчтобы открыть редактор.", 13,
TextDim, TextAnchor.MiddleLeft);
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(hint, 42f);
return panel;
}
private RectTransform BuildDeckPanel(RectTransform parent)
{
var panel = UiKit.CreatePanel(parent, "DeckPanel", new Color(0f, 0f, 0f, 0f));
UiKit.Stretch(panel);
UiKit.AddVerticalLayout(panel, 10, 6);
var header = UiKit.CreatePanel(panel, "Header", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(header, 36f);
UiKit.AddHorizontalLayout(header, 0, 8);
var back = UiKit.CreateButton(header, "BackToList", "← К списку колод", BackToDeckList, 14);
SetFixedSize(back, 180f, 34f);
_deckCount = UiKit.CreateText(
header, "DeckCount", "0/50", 16, TextPrimary, TextAnchor.MiddleRight);
_deckCount.fontStyle = FontStyle.Bold;
SetFlexible(_deckCount, 1f, 34f);
_nameInput = UiKit.CreateInput(panel, "Name", "Название колоды");
UiKit.SetPreferredHeight(_nameInput, 44f);
_nameInput.onValueChanged.AddListener(_ =>
{
_dirty = true;
UpdateStats();
});
_stats = UiKit.CreateText(
panel, "Stats", string.Empty, 13, TextMuted, TextAnchor.UpperLeft);
_stats.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_stats, 44f);
var scroll = UiKit.CreateScrollView(panel, "DeckCardScroll", out _deckCardContent, 6f);
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
scrollElement.flexibleHeight = 1f;
scrollElement.minHeight = 200f;
_saveButton = UiKit.CreateButton(panel, "Save", "Сохранить", SaveDeck, 16);
UiKit.SetPreferredHeight(_saveButton, 42f);
var row = UiKit.CreatePanel(panel, "Actions", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(row, 40f);
UiKit.AddHorizontalLayout(row, 0, 8);
_botButton = UiKit.CreateButton(row, "Bot", "С ботом", PlayBot, 15);
_duelButton = UiKit.CreateButton(row, "Duel", "В бой", PlayDeck, 15);
SetFlexible(_botButton, 1f, 38f);
SetFlexible(_duelButton, 1f, 38f);
_deleteButton = UiKit.CreateButton(panel, "Delete", "Удалить колоду", RequestDeleteCurrent, 14);
UiKit.SetPreferredHeight(_deleteButton, 34f);
SetButtonColor(_deleteButton, new Color(0.30f, 0.08f, 0.11f, 0.95f));
var hint = UiKit.CreateText(
panel, "Hint", "ЛКМ — убрать копию,\nПКМ — посмотреть карту.", 13,
TextDim, TextAnchor.MiddleLeft);
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(hint, 36f);
return panel;
}
// ------------------------------------------------------------ оверлеи
private RectTransform BuildFilterOverlay()
{
var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f));
var card = UiKit.CreateCenteredCard(
overlay, "FilterCard", new Vector2(900f, 1000f), 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);
_unownedChips = BuildFilterSection(card, "Отображение", 44f);
_rarityChips = BuildFilterSection(card, "Редкость", 44f);
_gradeChips = BuildFilterSection(card, "Ранг", 44f);
_triggerChips = BuildFilterSection(card, "Триггер", 44f);
_clanChips = BuildFilterSection(card, "Клан", 220f, flexible: true);
var footer = UiKit.CreatePanel(card, "Footer", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(footer, 48f);
UiKit.AddHorizontalLayout(footer, 0, 8);
var reset = UiKit.CreateButton(footer, "Reset", "Сбросить", ResetDraftFilters, 16);
SetFixedSize(reset, 200f, 44f);
var cancel = UiKit.CreateButton(footer, "Cancel", "Отмена", CloseFilters, 16);
SetFixedSize(cancel, 180f, 44f);
var apply = UiKit.CreateButton(footer, "Apply", "Применить", ApplyFilters, 16);
SetFixedSize(apply, 220f, 44f);
return overlay;
}
private static RectTransform BuildFilterSection(
RectTransform parent,
string title,
float height,
bool flexible = false)
{
var caption = UiKit.CreateText(
parent, "Caption_" + title, title, 17, TextMuted, TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(caption, 24f);
var container = UiKit.CreatePanel(parent, "Chips_" + title, new Color(0f, 0f, 0f, 0f));
var element = container.gameObject.AddComponent<LayoutElement>();
element.preferredHeight = height;
if (flexible)
{
element.flexibleHeight = 1f;
element.minHeight = 140f;
}
else
{
element.flexibleHeight = 0f;
}
if (flexible)
{
var scroll = UiKit.CreateScrollView(container, "Scroll", out var content, 4f);
UiKit.Stretch(scroll);
return content;
}
UiKit.AddHorizontalLayout(container, 0, 6);
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(
_root, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.78f));
var card = UiKit.CreateCenteredCard(
overlay, "ConfirmCard", new Vector2(640f, 320f), PanelFill);
UiKit.AddVerticalLayout(card, 30, 12);
_confirmTitle = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, TextPrimary);
_confirmTitle.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(_confirmTitle, 42f);
_confirmText = UiKit.CreateText(
card, "Text", string.Empty, 19, TextMuted);
_confirmText.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_confirmText, 70f);
var yes = UiKit.CreateButton(card, "Yes", "Подтвердить", ConfirmAction, 20);
UiKit.SetPreferredHeight(yes, 48f);
var no = UiKit.CreateButton(card, "No", "Отмена", CancelDelete, 20);
UiKit.SetPreferredHeight(no, 44f);
return overlay;
}
private RectTransform BuildCardOverlay()
{
var overlay = UiKit.CreatePanel(_root, "CardOverlay", new Color(0f, 0f, 0f, 0.82f));
var card = UiKit.CreateCenteredCard(
overlay, "CardView", new Vector2(1040f, 660f), new Color(0.05f, 0.07f, 0.11f, 1f));
var body = UiKit.CreatePanel(card, "Body", new Color(0f, 0f, 0f, 0f));
body.anchorMin = Vector2.zero;
body.anchorMax = Vector2.one;
body.offsetMin = new Vector2(26f, 26f);
body.offsetMax = new Vector2(-26f, -26f);
UiKit.AddHorizontalLayout(body, 0, 22);
var art = UiKit.CreatePanel(body, "Art", new Color(0.2f, 0.3f, 0.5f, 1f));
SetFixedSize(art, 360f, 540f);
_cardArt = art.GetComponent<Image>();
_cardArtName = UiKit.CreateText(
art, "ArtName", string.Empty, 18,
new Color(1f, 1f, 1f, 0.92f), TextAnchor.MiddleCenter);
UiKit.MakeLiteral(_cardArtName);
UiKit.Stretch(_cardArtName.rectTransform);
_cardArtName.rectTransform.offsetMin = new Vector2(14f, 16f);
_cardArtName.rectTransform.offsetMax = new Vector2(-14f, -16f);
_cardArtName.horizontalOverflow = HorizontalWrapMode.Wrap;
var info = UiKit.CreatePanel(body, "Info", new Color(0f, 0f, 0f, 0f));
var infoElement = info.gameObject.AddComponent<LayoutElement>();
infoElement.flexibleWidth = 1f;
infoElement.flexibleHeight = 1f;
UiKit.AddVerticalLayout(info, 4, 6);
_cardTitle = UiKit.CreateText(
info, "Title", string.Empty, 30, Color.white, TextAnchor.MiddleLeft);
UiKit.MakeLiteral(_cardTitle);
_cardTitle.fontStyle = FontStyle.Bold;
_cardTitle.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardTitle, 44f);
_cardSubtitle = UiKit.CreateText(
info, "Subtitle", string.Empty, 16, TextMuted, TextAnchor.MiddleLeft);
UiKit.MakeLiteral(_cardSubtitle);
UiKit.SetPreferredHeight(_cardSubtitle, 26f);
_cardStats = UiKit.CreateText(
info, "Stats", string.Empty, 15,
new Color(0.72f, 0.78f, 0.92f), TextAnchor.UpperLeft);
_cardStats.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardStats, 88f);
var abilitiesCaption = UiKit.CreateText(
info, "AbilitiesCaption", "Способности", 16, TextMuted, TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(abilitiesCaption, 24f);
_cardAbilities = UiKit.CreateText(
info, "Abilities", string.Empty, 15,
new Color(0.82f, 0.86f, 0.96f), TextAnchor.UpperLeft);
UiKit.MakeLiteral(_cardAbilities);
_cardAbilities.horizontalOverflow = HorizontalWrapMode.Wrap;
var abilitiesElement = _cardAbilities.gameObject.AddComponent<LayoutElement>();
abilitiesElement.flexibleHeight = 1f;
abilitiesElement.minHeight = 100f;
_cardOwned = UiKit.CreateText(
info, "Owned", string.Empty, 15,
new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(_cardOwned, 24f);
_cardStatus = UiKit.CreateText(
info, "Status", string.Empty, 14, TextDim, TextAnchor.UpperLeft);
_cardStatus.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(_cardStatus, 38f);
var actions = UiKit.CreatePanel(info, "Actions", new Color(0f, 0f, 0f, 0f));
UiKit.SetPreferredHeight(actions, 50f);
UiKit.AddHorizontalLayout(actions, 0, 8);
_dismantleButton = UiKit.CreateButton(
actions, "Dismantle", "Разобрать", DismantleSelected, 16);
SetFixedSize(_dismantleButton, 225f, 46f);
_craftButton = UiKit.CreateButton(
actions, "Craft", "Скрафтить", CraftSelected, 16);
SetFixedSize(_craftButton, 225f, 46f);
var close = UiKit.CreateButton(actions, "Close", "Закрыть", CloseCardOverlay, 16);
SetFixedSize(close, 110f, 46f);
return overlay;
}
// ------------------------------------------------------------ загрузка
private async Task LoadAsync()
{
if (_loading)
return;
_loading = true;
SetStatus("Загружаем коллекцию и колоды…", false);
ClearChildren(_deckListContent);
try
{
await _auth.EnsureAccessAsync();
var collection = await _meta.GetCollectionAsync();
var catalog = await _meta.GetCatalogAsync();
var decks = await _meta.GetDecksAsync();
if (!_root.gameObject.activeSelf)
return;
_collection.Clear();
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
_dust = collection.Balances != null ? collection.Balances.Dust : 0;
_catalog.Clear();
foreach (var item in catalog)
_catalog[item.PrintingId] = item;
_decks.Clear();
_decks.AddRange(decks);
BuildDeckList();
RebuildCollection();
SetStatus(
_decks.Count == 0
? "Колод пока нет. Пыль: " + _dust + ". ПКМ по карте — подробности."
: "Карт в коллекции: " + _collection.Count + " · пыль " + _dust
+ " · ПКМ по карте — подробности.",
false);
}
catch (ApiException exception)
{
SetStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось загрузить декбилдер.", true);
}
finally
{
_loading = false;
}
}
// ------------------------------------------------------------ список колод
private void BuildDeckList()
{
ClearChildren(_deckListContent);
if (_decks.Count == 0)
{
var empty = UiKit.CreateText(
_deckListContent, "Empty", "Нет колод", 15, TextDim);
UiKit.SetPreferredHeight(empty, 90f);
return;
}
foreach (var deck in _decks)
{
var row = UiKit.CreatePanel(
_deckListContent, "DeckRow_" + deck.Id, RowFill);
var rowElement = row.gameObject.AddComponent<LayoutElement>();
rowElement.preferredHeight = RowHeight;
var button = row.gameObject.AddComponent<Button>();
button.targetGraphic = row.GetComponent<Image>();
button.onClick.AddListener(() => OpenDeck(deck));
var isOpen = _editing != null && _editing.Id == deck.Id;
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));
var contentElement = content.gameObject.AddComponent<LayoutElement>();
contentElement.flexibleWidth = 1f;
contentElement.preferredHeight = 64f;
UiKit.AddVerticalLayout(content, 0, 2);
var name = UiKit.CreateText(
content, "Name", deck.Name, 16, TextPrimary, TextAnchor.MiddleLeft);
UiKit.MakeLiteral(name);
name.fontStyle = FontStyle.Bold;
name.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(name, 24f);
var validation = DeckListModel.Validate(deck.Cards, _catalog);
var info = UiKit.CreateText(
content,
"Info",
deck.Cards.Count + "/50 · T " + validation.Triggers + "/16",
12,
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 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;
}
// ------------------------------------------------------------ редактор колоды
private void RebuildDeckCards()
{
ClearChildren(_deckCardContent);
var groups = DeckListModel.GroupDeck(_cards, _catalog);
if (groups.Count == 0)
{
var empty = UiKit.CreateText(
_deckCardContent, "Empty", "Колода пуста", 15, TextDim);
UiKit.SetPreferredHeight(empty, 90f);
return;
}
foreach (var group in groups)
{
var row = UiKit.CreatePanel(
_deckCardContent, "DeckCard_" + group.NameId, RowFill);
var rowElement = row.gameObject.AddComponent<LayoutElement>();
rowElement.preferredHeight = 84f;
UiKit.AddHorizontalLayout(row, 6, 8);
var button = row.gameObject.AddComponent<Button>();
button.targetGraphic = row.GetComponent<Image>();
var printingId = group.PrintingIds[group.PrintingIds.Count - 1];
button.onClick.AddListener(() => RemoveCard(printingId));
if (group.Meta != null)
AddRightClick(row.gameObject, printingId, group.Meta);
var art = UiKit.CreatePanel(row, "Art", new Color(0.1f, 0.2f, 0.4f, 1f));
SetFixedSize(art, 64f, 54f);
CardImageService.ApplyIllustration(
art.GetComponent<Image>(),
group.Meta != null ? group.Meta.ImageKey : null,
CardArt.ForCard(
group.Meta != null ? group.Meta.NameId : group.NameId,
group.Meta != null ? group.Meta.Clan : "None"));
var texts = UiKit.CreatePanel(row, "Texts", new Color(0f, 0f, 0f, 0f));
var textsElement = texts.gameObject.AddComponent<LayoutElement>();
textsElement.flexibleWidth = 1f;
textsElement.preferredHeight = 66f;
UiKit.AddVerticalLayout(texts, 2, 0);
var name = UiKit.CreateText(
texts, "Name", group.Name, 14, TextPrimary, TextAnchor.UpperLeft);
UiKit.MakeLiteral(name);
name.fontStyle = FontStyle.Bold;
name.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(name, 34f);
var metaLine = "G" + (group.Grade == int.MaxValue ? "?" : group.Grade.ToString())
+ " · ×" + group.Count
+ (group.Meta != null
? " · " + TriggerShort(group.Meta.Trigger) + " · " + group.Meta.Clan
: string.Empty);
var info = UiKit.CreateText(
texts, "Info", metaLine, 11, TextMuted, TextAnchor.MiddleLeft);
UiKit.MakeLiteral(info);
UiKit.SetPreferredHeight(info, 18f);
}
}
private void ShowDeckList()
{
_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()
{
_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();
RebuildCollection();
ShowDeckList();
}
private void NewDeck()
{
_editing = null;
_cards.Clear();
_dirty = true;
_nameInput.SetTextWithoutNotify("Новая колода");
ShowDeckPanel();
}
private void OpenDeck(DeckDto deck)
{
_editing = deck;
_cards.Clear();
_cards.AddRange(deck.Cards);
_nameInput.SetTextWithoutNotify(deck.Name);
_dirty = false;
ShowDeckPanel();
}
private void RebuildDeckPanel()
{
UpdateStats();
RebuildDeckCards();
RefreshCounts();
}
private void RefreshAfterDeckChange()
{
UpdateStats();
RebuildDeckCards();
RefreshCounts();
}
// ------------------------------------------------------------ коллекция
private void RebuildCollection()
{
ClearChildren(_collectionContent);
_countTexts.Clear();
var ownedByPrinting = new Dictionary<string, CollectionItemDto>(StringComparer.Ordinal);
foreach (var item in _collection)
ownedByPrinting[item.PrintingId] = item;
var source = _filter.IncludeUnowned
? _catalog.Values.Select(meta => new
{
Meta = meta,
Item = ownedByPrinting.TryGetValue(meta.PrintingId, out var owned)
? owned
: new CollectionItemDto
{
PrintingId = meta.PrintingId,
NameId = meta.NameId,
Count = 0,
},
})
: _collection.Select(item => new { Meta = MetaOf(item), Item = item });
var items = source
.Where(pair => pair.Meta != null && _filter.Matches(pair.Meta))
.OrderBy(pair => pair.Meta.Clan, StringComparer.Ordinal)
.ThenBy(pair => pair.Meta.Grade)
.ThenBy(pair => Loc.CardName(pair.Meta), StringComparer.Ordinal);
foreach (var pair in items)
AddCollectionCell(pair.Item, pair.Meta);
UpdateDismantleButton();
}
private void AddCollectionCell(CollectionItemDto item, CardCatalogItemDto meta)
{
var cell = UiKit.CreatePanel(
_collectionContent, "Owned_" + item.PrintingId, RowFill);
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 && item.Count > 0)
{
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, 234f);
var artImage = art.GetComponent<Image>();
CardImageService.Apply(
artImage,
meta.ImageKey,
CardArt.ForCard(meta.NameId, meta.Clan));
if (item.Count <= 0)
artImage.color = new Color(0.45f, 0.45f, 0.45f, 1f);
var qty = UiKit.CreatePanel(cell, "Qty", new Color(0.02f, 0.05f, 0.12f, 0.95f));
UiKit.SetPreferredHeight(qty, 46f);
var counts = UiKit.CreateText(
qty,
"Counts",
CountText(item),
12,
new Color(0.90f, 0.86f, 0.70f),
TextAnchor.MiddleCenter);
UiKit.Stretch(counts.rectTransform);
counts.rectTransform.offsetMin = new Vector2(4f, 0f);
counts.rectTransform.offsetMax = new Vector2(-4f, 0f);
_countTexts[item.PrintingId] = counts;
}
private void RefreshCounts()
{
var triggerCap = TriggerCount() >= 16;
foreach (var pair in _countTexts)
{
var item = _collection.Find(candidate => candidate.PrintingId == pair.Key);
if (item == null)
continue;
var meta = MetaOf(item);
var used = UsedByName(item.NameId);
var atCap = _deckOpen
&& (used >= 4 || (meta != null && IsTrigger(meta.Trigger) && triggerCap));
pair.Value.text = CountText(item);
pair.Value.color = item.Count <= 0
? new Color(0.50f, 0.50f, 0.50f)
: atCap
? new Color(1f, 0.55f, 0.45f)
: new Color(0.90f, 0.86f, 0.70f);
}
}
private string CountText(CollectionItemDto item) =>
_deckOpen
? "в колоде ×" + UsedByName(item.NameId) + " · есть ×" + item.Count
: "есть ×" + item.Count;
private void AddCard(CollectionItemDto item)
{
if (!_catalog.TryGetValue(item.PrintingId, out var itemMeta))
return;
if (_cards.Count >= 50)
{
SetStatus("В колоде уже 50 карт.", true);
return;
}
if (UsedByName(item.NameId) >= 4)
{
SetStatus("Не больше 4 копий одной карты.", true);
return;
}
if (IsTrigger(itemMeta.Trigger) && TriggerCount() >= 16)
{
SetStatus("В колоде уже 16 триггеров.", true);
return;
}
if (_cards.Count(card => card == item.PrintingId) >= item.Count)
{
SetStatus("В коллекции больше нет свободных копий.", true);
return;
}
_cards.Add(item.PrintingId);
_dirty = true;
SetStatus(string.Empty, false);
RefreshAfterDeckChange();
}
private void RemoveCard(string printingId)
{
var index = _cards.LastIndexOf(printingId);
if (index >= 0)
{
_cards.RemoveAt(index);
_dirty = true;
}
RefreshAfterDeckChange();
}
// ------------------------------------------------------------ фильтры
private void OpenFilters()
{
_draftFilter.CopyFrom(_filter);
_searchInput.SetTextWithoutNotify(_draftFilter.Search ?? string.Empty);
RebuildFilterChips();
_filterOverlay.SetAsLastSibling();
_filterOverlay.gameObject.SetActive(true);
}
private void CloseFilters()
{
_filterOverlay.gameObject.SetActive(false);
}
private void ApplyFilters()
{
_filter.CopyFrom(_draftFilter);
UpdateFilterSummary();
RebuildCollection();
CloseFilters();
}
private void ResetDraftFilters()
{
_draftFilter.Clear();
_searchInput.SetTextWithoutNotify(string.Empty);
RebuildFilterChips();
}
private void RebuildFilterChips()
{
ClearChildren(_unownedChips);
Chip(
_unownedChips,
"Показывать вне коллекции",
_draftFilter.IncludeUnowned,
ToggleUnowned,
360f);
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++)
{
var value = grade;
Chip(
_gradeChips,
"G" + value,
_draftFilter.Grades.Contains(value),
() => ToggleGrade(value),
92f);
}
ClearChildren(_triggerChips);
foreach (var trigger in new[] { "None", "Critical", "Draw", "Stand", "Heal" })
{
var value = trigger;
Chip(
_triggerChips,
TriggerLabel(value),
_draftFilter.Triggers.Contains(value),
() => ToggleTrigger(value),
150f);
}
ClearChildren(_clanChips);
foreach (var clan in _catalog.Values
.Select(item => item.Clan)
.Distinct()
.OrderBy(item => item, StringComparer.Ordinal))
{
var value = clan;
Chip(
_clanChips,
value,
_draftFilter.Clans.Contains(value),
() => ToggleClan(value),
0f,
fullWidth: true);
}
if (_clanChips.childCount == 0)
{
var empty = UiKit.CreateText(
_clanChips, "Empty", "Кланы появятся после загрузки коллекции.",
14, TextDim);
UiKit.SetPreferredHeight(empty, 40f);
}
}
private void ToggleGrade(int grade)
{
if (!_draftFilter.Grades.Add(grade))
_draftFilter.Grades.Remove(grade);
RebuildFilterChips();
}
private void ToggleTrigger(string trigger)
{
if (!_draftFilter.Triggers.Add(trigger))
_draftFilter.Triggers.Remove(trigger);
RebuildFilterChips();
}
private void ToggleClan(string clan)
{
if (!_draftFilter.Clans.Add(clan))
_draftFilter.Clans.Remove(clan);
RebuildFilterChips();
}
private void ToggleRarity(string rarity)
{
if (!_draftFilter.Rarities.Add(rarity))
_draftFilter.Rarities.Remove(rarity);
RebuildFilterChips();
}
private void ToggleUnowned()
{
_draftFilter.IncludeUnowned = !_draftFilter.IncludeUnowned;
RebuildFilterChips();
}
private static Button Chip(
Transform parent,
string label,
bool active,
Action onClick,
float width,
bool fullWidth = false)
{
var button = UiKit.CreateButton(
parent, "Chip_" + label, label, () => onClick(), 14);
if (fullWidth)
{
UiKit.SetPreferredHeight(button, 34f);
var element = button.gameObject.GetComponent<LayoutElement>();
element.flexibleWidth = 1f;
}
else
{
SetFixedSize(button, width, 38f);
}
SetButtonColor(button, active ? ChipOn : ChipOff);
return button;
}
private void UpdateFilterSummary()
{
if (_filter.IsEmpty)
{
_filterSummary.text = "Все карты";
return;
}
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());
if (_filter.IncludeUnowned)
parts.Add("вне коллекции");
_filterSummary.text = string.Join(" · ", parts);
}
private static string TriggerLabel(string trigger) => trigger switch
{
"None" => "Без триггера",
"Critical" => "Критический",
"Draw" => "Добор",
"Stand" => "Стойка",
"Heal" => "Лечение",
_ => trigger,
};
// ------------------------------------------------------------ статистика и валидация
private void UpdateStats()
{
var grades = new int[4];
foreach (var printingId in _cards)
{
if (_catalog.TryGetValue(printingId, out var meta)
&& meta.Grade >= 0 && meta.Grade < grades.Length)
{
grades[meta.Grade]++;
}
}
var validation = DeckListModel.Validate(_cards, _catalog);
_deckCount.text = _cards.Count + "/50 · T " + validation.Triggers + "/16";
_deckCount.color = validation.IsValid
? new Color(0.55f, 0.9f, 0.62f)
: new Color(1f, 0.72f, 0.5f);
_stats.text = "Всего: " + _cards.Count + "/50 G0 " + grades[0]
+ " · G1 " + grades[1] + " · G2 " + grades[2] + " · G3 " + grades[3]
+ " · T " + validation.Triggers + "/16"
+ (_dirty ? " · не сохранено" : string.Empty);
_stats.color = validation.IsValid ? TextMuted : new Color(1f, 0.68f, 0.45f);
var canSave = validation.IsValid && !_busy;
var canPlay = validation.IsValid && !_busy && !_dirty && _editing != null;
_saveButton.interactable = canSave;
_duelButton.interactable = canPlay;
_botButton.interactable = canPlay;
_deleteButton.interactable = _editing != null && !_busy;
}
private string GradeLine(IEnumerable<string> cards)
{
var grades = new int[4];
foreach (var printingId in cards)
{
if (_catalog.TryGetValue(printingId, out var meta)
&& meta.Grade >= 0 && meta.Grade < grades.Length)
{
grades[meta.Grade]++;
}
}
return "G0 " + grades[0] + " · G1 " + grades[1]
+ " · G2 " + grades[2] + " · G3 " + grades[3];
}
private static string FormatName(string format) =>
string.Equals(format, "classic", StringComparison.OrdinalIgnoreCase)
? "Classic"
: format;
private int UsedByName(string nameId) =>
_cards.Count(printingId =>
_catalog.TryGetValue(printingId, out var meta) && meta.NameId == nameId);
private int TriggerCount() =>
_cards.Count(printingId =>
_catalog.TryGetValue(printingId, out var meta) && IsTrigger(meta.Trigger));
private static bool IsTrigger(string trigger) => DeckListModel.IsTrigger(trigger);
private static string TriggerShort(string trigger) => trigger switch
{
"Critical" => "K",
"Draw" => "D",
"Stand" => "S",
"Heal" => "H",
_ => "—",
};
// ------------------------------------------------------------ сохранение и бой
private async void SaveDeck()
{
if (_busy)
return;
var validation = DeckListModel.Validate(_cards, _catalog);
if (!validation.IsValid)
{
SetStatus("Нужно ровно 50 карт, 16 триггеров и не больше 4 копий.", true);
return;
}
_busy = true;
UpdateStats();
SetStatus("Сохраняем…", false);
try
{
await _auth.EnsureAccessAsync();
var name = string.IsNullOrWhiteSpace(_nameInput.text)
? "Новая колода"
: _nameInput.text.Trim();
var result = _editing == null
? await _meta.CreateDeckAsync(name, new List<string>(_cards))
: await _meta.UpdateDeckAsync(_editing.Id, name, new List<string>(_cards));
_editing = result;
_cards.Clear();
_cards.AddRange(result.Cards);
_nameInput.SetTextWithoutNotify(result.Name);
_dirty = false;
UpsertDeck(result);
BuildDeckList();
RebuildDeckPanel();
SetStatus("Сохранено: " + result.Cards.Count + " карт.", false);
}
catch (ApiException exception)
{
var details = exception.Violations.Count > 0
? "\n" + string.Join("\n", exception.Violations.Take(3).Select(v => v.Message))
: string.Empty;
SetStatus(exception.Message + details, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось сохранить колоду.", true);
}
finally
{
_busy = false;
UpdateStats();
}
}
private void PlayDeck()
{
if (_editing == null || _dirty)
{
SetStatus("Сначала сохраните колоду.", true);
return;
}
_onPlay(_editing);
}
private async void PlayBot()
{
if (_busy)
return;
if (_editing == null || _dirty)
{
SetStatus("Сначала сохраните колоду.", true);
return;
}
_busy = true;
UpdateStats();
SetStatus("Готовим тренировочный матч…", false);
try
{
await _auth.EnsureAccessAsync();
var response = await _meta.StartPracticeAsync(_editing.Id);
_onPracticeStarted(new MatchConnectionInfo
{
MatchId = response.MatchId,
Seat = response.Seat,
Ticket = response.Ticket,
WsUrl = response.WsUrl,
DeckName = _editing.Name,
});
}
catch (ApiException exception)
{
SetStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось начать матч с ботом.", true);
}
finally
{
_busy = false;
UpdateStats();
}
}
// ------------------------------------------------------------ удаление
private void RequestDeleteCurrent()
{
if (_editing == null)
{
SetStatus("Сначала сохраните колоду.", true);
return;
}
RequestDelete(_editing);
}
private async void DeleteDeck(DeckDto deck)
{
if (_busy)
return;
_busy = true;
try
{
await _meta.DeleteDeckAsync(deck.Id);
_decks.RemoveAll(item => item.Id == deck.Id);
if (_editing != null && _editing.Id == deck.Id)
_editing = null;
BuildDeckList();
if (_deckPanel.gameObject.activeSelf)
BackToDeckList();
SetStatus("Колода удалена.", false);
}
catch (ApiException exception)
{
SetStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось удалить колоду.", true);
}
finally
{
_busy = false;
}
}
private void RequestDelete(DeckDto deck)
{
_confirmTitle.text = "Удалить колоду?";
_confirmText.text = deck.Name + "\nКолода будет удалена без восстановления.";
_confirmAction = () => DeleteDeck(deck);
ShowConfirm();
}
private void ConfirmDismantleExtras()
{
var plan = DeckListModel.PlanExtras(_collection);
if (plan.IsEmpty || _busy)
return;
_confirmTitle.text = "Распылить лишние карты?";
_confirmText.text = "Будет распылено " + plan.CardCount
+ " карт на " + plan.Dust + " пыли.";
_confirmAction = () => DismantleExtras(plan);
ShowConfirm();
}
private void ShowConfirm()
{
_confirmOverlay.SetAsLastSibling();
_confirmOverlay.gameObject.SetActive(true);
}
private void CancelDelete()
{
_confirmAction = null;
_confirmOverlay.gameObject.SetActive(false);
}
private void ConfirmAction()
{
var action = _confirmAction;
_confirmAction = null;
_confirmOverlay.gameObject.SetActive(false);
action?.Invoke();
}
private async void DismantleExtras(DismantlePlan plan)
{
if (_busy || plan == null || plan.IsEmpty)
return;
_busy = true;
UpdateDismantleButton();
SetStatus("Распыляем…", false);
try
{
await _auth.EnsureAccessAsync();
foreach (var item in plan.Items)
await _meta.DismantleAsync(item.PrintingId, item.Count);
await ReloadCollectionAsync(null);
SetStatus(
"Распылено " + plan.CardCount + " карт на " + plan.Dust + " пыли.",
false);
}
catch (ApiException exception)
{
SetStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось распылить карты.", true);
}
finally
{
_busy = false;
UpdateDismantleButton();
}
}
private void UpdateDismantleButton()
{
if (_dismantleAllButton == null)
return;
var plan = DeckListModel.PlanExtras(_collection);
_dismantleAllButton.gameObject.SetActive(!plan.IsEmpty);
_dismantleAllButton.interactable = !plan.IsEmpty && !_busy;
}
// ------------------------------------------------------------ карточка
private void AddRightClick(GameObject target, string printingId, CardCatalogItemDto meta)
{
var relay = target.GetComponent<RightClickRelay>()
?? target.AddComponent<RightClickRelay>();
relay.Clicked = _ => ShowCardOverlay(printingId, meta);
}
private void ShowCardOverlay(string printingId, CardCatalogItemDto meta)
{
if (meta == null)
return;
_selectedMeta = meta;
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
_cardStatus.text = string.Empty;
UpdateCardOverlay();
_cardOverlay.SetAsLastSibling();
_cardOverlay.gameObject.SetActive(true);
}
private void UpdateCardOverlay()
{
if (_selectedMeta == null)
return;
var meta = _selectedMeta;
CardImageService.Apply(
_cardArt,
meta.ImageKey,
CardArt.ForCard(meta.NameId, meta.Clan));
var otherName = Loc.OtherCardName(meta);
_cardArtName.text = Loc.CardName(meta);
_cardTitle.text = Loc.CardName(meta);
_cardSubtitle.text = (string.IsNullOrEmpty(otherName) ? string.Empty : otherName + " · ")
+ meta.Clan + " · G" + meta.Grade;
_cardStats.text = "Тип: " + meta.Type + " · Редкость: " + meta.Rarity + "\n"
+ "Power " + meta.Power + " · Shield " + meta.Shield + " · Critical " + meta.Critical + "\n"
+ "Триггер: " + meta.Trigger
+ " · Sentinel: " + (meta.IsSentinel ? "да" : "нет")
+ " · Boost: " + (meta.HasBoost ? "да" : "нет")
+ " · Intercept: " + (meta.HasIntercept ? "да" : "нет") + "\n"
+ "Принт: " + meta.PrintingId;
var abilityText = Loc.CardText(meta);
_cardAbilities.text = string.IsNullOrWhiteSpace(abilityText)
? Loc.Translate("Текст способности отсутствует.")
: abilityText;
_cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0)
+ " · в колоде: " + UsedByName(meta.NameId)
+ " · пыль: " + _dust;
var canChange = CanChange(meta.Rarity);
var craftCost = CraftCostFor(meta.Rarity);
_dismantleButton.interactable = canChange && !_busy
&& _selectedItem != null && _selectedItem.Count > 0;
_craftButton.interactable = canChange && !_busy && _dust >= craftCost;
SetButtonLabel(
_dismantleButton,
canChange ? "Разобрать (+" + DeckListModel.DustFor(meta.Rarity) + " пыли)" : "Нельзя разобрать");
SetButtonLabel(
_craftButton,
canChange
? "Скрафтить (−" + craftCost + " пыли)"
: "Нельзя скрафтить");
}
private static bool CanChange(string rarity) =>
rarity == "C" || rarity == "R" || rarity == "RR" || rarity == "RRR";
private async void DismantleSelected()
{
if (_busy || _selectedItem == null || _selectedItem.Count <= 0
|| _selectedMeta == null || !CanChange(_selectedMeta.Rarity))
return;
var printingId = _selectedItem.PrintingId;
_busy = true;
UpdateCardOverlay();
_cardStatus.text = "Разбираем…";
_cardStatus.color = TextMuted;
try
{
await _auth.EnsureAccessAsync();
await _meta.DismantleAsync(printingId, 1);
await ReloadCollectionAsync(printingId);
SetCardStatus("Карта разобрана.", false);
}
catch (ApiException exception)
{
SetCardStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetCardStatus("Не удалось разобрать карту.", true);
}
finally
{
_busy = false;
UpdateCardOverlay();
}
}
private async void CraftSelected()
{
if (_busy || _selectedMeta == null || !CanChange(_selectedMeta.Rarity))
return;
var printingId = _selectedMeta.PrintingId;
_busy = true;
UpdateCardOverlay();
_cardStatus.text = "Крафтим…";
_cardStatus.color = TextMuted;
try
{
await _auth.EnsureAccessAsync();
await _meta.CraftAsync(printingId);
await ReloadCollectionAsync(printingId);
SetCardStatus("Карта скрафчена.", false);
}
catch (ApiException exception)
{
SetCardStatus(exception.Message, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetCardStatus("Не удалось скрафтить карту.", true);
}
finally
{
_busy = false;
UpdateCardOverlay();
}
}
private async Task ReloadCollectionAsync(string printingId)
{
var collection = await _meta.GetCollectionAsync();
_collection.Clear();
_collection.AddRange(collection.Items ?? new List<CollectionItemDto>());
_dust = collection.Balances != null ? collection.Balances.Dust : 0;
RebuildCollection();
RefreshCounts();
UpdateDismantleButton();
_selectedItem = printingId == null
? null
: _collection.Find(item => item.PrintingId == printingId);
if (_selectedItem == null)
{
_cardOverlay.gameObject.SetActive(false);
SetStatus("Карт в коллекции: " + _collection.Count + ".", false);
}
else
{
UpdateCardOverlay();
}
}
private void SetCardStatus(string text, bool error)
{
_cardStatus.text = text;
_cardStatus.color = error
? new Color(1f, 0.55f, 0.45f)
: new Color(0.45f, 0.85f, 0.55f);
}
private void CloseCardOverlay()
{
_selectedItem = null;
_selectedMeta = null;
_cardOverlay.gameObject.SetActive(false);
}
// ------------------------------------------------------------ утилиты
private void SetStatus(string text, bool error)
{
if (_status == null)
return;
_status.text = text ?? string.Empty;
_status.color = error
? new Color(1f, 0.55f, 0.45f)
: TextMuted;
}
private void UpsertDeck(DeckDto deck)
{
var index = _decks.FindIndex(item => item.Id == deck.Id);
if (index >= 0)
_decks[index] = deck;
else
_decks.Add(deck);
}
private CardCatalogItemDto MetaOf(CollectionItemDto item) =>
_catalog.TryGetValue(item.PrintingId, out var meta) ? meta : null;
private static int CraftCostFor(string rarity) => rarity switch
{
"C" => 20,
"R" => 40,
"RR" => 100,
"RRR" => 400,
_ => 0,
};
private static void SetButtonLabel(Button button, string label)
{
var text = button.GetComponentInChildren<Text>();
if (text != null)
text.text = label;
}
private static void SetButtonColor(Button button, Color color)
{
var image = button.GetComponent<Image>();
if (image != null)
image.color = color;
}
private static void SetFixedSize(Component component, float width, float height)
{
var element = component.gameObject.GetComponent<LayoutElement>()
?? component.gameObject.AddComponent<LayoutElement>();
element.preferredWidth = width;
element.preferredHeight = height;
element.flexibleWidth = 0f;
element.flexibleHeight = 0f;
}
private static void SetFlexible(Component component, float widthWeight, float height)
{
var element = component.gameObject.GetComponent<LayoutElement>()
?? component.gameObject.AddComponent<LayoutElement>();
element.preferredHeight = height;
element.flexibleWidth = widthWeight;
element.flexibleHeight = 0f;
}
private static void ClearChildren(Transform parent)
{
for (var i = parent.childCount - 1; i >= 0; i--)
{
var child = parent.GetChild(i).gameObject;
if (Application.isPlaying)
UnityEngine.Object.Destroy(child);
else
UnityEngine.Object.DestroyImmediate(child);
}
}
}
}