using System;
using System.Threading;
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
{
/// Очередь classic-ranked и переход к матчу по тикету.
public sealed class QueueView
{
private readonly MetaService _meta;
private readonly AuthService _auth;
private readonly Action _onBack;
private readonly Action _onMatched;
private readonly RectTransform _root;
private readonly Text _deckName;
private readonly Text _status;
private readonly Text _details;
private readonly Button _cancel;
private readonly Button _retry;
private DeckDto _deck;
private CancellationTokenSource _cts;
private Guid? _queueId;
private bool _matched;
private bool _busy;
public QueueView(
Transform parent,
MetaService meta,
AuthService auth,
Action onBack,
Action onMatched)
{
_meta = meta;
_auth = auth;
_onBack = onBack;
_onMatched = onMatched;
_root = UiKit.CreatePanel(parent, "QueueScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
var card = UiKit.CreateCenteredCard(
_root, "QueueCard", new Vector2(780f, 520f),
new Color(0.07f, 0.085f, 0.115f, 1f));
UiKit.AddVerticalLayout(card, 36, 14);
var title = UiKit.CreateText(card, "Title", "Поиск противника", 42, Color.white);
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 58f);
_deckName = UiKit.CreateText(
card, "Deck", "Колода", 22,
new Color(0.72f, 0.78f, 0.92f));
UiKit.SetPreferredHeight(_deckName, 34f);
var mode = UiKit.CreateText(
card, "Mode", "Classic Ranked · скрытый MMR", 18,
new Color(0.58f, 0.65f, 0.78f));
UiKit.SetPreferredHeight(mode, 26f);
_status = UiKit.CreateText(
card, "Status", "Готовим очередь…", 26,
new Color(0.86f, 0.90f, 1f));
UiKit.SetPreferredHeight(_status, 74f);
_details = UiKit.CreateText(
card, "Details", string.Empty, 18,
new Color(0.62f, 0.68f, 0.82f));
UiKit.SetPreferredHeight(_details, 38f);
_cancel = UiKit.CreateButton(card, "Cancel", "Отменить поиск", OnCancelClicked, 21);
UiKit.SetPreferredHeight(_cancel, 52f);
_retry = UiKit.CreateButton(card, "Retry", "Повторить поиск", OnRetryClicked, 19);
UiKit.SetPreferredHeight(_retry, 44f);
_retry.gameObject.SetActive(false);
_root.gameObject.SetActive(false);
}
public void Show(DeckDto deck)
{
_deck = deck;
UiKit.SetActive(_root, true);
_deckName.text = "Колода: " + (deck != null ? deck.Name : "—");
_retry.gameObject.SetActive(false);
_ = StartAsync();
}
public void Hide()
{
CancelPolling();
if (_queueId.HasValue && !_matched)
{
var queueId = _queueId.Value;
_queueId = null;
_ = CancelServerQueueAsync(queueId);
}
_matched = false;
UiKit.SetActive(_root, false);
}
private async Task StartAsync()
{
if (_busy || _deck == null)
return;
CancelPolling();
_busy = true;
_matched = false;
_queueId = null;
_retry.gameObject.SetActive(false);
_cancel.interactable = false;
SetStatus("Готовим очередь…", new Color(0.72f, 0.78f, 0.92f));
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
await _auth.EnsureAccessAsync();
var accepted = await _meta.EnqueueAsync(_deck.Id, "classic-ranked");
_queueId = accepted.QueueId;
SetStatus("Ищем соперника…", new Color(0.86f, 0.90f, 1f));
_cancel.interactable = true;
while (!token.IsCancellationRequested)
{
var status = await _meta.GetQueueStatusAsync(accepted.QueueId);
if (token.IsCancellationRequested)
return;
if (status.Status == "matched")
{
_matched = true;
await EnterMatchAsync(status);
return;
}
if (status.Status is "failed" or "expired" or "cancelled")
{
_queueId = null;
SetStatus(
status.Status == "failed"
? "Не удалось создать матч."
: "Очередь закрыта: " + status.Status + ".",
new Color(1f, 0.60f, 0.45f));
_details.text = status.Reason ?? string.Empty;
_retry.gameObject.SetActive(true);
return;
}
_details.text = "Ожидание: " + status.WaitedSeconds + " с · окно MMR: ±" + status.Window;
_cancel.interactable = true;
await Task.Delay(1000, token);
}
}
catch (OperationCanceledException)
{
// пользователь отменил
}
catch (ApiException exception)
{
SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f));
_retry.gameObject.SetActive(true);
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Очередь недоступна. Проверьте сервер.", new Color(1f, 0.55f, 0.45f));
_retry.gameObject.SetActive(true);
}
finally
{
_busy = false;
_cancel.interactable = true;
}
}
private async Task EnterMatchAsync(QueueStatusResponse status)
{
SetStatus("Матч найден! Подключаемся…", new Color(0.45f, 0.85f, 0.55f));
_cancel.interactable = false;
string ticket = status.Ticket;
string wsUrl = status.WsUrl;
if (string.IsNullOrEmpty(ticket) && !string.IsNullOrEmpty(status.MatchId))
{
var issued = await _meta.CreateTicketAsync(status.MatchId);
ticket = issued.Ticket;
wsUrl = issued.WsUrl;
}
if (string.IsNullOrEmpty(ticket))
throw new ApiException(500, "ticket.missing", "Сервер не выдал тикет.");
if (string.IsNullOrEmpty(wsUrl))
wsUrl = _meta.BaseUrl.Replace("https://", "wss://").Replace("http://", "ws://")
.TrimEnd('/') + "/game";
_onMatched(new MatchConnectionInfo
{
MatchId = status.MatchId,
Seat = status.Seat,
Ticket = ticket,
WsUrl = wsUrl,
DeckName = _deck != null ? _deck.Name : string.Empty,
});
}
private async void OnCancelClicked()
{
await CancelAndBackAsync();
}
private async void OnRetryClicked()
{
await StartAsync();
}
private async Task CancelAndBackAsync()
{
CancelPolling();
if (_queueId.HasValue && !_matched)
{
var queueId = _queueId.Value;
_queueId = null;
await CancelServerQueueAsync(queueId);
}
_matched = false;
_onBack();
}
private async Task CancelServerQueueAsync(Guid queueId)
{
try
{
await _meta.CancelQueueAsync(queueId);
}
catch (Exception exception)
{
Debug.LogException(exception);
}
}
private void CancelPolling()
{
if (_cts == null)
return;
try
{
_cts.Cancel();
}
catch (Exception)
{
// ignore
}
_cts.Dispose();
_cts = null;
}
private void SetStatus(string text, Color color)
{
_status.text = text ?? string.Empty;
_status.color = color;
}
}
}