889 lines
34 KiB
C#
889 lines
34 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using Vanguard.Api;
|
||
using Vanguard.Api.Contracts;
|
||
using Vanguard.Auth;
|
||
using Vanguard.Meta;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>
|
||
/// Простой декбилдер: коллекция слева, текущая колода справа,
|
||
/// фильтры в оверлее, статистика по триггерам и грейдам.
|
||
/// </summary>
|
||
public sealed class DeckBuilderView
|
||
{
|
||
private readonly MetaService _meta;
|
||
private readonly AuthService _auth;
|
||
private readonly Action _onBack;
|
||
private readonly Action<DeckDto> _onPlay;
|
||
|
||
private readonly RectTransform _root;
|
||
private readonly RectTransform _listView;
|
||
private readonly RectTransform _editorView;
|
||
private RectTransform _listContent;
|
||
private RectTransform _collectionContent;
|
||
private RectTransform _deckContent;
|
||
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 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> _collectionCountTexts =
|
||
new Dictionary<string, Text>(StringComparer.Ordinal);
|
||
private DeckDto _editing;
|
||
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)
|
||
{
|
||
_meta = meta;
|
||
_auth = auth;
|
||
_onBack = onBack;
|
||
_onPlay = onPlay;
|
||
|
||
_root = UiKit.CreatePanel(parent, "DeckBuilderScreen", new Color(0.018f, 0.03f, 0.06f, 0.97f));
|
||
|
||
_listView = BuildListView();
|
||
_editorView = BuildEditorView();
|
||
_filterOverlay = BuildFilterOverlay();
|
||
_confirmOverlay = BuildDeleteConfirm();
|
||
_listView.gameObject.SetActive(false);
|
||
_editorView.gameObject.SetActive(false);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.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);
|
||
_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, "Колоды", out _);
|
||
var create = UiKit.CreateButton(top, "Create", "Создать колоду", NewDeck, 19);
|
||
UiKit.SetPreferredHeight(create, 46f, 0f);
|
||
|
||
var body = UiKit.CreatePanel(view, "Body", new Color(0f, 0f, 0f, 0f));
|
||
body.anchorMin = Vector2.zero;
|
||
body.anchorMax = Vector2.one;
|
||
body.offsetMin = new Vector2(44f, 30f);
|
||
body.offsetMax = new Vector2(-44f, -110f);
|
||
UiKit.AddVerticalLayout(body, 8, 10);
|
||
|
||
var hint = UiKit.CreateText(
|
||
body,
|
||
"Hint",
|
||
"Откройте колоду для редактирования или создайте новую. «В бой» ставит колоду в очередь.",
|
||
17,
|
||
new Color(0.60f, 0.68f, 0.84f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(hint, 26f);
|
||
|
||
var scroll = UiKit.CreateScrollView(body, "DeckListScroll", out _listContent, 8f);
|
||
var element = scroll.gameObject.AddComponent<LayoutElement>();
|
||
element.flexibleHeight = 1f;
|
||
element.minHeight = 200f;
|
||
|
||
_listStatus = UiKit.CreateText(
|
||
body, "Status", string.Empty, 18,
|
||
new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_listStatus, 34f);
|
||
|
||
return view;
|
||
}
|
||
|
||
private RectTransform BuildEditorView()
|
||
{
|
||
var view = UiKit.CreatePanel(_root, "DeckEditor", new Color(0.018f, 0.03f, 0.06f, 0.97f));
|
||
|
||
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);
|
||
|
||
var back = UiKit.CreateButton(top, "Back", "← Колоды", BackToCards, 18);
|
||
UiKit.SetPreferredHeight(back, 44f, 0f);
|
||
|
||
var title = UiKit.CreateText(top, "Title", "Конструктор", 28, Color.white, TextAnchor.MiddleLeft);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 44f, 0f);
|
||
|
||
_nameInput = UiKit.CreateInput(top, "Name", "Название колоды");
|
||
UiKit.SetPreferredHeight(_nameInput, 46f);
|
||
|
||
var save = UiKit.CreateButton(top, "Save", "Сохранить", SaveDeck, 18);
|
||
UiKit.SetPreferredHeight(save, 44f, 0f);
|
||
|
||
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, 30f);
|
||
body.offsetMax = new Vector2(-34f, -106f);
|
||
UiKit.AddHorizontalLayout(body, 0, 14);
|
||
|
||
var left = UiKit.CreatePanel(body, "CollectionArea", new Color(0.04f, 0.06f, 0.10f, 0.7f));
|
||
var leftElement = left.gameObject.AddComponent<LayoutElement>();
|
||
leftElement.flexibleWidth = 1f;
|
||
leftElement.flexibleHeight = 1f;
|
||
leftElement.minHeight = 300f;
|
||
UiKit.AddVerticalLayout(left, 10, 6);
|
||
|
||
var filterRow = UiKit.CreatePanel(left, "FilterRow", new Color(0f, 0f, 0f, 0f));
|
||
UiKit.SetPreferredHeight(filterRow, 48f);
|
||
UiKit.AddHorizontalLayout(filterRow, 0, 10);
|
||
|
||
var filters = UiKit.CreateButton(filterRow, "Filters", "Фильтры", OpenFilters, 18);
|
||
UiKit.SetPreferredHeight(filters, 44f, 0f);
|
||
|
||
var reset = UiKit.CreateButton(filterRow, "ResetFilters", "Сбросить фильтры", ResetFilters, 17);
|
||
UiKit.SetPreferredHeight(reset, 44f, 0f);
|
||
|
||
_filterSummary = UiKit.CreateText(
|
||
filterRow, "FilterSummary", "Фильтры: все", 16,
|
||
new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_filterSummary, 44f);
|
||
|
||
var statsPanel = UiKit.CreatePanel(left, "Stats", new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
UiKit.SetPreferredHeight(statsPanel, 76f);
|
||
UiKit.AddVerticalLayout(statsPanel, 8, 2);
|
||
|
||
_stats = UiKit.CreateText(
|
||
statsPanel, "Text", string.Empty, 16,
|
||
new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft);
|
||
_stats.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(_stats, 56f);
|
||
|
||
var gridScroll = UiKit.CreateGridScrollView(
|
||
left,
|
||
"CollectionGrid",
|
||
new Vector2(166f, 186f),
|
||
new Vector2(8f, 8f),
|
||
out _collectionContent);
|
||
var gridElement = gridScroll.gameObject.AddComponent<LayoutElement>();
|
||
gridElement.flexibleHeight = 1f;
|
||
gridElement.minHeight = 220f;
|
||
|
||
var right = UiKit.CreatePanel(body, "DeckArea", new Color(0.04f, 0.06f, 0.10f, 0.85f));
|
||
var rightElement = right.gameObject.AddComponent<LayoutElement>();
|
||
rightElement.preferredWidth = 430f;
|
||
rightElement.minWidth = 360f;
|
||
rightElement.flexibleWidth = 0f;
|
||
rightElement.flexibleHeight = 1f;
|
||
rightElement.minHeight = 300f;
|
||
UiKit.AddVerticalLayout(right, 10, 6);
|
||
|
||
_deckCount = UiKit.CreateText(
|
||
right, "DeckCount", "Колода: 0/50", 20, Color.white, TextAnchor.MiddleLeft);
|
||
_deckCount.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(_deckCount, 30f);
|
||
|
||
var deckScroll = UiKit.CreateHorizontalScrollView(
|
||
right, "DeckScroll", out _deckContent, 6f);
|
||
var deckElement = deckScroll.gameObject.AddComponent<LayoutElement>();
|
||
deckElement.flexibleHeight = 1f;
|
||
deckElement.minHeight = 220f;
|
||
|
||
var editorStatusPanel = UiKit.CreatePanel(right, "StatusPanel", new Color(0f, 0f, 0f, 0f));
|
||
UiKit.SetPreferredHeight(editorStatusPanel, 54f);
|
||
_editorStatus = UiKit.CreateText(
|
||
editorStatusPanel, "Status", string.Empty, 16,
|
||
new Color(0.62f, 0.68f, 0.82f), TextAnchor.UpperLeft);
|
||
_editorStatus.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(_editorStatus, 50f);
|
||
|
||
return view;
|
||
}
|
||
|
||
private RectTransform BuildFilterOverlay()
|
||
{
|
||
var overlay = UiKit.CreatePanel(
|
||
_editorView, "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 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 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 RectTransform BuildTopBar(RectTransform view, string titleText, out Text title)
|
||
{
|
||
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);
|
||
|
||
var back = UiKit.CreateButton(top, "Back", "← В лобби", () => _onBack(), 18);
|
||
UiKit.SetPreferredHeight(back, 44f, 0f);
|
||
|
||
title = UiKit.CreateText(top, "Title", titleText, 30, Color.white, TextAnchor.MiddleLeft);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 46f);
|
||
return top;
|
||
}
|
||
|
||
private async Task LoadAsync()
|
||
{
|
||
if (_loading)
|
||
return;
|
||
|
||
_loading = true;
|
||
_listStatus.text = "Загружаем коллекцию и колоды…";
|
||
_listStatus.color = new Color(0.62f, 0.68f, 0.82f);
|
||
ClearChildren(_listContent);
|
||
|
||
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>());
|
||
_catalog.Clear();
|
||
foreach (var item in catalog)
|
||
_catalog[item.PrintingId] = item;
|
||
RebuildClanFilters();
|
||
_decks.Clear();
|
||
_decks.AddRange(decks);
|
||
|
||
BuildDeckList();
|
||
_listStatus.text = _decks.Count == 0
|
||
? "Колод пока нет. Создайте первую."
|
||
: "Колод: " + _decks.Count + ".";
|
||
}
|
||
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(_listContent);
|
||
if (_decks.Count == 0)
|
||
return;
|
||
|
||
foreach (var deck in _decks)
|
||
{
|
||
var row = UiKit.CreatePanel(
|
||
_listContent, "Deck_" + deck.Id, new Color(0.07f, 0.09f, 0.14f, 1f));
|
||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||
rowElement.preferredHeight = 82f;
|
||
UiKit.AddHorizontalLayout(row, 14, 10);
|
||
|
||
var name = UiKit.CreateText(
|
||
row, "Name", deck.Name, 21, Color.white, TextAnchor.MiddleLeft);
|
||
name.fontStyle = FontStyle.Bold;
|
||
var nameElement = name.gameObject.AddComponent<LayoutElement>();
|
||
nameElement.flexibleWidth = 1f;
|
||
nameElement.preferredHeight = 40f;
|
||
|
||
var info = UiKit.CreateText(
|
||
row, "Info", deck.Cards.Count + " карт · " + deck.Format, 16,
|
||
new Color(0.60f, 0.68f, 0.84f), TextAnchor.MiddleLeft);
|
||
var infoElement = info.gameObject.AddComponent<LayoutElement>();
|
||
infoElement.preferredWidth = 190f;
|
||
infoElement.preferredHeight = 36f;
|
||
infoElement.flexibleWidth = 0f;
|
||
|
||
var edit = UiKit.CreateButton(row, "Edit", "Открыть", () => OpenDeck(deck), 17);
|
||
var editElement = edit.gameObject.AddComponent<LayoutElement>();
|
||
editElement.preferredWidth = 120f;
|
||
editElement.preferredHeight = 44f;
|
||
editElement.flexibleWidth = 0f;
|
||
|
||
var play = UiKit.CreateButton(row, "Play", "В бой", () => _onPlay(deck), 17);
|
||
var playElement = play.gameObject.AddComponent<LayoutElement>();
|
||
playElement.preferredWidth = 100f;
|
||
playElement.preferredHeight = 44f;
|
||
playElement.flexibleWidth = 0f;
|
||
|
||
var delete = UiKit.CreateButton(row, "Delete", "Удалить", () => RequestDelete(deck), 16);
|
||
var deleteElement = delete.gameObject.AddComponent<LayoutElement>();
|
||
deleteElement.preferredWidth = 105f;
|
||
deleteElement.preferredHeight = 44f;
|
||
deleteElement.flexibleWidth = 0f;
|
||
}
|
||
}
|
||
|
||
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);
|
||
_nameInput.text = _editing?.Name ?? "Новая колода";
|
||
_deckCount.text = "Колода: " + _cards.Count + "/50";
|
||
_editorStatus.text = string.Empty;
|
||
RebuildEditor();
|
||
}
|
||
|
||
private void BackToCards()
|
||
{
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_editorView.gameObject.SetActive(false);
|
||
_listView.gameObject.SetActive(true);
|
||
BuildDeckList();
|
||
}
|
||
|
||
private void RebuildEditor()
|
||
{
|
||
UpdateStats();
|
||
RebuildDeck();
|
||
RebuildCollection();
|
||
_deckCount.text = "Колода: " + _cards.Count + "/50";
|
||
}
|
||
|
||
private void RebuildCollection()
|
||
{
|
||
ClearChildren(_collectionContent);
|
||
_collectionCountTexts.Clear();
|
||
if (_catalog.Count == 0)
|
||
return;
|
||
|
||
foreach (var item in _collection.OrderBy(item => GradeOf(item)).ThenBy(item => item.NameEn, StringComparer.Ordinal))
|
||
{
|
||
if (!_catalog.TryGetValue(item.PrintingId, out var meta))
|
||
continue;
|
||
|
||
if (_filterGrade >= 0 && meta.Grade != _filterGrade)
|
||
continue;
|
||
if (_filterTrigger != "Все" && meta.Trigger != _filterTrigger)
|
||
continue;
|
||
if (_filterClan != "Все" && meta.Clan != _filterClan)
|
||
continue;
|
||
|
||
AddCollectionCell(item, meta);
|
||
}
|
||
}
|
||
|
||
private void AddCollectionCell(CollectionItemDto item, CardCatalogItemDto meta)
|
||
{
|
||
var cell = UiKit.CreatePanel(
|
||
_collectionContent, "Owned_" + item.PrintingId, new Color(0.08f, 0.11f, 0.17f, 1f));
|
||
var button = cell.gameObject.AddComponent<Button>();
|
||
var image = cell.GetComponent<Image>();
|
||
button.targetGraphic = image;
|
||
button.onClick.AddListener(() => AddCard(item));
|
||
|
||
UiKit.AddVerticalLayout(cell, 6, 2);
|
||
|
||
var name = UiKit.CreateText(
|
||
cell, "Name", item.NameEn, 14, Color.white, TextAnchor.UpperLeft);
|
||
name.fontStyle = FontStyle.Bold;
|
||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(name, 34f);
|
||
|
||
var printing = UiKit.CreateText(
|
||
cell, "Printing", item.PrintingId, 11,
|
||
new Color(0.56f, 0.64f, 0.78f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(printing, 16f);
|
||
|
||
var info = UiKit.CreateText(
|
||
cell, "Info", "G" + meta.Grade + " · " + meta.Trigger + " · " + meta.Clan,
|
||
12,
|
||
new Color(0.66f, 0.74f, 0.90f),
|
||
TextAnchor.MiddleLeft);
|
||
info.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(info, 30f);
|
||
|
||
var counts = UiKit.CreateText(
|
||
cell,
|
||
"Counts",
|
||
meta.Rarity + " · есть " + item.Count + " · в колоде " + UsedByName(item.NameId),
|
||
12,
|
||
new Color(0.56f, 0.64f, 0.78f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(counts, 16f);
|
||
_collectionCountTexts[item.PrintingId] = counts;
|
||
}
|
||
|
||
private void RebuildDeck()
|
||
{
|
||
ClearChildren(_deckContent);
|
||
if (_cards.Count == 0)
|
||
{
|
||
var empty = UiKit.CreateText(
|
||
_deckContent, "Empty", "Колода пуста", 16,
|
||
new Color(0.62f, 0.68f, 0.82f));
|
||
UiKit.SetPreferredHeight(empty, 120f);
|
||
return;
|
||
}
|
||
|
||
foreach (var group in _cards.GroupBy(card => card).OrderBy(group => group.Key))
|
||
{
|
||
var printingId = group.Key;
|
||
var count = group.Count();
|
||
var name = _catalog.TryGetValue(printingId, out var meta)
|
||
? meta.NameEn
|
||
: printingId;
|
||
|
||
var cell = UiKit.CreatePanel(
|
||
_deckContent, "Deck_" + printingId, new Color(0.09f, 0.12f, 0.20f, 1f));
|
||
var element = cell.gameObject.AddComponent<LayoutElement>();
|
||
element.preferredWidth = 142f;
|
||
element.preferredHeight = 190f;
|
||
element.flexibleWidth = 0f;
|
||
UiKit.AddVerticalLayout(cell, 6, 2);
|
||
|
||
var nameText = UiKit.CreateText(
|
||
cell, "Name", name, 13, Color.white, TextAnchor.UpperLeft);
|
||
nameText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(nameText, 38f);
|
||
|
||
var idText = UiKit.CreateText(
|
||
cell, "Printing", printingId, 10,
|
||
new Color(0.56f, 0.64f, 0.78f));
|
||
UiKit.SetPreferredHeight(idText, 16f);
|
||
|
||
var info = UiKit.CreateText(
|
||
cell,
|
||
"Info",
|
||
"×" + count + (meta != null ? " · G" + meta.Grade + " · " + meta.Trigger : string.Empty),
|
||
12,
|
||
new Color(0.70f, 0.78f, 0.94f));
|
||
UiKit.SetPreferredHeight(info, 22f);
|
||
|
||
var remove = UiKit.CreateButton(
|
||
cell, "Remove", "−", () => RemoveCard(printingId), 20);
|
||
UiKit.SetPreferredHeight(remove, 38f);
|
||
}
|
||
}
|
||
|
||
private void AddCard(CollectionItemDto item)
|
||
{
|
||
if (!_catalog.TryGetValue(item.PrintingId, out var meta))
|
||
return;
|
||
|
||
if (_cards.Count >= 50)
|
||
{
|
||
SetEditorStatus("В колоде уже 50 карт.");
|
||
return;
|
||
}
|
||
|
||
if (UsedByName(item.NameId) >= 4)
|
||
{
|
||
SetEditorStatus("Не больше 4 копий одной карты.");
|
||
return;
|
||
}
|
||
|
||
if (_cards.Count(card => card == item.PrintingId) >= item.Count)
|
||
{
|
||
SetEditorStatus("В коллекции больше нет свободных копий.");
|
||
return;
|
||
}
|
||
|
||
_cards.Add(item.PrintingId);
|
||
SetEditorStatus(string.Empty);
|
||
RefreshAfterDeckChange();
|
||
}
|
||
|
||
private void RemoveCard(string printingId)
|
||
{
|
||
var index = _cards.LastIndexOf(printingId);
|
||
if (index >= 0)
|
||
_cards.RemoveAt(index);
|
||
|
||
RefreshAfterDeckChange();
|
||
}
|
||
|
||
private void RefreshAfterDeckChange()
|
||
{
|
||
UpdateStats();
|
||
RebuildDeck();
|
||
RefreshCollectionCounts();
|
||
_deckCount.text = "Колода: " + _cards.Count + "/50";
|
||
}
|
||
|
||
private void RefreshCollectionCounts()
|
||
{
|
||
foreach (var pair in _collectionCountTexts)
|
||
{
|
||
var item = _collection.Find(candidate => candidate.PrintingId == pair.Key);
|
||
if (item == null || !_catalog.TryGetValue(pair.Key, out var meta))
|
||
continue;
|
||
|
||
pair.Value.text = meta.Rarity + " · есть " + item.Count
|
||
+ " · в колоде " + UsedByName(item.NameId);
|
||
}
|
||
}
|
||
|
||
private void UpdateStats()
|
||
{
|
||
var triggers = new Dictionary<string, int>(StringComparer.Ordinal)
|
||
{
|
||
["—"] = 0, ["K"] = 0, ["D"] = 0, ["S"] = 0, ["H"] = 0,
|
||
};
|
||
var grades = new int[4];
|
||
|
||
foreach (var printingId in _cards)
|
||
{
|
||
if (!_catalog.TryGetValue(printingId, out var meta))
|
||
continue;
|
||
|
||
if (meta.Grade >= 0 && meta.Grade < grades.Length)
|
||
grades[meta.Grade]++;
|
||
|
||
var key = meta.Trigger switch
|
||
{
|
||
"Critical" => "K",
|
||
"Draw" => "D",
|
||
"Stand" => "S",
|
||
"Heal" => "H",
|
||
_ => "—",
|
||
};
|
||
triggers[key]++;
|
||
}
|
||
|
||
_stats.text = "Триггеры: — " + triggers["—"] + " · K " + triggers["K"]
|
||
+ " · D " + triggers["D"] + " · S " + triggers["S"] + " · H " + triggers["H"]
|
||
+ "\nGrade: 0 — " + grades[0] + " · 1 — " + grades[1]
|
||
+ " · 2 — " + grades[2] + " · 3 — " + grades[3];
|
||
}
|
||
|
||
private int UsedByName(string nameId) =>
|
||
_cards.Count(printingId =>
|
||
_catalog.TryGetValue(printingId, out var meta) && meta.NameId == nameId);
|
||
|
||
private int GradeOf(CollectionItemDto item) =>
|
||
_catalog.TryGetValue(item.PrintingId, out var meta) ? meta.Grade : int.MaxValue;
|
||
|
||
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);
|
||
UpdateFilterSummary();
|
||
RebuildCollection();
|
||
}
|
||
|
||
private void OnTriggerFilter(string value)
|
||
{
|
||
_filterTrigger = value;
|
||
UpdateFilterSummary();
|
||
RebuildCollection();
|
||
}
|
||
|
||
private void OnClanFilter(string value)
|
||
{
|
||
_filterClan = value;
|
||
UpdateFilterSummary();
|
||
RebuildCollection();
|
||
}
|
||
|
||
private void ResetFilters()
|
||
{
|
||
_filterGrade = -1;
|
||
_filterTrigger = "Все";
|
||
_filterClan = "Все";
|
||
UpdateFilterSummary();
|
||
RebuildCollection();
|
||
}
|
||
|
||
private void UpdateFilterSummary()
|
||
{
|
||
_filterSummary.text = "Фильтры: "
|
||
+ (_filterClan == "Все" ? "клан все" : "клан " + _filterClan)
|
||
+ " · " + (_filterTrigger == "Все" ? "триггер все" : _filterTrigger)
|
||
+ " · " + (_filterGrade < 0 ? "grade все" : "grade " + _filterGrade);
|
||
}
|
||
|
||
private async void SaveDeck()
|
||
{
|
||
if (_busy)
|
||
return;
|
||
|
||
_busy = true;
|
||
SetEditorStatus("Сохраняем…");
|
||
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);
|
||
SetEditorStatus("Сохранено: " + result.Cards.Count + " карт.");
|
||
_deckCount.text = "Колода: " + _cards.Count + "/50";
|
||
RebuildEditor();
|
||
}
|
||
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);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetEditorStatus("Не удалось сохранить колоду.");
|
||
}
|
||
finally
|
||
{
|
||
_busy = false;
|
||
}
|
||
}
|
||
|
||
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 async void DeleteDeck(DeckDto deck)
|
||
{
|
||
if (_busy)
|
||
return;
|
||
|
||
_busy = true;
|
||
try
|
||
{
|
||
await _meta.DeleteDeckAsync(deck.Id);
|
||
_decks.RemoveAll(item => item.Id == deck.Id);
|
||
BuildDeckList();
|
||
_listStatus.text = "Колода удалена.";
|
||
}
|
||
catch (ApiException exception)
|
||
{
|
||
_listStatus.text = exception.Message;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
_listStatus.text = "Не удалось удалить колоду.";
|
||
}
|
||
finally
|
||
{
|
||
_busy = false;
|
||
}
|
||
}
|
||
|
||
private void SetEditorStatus(string text)
|
||
{
|
||
_editorStatus.text = text;
|
||
_editorStatus.color = text.StartsWith("Не удалось", StringComparison.Ordinal)
|
||
|| text.Contains("Не больше") || text.Contains("В колоде уже") || text.Contains("больше нет")
|
||
? new Color(1f, 0.55f, 0.45f)
|
||
: new Color(0.62f, 0.68f, 0.82f);
|
||
}
|
||
|
||
private void ShowList()
|
||
{
|
||
_editorView.gameObject.SetActive(false);
|
||
_filterOverlay.gameObject.SetActive(false);
|
||
_confirmOverlay.gameObject.SetActive(false);
|
||
_listView.gameObject.SetActive(true);
|
||
}
|
||
|
||
private static void ClearChildren(Transform parent)
|
||
{
|
||
for (var i = parent.childCount - 1; i >= 0; i--)
|
||
UnityEngine.Object.Destroy(parent.GetChild(i).gameObject);
|
||
}
|
||
}
|
||
}
|