281 lines
9.1 KiB
C#
281 lines
9.1 KiB
C#
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
|
||
{
|
||
/// <summary>Очередь classic-ranked и переход к матчу по тикету.</summary>
|
||
public sealed class QueueView
|
||
{
|
||
private readonly MetaService _meta;
|
||
private readonly AuthService _auth;
|
||
private readonly Action _onBack;
|
||
private readonly Action<MatchConnectionInfo> _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<MatchConnectionInfo> 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;
|
||
}
|
||
}
|
||
}
|