Show optional ability prompt overlay and fix board lock accumulation
CI / test (push) Has been cancelled
Details
CI / test (push) Has been cancelled
Details
This commit is contained in:
parent
ba932c0d17
commit
69f2121e16
|
|
@ -297,6 +297,7 @@ namespace Vanguard.Board3D
|
|||
|
||||
if (_inputLock != null
|
||||
&& _animationConfig != null
|
||||
&& _boardLockReleaseAt <= 0f
|
||||
&& AnyCardAnimating())
|
||||
{
|
||||
_inputLock.Acquire("board");
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ namespace Vanguard.UI
|
|||
private RectTransform _reconnectOverlay;
|
||||
private Text _reconnectOverlayText;
|
||||
private Text _reconnectOverlayHint;
|
||||
private RectTransform _abilityOverlay;
|
||||
private Text _abilityOverlayName;
|
||||
private RectTransform _abilityOverlayButtons;
|
||||
private int _abilityPromptInstanceId;
|
||||
private MatchConnectionInfo _info;
|
||||
private MatchView _view;
|
||||
private bool _connecting;
|
||||
|
|
@ -226,6 +230,7 @@ namespace Vanguard.UI
|
|||
BuildOverlaysAndZones();
|
||||
BuildPhasePanel();
|
||||
BuildCombatStats();
|
||||
BuildAbilityOverlay();
|
||||
BuildReconnectOverlay();
|
||||
_cardMenu = new CardActionMenuView();
|
||||
_cardMenu.Build(_root);
|
||||
|
|
@ -295,6 +300,7 @@ namespace Vanguard.UI
|
|||
_board3D?.Clear();
|
||||
_board3D?.SetVisible(true);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
_selfZones?.Update(null, self: true);
|
||||
_opponentZones?.Update(null, self: false);
|
||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||
|
|
@ -326,6 +332,7 @@ namespace Vanguard.UI
|
|||
_flash.gameObject.SetActive(false);
|
||||
_skipFeedback.gameObject.SetActive(false);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
_cardMenu?.Hide();
|
||||
_combatStats?.gameObject.SetActive(false);
|
||||
_board3D?.SetVisible(false);
|
||||
|
|
@ -344,6 +351,7 @@ namespace Vanguard.UI
|
|||
UpdateClock();
|
||||
UpdateFeedback();
|
||||
SanitizeInputLocks();
|
||||
SyncAbilityPrompt();
|
||||
UpdateAutoPhases();
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +372,8 @@ namespace Vanguard.UI
|
|||
_inputLock.Release("overlay");
|
||||
if (_cardMenu == null || !_cardMenu.IsVisible)
|
||||
_inputLock.Release("card-menu");
|
||||
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
||||
_inputLock.Release("ability");
|
||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||
{
|
||||
_feedbackLockHeld = false;
|
||||
|
|
@ -725,6 +735,7 @@ namespace Vanguard.UI
|
|||
}
|
||||
|
||||
Render();
|
||||
SyncAbilityPrompt();
|
||||
}
|
||||
|
||||
private void OnEvents(ServerEvents events)
|
||||
|
|
@ -1145,6 +1156,137 @@ namespace Vanguard.UI
|
|||
ApplyIntentPanelLayout();
|
||||
}
|
||||
|
||||
private void BuildAbilityOverlay()
|
||||
{
|
||||
_abilityOverlay = UiKit.CreatePanel(
|
||||
_root,
|
||||
"AbilityOverlay",
|
||||
new Color(0f, 0f, 0f, 0.72f));
|
||||
var card = UiKit.CreateCenteredCard(
|
||||
_abilityOverlay,
|
||||
"AbilityCard",
|
||||
new Vector2(640f, 300f),
|
||||
Panel);
|
||||
UiKit.AddVerticalLayout(card, 22, 10);
|
||||
|
||||
var title = UiKit.CreateText(
|
||||
card,
|
||||
"AbilityTitle",
|
||||
"Способность",
|
||||
22,
|
||||
new Color(1f, 0.78f, 0.42f),
|
||||
TextAnchor.MiddleCenter);
|
||||
title.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(title, 34f);
|
||||
|
||||
_abilityOverlayName = UiKit.CreateText(
|
||||
card,
|
||||
"AbilityName",
|
||||
string.Empty,
|
||||
26,
|
||||
Color.white,
|
||||
TextAnchor.MiddleCenter);
|
||||
_abilityOverlayName.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(_abilityOverlayName, 46f);
|
||||
|
||||
var hint = UiKit.CreateText(
|
||||
card,
|
||||
"AbilityHint",
|
||||
"Активировать способность?",
|
||||
17,
|
||||
new Color(0.78f, 0.84f, 0.96f),
|
||||
TextAnchor.MiddleCenter);
|
||||
UiKit.SetPreferredHeight(hint, 32f);
|
||||
|
||||
_abilityOverlayButtons = UiKit.CreatePanel(
|
||||
card, "AbilityButtons", Color.clear);
|
||||
UiKit.AddHorizontalLayout(_abilityOverlayButtons, 0, 10);
|
||||
UiKit.SetPreferredHeight(_abilityOverlayButtons, 48f);
|
||||
_abilityOverlay.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void SyncAbilityPrompt()
|
||||
{
|
||||
if (_abilityOverlay == null || _sending)
|
||||
return;
|
||||
|
||||
var ability = _view?.Ability;
|
||||
if (ability == null
|
||||
|| _view == null
|
||||
|| _view.ActivePlayer != _view.You)
|
||||
{
|
||||
HideAbilityOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_abilityOverlay.gameObject.activeSelf
|
||||
&& _abilityPromptInstanceId == ability.InstanceId)
|
||||
return;
|
||||
|
||||
var intents = (_view.LegalIntents ?? new List<IntentPayload>())
|
||||
.OfType<ResolveAbilityIntent>()
|
||||
.ToList();
|
||||
var accept = intents.FirstOrDefault(intent => intent.Accept);
|
||||
var skip = intents.FirstOrDefault(intent => !intent.Accept);
|
||||
if (accept == null && skip == null)
|
||||
return;
|
||||
|
||||
_abilityPromptInstanceId = ability.InstanceId;
|
||||
_abilityOverlayName.text = CardName(ability.PrintingId);
|
||||
UiKit.MakeLiteral(_abilityOverlayName);
|
||||
ClearChildren(_abilityOverlayButtons);
|
||||
|
||||
if (accept != null)
|
||||
{
|
||||
AddAbilityOverlayButton(
|
||||
"Активировать",
|
||||
accept,
|
||||
new Color(0.20f, 0.42f, 0.78f));
|
||||
}
|
||||
|
||||
if (skip != null)
|
||||
{
|
||||
AddAbilityOverlayButton(
|
||||
"Пропустить",
|
||||
skip,
|
||||
new Color(0.24f, 0.28f, 0.36f));
|
||||
}
|
||||
|
||||
_abilityOverlay.SetAsLastSibling();
|
||||
_abilityOverlay.gameObject.SetActive(true);
|
||||
_inputLock.Acquire("ability");
|
||||
}
|
||||
|
||||
private void AddAbilityOverlayButton(
|
||||
string label,
|
||||
ResolveAbilityIntent intent,
|
||||
Color color)
|
||||
{
|
||||
var captured = intent;
|
||||
var button = UiKit.CreateButton(
|
||||
_abilityOverlayButtons,
|
||||
"Ability_" + label,
|
||||
label,
|
||||
() =>
|
||||
{
|
||||
HideAbilityOverlay();
|
||||
OnIntentClicked(captured, ignoreInputLock: true);
|
||||
},
|
||||
18);
|
||||
button.image.color = color;
|
||||
UiKit.SetPreferredHeight(button, 46f);
|
||||
}
|
||||
|
||||
private void HideAbilityOverlay()
|
||||
{
|
||||
if (_abilityOverlay == null)
|
||||
return;
|
||||
|
||||
_abilityPromptInstanceId = 0;
|
||||
_abilityOverlay.gameObject.SetActive(false);
|
||||
_inputLock.Release("ability");
|
||||
}
|
||||
|
||||
private void BuildReconnectOverlay()
|
||||
{
|
||||
_reconnectOverlay = UiKit.CreatePanel(
|
||||
|
|
@ -2734,7 +2876,8 @@ namespace Vanguard.UI
|
|||
var global = playable
|
||||
.Where(intent => !IsBoardIntent(intent)
|
||||
&& !IsPhaseIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent)
|
||||
&& intent is not ActivateAbilityIntent
|
||||
&& intent is not ResolveAbilityIntent)
|
||||
.ToList();
|
||||
|
||||
if (_pendingIntents.Count > 0)
|
||||
|
|
|
|||
|
|
@ -258,6 +258,73 @@ namespace Vanguard.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Optional_Ability_Shows_Overlay_Instead_Of_Action_Panel()
|
||||
{
|
||||
var config = new AppConfig();
|
||||
config.Load();
|
||||
var tokens = new TokenStore();
|
||||
var api = new ApiClient(config, tokens);
|
||||
var player = new PlayerState();
|
||||
var auth = new AuthService(api, tokens, player);
|
||||
var meta = new MetaService(api, player);
|
||||
var canvas = UiKit.CreateCanvas("Ability Test Canvas");
|
||||
|
||||
try
|
||||
{
|
||||
var battle = new BattleView(canvas.transform, meta, () => { });
|
||||
var match = new MatchView
|
||||
{
|
||||
You = 0,
|
||||
ActivePlayer = 0,
|
||||
Ability = new AbilityView
|
||||
{
|
||||
Player = 0,
|
||||
InstanceId = 42,
|
||||
PrintingId = "BT01/003EN",
|
||||
Optional = true,
|
||||
},
|
||||
LegalIntents = new List<IntentPayload>
|
||||
{
|
||||
new ResolveAbilityIntent { Accept = true },
|
||||
new ResolveAbilityIntent { Accept = false },
|
||||
new ConcedeIntent(),
|
||||
},
|
||||
};
|
||||
|
||||
typeof(BattleView)
|
||||
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(battle, match);
|
||||
typeof(BattleView)
|
||||
.GetMethod("SyncAbilityPrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(battle, null);
|
||||
|
||||
var overlay = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.First(go => go.name == "AbilityOverlay");
|
||||
Assert.IsTrue(overlay.activeSelf);
|
||||
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == "Ability_Активировать"));
|
||||
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == "Ability_Пропустить"));
|
||||
|
||||
typeof(BattleView)
|
||||
.GetMethod("RebuildIntents", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(battle, new object[] { match });
|
||||
Assert.IsFalse(Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == "Intent_ResolveAbilityIntent"));
|
||||
|
||||
typeof(BattleView)
|
||||
.GetMethod("HideAbilityOverlay", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(battle, null);
|
||||
Assert.IsFalse(overlay.activeSelf);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(canvas.gameObject);
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private static RectTransform FindRect(string name) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Where(go => go.scene.IsValid() && go.name == name)
|
||||
|
|
|
|||
|
|
@ -1006,8 +1006,14 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
||||
взводится повторно из снапшота.
|
||||
- InputLock чистит зависшие причины, если owning-состояние уже неактивно
|
||||
(нет reconnect/overlay/card-menu/feedback). Это убирает блокировку
|
||||
(нет reconnect/overlay/ability/card-menu/feedback). Это убирает блокировку
|
||||
«Сменить» после хода противника, когда сервер уже прислал ride-интенты.
|
||||
Отдельно исправлено накопление `board`-лока: `GameBoard3D` больше не
|
||||
делает повторный `Acquire` на каждом снапшоте во время анимации.
|
||||
- Опциональные AUTO-способности (`PendingAbility`, например Barcgal после
|
||||
рейда на Marron) показываются модальным оверлеем с названием карты и
|
||||
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
||||
убраны. Боковая панель пока оставлена для остальных действий.
|
||||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||||
а не за дальним краем.
|
||||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||||
|
|
@ -1030,4 +1036,4 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **31/31**.
|
||||
`translations check` OK; клиент EditMode **32/32**.
|
||||
|
|
|
|||
|
|
@ -133,7 +133,11 @@
|
|||
показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся,
|
||||
поворачиваются в Rest и переворачиваются; подсветка целей и точки броска
|
||||
обновляются на каждом снимке. Зависшие причины `InputLockService`
|
||||
снимаются, когда reconnect/overlay/card-menu/feedback уже неактивны.
|
||||
снимаются, когда reconnect/overlay/ability/card-menu/feedback уже
|
||||
неактивны; `board`-лок больше не накапливается на каждом снапшоте.
|
||||
- Опциональные AUTO-способности (Barcgal после рейда Marron) открывают
|
||||
модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой
|
||||
панели этих кнопок нет. Сама панель пока оставлена.
|
||||
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
||||
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
||||
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
||||
|
|
@ -207,7 +211,7 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **31/31** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **32/32** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
|
|
|
|||
Loading…
Reference in New Issue