1601 lines
61 KiB
C#
1601 lines
61 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
using Vanguard.Api;
|
||
using Vanguard.Api.Contracts;
|
||
using Vanguard.Auth;
|
||
using Vanguard.Battle;
|
||
using Vanguard.Meta;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>
|
||
/// Декбилдер: слева коллекция с мультифильтрами, справа панель, которая
|
||
/// показывает либо список колод, либо карты открытой колоды.
|
||
/// </summary>
|
||
public sealed class DeckBuilderView
|
||
{
|
||
private const float PanelWidth = 380f;
|
||
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 _onBack;
|
||
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 _title;
|
||
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 RectTransform _filterOverlay;
|
||
private RectTransform _gradeChips;
|
||
private RectTransform _triggerChips;
|
||
private RectTransform _clanChips;
|
||
private RectTransform _confirmOverlay;
|
||
private Text _confirmText;
|
||
private DeckDto _pendingDelete;
|
||
|
||
private RectTransform _cardOverlay;
|
||
private Image _cardArt;
|
||
private Text _cardArtName;
|
||
private Text _cardTitle;
|
||
private Text _cardSubtitle;
|
||
private Text _cardStats;
|
||
private Text _cardAbilities;
|
||
private Text _cardOwned;
|
||
private Text _cardStatus;
|
||
private Button _dismantleButton;
|
||
private Button _craftButton;
|
||
private CollectionItemDto _selectedItem;
|
||
private CardCatalogItemDto _selectedMeta;
|
||
|
||
private readonly List<DeckDto> _decks = new List<DeckDto>();
|
||
private readonly List<CollectionItemDto> _collection = new List<CollectionItemDto>();
|
||
private readonly Dictionary<string, CardCatalogItemDto> _catalog =
|
||
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 _loading;
|
||
private bool _busy;
|
||
|
||
public DeckBuilderView(
|
||
Transform parent,
|
||
MetaService meta,
|
||
AuthService auth,
|
||
Action onBack,
|
||
Action<DeckDto> onPlay,
|
||
Action<MatchConnectionInfo> onPracticeStarted)
|
||
{
|
||
_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();
|
||
_cardOverlay = BuildCardOverlay();
|
||
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_root.gameObject.SetActive(false);
|
||
}
|
||
|
||
public void Show()
|
||
{
|
||
UiKit.SetActive(_root, true);
|
||
ShowDeckList();
|
||
_ = LoadAsync();
|
||
}
|
||
|
||
public void Hide()
|
||
{
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.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);
|
||
UiKit.AddHorizontalLayout(body, 0, 14);
|
||
|
||
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 scroll = UiKit.CreateGridScrollView(
|
||
collectionArea,
|
||
"CollectionGrid",
|
||
new Vector2(174f, 238f),
|
||
new Vector2(10f, 10f),
|
||
out _collectionContent);
|
||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||
scrollElement.flexibleHeight = 1f;
|
||
scrollElement.minHeight = 220f;
|
||
|
||
_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 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 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, 860f), PanelFill);
|
||
UiKit.AddVerticalLayout(card, 24, 10);
|
||
|
||
var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, TextPrimary);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 40f);
|
||
|
||
_gradeChips = BuildFilterSection(card, "Ранг", 44f);
|
||
_triggerChips = BuildFilterSection(card, "Триггер", 44f);
|
||
_clanChips = BuildFilterSection(card, "Клан", 300f, 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 BuildDeleteConfirm()
|
||
{
|
||
var overlay = UiKit.CreatePanel(
|
||
_root, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.78f));
|
||
|
||
var card = UiKit.CreateCenteredCard(
|
||
overlay, "ConfirmCard", new Vector2(640f, 300f), PanelFill);
|
||
UiKit.AddVerticalLayout(card, 30, 12);
|
||
|
||
var title = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, TextPrimary);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 42f);
|
||
|
||
_confirmText = UiKit.CreateText(
|
||
card, "Text", string.Empty, 19, TextMuted);
|
||
UiKit.SetPreferredHeight(_confirmText, 70f);
|
||
|
||
var yes = UiKit.CreateButton(card, "Yes", "Удалить", ConfirmDelete, 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;
|
||
|
||
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);
|
||
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 · "
|
||
+ FormatName(deck.Format),
|
||
12,
|
||
TextMuted,
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------ редактор колоды
|
||
|
||
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()
|
||
{
|
||
_title.text = "Колоды";
|
||
_deckListPanel.gameObject.SetActive(true);
|
||
_deckPanel.gameObject.SetActive(false);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
}
|
||
|
||
private void ShowDeckPanel()
|
||
{
|
||
_title.text = "Колода";
|
||
_deckListPanel.gameObject.SetActive(false);
|
||
_deckPanel.gameObject.SetActive(true);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
RebuildDeckPanel();
|
||
}
|
||
|
||
private void BackToDeckList()
|
||
{
|
||
_dirty = false;
|
||
_deckListPanel.gameObject.SetActive(true);
|
||
_deckPanel.gameObject.SetActive(false);
|
||
BuildDeckList();
|
||
RefreshCounts();
|
||
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 items = _collection
|
||
.Select(item => new { Item = item, Meta = MetaOf(item) })
|
||
.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);
|
||
}
|
||
|
||
private void AddCollectionCell(CollectionItemDto item, CardCatalogItemDto meta)
|
||
{
|
||
var cell = UiKit.CreatePanel(
|
||
_collectionContent, "Owned_" + item.PrintingId, RowFill);
|
||
UiKit.AddVerticalLayout(cell, 6, 2);
|
||
AddRightClick(cell.gameObject, item.PrintingId, meta);
|
||
|
||
var frame = cell.gameObject.AddComponent<Outline>();
|
||
frame.effectColor = CardArt.ClanColor(meta.Clan);
|
||
frame.effectDistance = new Vector2(2f, -2f);
|
||
|
||
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);
|
||
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 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 name = UiKit.CreateText(
|
||
cell, "Name", Loc.CardName(meta), 13, TextPrimary, TextAnchor.UpperLeft);
|
||
UiKit.MakeLiteral(name);
|
||
name.fontStyle = FontStyle.Bold;
|
||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(name, 32f);
|
||
|
||
var metaLine = UiKit.CreateText(
|
||
cell, "Meta", meta.Clan + " · " + meta.Rarity, 11, TextMuted, TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(metaLine, 16f);
|
||
|
||
var counts = UiKit.CreateText(
|
||
cell, "Counts", CountText(item), 13, new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(counts, 20f);
|
||
_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 = 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);
|
||
}
|
||
}
|
||
|
||
private string CountText(CollectionItemDto item) =>
|
||
"×" + item.Count + " · в колоде " + UsedByName(item.NameId);
|
||
|
||
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);
|
||
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();
|
||
RebuildFilterChips();
|
||
}
|
||
|
||
private void ResetFilters()
|
||
{
|
||
_filter.Clear();
|
||
UpdateFilterSummary();
|
||
RebuildCollection();
|
||
}
|
||
|
||
private void RebuildFilterChips()
|
||
{
|
||
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 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.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)));
|
||
|
||
_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)
|
||
{
|
||
_pendingDelete = deck;
|
||
_confirmText.text = deck.Name + "\nКолода будет удалена без восстановления.";
|
||
_confirmOverlay.SetAsLastSibling();
|
||
_confirmOverlay.gameObject.SetActive(true);
|
||
}
|
||
|
||
private void CancelDelete()
|
||
{
|
||
_pendingDelete = null;
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
}
|
||
|
||
private void ConfirmDelete()
|
||
{
|
||
var deck = _pendingDelete;
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_pendingDelete = null;
|
||
if (deck != null)
|
||
DeleteDeck(deck);
|
||
}
|
||
|
||
// ------------------------------------------------------------ карточка
|
||
|
||
private void AddRightClick(GameObject target, string printingId, CardCatalogItemDto meta)
|
||
{
|
||
var trigger = target.GetComponent<EventTrigger>()
|
||
?? target.AddComponent<EventTrigger>();
|
||
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerClick };
|
||
entry.callback.AddListener(data =>
|
||
{
|
||
if (data is PointerEventData pointer
|
||
&& pointer.button == PointerEventData.InputButton.Right)
|
||
{
|
||
ShowCardOverlay(printingId, meta);
|
||
}
|
||
});
|
||
trigger.triggers.Add(entry);
|
||
}
|
||
|
||
private void ShowCardOverlay(string printingId, CardCatalogItemDto meta)
|
||
{
|
||
if (meta == null)
|
||
return;
|
||
|
||
_selectedMeta = meta;
|
||
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
|
||
_cardStatus.text = string.Empty;
|
||
UpdateCardOverlay();
|
||
_cardOverlay.SetAsLastSibling();
|
||
_cardOverlay.gameObject.SetActive(true);
|
||
}
|
||
|
||
private void UpdateCardOverlay()
|
||
{
|
||
if (_selectedMeta == null)
|
||
return;
|
||
|
||
var meta = _selectedMeta;
|
||
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 ? "Разобрать (+" + 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();
|
||
_selectedItem = _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 DustFor(string rarity) => rarity switch
|
||
{
|
||
"C" => 5,
|
||
"R" => 10,
|
||
"RR" => 25,
|
||
"RRR" => 100,
|
||
_ => 0,
|
||
};
|
||
|
||
private static int CraftCostFor(string rarity) => rarity switch
|
||
{
|
||
"C" => 20,
|
||
"R" => 40,
|
||
"RR" => 100,
|
||
"RRR" => 400,
|
||
_ => 0,
|
||
};
|
||
|
||
private static void SetButtonLabel(Button button, string label)
|
||
{
|
||
var text = button.GetComponentInChildren<Text>();
|
||
if (text != null)
|
||
text.text = label;
|
||
}
|
||
|
||
private static 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);
|
||
}
|
||
}
|
||
}
|
||
}
|