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
|
if (_inputLock != null
|
||||||
&& _animationConfig != null
|
&& _animationConfig != null
|
||||||
|
&& _boardLockReleaseAt <= 0f
|
||||||
&& AnyCardAnimating())
|
&& AnyCardAnimating())
|
||||||
{
|
{
|
||||||
_inputLock.Acquire("board");
|
_inputLock.Acquire("board");
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ namespace Vanguard.UI
|
||||||
private RectTransform _reconnectOverlay;
|
private RectTransform _reconnectOverlay;
|
||||||
private Text _reconnectOverlayText;
|
private Text _reconnectOverlayText;
|
||||||
private Text _reconnectOverlayHint;
|
private Text _reconnectOverlayHint;
|
||||||
|
private RectTransform _abilityOverlay;
|
||||||
|
private Text _abilityOverlayName;
|
||||||
|
private RectTransform _abilityOverlayButtons;
|
||||||
|
private int _abilityPromptInstanceId;
|
||||||
private MatchConnectionInfo _info;
|
private MatchConnectionInfo _info;
|
||||||
private MatchView _view;
|
private MatchView _view;
|
||||||
private bool _connecting;
|
private bool _connecting;
|
||||||
|
|
@ -226,6 +230,7 @@ namespace Vanguard.UI
|
||||||
BuildOverlaysAndZones();
|
BuildOverlaysAndZones();
|
||||||
BuildPhasePanel();
|
BuildPhasePanel();
|
||||||
BuildCombatStats();
|
BuildCombatStats();
|
||||||
|
BuildAbilityOverlay();
|
||||||
BuildReconnectOverlay();
|
BuildReconnectOverlay();
|
||||||
_cardMenu = new CardActionMenuView();
|
_cardMenu = new CardActionMenuView();
|
||||||
_cardMenu.Build(_root);
|
_cardMenu.Build(_root);
|
||||||
|
|
@ -295,6 +300,7 @@ namespace Vanguard.UI
|
||||||
_board3D?.Clear();
|
_board3D?.Clear();
|
||||||
_board3D?.SetVisible(true);
|
_board3D?.SetVisible(true);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
|
HideAbilityOverlay();
|
||||||
_selfZones?.Update(null, self: true);
|
_selfZones?.Update(null, self: true);
|
||||||
_opponentZones?.Update(null, self: false);
|
_opponentZones?.Update(null, self: false);
|
||||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||||
|
|
@ -326,6 +332,7 @@ namespace Vanguard.UI
|
||||||
_flash.gameObject.SetActive(false);
|
_flash.gameObject.SetActive(false);
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
|
HideAbilityOverlay();
|
||||||
_cardMenu?.Hide();
|
_cardMenu?.Hide();
|
||||||
_combatStats?.gameObject.SetActive(false);
|
_combatStats?.gameObject.SetActive(false);
|
||||||
_board3D?.SetVisible(false);
|
_board3D?.SetVisible(false);
|
||||||
|
|
@ -344,6 +351,7 @@ namespace Vanguard.UI
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
UpdateFeedback();
|
UpdateFeedback();
|
||||||
SanitizeInputLocks();
|
SanitizeInputLocks();
|
||||||
|
SyncAbilityPrompt();
|
||||||
UpdateAutoPhases();
|
UpdateAutoPhases();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,6 +372,8 @@ namespace Vanguard.UI
|
||||||
_inputLock.Release("overlay");
|
_inputLock.Release("overlay");
|
||||||
if (_cardMenu == null || !_cardMenu.IsVisible)
|
if (_cardMenu == null || !_cardMenu.IsVisible)
|
||||||
_inputLock.Release("card-menu");
|
_inputLock.Release("card-menu");
|
||||||
|
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
||||||
|
_inputLock.Release("ability");
|
||||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||||
{
|
{
|
||||||
_feedbackLockHeld = false;
|
_feedbackLockHeld = false;
|
||||||
|
|
@ -725,6 +735,7 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
Render();
|
Render();
|
||||||
|
SyncAbilityPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEvents(ServerEvents events)
|
private void OnEvents(ServerEvents events)
|
||||||
|
|
@ -1145,6 +1156,137 @@ namespace Vanguard.UI
|
||||||
ApplyIntentPanelLayout();
|
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()
|
private void BuildReconnectOverlay()
|
||||||
{
|
{
|
||||||
_reconnectOverlay = UiKit.CreatePanel(
|
_reconnectOverlay = UiKit.CreatePanel(
|
||||||
|
|
@ -2734,7 +2876,8 @@ namespace Vanguard.UI
|
||||||
var global = playable
|
var global = playable
|
||||||
.Where(intent => !IsBoardIntent(intent)
|
.Where(intent => !IsBoardIntent(intent)
|
||||||
&& !IsPhaseIntent(intent)
|
&& !IsPhaseIntent(intent)
|
||||||
&& intent is not ActivateAbilityIntent)
|
&& intent is not ActivateAbilityIntent
|
||||||
|
&& intent is not ResolveAbilityIntent)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (_pendingIntents.Count > 0)
|
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) =>
|
private static RectTransform FindRect(string name) =>
|
||||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
.Where(go => go.scene.IsValid() && go.name == name)
|
.Where(go => go.scene.IsValid() && go.name == name)
|
||||||
|
|
|
||||||
|
|
@ -1006,8 +1006,14 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
||||||
взводится повторно из снапшота.
|
взводится повторно из снапшота.
|
||||||
- InputLock чистит зависшие причины, если owning-состояние уже неактивно
|
- InputLock чистит зависшие причины, если owning-состояние уже неактивно
|
||||||
(нет reconnect/overlay/card-menu/feedback). Это убирает блокировку
|
(нет reconnect/overlay/ability/card-menu/feedback). Это убирает блокировку
|
||||||
«Сменить» после хода противника, когда сервер уже прислал ride-интенты.
|
«Сменить» после хода противника, когда сервер уже прислал ride-интенты.
|
||||||
|
Отдельно исправлено накопление `board`-лока: `GameBoard3D` больше не
|
||||||
|
делает повторный `Acquire` на каждом снапшоте во время анимации.
|
||||||
|
- Опциональные AUTO-способности (`PendingAbility`, например Barcgal после
|
||||||
|
рейда на Marron) показываются модальным оверлеем с названием карты и
|
||||||
|
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
||||||
|
убраны. Боковая панель пока оставлена для остальных действий.
|
||||||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||||||
а не за дальним краем.
|
а не за дальним краем.
|
||||||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||||||
|
|
@ -1030,4 +1036,4 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||||||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||||
`translations check` OK; клиент EditMode **31/31**.
|
`translations check` OK; клиент EditMode **32/32**.
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,11 @@
|
||||||
показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся,
|
показывают имя карты и держатся 1.5–2 с. 3D-карты плавно снапятся,
|
||||||
поворачиваются в Rest и переворачиваются; подсветка целей и точки броска
|
поворачиваются в Rest и переворачиваются; подсветка целей и точки броска
|
||||||
обновляются на каждом снимке. Зависшие причины `InputLockService`
|
обновляются на каждом снимке. Зависшие причины `InputLockService`
|
||||||
снимаются, когда reconnect/overlay/card-menu/feedback уже неактивны.
|
снимаются, когда reconnect/overlay/ability/card-menu/feedback уже
|
||||||
|
неактивны; `board`-лок больше не накапливается на каждом снапшоте.
|
||||||
|
- Опциональные AUTO-способности (Barcgal после рейда Marron) открывают
|
||||||
|
модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой
|
||||||
|
панели этих кнопок нет. Сама панель пока оставлена.
|
||||||
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
||||||
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
||||||
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
||||||
|
|
@ -207,7 +211,7 @@ Standalone Windows:
|
||||||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **31/31** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **32/32** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue