1436 lines
55 KiB
C#
1436 lines
55 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>
|
||
/// Декбилдер в духе HS: коллекция в центре, узкая колонка колод/карт справа.
|
||
/// Список колод и редактор используют один и тот же блок коллекции.
|
||
/// </summary>
|
||
public sealed class DeckBuilderView
|
||
{
|
||
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 readonly RectTransform _listView;
|
||
private readonly RectTransform _editorView;
|
||
private RectTransform _deckListContent;
|
||
private RectTransform _listCollectionContent;
|
||
private RectTransform _editorCollectionContent;
|
||
private RectTransform _deckCardContent;
|
||
private InputField _nameInput;
|
||
private Text _listStatus;
|
||
private Text _editorStatus;
|
||
private Text _stats;
|
||
private Text _deckCount;
|
||
private Text _filterSummary;
|
||
private RectTransform _filterOverlay;
|
||
private RectTransform _clanContent;
|
||
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> _listCountTexts =
|
||
new Dictionary<string, Text>(StringComparer.Ordinal);
|
||
private readonly Dictionary<string, Text> _editorCountTexts =
|
||
new Dictionary<string, Text>(StringComparer.Ordinal);
|
||
|
||
private DeckDto _editing;
|
||
private int _dust;
|
||
private string _filterClan = "Все";
|
||
private string _filterTrigger = "Все";
|
||
private int _filterGrade = -1;
|
||
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));
|
||
_listView = BuildListView();
|
||
_editorView = BuildEditorView();
|
||
_filterOverlay = BuildFilterOverlay();
|
||
_confirmOverlay = BuildDeleteConfirm();
|
||
_cardOverlay = BuildCardOverlay();
|
||
|
||
_listView.gameObject.SetActive(false);
|
||
_editorView.gameObject.SetActive(false);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_root.gameObject.SetActive(false);
|
||
}
|
||
|
||
public void Show()
|
||
{
|
||
UiKit.SetActive(_root, true);
|
||
ShowList();
|
||
_ = LoadAsync();
|
||
}
|
||
|
||
public void Hide()
|
||
{
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_root.gameObject.SetActive(false);
|
||
}
|
||
|
||
private RectTransform BuildListView()
|
||
{
|
||
var view = UiKit.CreatePanel(_root, "DeckList", new Color(0.018f, 0.03f, 0.06f, 0.97f));
|
||
|
||
var top = BuildTopBar(view);
|
||
var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 17);
|
||
SetFixedSize(back, 120f, 44f);
|
||
|
||
var title = UiKit.CreateText(top, "Title", "Коллекция", 28, Color.white, TextAnchor.MiddleLeft);
|
||
title.fontStyle = FontStyle.Bold;
|
||
SetFixedSize(title, 150f, 44f);
|
||
|
||
_filterSummary = UiKit.CreateText(
|
||
top, "FilterSummary", "Все карты", 16,
|
||
new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft);
|
||
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);
|
||
|
||
var create = UiKit.CreateButton(top, "Create", "Создать колоду", NewDeck, 17);
|
||
SetFixedSize(create, 180f, 44f);
|
||
|
||
var body = UiKit.CreatePanel(view, "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 _listCollectionContent);
|
||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||
scrollElement.flexibleHeight = 1f;
|
||
scrollElement.minHeight = 220f;
|
||
|
||
_listStatus = UiKit.CreateText(
|
||
collectionArea, "Status", string.Empty, 16,
|
||
new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_listStatus, 32f);
|
||
|
||
var deckArea = UiKit.CreatePanel(
|
||
body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f));
|
||
var deckElement = deckArea.gameObject.AddComponent<LayoutElement>();
|
||
deckElement.preferredWidth = 300f;
|
||
deckElement.minWidth = 280f;
|
||
deckElement.flexibleWidth = 0f;
|
||
deckElement.flexibleHeight = 1f;
|
||
deckElement.minHeight = 300f;
|
||
UiKit.AddVerticalLayout(deckArea, 10, 6);
|
||
|
||
var deckTitle = UiKit.CreateText(
|
||
deckArea, "Title", "Мои колоды", 20, Color.white, TextAnchor.MiddleLeft);
|
||
deckTitle.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(deckTitle, 30f);
|
||
|
||
var deckScroll = UiKit.CreateScrollView(deckArea, "DeckListScroll", out _deckListContent, 8f);
|
||
var deckScrollElement = deckScroll.gameObject.AddComponent<LayoutElement>();
|
||
deckScrollElement.flexibleHeight = 1f;
|
||
deckScrollElement.minHeight = 200f;
|
||
|
||
var hint = UiKit.CreateText(
|
||
deckArea, "Hint", "Нажмите на колоду,\nчтобы открыть редактор.", 14,
|
||
new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft);
|
||
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(hint, 46f);
|
||
|
||
return view;
|
||
}
|
||
|
||
private RectTransform BuildEditorView()
|
||
{
|
||
var view = UiKit.CreatePanel(_root, "DeckEditor", new Color(0.018f, 0.03f, 0.06f, 0.97f));
|
||
|
||
var top = BuildTopBar(view);
|
||
var back = UiKit.CreateButton(top, "Back", "← Коллекция", BackToList, 17);
|
||
SetFixedSize(back, 120f, 44f);
|
||
|
||
_nameInput = UiKit.CreateInput(top, "Name", "Название колоды");
|
||
SetFlexible(_nameInput, 0.7f, 46f);
|
||
|
||
var filters = UiKit.CreateButton(top, "Filters", "Фильтры", OpenFilters, 17);
|
||
SetFixedSize(filters, 110f, 44f);
|
||
|
||
var reset = UiKit.CreateButton(top, "ResetFilters", "Сброс", ResetFilters, 17);
|
||
SetFixedSize(reset, 90f, 44f);
|
||
|
||
_stats = UiKit.CreateText(
|
||
top, "Stats", string.Empty, 15,
|
||
new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft);
|
||
_stats.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
SetFlexible(_stats, 1.4f, 50f);
|
||
|
||
var save = UiKit.CreateButton(top, "Save", "Сохранить", SaveDeck, 17);
|
||
SetFixedSize(save, 120f, 44f);
|
||
|
||
var bot = UiKit.CreateButton(top, "Bot", "С ботом", PlayBot, 17);
|
||
SetFixedSize(bot, 100f, 44f);
|
||
|
||
var duel = UiKit.CreateButton(top, "Duel", "В бой", PlayDeck, 17);
|
||
SetFixedSize(duel, 90f, 44f);
|
||
|
||
var delete = UiKit.CreateButton(top, "Delete", "Удалить", RequestDeleteCurrent, 17);
|
||
SetFixedSize(delete, 100f, 44f);
|
||
|
||
var body = UiKit.CreatePanel(view, "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 _editorCollectionContent);
|
||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||
scrollElement.flexibleHeight = 1f;
|
||
scrollElement.minHeight = 220f;
|
||
|
||
_editorStatus = UiKit.CreateText(
|
||
collectionArea, "Status", string.Empty, 16,
|
||
new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_editorStatus, 32f);
|
||
|
||
var deckArea = UiKit.CreatePanel(
|
||
body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f));
|
||
var deckElement = deckArea.gameObject.AddComponent<LayoutElement>();
|
||
deckElement.preferredWidth = 300f;
|
||
deckElement.minWidth = 280f;
|
||
deckElement.flexibleWidth = 0f;
|
||
deckElement.flexibleHeight = 1f;
|
||
deckElement.minHeight = 300f;
|
||
UiKit.AddVerticalLayout(deckArea, 10, 6);
|
||
|
||
_deckCount = UiKit.CreateText(
|
||
deckArea, "DeckCount", "Колода: 0/50", 19, Color.white, TextAnchor.MiddleLeft);
|
||
_deckCount.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(_deckCount, 28f);
|
||
|
||
var deckScroll = UiKit.CreateScrollView(deckArea, "DeckCardScroll", out _deckCardContent, 6f);
|
||
var deckScrollElement = deckScroll.gameObject.AddComponent<LayoutElement>();
|
||
deckScrollElement.flexibleHeight = 1f;
|
||
deckScrollElement.minHeight = 200f;
|
||
|
||
var hint = UiKit.CreateText(
|
||
deckArea, "Hint", "ЛКМ — убрать копию,\nПКМ — посмотреть карту.", 14,
|
||
new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft);
|
||
hint.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(hint, 46f);
|
||
|
||
return view;
|
||
}
|
||
|
||
private RectTransform BuildTopBar(RectTransform view)
|
||
{
|
||
var top = UiKit.CreatePanel(view, "TopBar", new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
top.anchorMin = new Vector2(0f, 1f);
|
||
top.anchorMax = new Vector2(1f, 1f);
|
||
top.pivot = new Vector2(0.5f, 1f);
|
||
top.anchoredPosition = Vector2.zero;
|
||
top.sizeDelta = new Vector2(0f, 88f);
|
||
UiKit.AddHorizontalLayout(top, 18, 12);
|
||
return top;
|
||
}
|
||
|
||
private RectTransform BuildFilterOverlay()
|
||
{
|
||
var overlay = UiKit.CreatePanel(_root, "FilterOverlay", new Color(0f, 0f, 0f, 0.78f));
|
||
|
||
var card = UiKit.CreateCenteredCard(
|
||
overlay, "FilterCard", new Vector2(860f, 820f),
|
||
new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
UiKit.AddVerticalLayout(card, 26, 10);
|
||
|
||
var title = UiKit.CreateText(card, "Title", "Фильтры коллекции", 28, Color.white);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 40f);
|
||
|
||
AddFilterSection(card, "Ранг", new[] { "Все", "0", "1", "2", "3" }, OnGradeFilter);
|
||
AddFilterSection(card, "Триггер", new[] { "Все", "None", "Critical", "Draw", "Stand", "Heal" }, OnTriggerFilter);
|
||
|
||
var clanTitle = UiKit.CreateText(
|
||
card, "ClanTitle", "Клан", 17, new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(clanTitle, 24f);
|
||
|
||
var clanScroll = UiKit.CreateScrollView(card, "ClanScroll", out _clanContent, 4f);
|
||
var clanElement = clanScroll.gameObject.AddComponent<LayoutElement>();
|
||
clanElement.flexibleHeight = 1f;
|
||
clanElement.minHeight = 150f;
|
||
|
||
var close = UiKit.CreateButton(card, "Close", "Готово", CloseFilters, 20);
|
||
UiKit.SetPreferredHeight(close, 48f);
|
||
|
||
return overlay;
|
||
}
|
||
|
||
private void RebuildClanFilters()
|
||
{
|
||
ClearChildren(_clanContent);
|
||
AddClanButton(_clanContent, "Все");
|
||
foreach (var clan in _catalog.Values.Select(item => item.Clan).Distinct().OrderBy(item => item))
|
||
AddClanButton(_clanContent, clan);
|
||
}
|
||
|
||
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),
|
||
new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
UiKit.AddVerticalLayout(card, 30, 12);
|
||
|
||
var title = UiKit.CreateText(card, "Title", "Удалить колоду?", 28, Color.white);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 42f);
|
||
|
||
_confirmText = UiKit.CreateText(
|
||
card, "Text", string.Empty, 19,
|
||
new Color(0.72f, 0.78f, 0.92f));
|
||
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,
|
||
new Color(0.62f, 0.70f, 0.84f), 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,
|
||
new Color(0.66f, 0.74f, 0.90f), 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,
|
||
new Color(0.62f, 0.68f, 0.82f), 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 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 = new Color(0.62f, 0.68f, 0.82f);
|
||
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 = new Color(0.62f, 0.68f, 0.82f);
|
||
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;
|
||
RebuildCollectionGrids();
|
||
_selectedItem = _collection.Find(item => item.PrintingId == printingId);
|
||
if (_selectedItem == null)
|
||
{
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_listStatus.text = "Карт в коллекции: " + _collection.Count + ".";
|
||
}
|
||
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 AddFilterSection(Transform parent, string title, string[] labels, Action<string> onSelected)
|
||
{
|
||
var caption = UiKit.CreateText(
|
||
parent, "Caption_" + title, title, 17,
|
||
new Color(0.66f, 0.74f, 0.90f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(caption, 24f);
|
||
|
||
var row = UiKit.CreatePanel(parent, "Row_" + title, new Color(0f, 0f, 0f, 0f));
|
||
UiKit.SetPreferredHeight(row, 44f);
|
||
UiKit.AddHorizontalLayout(row, 0, 6);
|
||
|
||
foreach (var label in labels)
|
||
{
|
||
var captured = label;
|
||
var button = UiKit.CreateButton(
|
||
row, "Filter_" + label, label, () => onSelected(captured), 16);
|
||
UiKit.SetPreferredHeight(button, 40f);
|
||
}
|
||
}
|
||
|
||
private void AddClanButton(Transform parent, string clan)
|
||
{
|
||
var captured = clan;
|
||
var button = UiKit.CreateButton(
|
||
parent, "Clan_" + clan, clan, () => OnClanFilter(captured), 16);
|
||
UiKit.SetPreferredHeight(button, 40f);
|
||
}
|
||
|
||
private async Task LoadAsync()
|
||
{
|
||
if (_loading)
|
||
return;
|
||
|
||
_loading = true;
|
||
_listStatus.text = "Загружаем коллекцию и колоды…";
|
||
_listStatus.color = new Color(0.62f, 0.68f, 0.82f);
|
||
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;
|
||
RebuildClanFilters();
|
||
|
||
_decks.Clear();
|
||
_decks.AddRange(decks);
|
||
BuildDeckList();
|
||
RebuildCollectionGrids();
|
||
_listStatus.text = _decks.Count == 0
|
||
? "Колод пока нет. Пыль: " + _dust + ". ПКМ по карте — подробности."
|
||
: "Карт в коллекции: " + _collection.Count + " · пыль " + _dust
|
||
+ " · ПКМ по карте — подробности.";
|
||
}
|
||
catch (ApiException exception)
|
||
{
|
||
_listStatus.text = exception.Message;
|
||
_listStatus.color = new Color(1f, 0.55f, 0.45f);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
_listStatus.text = "Не удалось загрузить декбилдер.";
|
||
_listStatus.color = new Color(1f, 0.55f, 0.45f);
|
||
}
|
||
finally
|
||
{
|
||
_loading = false;
|
||
}
|
||
}
|
||
|
||
private void BuildDeckList()
|
||
{
|
||
ClearChildren(_deckListContent);
|
||
|
||
if (_decks.Count == 0)
|
||
{
|
||
var empty = UiKit.CreateText(
|
||
_deckListContent, "Empty", "Нет колод", 16,
|
||
new Color(0.58f, 0.66f, 0.80f));
|
||
UiKit.SetPreferredHeight(empty, 90f);
|
||
return;
|
||
}
|
||
|
||
foreach (var deck in _decks)
|
||
{
|
||
var row = UiKit.CreatePanel(
|
||
_deckListContent, "DeckRow_" + deck.Id, new Color(0.08f, 0.11f, 0.17f, 1f));
|
||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||
rowElement.preferredHeight = 76f;
|
||
UiKit.AddVerticalLayout(row, 10, 2);
|
||
|
||
var button = row.gameObject.AddComponent<Button>();
|
||
button.targetGraphic = row.GetComponent<Image>();
|
||
button.onClick.AddListener(() => OpenDeck(deck));
|
||
|
||
var name = UiKit.CreateText(
|
||
row, "Name", deck.Name, 17, Color.white, TextAnchor.MiddleLeft);
|
||
name.fontStyle = FontStyle.Bold;
|
||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(name, 30f);
|
||
|
||
var count = UiKit.CreateText(
|
||
row, "Count", deck.Cards.Count + "/50 · " + deck.Format, 13,
|
||
new Color(0.62f, 0.70f, 0.84f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(count, 20f);
|
||
}
|
||
}
|
||
|
||
private void NewDeck()
|
||
{
|
||
_editing = null;
|
||
_cards.Clear();
|
||
_nameInput.text = "Новая колода";
|
||
ShowEditor();
|
||
}
|
||
|
||
private void OpenDeck(DeckDto deck)
|
||
{
|
||
_editing = deck;
|
||
_cards.Clear();
|
||
_cards.AddRange(deck.Cards);
|
||
_nameInput.text = deck.Name;
|
||
ShowEditor();
|
||
}
|
||
|
||
private void ShowEditor()
|
||
{
|
||
_listView.gameObject.SetActive(false);
|
||
_editorView.gameObject.SetActive(true);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_editorStatus.text = string.Empty;
|
||
RebuildEditor();
|
||
}
|
||
|
||
private void BackToList()
|
||
{
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_editorView.gameObject.SetActive(false);
|
||
_listView.gameObject.SetActive(true);
|
||
RefreshAllCounts();
|
||
}
|
||
|
||
private void ShowList()
|
||
{
|
||
_editorView.gameObject.SetActive(false);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_cardOverlay.gameObject.SetActive(false);
|
||
_listView.gameObject.SetActive(true);
|
||
}
|
||
|
||
private void RebuildEditor()
|
||
{
|
||
UpdateStats();
|
||
RebuildDeckCards();
|
||
RebuildEditorCollection();
|
||
RefreshAllCounts();
|
||
}
|
||
|
||
private void RebuildCollectionGrids()
|
||
{
|
||
RebuildListCollection();
|
||
RebuildEditorCollection();
|
||
}
|
||
|
||
private void RebuildListCollection()
|
||
{
|
||
BuildCollectionGrid(_listCollectionContent, _listCountTexts, showDeckCount: false, onAdd: null);
|
||
}
|
||
|
||
private void RebuildEditorCollection()
|
||
{
|
||
BuildCollectionGrid(_editorCollectionContent, _editorCountTexts, showDeckCount: true, onAdd: AddCard);
|
||
}
|
||
|
||
private void BuildCollectionGrid(
|
||
RectTransform content,
|
||
Dictionary<string, Text> countTexts,
|
||
bool showDeckCount,
|
||
Action<CollectionItemDto> onAdd)
|
||
{
|
||
ClearChildren(content);
|
||
countTexts.Clear();
|
||
|
||
var items = _collection
|
||
.Select(item => new { Item = item, Meta = MetaOf(item) })
|
||
.Where(pair => pair.Meta != null && MatchesFilter(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(content, pair.Item, pair.Meta, countTexts, showDeckCount, onAdd);
|
||
}
|
||
|
||
private void AddCollectionCell(
|
||
RectTransform content,
|
||
CollectionItemDto item,
|
||
CardCatalogItemDto meta,
|
||
Dictionary<string, Text> countTexts,
|
||
bool showDeckCount,
|
||
Action<CollectionItemDto> onAdd)
|
||
{
|
||
var cell = UiKit.CreatePanel(
|
||
content, "Owned_" + item.PrintingId, new Color(0.08f, 0.11f, 0.17f, 1f));
|
||
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);
|
||
|
||
if (onAdd != null)
|
||
{
|
||
var button = cell.gameObject.AddComponent<Button>();
|
||
button.targetGraphic = cell.GetComponent<Image>();
|
||
button.onClick.AddListener(() => onAdd(item));
|
||
}
|
||
|
||
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
||
UiKit.SetPreferredHeight(art, 112f);
|
||
var artImage = art.GetComponent<Image>();
|
||
CardImageService.Apply(
|
||
artImage,
|
||
meta.ImageKey,
|
||
CardArt.ForCard(meta.NameId, meta.Clan));
|
||
|
||
var artName = UiKit.CreateText(
|
||
art, "ArtName", Loc.CardName(meta), 12,
|
||
new Color(1f, 1f, 1f, 0.9f), TextAnchor.MiddleCenter);
|
||
UiKit.MakeLiteral(artName);
|
||
UiKit.Stretch(artName.rectTransform);
|
||
artName.rectTransform.offsetMin = new Vector2(6f, 8f);
|
||
artName.rectTransform.offsetMax = new Vector2(-6f, -6f);
|
||
artName.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
|
||
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, Color.white, 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,
|
||
new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(metaLine, 16f);
|
||
|
||
var counts = UiKit.CreateText(
|
||
cell, "Counts", CountText(item, showDeckCount), 13,
|
||
new Color(0.98f, 0.82f, 0.42f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(counts, 20f);
|
||
countTexts[item.PrintingId] = counts;
|
||
}
|
||
|
||
private void RebuildDeckCards()
|
||
{
|
||
ClearChildren(_deckCardContent);
|
||
|
||
if (_cards.Count == 0)
|
||
{
|
||
var empty = UiKit.CreateText(
|
||
_deckCardContent, "Empty", "Колода пуста", 15,
|
||
new Color(0.58f, 0.66f, 0.80f));
|
||
UiKit.SetPreferredHeight(empty, 90f);
|
||
return;
|
||
}
|
||
|
||
foreach (var group in _cards.GroupBy(card => card).OrderBy(group => group.Key))
|
||
{
|
||
var printingId = group.Key;
|
||
var count = group.Count();
|
||
var meta = _catalog.TryGetValue(printingId, out var found) ? found : null;
|
||
var name = meta != null ? Loc.CardName(meta) : printingId;
|
||
|
||
var row = UiKit.CreatePanel(
|
||
_deckCardContent, "DeckCard_" + printingId, new Color(0.08f, 0.11f, 0.17f, 1f));
|
||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||
rowElement.preferredHeight = 64f;
|
||
UiKit.AddHorizontalLayout(row, 6, 6);
|
||
|
||
var button = row.gameObject.AddComponent<Button>();
|
||
button.targetGraphic = row.GetComponent<Image>();
|
||
button.onClick.AddListener(() => RemoveCard(printingId));
|
||
if (meta != null)
|
||
AddRightClick(row.gameObject, printingId, meta);
|
||
|
||
var art = UiKit.CreatePanel(
|
||
row, "Art", Color.white);
|
||
SetFixedSize(art, 36f, 50f);
|
||
var artImage = art.GetComponent<Image>();
|
||
CardImageService.Apply(
|
||
artImage,
|
||
meta != null ? meta.ImageKey : null,
|
||
CardArt.ForCard(
|
||
meta != null ? meta.NameId : printingId,
|
||
meta != null ? meta.Clan : "None"));
|
||
|
||
var artText = UiKit.CreateText(
|
||
art, "ArtText", meta != null ? "G" + meta.Grade : "?", 13,
|
||
Color.white, TextAnchor.MiddleCenter);
|
||
UiKit.Stretch(artText.rectTransform);
|
||
|
||
var texts = UiKit.CreatePanel(row, "Texts", new Color(0f, 0f, 0f, 0f));
|
||
var textsElement = texts.gameObject.AddComponent<LayoutElement>();
|
||
textsElement.flexibleWidth = 1f;
|
||
textsElement.preferredHeight = 52f;
|
||
UiKit.AddVerticalLayout(texts, 2, 0);
|
||
|
||
var nameText = UiKit.CreateText(
|
||
texts, "Name", name, 13, Color.white, TextAnchor.MiddleLeft);
|
||
UiKit.MakeLiteral(nameText);
|
||
nameText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(nameText, 30f);
|
||
|
||
var info = UiKit.CreateText(
|
||
texts, "Info",
|
||
"×" + count + (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty),
|
||
12,
|
||
new Color(0.62f, 0.70f, 0.84f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(info, 18f);
|
||
}
|
||
}
|
||
|
||
private void AddCard(CollectionItemDto item)
|
||
{
|
||
if (!_catalog.TryGetValue(item.PrintingId, out var itemMeta))
|
||
return;
|
||
|
||
if (_cards.Count >= 50)
|
||
{
|
||
SetEditorStatus("В колоде уже 50 карт.", true);
|
||
return;
|
||
}
|
||
|
||
if (UsedByName(item.NameId) >= 4)
|
||
{
|
||
SetEditorStatus("Не больше 4 копий одной карты.", true);
|
||
return;
|
||
}
|
||
|
||
if (IsTrigger(itemMeta.Trigger) && TriggerCount() >= 16)
|
||
{
|
||
SetEditorStatus("В колоде уже 16 триггеров.", true);
|
||
return;
|
||
}
|
||
|
||
if (_cards.Count(card => card == item.PrintingId) >= item.Count)
|
||
{
|
||
SetEditorStatus("В коллекции больше нет свободных копий.", true);
|
||
return;
|
||
}
|
||
|
||
_cards.Add(item.PrintingId);
|
||
SetEditorStatus(string.Empty, false);
|
||
RefreshAfterDeckChange();
|
||
}
|
||
|
||
private void RemoveCard(string printingId)
|
||
{
|
||
var index = _cards.LastIndexOf(printingId);
|
||
if (index >= 0)
|
||
_cards.RemoveAt(index);
|
||
|
||
RefreshAfterDeckChange();
|
||
}
|
||
|
||
private void RefreshAfterDeckChange()
|
||
{
|
||
UpdateStats();
|
||
RebuildDeckCards();
|
||
RefreshAllCounts();
|
||
}
|
||
|
||
private void RefreshAllCounts()
|
||
{
|
||
RefreshCounts(_listCountTexts, showDeckCount: false);
|
||
RefreshCounts(_editorCountTexts, showDeckCount: true);
|
||
}
|
||
|
||
private void RefreshCounts(Dictionary<string, Text> countTexts, bool showDeckCount)
|
||
{
|
||
foreach (var pair in countTexts)
|
||
{
|
||
var item = _collection.Find(candidate => candidate.PrintingId == pair.Key);
|
||
if (item == null)
|
||
continue;
|
||
|
||
pair.Value.text = CountText(item, showDeckCount);
|
||
}
|
||
}
|
||
|
||
private string CountText(CollectionItemDto item, bool showDeckCount)
|
||
{
|
||
var text = "×" + item.Count;
|
||
if (showDeckCount)
|
||
text += " · в колоде " + UsedByName(item.NameId);
|
||
return text;
|
||
}
|
||
|
||
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]++;
|
||
}
|
||
}
|
||
|
||
_stats.text = "Всего: " + _cards.Count + "/50 G0 " + grades[0]
|
||
+ " · G1 " + grades[1] + " · G2 " + grades[2] + " · G3 " + grades[3]
|
||
+ " · T " + TriggerCount() + "/16";
|
||
_deckCount.text = "Колода: " + _cards.Count + "/50 · триггеры: " + TriggerCount() + "/16";
|
||
}
|
||
|
||
private void OpenFilters()
|
||
{
|
||
_filterOverlay.SetAsLastSibling();
|
||
_filterOverlay.gameObject.SetActive(true);
|
||
}
|
||
|
||
private void CloseFilters()
|
||
{
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
}
|
||
|
||
private void OnGradeFilter(string value)
|
||
{
|
||
_filterGrade = value == "Все" ? -1 : int.Parse(value);
|
||
OnFiltersChanged();
|
||
}
|
||
|
||
private void OnTriggerFilter(string value)
|
||
{
|
||
_filterTrigger = value;
|
||
OnFiltersChanged();
|
||
}
|
||
|
||
private void OnClanFilter(string value)
|
||
{
|
||
_filterClan = value;
|
||
OnFiltersChanged();
|
||
}
|
||
|
||
private void ResetFilters()
|
||
{
|
||
_filterGrade = -1;
|
||
_filterTrigger = "Все";
|
||
_filterClan = "Все";
|
||
OnFiltersChanged();
|
||
}
|
||
|
||
private void OnFiltersChanged()
|
||
{
|
||
UpdateFilterSummary();
|
||
RebuildCollectionGrids();
|
||
}
|
||
|
||
private void UpdateFilterSummary()
|
||
{
|
||
_filterSummary.text = (_filterClan == "Все" ? "Клан: все" : "Клан: " + _filterClan)
|
||
+ " · " + (_filterTrigger == "Все" ? "Триггер: все" : "Триггер: " + _filterTrigger)
|
||
+ " · " + (_filterGrade < 0 ? "Ранг: все" : "Ранг: " + _filterGrade);
|
||
}
|
||
|
||
private async void SaveDeck()
|
||
{
|
||
if (_busy)
|
||
return;
|
||
|
||
if (_cards.Count != 50 || TriggerCount() != 16)
|
||
{
|
||
SetEditorStatus("Нужно ровно 50 карт и 16 триггеров.", true);
|
||
return;
|
||
}
|
||
|
||
_busy = true;
|
||
SetEditorStatus("Сохраняем…", 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.text = result.Name;
|
||
UpsertDeck(result);
|
||
BuildDeckList();
|
||
RebuildEditor();
|
||
SetEditorStatus("Сохранено: " + 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;
|
||
SetEditorStatus(exception.Message + details, true);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetEditorStatus("Не удалось сохранить колоду.", true);
|
||
}
|
||
finally
|
||
{
|
||
_busy = false;
|
||
}
|
||
}
|
||
|
||
private void PlayDeck()
|
||
{
|
||
if (_editing == null)
|
||
{
|
||
SetEditorStatus("Сначала сохраните колоду.", true);
|
||
return;
|
||
}
|
||
|
||
_onPlay(_editing);
|
||
}
|
||
|
||
private async void PlayBot()
|
||
{
|
||
if (_busy)
|
||
return;
|
||
|
||
if (_editing == null)
|
||
{
|
||
SetEditorStatus("Сначала сохраните колоду.", true);
|
||
return;
|
||
}
|
||
|
||
_busy = true;
|
||
SetEditorStatus("Готовим тренировочный матч…", 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)
|
||
{
|
||
SetEditorStatus(exception.Message, true);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetEditorStatus("Не удалось начать матч с ботом.", true);
|
||
}
|
||
finally
|
||
{
|
||
_busy = false;
|
||
}
|
||
}
|
||
|
||
private void RequestDeleteCurrent()
|
||
{
|
||
if (_editing == null)
|
||
{
|
||
SetEditorStatus("Сначала сохраните колоду.", 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 (_editorView.gameObject.activeSelf)
|
||
BackToList();
|
||
_listStatus.text = "Колода удалена.";
|
||
}
|
||
catch (ApiException exception)
|
||
{
|
||
_listStatus.text = exception.Message;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
_listStatus.text = "Не удалось удалить колоду.";
|
||
}
|
||
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 SetEditorStatus(string text, bool error)
|
||
{
|
||
if (_editorStatus == null)
|
||
return;
|
||
|
||
_editorStatus.text = text;
|
||
_editorStatus.color = error
|
||
? new Color(1f, 0.55f, 0.45f)
|
||
: new Color(0.62f, 0.68f, 0.82f);
|
||
}
|
||
|
||
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 bool MatchesFilter(CardCatalogItemDto meta)
|
||
{
|
||
if (_filterGrade >= 0 && meta.Grade != _filterGrade)
|
||
return false;
|
||
if (_filterTrigger != "Все" && meta.Trigger != _filterTrigger)
|
||
return false;
|
||
if (_filterClan != "Все" && meta.Clan != _filterClan)
|
||
return false;
|
||
return true;
|
||
}
|
||
|
||
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) =>
|
||
!string.IsNullOrEmpty(trigger)
|
||
&& !string.Equals(trigger, "None", StringComparison.Ordinal);
|
||
|
||
private static string TriggerShort(string trigger) => trigger switch
|
||
{
|
||
"Critical" => "K",
|
||
"Draw" => "D",
|
||
"Stand" => "S",
|
||
"Heal" => "H",
|
||
_ => "—",
|
||
};
|
||
|
||
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 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);
|
||
}
|
||
}
|
||
}
|
||
}
|