Recover stale battle input locks
CI / test (push) Successful in 2m52s Details

This commit is contained in:
MpakobecEx 2026-10-23 18:12:19 +05:00
parent d6784b7ca9
commit ba932c0d17
4 changed files with 50 additions and 1 deletions

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Vanguard.Battle namespace Vanguard.Battle
{ {
@ -14,6 +15,11 @@ namespace Vanguard.Battle
public bool IsLocked => _reasons.Count > 0; 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<bool> Changed; public event Action<bool> Changed;
public void Acquire(string reason) public void Acquire(string reason)

View File

@ -33,6 +33,7 @@ namespace Vanguard.UI
private readonly AnimationConfig _animationConfig; private readonly AnimationConfig _animationConfig;
private readonly InputLockService _inputLock; private readonly InputLockService _inputLock;
private bool _feedbackLockHeld; private bool _feedbackLockHeld;
private string _lastInputLockSummary;
private readonly Dictionary<string, CardCatalogItemDto> _catalog = private readonly Dictionary<string, CardCatalogItemDto> _catalog =
new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal); new Dictionary<string, CardCatalogItemDto>(StringComparer.Ordinal);
private bool _catalogLoading; private bool _catalogLoading;
@ -342,9 +343,47 @@ namespace Vanguard.UI
UpdateClock(); UpdateClock();
UpdateFeedback(); UpdateFeedback();
SanitizeInputLocks();
UpdateAutoPhases(); 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() private void UpdateAutoPhases()
{ {
if (_view == null || _sending || _reconnecting) if (_view == null || _sending || _reconnecting)

View File

@ -1005,6 +1005,9 @@ Battle; Boot и общие сервисы остаются персистент
завершается автоматически, кнопка «Закончить муллиган» скрывается и не завершается автоматически, кнопка «Закончить муллиган» скрывается и не
блокируется feedback/overlay-локом; при реконнекте авто-завершение блокируется feedback/overlay-локом; при реконнекте авто-завершение
взводится повторно из снапшота. взводится повторно из снапшота.
- InputLock чистит зависшие причины, если owning-состояние уже неактивно
(нет reconnect/overlay/card-menu/feedback). Это убирает блокировку
«Сменить» после хода противника, когда сервер уже прислал ride-интенты.
- Trigger-зона противника зеркальна: над его колодой в сторону центра, - Trigger-зона противника зеркальна: над его колодой в сторону центра,
а не за дальним краем. а не за дальним краем.
- Карты на столе и в руке масштабируются общим `cardScale = 1.25` - Карты на столе и в руке масштабируются общим `cardScale = 1.25`

View File

@ -132,7 +132,8 @@
`AnimationConfig` (`Resources/AnimationConfig`). Guard/drive/intercept `AnimationConfig` (`Resources/AnimationConfig`). Guard/drive/intercept
показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся, показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся,
поворачиваются в Rest и переворачиваются; подсветка целей и точки броска поворачиваются в Rest и переворачиваются; подсветка целей и точки броска
обновляются на каждом снимке. обновляются на каждом снимке. Зависшие причины `InputLockService`
снимаются, когда reconnect/overlay/card-menu/feedback уже неактивны.
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против - Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка, том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,