275 lines
10 KiB
C#
275 lines
10 KiB
C#
using System;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using Vanguard.Api;
|
||
using Vanguard.Api.Contracts;
|
||
using Vanguard.Meta;
|
||
using Vanguard.Shared.Protocol;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>Просмотр реплея: события и кадры состояния, собранные сервером.</summary>
|
||
public sealed class ReplayView
|
||
{
|
||
private readonly MetaService _meta;
|
||
private readonly Action _onClose;
|
||
|
||
private readonly RectTransform _root;
|
||
private readonly Text _title;
|
||
private readonly Text _result;
|
||
private readonly Text _frameLabel;
|
||
private readonly Text _summary;
|
||
private readonly Text _status;
|
||
private readonly RectTransform _events;
|
||
private ReplayResponseDto _replay;
|
||
private int _frameIndex;
|
||
private bool _loading;
|
||
|
||
public ReplayView(Transform parent, MetaService meta, Action onClose)
|
||
{
|
||
_meta = meta;
|
||
_onClose = onClose;
|
||
|
||
_root = UiKit.CreatePanel(parent, "ReplayScreen", new Color(0.015f, 0.025f, 0.05f, 0.97f));
|
||
|
||
var top = UiKit.CreatePanel(_root, "TopBar", new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
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, 22, 16);
|
||
|
||
var back = UiKit.CreateButton(top, "Back", "← К истории", () => _onClose(), 18);
|
||
UiKit.SetPreferredHeight(back, 44f, 0f);
|
||
|
||
_title = UiKit.CreateText(top, "Title", "Реплей", 30, Color.white, TextAnchor.MiddleLeft);
|
||
_title.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(_title, 46f);
|
||
|
||
_result = UiKit.CreateText(top, "Result", string.Empty, 20,
|
||
new Color(0.85f, 0.90f, 1f), TextAnchor.MiddleRight);
|
||
UiKit.SetPreferredHeight(_result, 36f);
|
||
|
||
var body = UiKit.CreatePanel(_root, "Body", new Color(0f, 0f, 0f, 0f));
|
||
body.anchorMin = Vector2.zero;
|
||
body.anchorMax = Vector2.one;
|
||
body.offsetMin = new Vector2(40f, 28f);
|
||
body.offsetMax = new Vector2(-40f, -110f);
|
||
UiKit.AddVerticalLayout(body, 8, 10);
|
||
|
||
var summaryPanel = UiKit.CreatePanel(body, "SummaryPanel", new Color(0.06f, 0.08f, 0.13f, 1f));
|
||
UiKit.SetPreferredHeight(summaryPanel, 250f);
|
||
UiKit.AddVerticalLayout(summaryPanel, 18, 8);
|
||
|
||
_frameLabel = UiKit.CreateText(
|
||
summaryPanel, "Frame", "Кадр", 19, Color.white, TextAnchor.MiddleLeft);
|
||
_frameLabel.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(_frameLabel, 28f);
|
||
|
||
_summary = UiKit.CreateText(
|
||
summaryPanel, "Summary", string.Empty, 16,
|
||
new Color(0.72f, 0.78f, 0.92f), TextAnchor.UpperLeft);
|
||
_summary.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
UiKit.SetPreferredHeight(_summary, 170f);
|
||
|
||
var controls = UiKit.CreatePanel(body, "Controls", new Color(0f, 0f, 0f, 0f));
|
||
UiKit.SetPreferredHeight(controls, 52f);
|
||
UiKit.AddHorizontalLayout(controls, 0, 12);
|
||
|
||
var prev = UiKit.CreateButton(controls, "Prev", "◀ Кадр", OnPrev, 18);
|
||
UiKit.SetPreferredHeight(prev, 46f);
|
||
var next = UiKit.CreateButton(controls, "Next", "Кадр ▶", OnNext, 18);
|
||
UiKit.SetPreferredHeight(next, 46f);
|
||
|
||
var eventsTitle = UiKit.CreateText(
|
||
body, "EventsTitle", "События", 22, Color.white, TextAnchor.MiddleLeft);
|
||
eventsTitle.fontStyle = FontStyle.Bold;
|
||
UiKit.SetPreferredHeight(eventsTitle, 30f);
|
||
|
||
var scroll = UiKit.CreateScrollView(body, "EventsScroll", out _events, 4f);
|
||
var scrollElement = scroll.gameObject.AddComponent<LayoutElement>();
|
||
scrollElement.flexibleHeight = 1f;
|
||
scrollElement.minHeight = 160f;
|
||
|
||
_status = UiKit.CreateText(
|
||
body, "Status", string.Empty, 17,
|
||
new Color(0.62f, 0.68f, 0.82f), TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(_status, 30f);
|
||
|
||
_root.gameObject.SetActive(false);
|
||
}
|
||
|
||
public void Show(string matchId)
|
||
{
|
||
UiKit.SetActive(_root, true);
|
||
_replay = null;
|
||
_frameIndex = 0;
|
||
SetStatus("Загружаем реплей…", new Color(0.62f, 0.68f, 0.82f));
|
||
_ = LoadAsync(matchId);
|
||
}
|
||
|
||
public void Hide() => UiKit.SetActive(_root, false);
|
||
|
||
private async Task LoadAsync(string matchId)
|
||
{
|
||
if (_loading)
|
||
return;
|
||
|
||
_loading = true;
|
||
try
|
||
{
|
||
_replay = await _meta.GetReplayAsync(matchId);
|
||
if (_root == null || !_root.gameObject.activeSelf)
|
||
return;
|
||
|
||
_title.text = "Реплей · " + _replay.MatchId;
|
||
_result.text = _replay.Winner == _replay.ViewerSeat
|
||
? "Победа"
|
||
: _replay.Winner < 0 ? "Ничья" : "Поражение";
|
||
_result.color = _replay.Winner == _replay.ViewerSeat
|
||
? new Color(0.45f, 0.85f, 0.55f)
|
||
: new Color(1f, 0.58f, 0.45f);
|
||
|
||
_frameIndex = Math.Max(0, (_replay.Frames?.Count ?? 0) - 1);
|
||
RebuildEvents();
|
||
UpdateFrame();
|
||
SetStatus(
|
||
"Кадров: " + (_replay.Frames?.Count ?? 0) + " · событий: "
|
||
+ (_replay.Events?.Count ?? 0),
|
||
new Color(0.62f, 0.68f, 0.82f));
|
||
}
|
||
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
|
||
{
|
||
_loading = false;
|
||
}
|
||
}
|
||
|
||
private void RebuildEvents()
|
||
{
|
||
ClearChildren(_events);
|
||
if (_replay?.Events == null || _replay.Events.Count == 0)
|
||
{
|
||
var empty = UiKit.CreateText(
|
||
_events, "Empty", "Событий нет.", 16,
|
||
new Color(0.62f, 0.68f, 0.82f));
|
||
UiKit.SetPreferredHeight(empty, 60f);
|
||
return;
|
||
}
|
||
|
||
foreach (var item in _replay.Events)
|
||
{
|
||
var line = UiKit.CreateText(
|
||
_events,
|
||
"Event",
|
||
"#" + item.Seq + " " + item.Kind,
|
||
15,
|
||
new Color(0.72f, 0.78f, 0.92f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.SetPreferredHeight(line, 22f);
|
||
}
|
||
}
|
||
|
||
private void OnPrev()
|
||
{
|
||
if (_replay?.Frames == null || _replay.Frames.Count == 0)
|
||
return;
|
||
|
||
_frameIndex = Math.Max(0, _frameIndex - 1);
|
||
UpdateFrame();
|
||
}
|
||
|
||
private void OnNext()
|
||
{
|
||
if (_replay?.Frames == null || _replay.Frames.Count == 0)
|
||
return;
|
||
|
||
_frameIndex = Math.Min(_replay.Frames.Count - 1, _frameIndex + 1);
|
||
UpdateFrame();
|
||
}
|
||
|
||
private void UpdateFrame()
|
||
{
|
||
if (_replay?.Frames == null || _replay.Frames.Count == 0)
|
||
{
|
||
_frameLabel.text = "Кадров нет";
|
||
_summary.text = string.Empty;
|
||
return;
|
||
}
|
||
|
||
var frame = _replay.Frames[Mathf.Clamp(_frameIndex, 0, _replay.Frames.Count - 1)];
|
||
_frameLabel.text = "Кадр " + (_frameIndex + 1) + " / " + _replay.Frames.Count
|
||
+ " · eventSeq " + frame.EventSeq;
|
||
|
||
var view = frame.View;
|
||
if (view == null)
|
||
{
|
||
_summary.text = "Состояние кадра недоступно.";
|
||
return;
|
||
}
|
||
|
||
var text = "Фаза: " + view.Phase + " · шаг: " + view.Step
|
||
+ " · ход " + view.TurnNumber
|
||
+ " · решает: " + (view.ActivePlayer == view.You ? "вы" : "противник")
|
||
+ "\n";
|
||
|
||
var opponent = view.You == 0 ? 1 : 0;
|
||
foreach (var index in new[] { view.You, opponent })
|
||
{
|
||
if (view.Players == null || index >= view.Players.Count)
|
||
continue;
|
||
|
||
var player = view.Players[index];
|
||
text += (index == view.You ? "Вы: " : "Противник: ") + player.Name
|
||
+ " · рука " + player.HandCount
|
||
+ " · колода " + player.DeckCount
|
||
+ " · урон " + (player.FaceDownDamage + player.FaceUpDamage.Count)
|
||
+ " · Vanguard " + (player.Vanguard?.PrintingId ?? "—")
|
||
+ "\n RG: " + RearGuards(player) + "\n";
|
||
}
|
||
|
||
_summary.text = text;
|
||
}
|
||
|
||
private static string RearGuards(PlayerView player)
|
||
{
|
||
if (player.RearGuards == null)
|
||
return "—";
|
||
|
||
var parts = new System.Collections.Generic.List<string>();
|
||
for (var i = 0; i < player.RearGuards.Length; i++)
|
||
{
|
||
var card = player.RearGuards[i];
|
||
parts.Add("R" + (i + 1) + ":" + (card?.PrintingId ?? "—"));
|
||
}
|
||
|
||
return string.Join(" · ", parts);
|
||
}
|
||
|
||
private void SetStatus(string text, Color color)
|
||
{
|
||
if (_status == null)
|
||
return;
|
||
|
||
_status.text = text ?? string.Empty;
|
||
_status.color = color;
|
||
}
|
||
|
||
private static void ClearChildren(Transform parent)
|
||
{
|
||
for (var i = parent.childCount - 1; i >= 0; i--)
|
||
UnityEngine.Object.Destroy(parent.GetChild(i).gameObject);
|
||
}
|
||
}
|
||
}
|