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 { /// /// Серый боевой экран: доска, рука, часы и кнопки всех легальных интентов. /// Красоту и анимации добавим поверх этой рабочей логики. /// 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 Dictionary _catalog = new Dictionary(StringComparer.Ordinal); private bool _catalogLoading; 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(); 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(); 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); _ = LoadCatalogAsync(); _ = 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 LoadCatalogAsync() { if (_catalogLoading || _catalog.Count > 0) return; _catalogLoading = true; try { var items = await _meta.GetCatalogAsync(); foreach (var item in items) _catalog[item.PrintingId] = item; if (_root.gameObject.activeSelf && _view != null) Render(); } catch (Exception exception) { Debug.LogWarning("Battle catalog load failed: " + exception.Message); } finally { _catalogLoading = false; } } 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); AddFieldBlock(opponent, false); AddCenterPanel(_view); AddFieldBlock(self, true); AddHand(self); RebuildIntents(_view); UpdateClock(); if (_view.IsFinished) ShowResult(_view); } private void AddFieldBlock(PlayerView player, bool self) { if (player == null) return; var block = UiKit.CreatePanel( _board, self ? "SelfField" : "OpponentField", new Color(0.05f, 0.07f, 0.11f, 0.96f)); var blockElement = block.gameObject.AddComponent(); blockElement.preferredHeight = self ? 214f : 204f; UiKit.AddVerticalLayout(block, 7, 3); 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, 15, self ? Color.white : new Color(0.94f, 0.82f, 0.70f), TextAnchor.MiddleLeft); header.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(header, 25f); var row = UiKit.CreatePanel(block, "Field", new Color(0f, 0f, 0f, 0f)); var rowElement = row.gameObject.AddComponent(); rowElement.flexibleHeight = 1f; rowElement.minHeight = 166f; UiKit.AddHorizontalLayout(row, 3, 7); AddCardBox(row, player.Vanguard, "V", compact: false); 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), compact: false); } } private void AddCenterPanel(MatchView view) { var panel = UiKit.CreatePanel(_board, "MatchInfo", new Color(0.06f, 0.09f, 0.15f, 0.98f)); var element = panel.gameObject.AddComponent(); element.preferredHeight = 76f; UiKit.AddVerticalLayout(panel, 8, 2); var active = view.ActivePlayer == view.You ? "решаете вы" : "решает противник"; var main = UiKit.CreateText( panel, "Main", "Ход " + view.TurnNumber + " · " + view.Phase + " · " + view.Step + " · " + active, 17, view.ActivePlayer == view.You ? new Color(0.55f, 0.86f, 1f) : new Color(1f, 0.78f, 0.52f), TextAnchor.MiddleLeft); main.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(main, 26f); var details = new List(); if (view.Battle != null) details.Add("Бой: " + SlotName(view.Battle.AttackerSlot) + " → " + SlotName(view.Battle.TargetSlot) + " · щит " + view.Battle.GuardShield + " · проверок " + view.Battle.ChecksRemaining); if (view.Choice != null) details.Add(view.Choice.Prompt); if (view.Ability != null) details.Add("Способность: " + CardName(view.Ability.PrintingId)); if (view.Trigger != null) details.Add("Триггер: " + CardName(view.Trigger.PrintingId) + " · " + view.Trigger.Trigger); if (view.OpponentChoosing) details.Add("Противник выбирает действие…"); if (details.Count == 0) details.Add("Выберите действие в панели справа."); var detail = UiKit.CreateText( panel, "Detail", string.Join(" · ", details), 14, Muted, TextAnchor.MiddleLeft); detail.horizontalOverflow = HorizontalWrapMode.Wrap; UiKit.SetPreferredHeight(detail, 28f); } private void AddHand(PlayerView self) { var hand = UiKit.CreatePanel(_board, "HandBlock", new Color(0.05f, 0.07f, 0.11f, 0.96f)); var handElement = hand.gameObject.AddComponent(); handElement.preferredHeight = 188f; UiKit.AddVerticalLayout(hand, 6, 3); var header = UiKit.CreateText( hand, "Header", "РУКА · " + (self?.Hand?.Count ?? 0) + " карт", 16, Color.white, TextAnchor.MiddleLeft); header.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(header, 22f); var scroll = UiKit.CreateHorizontalScrollView(hand, "HandScroll", out var content, 6f); var scrollElement = scroll.gameObject.AddComponent(); scrollElement.flexibleHeight = 1f; scrollElement.minHeight = 130f; if (self?.Hand == null || self.Hand.Count == 0) { var empty = UiKit.CreateText( content, "Empty", "Рука пуста", 17, Muted); UiKit.SetPreferredHeight(empty, 110f); return; } foreach (var card in self.Hand) AddCardBox(content, card, "Рука", compact: true); } private void AddInfoLine(Transform parent, string text) { var line = UiKit.CreateText( parent, "Info", text, 16, Muted, TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(line, 22f); } private void AddCardBox(Transform parent, CardView card, string slot, bool compact) { var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found) ? found : null; var clanColor = CardArt.ClanColor(meta != null ? meta.Clan : "None"); var frameColor = card == null ? new Color(0.16f, 0.18f, 0.24f) : CardArt.ClanColor(meta != null ? meta.Clan : "None"); var panel = UiKit.CreatePanel( parent, "Card_" + slot, card == null ? new Color(0.06f, 0.08f, 0.12f, 0.9f) : card.FaceUp ? new Color(0.09f, 0.11f, 0.16f, 1f) : new Color(0.05f, 0.08f, 0.14f, 1f)); var element = panel.gameObject.AddComponent(); element.preferredWidth = compact ? 106f : 124f; element.preferredHeight = compact ? 142f : 162f; element.flexibleWidth = 0f; var outline = panel.gameObject.AddComponent(); outline.effectColor = frameColor; outline.effectDistance = new Vector2(2f, -2f); UiKit.AddVerticalLayout(panel, 4, 2); var slotText = UiKit.CreateText( panel, "Slot", slot, 10, new Color(0.58f, 0.66f, 0.80f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(slotText, 14f); if (card == null) { var emptyArt = UiKit.CreatePanel(panel, "EmptyArt", new Color(0.04f, 0.06f, 0.09f, 1f)); UiKit.SetPreferredHeight(emptyArt, compact ? 48f : 64f); var empty = UiKit.CreateText( emptyArt, "Empty", "—", 22, new Color(1f, 1f, 1f, 0.18f)); UiKit.Stretch(empty.rectTransform); var emptyLabel = UiKit.CreateText( panel, "EmptyLabel", "Пусто", 11, new Color(1f, 1f, 1f, 0.28f)); UiKit.SetPreferredHeight(emptyLabel, 24f); return; } var art = UiKit.CreatePanel(panel, "Art", Color.white); UiKit.SetPreferredHeight(art, compact ? 48f : 64f); var artImage = art.GetComponent(); artImage.sprite = card.FaceUp ? CardArt.ForCard( meta != null ? meta.NameId : card.PrintingId, meta != null ? meta.Clan : "None") : CardArt.Back; artImage.color = Color.white; artImage.preserveAspect = false; if (card.FaceUp) { var artName = UiKit.CreateText( art, "ArtName", meta != null ? meta.NameEn : card.PrintingId, 10, new Color(1f, 1f, 1f, 0.94f), TextAnchor.MiddleCenter); UiKit.Stretch(artName.rectTransform); artName.rectTransform.offsetMin = new Vector2(3f, 3f); artName.rectTransform.offsetMax = new Vector2(-3f, -2f); artName.horizontalOverflow = HorizontalWrapMode.Wrap; if (meta != null) { var grade = UiKit.CreateText( art, "Grade", "G" + meta.Grade, 10, Color.white, TextAnchor.MiddleCenter); var gradeRect = grade.rectTransform; gradeRect.anchorMin = new Vector2(1f, 1f); gradeRect.anchorMax = new Vector2(1f, 1f); gradeRect.pivot = new Vector2(1f, 1f); gradeRect.anchoredPosition = new Vector2(-3f, -3f); gradeRect.sizeDelta = new Vector2(25f, 16f); } } else { var hidden = UiKit.CreateText( art, "Hidden", "?", 26, new Color(1f, 1f, 1f, 0.8f)); UiKit.Stretch(hidden.rectTransform); } if (card.Rested) { var rested = UiKit.CreateText( art, "Rested", "REST", 10, new Color(1f, 0.55f, 0.35f), TextAnchor.MiddleCenter); var restedRect = rested.rectTransform; restedRect.anchorMin = new Vector2(0f, 0f); restedRect.anchorMax = new Vector2(0f, 0f); restedRect.pivot = new Vector2(0f, 0f); restedRect.anchoredPosition = new Vector2(3f, 3f); restedRect.sizeDelta = new Vector2(34f, 16f); } var name = UiKit.CreateText( panel, "Name", meta != null ? meta.NameEn : card.PrintingId, 10, card.FaceUp ? Color.white : Muted, TextAnchor.UpperLeft); name.horizontalOverflow = HorizontalWrapMode.Wrap; name.fontStyle = FontStyle.Bold; UiKit.SetPreferredHeight(name, compact ? 26f : 28f); var stats = UiKit.CreateText( panel, "Stats", card.FaceUp ? "P " + card.Power + " · C " + card.Critical + (meta != null ? " · " + TriggerShort(meta.Trigger) : string.Empty) : "СКРЫТА", 11, card.FaceUp && card.Power != card.BasePower ? new Color(1f, 0.82f, 0.42f) : new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft); UiKit.SetPreferredHeight(stats, 18f); } private static string TriggerShort(string trigger) => trigger switch { "Critical" => "K", "Draw" => "D", "Stand" => "S", "Heal" => "H", _ => "—", }; private string CardName(string printingId) => _catalog.TryGetValue(printingId, out var meta) ? meta.NameEn : printingId; private void RebuildIntents(MatchView view) { ClearChildren(_intents); var legal = view.LegalIntents ?? new List(); 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--) { var child = parent.GetChild(i).gameObject; if (Application.isPlaying) UnityEngine.Object.Destroy(child); else UnityEngine.Object.DestroyImmediate(child); } } 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: " + CardName(ride.PrintingId); case CallIntent call: return "Call " + CardName(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: " + CardName(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; } } }