diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index b8651fe..f467850 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -276,7 +276,8 @@ - [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер, сила/крит; REST/скрытая карта. - [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»). -- [x] Ввод: кнопки легальных интентов; подсветка целей — дальше. +- [x] Ввод: кнопки легальных интентов; клик по картам/зонам, + подсветка целей, группировка действий и журнал боя. - [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером. - [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard. - [ ] Скип/ускорение анимаций. diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index c4c9afe..4f77b04 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -41,7 +41,9 @@ namespace Vanguard.UI private readonly Text _concedeLabel; private readonly Button _reconnect; private readonly RectTransform _result; + private RectTransform _logContent; private Text _resultText; + private readonly List _logLines = new List(); private GameConnection _connection; private CancellationTokenSource _cts; @@ -52,9 +54,12 @@ namespace Vanguard.UI private bool _snapshotPending; private bool _finished; private bool _confirmConcede; + private bool _showAllIntents; private float _confirmDeadline; private long _clockServerMs; private long _clockReceivedMs; + private readonly List _pendingIntents = new List(); + private readonly HashSet _highlightKeys = new HashSet(StringComparer.Ordinal); public BattleView(Transform parent, MetaService meta, Action onExit) { @@ -116,7 +121,17 @@ namespace Vanguard.UI _status = UiKit.CreateText( right, "Status", string.Empty, 16, Muted, TextAnchor.UpperLeft); _status.horizontalOverflow = HorizontalWrapMode.Wrap; - UiKit.SetPreferredHeight(_status, 110f); + UiKit.SetPreferredHeight(_status, 74f); + + var logTitle = UiKit.CreateText( + right, "LogTitle", "Журнал", 16, new Color(0.72f, 0.80f, 0.94f), TextAnchor.MiddleLeft); + logTitle.fontStyle = FontStyle.Bold; + UiKit.SetPreferredHeight(logTitle, 22f); + + var logScroll = UiKit.CreateScrollView(right, "LogScroll", out _logContent, 3f); + var logElement = logScroll.gameObject.AddComponent(); + logElement.preferredHeight = 142f; + logElement.minHeight = 80f; _reconnect = UiKit.CreateButton(right, "Reconnect", "Переподключиться", OnReconnectClicked, 18); UiKit.SetPreferredHeight(_reconnect, 46f); @@ -138,6 +153,11 @@ namespace Vanguard.UI _concede.interactable = true; _reconnect.gameObject.SetActive(false); _result.gameObject.SetActive(false); + _pendingIntents.Clear(); + _highlightKeys.Clear(); + _showAllIntents = false; + _logLines.Clear(); + RebuildLog(); _topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId; SetStatus("Подключаемся к матчу…", Muted); _root.gameObject.SetActive(true); @@ -286,6 +306,8 @@ namespace Vanguard.UI _view = snapshot.View; _finished = _view.IsFinished; + _pendingIntents.Clear(); + _highlightKeys.Clear(); if (_view.Clock != null) { _clockServerMs = _view.Clock.ServerTimeMs; @@ -297,9 +319,78 @@ namespace Vanguard.UI private void OnEvents(ServerEvents events) { + if (events?.Events != null) + { + foreach (var envelope in events.Events) + { + var line = Describe(envelope.Event); + if (!string.IsNullOrWhiteSpace(line)) + _logLines.Add(line); + } + + while (_logLines.Count > 40) + _logLines.RemoveAt(0); + + RebuildLog(); + } + _ = RequestSnapshotAsync(); } + private void RebuildLog() + { + if (_logContent == null) + return; + + ClearChildren(_logContent); + var start = Mathf.Max(0, _logLines.Count - 8); + for (var i = start; i < _logLines.Count; i++) + { + var line = UiKit.CreateText( + _logContent, "LogLine", _logLines[i], 13, + i == _logLines.Count - 1 + ? new Color(0.92f, 0.94f, 1f) + : new Color(0.60f, 0.68f, 0.82f), + TextAnchor.MiddleLeft); + line.horizontalOverflow = HorizontalWrapMode.Wrap; + UiKit.SetPreferredHeight(line, 30f); + } + } + + private string Describe(EventView view) + { + if (view == null) + return null; + + return view switch + { + UnitRodeView ride => "Ride: " + CardName(ride.PrintingId), + UnitCalledView call => "Call: " + CardName(call.PrintingId) + " → " + SlotName(call.Slot), + UnitMovedView move => "Move: " + SlotName(move.FromSlot) + " → " + SlotName(move.ToSlot), + AttackDeclaredView attack => "Атака: " + SlotName(attack.AttackerSlot) + + " → " + SlotName(attack.TargetSlot), + GuardAddedView guard => "Guard: " + CardName(guard.PrintingId) + + " (+" + guard.Shield + ")", + PerfectGuardView perfect => "Perfect Guard: " + CardName(perfect.SentinelPrintingId), + DriveCheckedView drive => "Drive: " + CardName(drive.PrintingId) + + " · " + drive.Trigger, + TriggerResolvedView trigger => "Триггер: " + CardName(trigger.PrintingId), + BattleResolvedView battle => battle.Hit ? "Атака прошла." : "Атака отбита.", + DamageTakenView damage => "Урон: " + PlayerName(damage.Player) + + " +" + damage.Amount + " (всего " + damage.Total + ")", + HealedView heal => "Heal: " + PlayerName(heal.Player), + UnitRetiredView retire => "Retire: " + SlotName(retire.Slot), + InterceptedView intercept => "Intercept: " + CardName(intercept.PrintingId), + CardDrawnView draw => "Карта: " + PlayerName(draw.Player), + PhaseChangedView phase => "Фаза: " + phase.Phase, + GameFinishedView finish => "Партия завершена.", + _ => null, + }; + } + + private string PlayerName(int player) => + _view == null ? "P" + player : player == _view.You ? "Вы" : "Противник"; + private void OnClock(ServerClock clock) { if (!_root.gameObject.activeSelf || clock?.Clock == null) @@ -358,6 +449,8 @@ namespace Vanguard.UI if (_sending || _connection == null || _cts == null || _view == null) return; + _pendingIntents.Clear(); + _highlightKeys.Clear(); _sending = true; SetStatus("Отправляем: " + Label(intent), Muted); @@ -504,13 +597,13 @@ namespace Vanguard.UI rowElement.minHeight = 166f; UiKit.AddHorizontalLayout(row, 3, 7); - AddCardBox(row, player.Vanguard, "V", compact: false); + AddCardBox(row, player.Vanguard, "V", compact: false, player.Index, -1); 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); + AddCardBox(row, card, "R" + (i + 1), compact: false, player.Index, i); } } @@ -588,7 +681,7 @@ namespace Vanguard.UI } foreach (var card in self.Hand) - AddCardBox(content, card, "Рука", compact: true); + AddCardBox(content, card, "Рука", compact: true, self.Index, -2); } private void AddInfoLine(Transform parent, string text) @@ -598,7 +691,13 @@ namespace Vanguard.UI UiKit.SetPreferredHeight(line, 22f); } - private void AddCardBox(Transform parent, CardView card, string slot, bool compact) + private void AddCardBox( + Transform parent, + CardView card, + string slot, + bool compact, + int playerIndex, + int slotIndex) { var meta = card != null && _catalog.TryGetValue(card.PrintingId, out var found) ? found @@ -619,9 +718,19 @@ namespace Vanguard.UI element.preferredHeight = compact ? 142f : 162f; element.flexibleWidth = 0f; + var highlighted = _highlightKeys.Contains( + TargetKey(playerIndex, slotIndex, card != null ? card.PrintingId : null)); + var button = panel.gameObject.AddComponent