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

1406 lines
54 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.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.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);
_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.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);
_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));
_cardArtName.text = meta.NameEn;
_cardTitle.text = meta.NameEn;
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
+ 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;
_cardAbilities.text = string.IsNullOrWhiteSpace(meta.TextRu)
? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn)
: meta.TextRu;
_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 => pair.Meta.NameEn, 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", meta.NameEn, 12,
new Color(1f, 1f, 1f, 0.9f), TextAnchor.MiddleCenter);
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", meta.NameEn, 13, Color.white, TextAnchor.UpperLeft);
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 ? meta.NameEn : 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);
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 _))
return;
if (_cards.Count >= 50)
{
SetEditorStatus("В колоде уже 50 карт.", true);
return;
}
if (UsedByName(item.NameId) >= 4)
{
SetEditorStatus("Не больше 4 копий одной карты.", 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];
_deckCount.text = "Колода: " + _cards.Count + "/50";
}
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;
_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 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);
}
}
}
}