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;
}
}
}