VanguardReMaster/client/Assets/Vanguard/Runtime/UI/QueueView.cs

281 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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