From ba932c0d17e068c8ed2685467dcdb6666f516bd3 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Fri, 23 Oct 2026 18:12:19 +0500 Subject: [PATCH] Recover stale battle input locks --- .../Runtime/Battle/InputLockService.cs | 6 +++ .../Assets/Vanguard/Runtime/UI/BattleView.cs | 39 +++++++++++++++++++ docs/HANDOFF.md | 3 ++ docs/SESSION-STATE.md | 3 +- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/client/Assets/Vanguard/Runtime/Battle/InputLockService.cs b/client/Assets/Vanguard/Runtime/Battle/InputLockService.cs index 003e34b..d0d859d 100644 --- a/client/Assets/Vanguard/Runtime/Battle/InputLockService.cs +++ b/client/Assets/Vanguard/Runtime/Battle/InputLockService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Vanguard.Battle { @@ -14,6 +15,11 @@ namespace Vanguard.Battle public bool IsLocked => _reasons.Count > 0; + public string Describe() => + _reasons.Count == 0 + ? "none" + : string.Join(",", _reasons.Select(pair => pair.Key + "=" + pair.Value)); + public event Action Changed; public void Acquire(string reason) diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index 7d4c70c..3a077e6 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -33,6 +33,7 @@ namespace Vanguard.UI private readonly AnimationConfig _animationConfig; private readonly InputLockService _inputLock; private bool _feedbackLockHeld; + private string _lastInputLockSummary; private readonly Dictionary _catalog = new Dictionary(StringComparer.Ordinal); private bool _catalogLoading; @@ -342,9 +343,47 @@ namespace Vanguard.UI UpdateClock(); UpdateFeedback(); + SanitizeInputLocks(); UpdateAutoPhases(); } + private void SanitizeInputLocks() + { + if (!_inputLock.IsLocked) + { + _lastInputLockSummary = null; + return; + } + + // Locks must never outlive the UI/gameplay state that owns them. + // This also recovers from a missed close callback after reconnects, + // overlays or card menus. + if (!_reconnecting) + _inputLock.Release("reconnect"); + if (_overlays == null || !_overlays.IsOpen) + _inputLock.Release("overlay"); + if (_cardMenu == null || !_cardMenu.IsVisible) + _inputLock.Release("card-menu"); + if (!_feedbackPlaying && _feedbackQueue.Count == 0) + { + _feedbackLockHeld = false; + _inputLock.Release("feedback"); + } + + var summary = _inputLock.Describe(); + if (!_inputLock.IsLocked) + { + _lastInputLockSummary = null; + return; + } + + if (!string.Equals(summary, _lastInputLockSummary, StringComparison.Ordinal)) + { + _lastInputLockSummary = summary; + Debug.LogWarning("Battle input lock held: " + summary); + } + } + private void UpdateAutoPhases() { if (_view == null || _sending || _reconnecting) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 5cb58ff..e52afd8 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1005,6 +1005,9 @@ Battle; Boot и общие сервисы остаются персистент завершается автоматически, кнопка «Закончить муллиган» скрывается и не блокируется feedback/overlay-локом; при реконнекте авто-завершение взводится повторно из снапшота. +- InputLock чистит зависшие причины, если owning-состояние уже неактивно + (нет reconnect/overlay/card-menu/feedback). Это убирает блокировку + «Сменить» после хода противника, когда сервер уже прислал ride-интенты. - Trigger-зона противника зеркальна: над его колодой в сторону центра, а не за дальним краем. - Карты на столе и в руке масштабируются общим `cardScale = 1.25` diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 180e5e8..8cece45 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -132,7 +132,8 @@ `AnimationConfig` (`Resources/AnimationConfig`). Guard/drive/intercept показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся, поворачиваются в Rest и переворачиваются; подсветка целей и точки броска - обновляются на каждом снимке. + обновляются на каждом снимке. Зависшие причины `InputLockService` + снимаются, когда reconnect/overlay/card-menu/feedback уже неактивны. - Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против Power защищающегося + GuardShield), пересчитывается на каждом снимке, в том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,