using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using Vanguard.Api; using Vanguard.Api.Contracts; using Vanguard.Auth; using Vanguard.Battle; using Vanguard.Meta; namespace Vanguard.UI { /// /// Оверлей входа в бой: выбор колоды и режима (рейтинговый или с ботом). /// Колода проверяется сервером до запуска. /// public sealed class BattleEntryOverlay { public const string RootName = "BattleEntryOverlay"; private static readonly Color PanelFill = new Color(0.024f, 0.058f, 0.15f, 0.98f); private static readonly Color Border = new Color(0.18f, 0.49f, 1f, 0.7f); private static readonly Color TextPrimary = new Color(0.88f, 0.93f, 1f); private static readonly Color TextMuted = new Color(0.58f, 0.69f, 0.88f); private static readonly Color TextDim = new Color(0.44f, 0.55f, 0.75f); private readonly MetaService _meta; private readonly AuthService _auth; private readonly Action _onRanked; private readonly Action _onPractice; private readonly Action _onBuildDeck; private readonly RectTransform _root; private readonly RectTransform _deckContent; private readonly Text _status; private readonly Button _play; private readonly Text _playLabel; private readonly Button _rankedMode; private readonly Button _botMode; private readonly List _decks = new List(); private DeckDto _selected; private bool? _valid; private bool _ranked = true; private bool _busy; private bool _loading; private int _validationVersion; public BattleEntryOverlay( Transform parent, MetaService meta, AuthService auth, Action onRanked, Action onPractice, Action onBuildDeck) { _meta = meta; _auth = auth; _onRanked = onRanked; _onPractice = onPractice; _onBuildDeck = onBuildDeck; _root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0.001f)); _root.GetComponent().raycastTarget = true; var panel = UiKit.CreateCenteredCard( _root, "Panel", new Vector2(820f, 720f), Color.white); var panelImage = panel.GetComponent(); panelImage.sprite = NeonUi.BodySliced(20, PanelFill, Border, 2); panelImage.type = Image.Type.Sliced; panelImage.raycastTarget = true; var title = UiKit.CreateText(panel, "Title", "В бой", 34, TextPrimary); title.fontStyle = FontStyle.Bold; PlaceTop(title.rectTransform, new Vector2(0f, -26f), 48f); var subtitle = UiKit.CreateText( panel, "Subtitle", "Выберите колоду и режим", 15, TextMuted); PlaceTop(subtitle.rectTransform, new Vector2(0f, -72f), 24f); var close = UiKit.CreateButton(panel, "Close", "×", () => Close(), 22); var closeRect = close.GetComponent(); closeRect.anchorMin = new Vector2(1f, 1f); closeRect.anchorMax = new Vector2(1f, 1f); closeRect.pivot = new Vector2(1f, 1f); closeRect.sizeDelta = new Vector2(40f, 40f); closeRect.anchoredPosition = new Vector2(-20f, -20f); var closeImage = close.GetComponent(); if (closeImage != null) { closeImage.sprite = NeonUi.BodySliced( 10, new Color(0.08f, 0.14f, 0.26f, 0.9f), Border, 1); closeImage.type = Image.Type.Sliced; closeImage.color = Color.white; } var closeLabel = close.GetComponentInChildren(); if (closeLabel != null) UiKit.MakeLiteral(closeLabel); _rankedMode = ModeButton(panel, "ModeRanked", "Рейтинговый", -160f, true); _botMode = ModeButton(panel, "ModeBot", "С ботом", 160f, false); _rankedMode.onClick.AddListener(() => SetMode(true)); _botMode.onClick.AddListener(() => SetMode(false)); var scroll = UiKit.CreateScrollView(panel, "DeckScroll", out _deckContent, 8f); scroll.anchorMin = new Vector2(0f, 0f); scroll.anchorMax = new Vector2(1f, 1f); scroll.offsetMin = new Vector2(30f, 196f); scroll.offsetMax = new Vector2(-30f, -170f); _status = UiKit.CreateText( panel, "Status", string.Empty, 16, TextMuted, TextAnchor.MiddleCenter); var statusRect = _status.rectTransform; statusRect.anchorMin = new Vector2(0f, 0f); statusRect.anchorMax = new Vector2(1f, 0f); statusRect.pivot = new Vector2(0.5f, 0f); statusRect.offsetMin = new Vector2(30f, 140f); statusRect.offsetMax = new Vector2(-30f, 172f); var build = UiKit.CreateButton( panel, "BuildDeck", "Собрать колоду", () => { Close(); _onBuildDeck(); }, 16); var buildRect = build.GetComponent(); buildRect.anchorMin = new Vector2(0.5f, 0f); buildRect.anchorMax = new Vector2(0.5f, 0f); buildRect.pivot = new Vector2(0.5f, 0f); buildRect.sizeDelta = new Vector2(240f, 48f); buildRect.anchoredPosition = new Vector2(-200f, 36f); var buildImage = build.GetComponent(); if (buildImage != null) ApplyFill(buildImage, new Color(0.06f, 0.12f, 0.24f, 0.95f), Border); _play = UiKit.CreateButton(panel, "Play", "В бой", Play, 20); var playRect = _play.GetComponent(); playRect.anchorMin = new Vector2(0.5f, 0f); playRect.anchorMax = new Vector2(0.5f, 0f); playRect.pivot = new Vector2(0.5f, 0f); playRect.sizeDelta = new Vector2(300f, 56f); playRect.anchoredPosition = new Vector2(130f, 30f); var playImage = _play.GetComponent(); if (playImage != null) ApplyFill(playImage, new Color(0.10f, 0.37f, 0.79f, 1f), Border); _playLabel = _play.GetComponentInChildren(); UpdateButtons(); _root.gameObject.SetActive(false); } public bool IsOpen => _root.gameObject.activeSelf; public void Show() { UiKit.SetActive(_root, true); _selected = null; _valid = null; SetStatus(string.Empty); UpdateButtons(); _ = LoadDecksAsync(); } public void Close() => UiKit.SetActive(_root, false); // ------------------------------------------------------------ колоды private async Task LoadDecksAsync() { if (_loading) return; _loading = true; SetStatus("Загружаем колоды…"); try { await _auth.EnsureAccessAsync(); var decks = await _meta.GetDecksAsync(); _decks.Clear(); if (decks != null) _decks.AddRange(decks); RebuildDeckList(); if (_decks.Count == 0) SetStatus("Нет колод. Соберите колоду."); else SetStatus(string.Empty); } catch (ApiException exception) { SetStatus(exception.Message); } catch (Exception exception) { Debug.LogException(exception); SetStatus("Не удалось загрузить колоды."); } finally { _loading = false; } } private void RebuildDeckList() { for (var i = _deckContent.childCount - 1; i >= 0; i--) UnityEngine.Object.Destroy(_deckContent.GetChild(i).gameObject); foreach (var deck in _decks) { var item = UiKit.CreateButton( _deckContent, "Deck_" + deck.Id, string.Empty, () => Select(deck), 15); var image = item.GetComponent(); ApplyFill(image, DeckFill(false), Border); var element = item.gameObject.AddComponent(); element.preferredHeight = 64f; var name = UiKit.CreateText( item.transform, "Name", deck.Name, 17, TextPrimary, TextAnchor.MiddleLeft); var nameRect = name.rectTransform; nameRect.anchorMin = new Vector2(0f, 0.5f); nameRect.anchorMax = new Vector2(0.7f, 0.5f); nameRect.offsetMin = new Vector2(18f, 4f); nameRect.offsetMax = new Vector2(0f, 24f); UiKit.MakeLiteral(name); var count = UiKit.CreateText( item.transform, "Count", deck.Cards.Count + Loc.Translate(" карт"), 13, TextDim, TextAnchor.MiddleRight); var countRect = count.rectTransform; countRect.anchorMin = new Vector2(0.7f, 0.5f); countRect.anchorMax = new Vector2(1f, 0.5f); countRect.offsetMin = new Vector2(0f, 4f); countRect.offsetMax = new Vector2(-18f, 24f); UiKit.MakeLiteral(count); } } private async void Select(DeckDto deck) { if (_busy) return; _selected = deck; _valid = null; UpdateButtons(); HighlightSelection(); var version = ++_validationVersion; SetStatus("Проверяем колоду…"); try { var result = await _meta.ValidateDeckAsync(deck.Id, deck.Cards); if (version != _validationVersion) return; _valid = result != null && result.Valid; SetStatus(_valid == true ? "Колода готова." : FirstViolation(result)); } catch (ApiException exception) { _valid = false; SetStatus(exception.Message); } catch (Exception exception) { Debug.LogException(exception); _valid = false; SetStatus("Не удалось проверить колоду."); } UpdateButtons(); } private static string FirstViolation(DeckValidationResponse result) { if (result == null || result.Violations == null || result.Violations.Count == 0) return "Колода не прошла проверку."; return result.Violations[0].Message; } private void HighlightSelection() { foreach (Transform child in _deckContent) { var image = child.GetComponent(); if (image == null) continue; var selected = _selected != null && child.name == "Deck_" + _selected.Id; ApplyFill(image, DeckFill(selected), selected ? BorderStrong : Border); } } private static Color DeckFill(bool selected) => selected ? new Color(0.16f, 0.40f, 0.80f, 0.95f) : new Color(0.05f, 0.10f, 0.22f, 0.9f); private static readonly Color BorderStrong = new Color(0.34f, 0.72f, 1f, 0.9f); private static void ApplyFill(Image image, Color fill, Color border) { image.sprite = NeonUi.BodySliced(12, fill, border, 1); image.type = Image.Type.Sliced; image.color = Color.white; } // ------------------------------------------------------------ режим и запуск private Button ModeButton( Transform parent, string name, string label, float x, bool active) { var button = UiKit.CreateButton(parent, name, label, null, 16); var rect = button.GetComponent(); rect.anchorMin = new Vector2(0.5f, 1f); rect.anchorMax = new Vector2(0.5f, 1f); rect.pivot = new Vector2(0.5f, 1f); rect.sizeDelta = new Vector2(300f, 52f); rect.anchoredPosition = new Vector2(x, -108f); var image = button.GetComponent(); ApplyFill(image, DeckFill(active), active ? BorderStrong : Border); return button; } private void SetMode(bool ranked) { _ranked = ranked; ApplyFill(_rankedMode.GetComponent(), DeckFill(ranked), ranked ? BorderStrong : Border); ApplyFill(_botMode.GetComponent(), DeckFill(!ranked), !ranked ? BorderStrong : Border); } private async void Play() { if (_busy || _selected == null || _valid != true) return; if (_ranked) { var deck = _selected; Close(); _onRanked(deck); return; } _busy = true; SetStatus("Готовим тренировочный матч…"); UpdateButtons(); try { await _auth.EnsureAccessAsync(); var response = await _meta.StartPracticeAsync(_selected.Id); var deckName = _selected.Name; Close(); _onPractice(new MatchConnectionInfo { MatchId = response.MatchId, Seat = response.Seat, Ticket = response.Ticket, WsUrl = response.WsUrl, DeckName = deckName, }); } catch (ApiException exception) { SetStatus(exception.Message); } catch (Exception exception) { Debug.LogException(exception); SetStatus("Не удалось начать матч с ботом."); } finally { _busy = false; UpdateButtons(); } } private void UpdateButtons() { var ready = !_busy && _selected != null && _valid == true; _play.interactable = ready; if (_playLabel != null) _playLabel.color = ready ? Color.white : new Color(1f, 1f, 1f, 0.45f); } private void SetStatus(string text) { if (_status != null) _status.text = text ?? string.Empty; } private static void PlaceTop(RectTransform rect, Vector2 position, float height) { rect.anchorMin = new Vector2(0f, 1f); rect.anchorMax = new Vector2(1f, 1f); rect.pivot = new Vector2(0.5f, 1f); rect.sizeDelta = new Vector2(-60f, height); rect.anchoredPosition = position; } } }