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 { /// Просмотр реплея: события и кадры состояния, собранные сервером. 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(); 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(); 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); } } }