746 lines
28 KiB
C#
746 lines
28 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using Vanguard.Api;
|
||
using Vanguard.Api.Contracts;
|
||
using Vanguard.Battle;
|
||
using Vanguard.Meta;
|
||
using Vanguard.Shared.Protocol;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>
|
||
/// Серый боевой экран: доска, рука, часы и кнопки всех легальных интентов.
|
||
/// Красоту и анимации добавим поверх этой рабочей логики.
|
||
/// </summary>
|
||
public sealed class BattleView
|
||
{
|
||
private static readonly Color Background = new Color(0.035f, 0.045f, 0.065f, 1f);
|
||
private static readonly Color Panel = new Color(0.07f, 0.085f, 0.115f, 1f);
|
||
private static readonly Color Card = new Color(0.10f, 0.13f, 0.18f, 1f);
|
||
private static readonly Color Muted = new Color(0.62f, 0.68f, 0.82f);
|
||
private static readonly Color Accent = new Color(0.20f, 0.42f, 0.78f, 1f);
|
||
|
||
private readonly MetaService _meta;
|
||
private readonly Action _onExit;
|
||
|
||
private readonly RectTransform _root;
|
||
private readonly RectTransform _board;
|
||
private RectTransform _intents;
|
||
private readonly Text _topInfo;
|
||
private readonly Text _clock;
|
||
private readonly Text _status;
|
||
private readonly Button _concede;
|
||
private readonly Text _concedeLabel;
|
||
private readonly Button _reconnect;
|
||
private readonly RectTransform _result;
|
||
private Text _resultText;
|
||
|
||
private GameConnection _connection;
|
||
private CancellationTokenSource _cts;
|
||
private MatchConnectionInfo _info;
|
||
private MatchView _view;
|
||
private bool _connecting;
|
||
private bool _sending;
|
||
private bool _snapshotPending;
|
||
private bool _finished;
|
||
private bool _confirmConcede;
|
||
private float _confirmDeadline;
|
||
private long _clockServerMs;
|
||
private long _clockReceivedMs;
|
||
|
||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||
{
|
||
_meta = meta;
|
||
_onExit = onExit;
|
||
|
||
_root = UiKit.CreatePanel(parent, "BattleScreen", Background);
|
||
|
||
var top = UiKit.CreatePanel(_root, "TopBar", Panel);
|
||
top.anchorMin = new Vector2(0f, 1f);
|
||
top.anchorMax = new Vector2(1f, 1f);
|
||
top.pivot = new Vector2(0.5f, 1f);
|
||
top.anchoredPosition = Vector2.zero;
|
||
top.sizeDelta = new Vector2(0f, 88f);
|
||
UiKit.AddHorizontalLayout(top, 20, 14);
|
||
|
||
var title = UiKit.CreateText(top, "Title", "Бой", 32, Color.white, TextAnchor.MiddleLeft);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 46f, 0f);
|
||
|
||
_topInfo = UiKit.CreateText(
|
||
top, "Match", string.Empty, 17, Muted, TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_topInfo, 38f);
|
||
|
||
_clock = UiKit.CreateText(
|
||
top, "Clock", "—", 22, Color.white, TextAnchor.MiddleRight);
|
||
UiKit.SetPreferredHeight(_clock, 44f, 0f);
|
||
|
||
_concede = UiKit.CreateButton(top, "Concede", "Сдаться", OnConcedeClicked, 18);
|
||
_concedeLabel = _concede.GetComponentInChildren<Text>();
|
||
UiKit.SetPreferredHeight(_concede, 44f, 0f);
|
||
|
||
var left = UiKit.CreatePanel(_root, "BoardPanel", Background);
|
||
left.anchorMin = Vector2.zero;
|
||
left.anchorMax = Vector2.one;
|
||
left.offsetMin = new Vector2(36f, 28f);
|
||
left.offsetMax = new Vector2(-564f, -108f);
|
||
UiKit.AddVerticalLayout(left, 8, 8);
|
||
_board = left;
|
||
|
||
var right = UiKit.CreatePanel(_root, "IntentPanel", Panel);
|
||
right.anchorMin = new Vector2(1f, 0f);
|
||
right.anchorMax = new Vector2(1f, 1f);
|
||
right.pivot = new Vector2(1f, 0.5f);
|
||
right.offsetMin = new Vector2(-528f, 28f);
|
||
right.offsetMax = new Vector2(-36f, -108f);
|
||
UiKit.AddVerticalLayout(right, 14, 10);
|
||
|
||
var actionsTitle = UiKit.CreateText(
|
||
right, "ActionsTitle", "Действия", 24, Color.white, TextAnchor.MiddleLeft);
|
||
actionsTitle.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(actionsTitle, 32f);
|
||
|
||
var scroll = UiKit.CreateScrollView(right, "IntentScroll", out _intents, 6f);
|
||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||
scrollElement.flexibleHeight = 1f;
|
||
scrollElement.minHeight = 200f;
|
||
|
||
_status = UiKit.CreateText(
|
||
right, "Status", string.Empty, 16, Muted, TextAnchor.UpperLeft);
|
||
_status.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(_status, 110f);
|
||
|
||
_reconnect = UiKit.CreateButton(right, "Reconnect", "Переподключиться", OnReconnectClicked, 18);
|
||
UiKit.SetPreferredHeight(_reconnect, 46f);
|
||
_reconnect.gameObject.SetActive(false);
|
||
|
||
_result = BuildResultPanel(_root);
|
||
_result.gameObject.SetActive(false);
|
||
|
||
_root.gameObject.SetActive(false);
|
||
}
|
||
|
||
public void Show(MatchConnectionInfo info)
|
||
{
|
||
_info = info;
|
||
_view = null;
|
||
_finished = false;
|
||
_confirmConcede = false;
|
||
_concedeLabel.text = "Сдаться";
|
||
_concede.interactable = true;
|
||
_reconnect.gameObject.SetActive(false);
|
||
_result.gameObject.SetActive(false);
|
||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||
SetStatus("Подключаемся к матчу…", Muted);
|
||
_root.gameObject.SetActive(true);
|
||
_ = ConnectAsync();
|
||
}
|
||
|
||
public void Hide()
|
||
{
|
||
_cts?.Cancel();
|
||
_cts?.Dispose();
|
||
_cts = null;
|
||
|
||
if (_connection != null)
|
||
{
|
||
var connection = _connection;
|
||
_connection = null;
|
||
_ = connection.DisposeAsync().AsTask();
|
||
}
|
||
|
||
_root.gameObject.SetActive(false);
|
||
_result.gameObject.SetActive(false);
|
||
_view = null;
|
||
}
|
||
|
||
public void Tick()
|
||
{
|
||
if (_confirmConcede && Time.unscaledTime > _confirmDeadline)
|
||
{
|
||
_confirmConcede = false;
|
||
_concedeLabel.text = "Сдаться";
|
||
}
|
||
|
||
UpdateClock();
|
||
}
|
||
|
||
private RectTransform BuildResultPanel(Transform parent)
|
||
{
|
||
var overlay = UiKit.CreatePanel(parent, "BattleResult", new Color(0f, 0f, 0f, 0.75f));
|
||
var card = UiKit.CreateCenteredCard(
|
||
overlay, "ResultCard", new Vector2(640f, 330f),
|
||
new Color(0.08f, 0.10f, 0.14f, 1f));
|
||
UiKit.AddVerticalLayout(card, 32, 14);
|
||
|
||
var title = UiKit.CreateText(card, "Title", "Матч завершён", 32, Color.white);
|
||
title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(title, 48f);
|
||
|
||
_resultText = UiKit.CreateText(card, "Text", string.Empty, 22, Muted);
|
||
UiKit.SetPreferredHeight(_resultText, 90f);
|
||
|
||
var exit = UiKit.CreateButton(card, "Exit", "В меню", OnExitClicked, 21);
|
||
UiKit.SetPreferredHeight(exit, 52f);
|
||
|
||
return overlay;
|
||
}
|
||
|
||
private async Task ConnectAsync()
|
||
{
|
||
if (_connecting || _info == null)
|
||
return;
|
||
|
||
_connecting = true;
|
||
_reconnect.gameObject.SetActive(false);
|
||
_cts?.Cancel();
|
||
_cts?.Dispose();
|
||
_cts = new CancellationTokenSource();
|
||
|
||
try
|
||
{
|
||
if (_connection != null)
|
||
{
|
||
await _connection.DisposeAsync();
|
||
_connection = null;
|
||
}
|
||
|
||
var connection = new GameConnection();
|
||
_connection = connection;
|
||
connection.HelloReceived += OnHello;
|
||
connection.SnapshotReceived += OnSnapshot;
|
||
connection.EventsReceived += OnEvents;
|
||
connection.ClockReceived += OnClock;
|
||
connection.ErrorReceived += OnError;
|
||
connection.Closed += OnClosed;
|
||
|
||
await connection.ConnectAsync(
|
||
_info.WsUrl,
|
||
_info.Ticket,
|
||
Application.version,
|
||
_view != null ? _view.EventSeq : 0,
|
||
_cts.Token);
|
||
|
||
SetStatus("Соединение открыто. Ожидаем состояние…", Muted);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// экран закрыли
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetStatus("Не удалось подключиться: " + exception.Message, new Color(1f, 0.55f, 0.45f));
|
||
_reconnect.gameObject.SetActive(true);
|
||
}
|
||
finally
|
||
{
|
||
_connecting = false;
|
||
}
|
||
}
|
||
|
||
private void OnHello(ServerHello hello)
|
||
{
|
||
if (_info != null)
|
||
_info.Seat = hello.PlayerIndex;
|
||
}
|
||
|
||
private void OnSnapshot(ServerSnapshot snapshot)
|
||
{
|
||
if (!_root.gameObject.activeSelf || snapshot?.View == null)
|
||
return;
|
||
|
||
_view = snapshot.View;
|
||
_finished = _view.IsFinished;
|
||
if (_view.Clock != null)
|
||
{
|
||
_clockServerMs = _view.Clock.ServerTimeMs;
|
||
_clockReceivedMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
}
|
||
|
||
Render();
|
||
}
|
||
|
||
private void OnEvents(ServerEvents events)
|
||
{
|
||
_ = RequestSnapshotAsync();
|
||
}
|
||
|
||
private void OnClock(ServerClock clock)
|
||
{
|
||
if (!_root.gameObject.activeSelf || clock?.Clock == null)
|
||
return;
|
||
|
||
_clockServerMs = clock.Clock.ServerTimeMs;
|
||
_clockReceivedMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
|
||
if (_view != null)
|
||
_view.Clock = clock.Clock;
|
||
|
||
UpdateClock();
|
||
}
|
||
|
||
private void OnError(ServerError error)
|
||
{
|
||
SetStatus(error.Code + ": " + error.Message, new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
|
||
private void OnClosed(string reason)
|
||
{
|
||
if (!_root.gameObject.activeSelf || _finished)
|
||
return;
|
||
|
||
SetStatus("Соединение потеряно: " + reason, new Color(1f, 0.60f, 0.45f));
|
||
_reconnect.gameObject.SetActive(true);
|
||
}
|
||
|
||
private async Task RequestSnapshotAsync()
|
||
{
|
||
if (_snapshotPending || _connection == null || !_connection.IsConnected)
|
||
return;
|
||
|
||
_snapshotPending = true;
|
||
try
|
||
{
|
||
await _connection.RequestSnapshotAsync(_cts != null ? _cts.Token : CancellationToken.None);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// экран закрыли
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetStatus("Не удалось обновить состояние.", new Color(1f, 0.60f, 0.45f));
|
||
}
|
||
finally
|
||
{
|
||
_snapshotPending = false;
|
||
}
|
||
}
|
||
|
||
private async void OnIntentClicked(IntentPayload intent)
|
||
{
|
||
if (_sending || _connection == null || _cts == null || _view == null)
|
||
return;
|
||
|
||
_sending = true;
|
||
SetStatus("Отправляем: " + Label(intent), Muted);
|
||
|
||
try
|
||
{
|
||
var result = await _connection.SubmitAsync(intent, _cts.Token);
|
||
|
||
if (!result.Accepted)
|
||
{
|
||
SetStatus(
|
||
"Отклонено (" + result.Code + "): " + (result.Reason ?? "без причины"),
|
||
new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
else
|
||
{
|
||
SetStatus(result.Duplicate ? "Повтор уже применён." : "Принято.", Muted);
|
||
await RequestSnapshotAsync();
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// экран закрыли
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetStatus("Ошибка отправки интента: " + exception.Message, new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
finally
|
||
{
|
||
_sending = false;
|
||
}
|
||
}
|
||
|
||
private void OnConcedeClicked()
|
||
{
|
||
if (_view == null || _finished)
|
||
return;
|
||
|
||
if (!_confirmConcede)
|
||
{
|
||
_confirmConcede = true;
|
||
_confirmDeadline = Time.unscaledTime + 4f;
|
||
_concedeLabel.text = "Подтвердить сдачу";
|
||
SetStatus("Нажмите «Подтвердить сдачу» ещё раз.", new Color(1f, 0.76f, 0.38f));
|
||
return;
|
||
}
|
||
|
||
_confirmConcede = false;
|
||
_concedeLabel.text = "Сдаться";
|
||
OnIntentClicked(new ConcedeIntent());
|
||
}
|
||
|
||
private async void OnReconnectClicked()
|
||
{
|
||
if (_info == null)
|
||
return;
|
||
|
||
_reconnect.interactable = false;
|
||
SetStatus("Получаем новый тикет…", Muted);
|
||
|
||
try
|
||
{
|
||
var ticket = await _meta.CreateTicketAsync(_info.MatchId);
|
||
_info.Ticket = ticket.Ticket;
|
||
_info.WsUrl = ticket.WsUrl;
|
||
await ConnectAsync();
|
||
}
|
||
catch (ApiException exception)
|
||
{
|
||
SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
SetStatus("Переподключение не удалось.", new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
finally
|
||
{
|
||
_reconnect.interactable = true;
|
||
}
|
||
}
|
||
|
||
private void OnExitClicked()
|
||
{
|
||
Hide();
|
||
_onExit();
|
||
}
|
||
|
||
private void Render()
|
||
{
|
||
if (_view == null)
|
||
return;
|
||
|
||
ClearChildren(_board);
|
||
var opponentIndex = _view.You == 0 ? 1 : 0;
|
||
var opponent = _view.Players.FirstOrDefault(player => player.Index == opponentIndex)
|
||
?? (_view.Players.Count > opponentIndex ? _view.Players[opponentIndex] : null);
|
||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You)
|
||
?? (_view.Players.Count > _view.You ? _view.Players[_view.You] : null);
|
||
|
||
AddPlayerBlock(opponent, false);
|
||
AddMatchInfo(_view);
|
||
AddPlayerBlock(self, true);
|
||
AddHand(self);
|
||
RebuildIntents(_view);
|
||
UpdateClock();
|
||
|
||
if (_view.IsFinished)
|
||
ShowResult(_view);
|
||
}
|
||
|
||
private void AddPlayerBlock(PlayerView player, bool self)
|
||
{
|
||
if (player == null)
|
||
return;
|
||
|
||
var block = UiKit.CreatePanel(
|
||
_board,
|
||
self ? "SelfBlock" : "OpponentBlock",
|
||
Panel);
|
||
var blockElement = block.gameObject.AddComponent<LayoutElement>();
|
||
blockElement.preferredHeight = self ? 250f : 230f;
|
||
UiKit.AddVerticalLayout(block, 8, 4);
|
||
|
||
var header = UiKit.CreateText(
|
||
block,
|
||
"Header",
|
||
(self ? "Вы: " : "Противник: ") + player.Name
|
||
+ " · рука " + player.HandCount
|
||
+ " · колода " + player.DeckCount
|
||
+ " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count)
|
||
+ " · soul " + player.Soul.Count
|
||
+ " · drop " + player.Drop.Count,
|
||
17,
|
||
self ? Color.white : new Color(0.86f, 0.80f, 0.72f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(header, 26f);
|
||
|
||
var row = UiKit.CreatePanel(block, "Field", new Color(0f, 0f, 0f, 0f));
|
||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||
rowElement.flexibleHeight = 1f;
|
||
rowElement.minHeight = 150f;
|
||
UiKit.AddHorizontalLayout(row, 0, 6);
|
||
|
||
AddCardBox(row, player.Vanguard, "Vanguard");
|
||
for (var i = 0; i < 5; i++)
|
||
{
|
||
var card = player.RearGuards != null && i < player.RearGuards.Length
|
||
? player.RearGuards[i]
|
||
: null;
|
||
AddCardBox(row, card, "R" + (i + 1));
|
||
}
|
||
}
|
||
|
||
private void AddHand(PlayerView self)
|
||
{
|
||
var hand = UiKit.CreatePanel(_board, "HandBlock", Panel);
|
||
var handElement = hand.gameObject.AddComponent<LayoutElement>();
|
||
handElement.preferredHeight = 190f;
|
||
UiKit.AddVerticalLayout(hand, 8, 4);
|
||
|
||
var header = UiKit.CreateText(
|
||
hand, "Header", "Рука", 17, Color.white, TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(header, 22f);
|
||
|
||
var row = UiKit.CreatePanel(hand, "Cards", new Color(0f, 0f, 0f, 0f));
|
||
var rowElement = row.gameObject.AddComponent<LayoutElement>();
|
||
rowElement.flexibleHeight = 1f;
|
||
UiKit.AddHorizontalLayout(row, 0, 6);
|
||
|
||
if (self?.Hand == null || self.Hand.Count == 0)
|
||
{
|
||
var empty = UiKit.CreateText(row, "Empty", "Рука пуста", 18, Muted);
|
||
UiKit.SetPreferredHeight(empty, 120f);
|
||
return;
|
||
}
|
||
|
||
foreach (var card in self.Hand)
|
||
AddCardBox(row, card, "Рука");
|
||
}
|
||
|
||
private void AddMatchInfo(MatchView view)
|
||
{
|
||
var info = UiKit.CreatePanel(_board, "MatchInfo", Panel);
|
||
var element = info.gameObject.AddComponent<LayoutElement>();
|
||
element.preferredHeight = 150f;
|
||
UiKit.AddVerticalLayout(info, 10, 3);
|
||
|
||
var active = view.ActivePlayer == view.You ? "вы" : "противник";
|
||
AddInfoLine(info, "Фаза: " + view.Phase + " · шаг: " + view.Step
|
||
+ " · ход " + view.TurnNumber + " · решает: " + active);
|
||
|
||
if (view.Battle != null)
|
||
{
|
||
AddInfoLine(
|
||
info,
|
||
"Атака: " + SlotName(view.Battle.AttackerSlot)
|
||
+ " → " + SlotName(view.Battle.TargetSlot)
|
||
+ " · щит " + view.Battle.GuardShield
|
||
+ " · проверок " + view.Battle.ChecksRemaining
|
||
+ (view.Battle.Nullified ? " · отменено" : string.Empty));
|
||
}
|
||
|
||
if (view.Choice != null)
|
||
AddInfoLine(info, "Выбор: " + view.Choice.Prompt);
|
||
|
||
if (view.Ability != null)
|
||
AddInfoLine(
|
||
info,
|
||
"Способность: " + view.Ability.PrintingId
|
||
+ (view.Ability.Optional ? " (опционально)" : string.Empty));
|
||
|
||
if (view.Trigger != null)
|
||
{
|
||
AddInfoLine(
|
||
info,
|
||
"Триггер: " + view.Trigger.PrintingId
|
||
+ " / " + view.Trigger.Trigger
|
||
+ " / урон " + view.Trigger.RemainingDamage);
|
||
}
|
||
|
||
if (view.Order != null && view.Order.Options.Count > 0)
|
||
AddInfoLine(info, "Нужно выбрать порядок эффектов: " + view.Order.Options.Count);
|
||
|
||
if (view.OpponentChoosing)
|
||
AddInfoLine(info, "Противник что-то выбирает…");
|
||
}
|
||
|
||
private static void AddInfoLine(Transform parent, string text)
|
||
{
|
||
var line = UiKit.CreateText(
|
||
parent, "Info", text, 16, Muted, TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(line, 22f);
|
||
}
|
||
|
||
private static void AddCardBox(Transform parent, CardView card, string slot)
|
||
{
|
||
var panel = UiKit.CreatePanel(parent, "Card_" + slot, Card);
|
||
var element = panel.gameObject.AddComponent<LayoutElement>();
|
||
element.preferredWidth = 132f;
|
||
element.preferredHeight = 148f;
|
||
element.flexibleWidth = 0f;
|
||
UiKit.AddVerticalLayout(panel, 5, 2);
|
||
|
||
var slotText = UiKit.CreateText(panel, "Slot", slot, 12, Muted);
|
||
UiKit.SetPreferredHeight(slotText, 16f);
|
||
|
||
if (card == null)
|
||
{
|
||
var empty = UiKit.CreateText(panel, "Empty", "Пусто", 14, new Color(1f, 1f, 1f, 0.35f));
|
||
UiKit.SetPreferredHeight(empty, 60f);
|
||
return;
|
||
}
|
||
|
||
var printing = UiKit.CreateText(
|
||
panel, "Printing", card.PrintingId, 12, Color.white, TextAnchor.UpperCenter);
|
||
printing.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(printing, 42f);
|
||
|
||
var stats = UiKit.CreateText(
|
||
panel, "Stats", card.Power + " / " + card.Critical, 16, Color.white);
|
||
UiKit.SetPreferredHeight(stats, 22f);
|
||
|
||
var stateText = (card.Rested ? "REST " : string.Empty)
|
||
+ (card.FaceUp ? string.Empty : "FACE-DOWN");
|
||
if (!string.IsNullOrEmpty(stateText))
|
||
{
|
||
var state = UiKit.CreateText(panel, "State", stateText.Trim(), 12, new Color(1f, 0.76f, 0.38f));
|
||
UiKit.SetPreferredHeight(state, 18f);
|
||
}
|
||
}
|
||
|
||
private void RebuildIntents(MatchView view)
|
||
{
|
||
ClearChildren(_intents);
|
||
|
||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||
|
||
if (playable.Count == 0)
|
||
{
|
||
var waiting = UiKit.CreateText(
|
||
_intents,
|
||
"Waiting",
|
||
view.IsFinished ? "Матч завершён." : "Сейчас решает противник…",
|
||
18,
|
||
Muted);
|
||
UiKit.SetPreferredHeight(waiting, 110f);
|
||
return;
|
||
}
|
||
|
||
foreach (var intent in playable)
|
||
{
|
||
var captured = intent;
|
||
var button = UiKit.CreateButton(
|
||
_intents,
|
||
"Intent_" + captured.GetType().Name,
|
||
Label(captured),
|
||
() => OnIntentClicked(captured),
|
||
17);
|
||
UiKit.SetPreferredHeight(button, 50f);
|
||
}
|
||
}
|
||
|
||
private void ShowResult(MatchView view)
|
||
{
|
||
if (_result.gameObject.activeSelf)
|
||
return;
|
||
|
||
_result.SetAsLastSibling();
|
||
_result.gameObject.SetActive(true);
|
||
_concede.interactable = false;
|
||
|
||
_resultText.text = view.Winner < 0
|
||
? "Ничья или матч прерван."
|
||
: view.Winner == view.You
|
||
? "Победа!"
|
||
: "Поражение.";
|
||
|
||
_resultText.text += "\nМатч " + view.MatchId + " завершён.";
|
||
}
|
||
|
||
private void UpdateClock()
|
||
{
|
||
if (_view?.Clock == null || !_view.Clock.IsRunning)
|
||
{
|
||
_clock.text = "—";
|
||
return;
|
||
}
|
||
|
||
var localNow = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
var serverNow = _clockServerMs + (localNow - _clockReceivedMs);
|
||
var remaining = Math.Max(0, _view.Clock.TurnDeadlineMs - serverNow);
|
||
var bank = _view.Clock.DecisionPlayer == _view.You
|
||
? _view.Clock.Bank0Ms
|
||
: _view.Clock.Bank1Ms;
|
||
|
||
_clock.text = Format(remaining) + " · банк " + Format(bank);
|
||
}
|
||
|
||
private static string Format(long milliseconds)
|
||
{
|
||
var totalSeconds = Math.Max(0, milliseconds / 1000);
|
||
var minutes = totalSeconds / 60;
|
||
var seconds = totalSeconds % 60;
|
||
return minutes.ToString("00") + ":" + seconds.ToString("00");
|
||
}
|
||
|
||
private static void ClearChildren(Transform parent)
|
||
{
|
||
for (var i = parent.childCount - 1; i >= 0; i--)
|
||
UnityEngine.Object.Destroy(parent.GetChild(i).gameObject);
|
||
}
|
||
|
||
private static string SlotName(int slot)
|
||
{
|
||
if (slot < 0)
|
||
return "Vanguard";
|
||
|
||
return "R" + (slot + 1);
|
||
}
|
||
|
||
private string Label(IntentPayload intent)
|
||
{
|
||
switch (intent)
|
||
{
|
||
case FinishMulliganIntent _: return "Закончить маллиган";
|
||
case MulliganIntent mulligan: return "Маллиган: " + mulligan.HandIndexes.Count + " карт";
|
||
case SkipRideIntent _: return "Пропустить Ride";
|
||
case RideIntent ride: return "Ride: " + ride.PrintingId;
|
||
case CallIntent call: return "Call " + call.PrintingId + " → " + SlotName(call.Slot);
|
||
case MoveIntent move: return "Move " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot);
|
||
case EnterBattleIntent _: return "В фазу боя";
|
||
case AttackIntent attack:
|
||
return "Атака " + SlotName(attack.AttackerSlot) + " → " + SlotName(attack.TargetSlot)
|
||
+ (attack.BoosterSlot >= 0 ? " + boost" : string.Empty);
|
||
case GuardIntent guard: return "Guard: " + guard.PrintingId;
|
||
case InterceptIntent intercept: return "Intercept: " + SlotName(intercept.Slot);
|
||
case PassGuardIntent _: return "Пропустить Guard";
|
||
case ResolveTriggerIntent trigger:
|
||
return "Триггер: сила " + SlotName(trigger.PowerTarget)
|
||
+ ", эффект " + SlotName(trigger.EffectTarget);
|
||
case SelectTriggerIntent select: return "Выбрать триггер #" + select.Index;
|
||
case ResolveAbilityIntent ability:
|
||
return ability.Accept ? "Применить способность" : "Пропустить способность";
|
||
case ChooseIntent choose: return ChoiceLabel(choose.Index);
|
||
case ActivateAbilityIntent activate:
|
||
return "Способность #" + activate.InstanceId;
|
||
case EndTurnIntent _: return "Закончить ход";
|
||
case ConcedeIntent _: return "Сдаться";
|
||
default: return intent.GetType().Name;
|
||
}
|
||
}
|
||
|
||
private string ChoiceLabel(int index)
|
||
{
|
||
if (_view?.Choice?.Options != null
|
||
&& index >= 0
|
||
&& index < _view.Choice.Options.Count)
|
||
{
|
||
return "Выбрать: " + _view.Choice.Options[index].Label;
|
||
}
|
||
|
||
return "Выбрать вариант #" + index;
|
||
}
|
||
|
||
private void SetStatus(string text, Color color)
|
||
{
|
||
_status.text = text ?? string.Empty;
|
||
_status.color = color;
|
||
}
|
||
}
|
||
}
|