Add card-tile overlay for deck search choices
CI / test (push) Has been cancelled Details

This commit is contained in:
MpakobecEx 2026-10-23 19:43:23 +05:00
parent f1ad0a75d3
commit bf41fbe6d0
4 changed files with 243 additions and 5 deletions

View File

@ -71,6 +71,10 @@ namespace Vanguard.UI
private Text _abilityOverlayName; private Text _abilityOverlayName;
private RectTransform _abilityOverlayButtons; private RectTransform _abilityOverlayButtons;
private int _abilityPromptInstanceId; private int _abilityPromptInstanceId;
private RectTransform _cardChoiceOverlay;
private Text _cardChoiceTitle;
private RectTransform _cardChoiceContent;
private string _cardChoiceKey;
private MatchConnectionInfo _info; private MatchConnectionInfo _info;
private MatchView _view; private MatchView _view;
private bool _connecting; private bool _connecting;
@ -238,6 +242,7 @@ namespace Vanguard.UI
BuildPhasePanel(); BuildPhasePanel();
BuildCombatStats(); BuildCombatStats();
BuildAbilityOverlay(); BuildAbilityOverlay();
BuildCardChoiceOverlay();
BuildReconnectOverlay(); BuildReconnectOverlay();
_cardMenu = new CardActionMenuView(); _cardMenu = new CardActionMenuView();
_cardMenu.Build(_root); _cardMenu.Build(_root);
@ -310,6 +315,7 @@ namespace Vanguard.UI
_board3D?.SetVisible(true); _board3D?.SetVisible(true);
_overlays?.CloseAll(); _overlays?.CloseAll();
HideAbilityOverlay(); HideAbilityOverlay();
HideCardChoiceOverlay();
_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;
@ -343,6 +349,7 @@ namespace Vanguard.UI
_skipFeedback.gameObject.SetActive(false); _skipFeedback.gameObject.SetActive(false);
_overlays?.CloseAll(); _overlays?.CloseAll();
HideAbilityOverlay(); HideAbilityOverlay();
HideCardChoiceOverlay();
_cardMenu?.Hide(); _cardMenu?.Hide();
_combatStats?.gameObject.SetActive(false); _combatStats?.gameObject.SetActive(false);
_board3D?.SetVisible(false); _board3D?.SetVisible(false);
@ -363,6 +370,7 @@ namespace Vanguard.UI
RefreshActionPrompt(); RefreshActionPrompt();
SanitizeInputLocks(); SanitizeInputLocks();
SyncAbilityPrompt(); SyncAbilityPrompt();
SyncCardChoicePrompt();
UpdateAutoPhases(); UpdateAutoPhases();
} }
@ -385,6 +393,8 @@ namespace Vanguard.UI
_inputLock.Release("card-menu"); _inputLock.Release("card-menu");
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf) if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
_inputLock.Release("ability"); _inputLock.Release("ability");
if (_cardChoiceOverlay == null || !_cardChoiceOverlay.gameObject.activeSelf)
_inputLock.Release("card-choice");
if (!_feedbackPlaying && _feedbackQueue.Count == 0) if (!_feedbackPlaying && _feedbackQueue.Count == 0)
{ {
_feedbackLockHeld = false; _feedbackLockHeld = false;
@ -747,6 +757,7 @@ namespace Vanguard.UI
Render(); Render();
SyncAbilityPrompt(); SyncAbilityPrompt();
SyncCardChoicePrompt();
} }
private void OnEvents(ServerEvents events) private void OnEvents(ServerEvents events)
@ -1307,6 +1318,175 @@ namespace Vanguard.UI
|| prompt.IndexOf("rear", StringComparison.OrdinalIgnoreCase) >= 0); || prompt.IndexOf("rear", StringComparison.OrdinalIgnoreCase) >= 0);
} }
private void BuildCardChoiceOverlay()
{
_cardChoiceOverlay = UiKit.CreatePanel(
_root,
"CardChoiceOverlay",
new Color(0f, 0f, 0f, 0.78f));
var card = UiKit.CreateCenteredCard(
_cardChoiceOverlay,
"CardChoiceCard",
new Vector2(1120f, 620f),
Panel);
UiKit.AddVerticalLayout(card, 20, 10);
var title = UiKit.CreateText(
card,
"CardChoiceTitle",
"ВЫБОР КАРТЫ",
22,
new Color(1f, 0.80f, 0.42f),
TextAnchor.MiddleCenter);
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 34f);
_cardChoiceTitle = UiKit.CreateText(
card,
"CardChoicePrompt",
string.Empty,
17,
new Color(0.82f, 0.88f, 0.98f),
TextAnchor.MiddleCenter);
UiKit.SetPreferredHeight(_cardChoiceTitle, 42f);
var scroll = UiKit.CreateGridScrollView(
card,
"CardChoiceScroll",
new Vector2(160f, 230f),
new Vector2(12f, 12f),
out _cardChoiceContent);
var element = scroll.gameObject.AddComponent<LayoutElement>();
element.minHeight = 400f;
element.flexibleHeight = 1f;
_cardChoiceOverlay.gameObject.SetActive(false);
}
private static bool TryGetChoicePrintingId(
ChoiceOptionView option,
out string printingId)
{
printingId = null;
var label = option?.Label ?? string.Empty;
var open = label.LastIndexOf('(');
var close = label.LastIndexOf(')');
if (open < 0 || close <= open + 1)
return false;
printingId = label.Substring(open + 1, close - open - 1).Trim();
return printingId.Length > 0;
}
private bool IsCardChoice()
{
var options = _view?.Choice?.Options;
return options is { Count: > 0 }
&& options.All(option => TryGetChoicePrintingId(option, out _));
}
private void SyncCardChoicePrompt()
{
if (_cardChoiceOverlay == null || _sending)
return;
var choice = _view?.Choice;
if (choice == null
|| _view == null
|| _view.ActivePlayer != _view.You
|| !IsCardChoice())
{
HideCardChoiceOverlay();
return;
}
var key = (choice.Prompt ?? string.Empty) + "|" +
string.Join(",", choice.Options.Select(option => option.Label));
if (_cardChoiceOverlay.gameObject.activeSelf
&& string.Equals(_cardChoiceKey, key, StringComparison.Ordinal))
return;
_cardChoiceKey = key;
_cardChoiceTitle.text = choice.Prompt ?? "Выберите карту";
UiKit.MakeLiteral(_cardChoiceTitle);
ClearChildren(_cardChoiceContent);
for (var i = 0; i < choice.Options.Count; i++)
{
var index = i;
var option = choice.Options[i];
if (!TryGetChoicePrintingId(option, out var printingId))
continue;
var tile = UiKit.CreatePanel(
_cardChoiceContent,
"CardChoice_" + index,
new Color(0.05f, 0.075f, 0.11f, 0.98f));
var tileElement = tile.gameObject.AddComponent<LayoutElement>();
tileElement.preferredWidth = 160f;
tileElement.preferredHeight = 230f;
tileElement.flexibleWidth = 0f;
tileElement.flexibleHeight = 0f;
var button = tile.gameObject.AddComponent<Button>();
button.targetGraphic = tile.GetComponent<Image>();
button.onClick.AddListener(() =>
{
HideCardChoiceOverlay();
OnIntentClicked(
new ChooseIntent { Index = index },
ignoreInputLock: true);
});
var art = new GameObject(
"Art", typeof(RectTransform), typeof(Image));
var artRect = (RectTransform)art.transform;
artRect.SetParent(tile, false);
artRect.anchorMin = Vector2.zero;
artRect.anchorMax = Vector2.one;
artRect.offsetMin = new Vector2(4f, 34f);
artRect.offsetMax = new Vector2(-4f, -4f);
var artImage = art.GetComponent<Image>();
artImage.raycastTarget = false;
artImage.preserveAspect = true;
var meta = ResolveCatalogItem(printingId);
var imageKey = meta?.ImageKey
?? CardImageService.ImageKeyForPrinting(printingId);
CardImageService.Apply(artImage, imageKey, null);
if (artImage.sprite == null)
artImage.color = CardArt.ClanColor(meta?.Clan);
var label = UiKit.CreateText(
tile,
"Name",
CardName(printingId),
13,
new Color(0.92f, 0.95f, 1f),
TextAnchor.MiddleCenter);
var labelRect = label.rectTransform;
labelRect.anchorMin = new Vector2(0f, 0f);
labelRect.anchorMax = new Vector2(1f, 0f);
labelRect.pivot = new Vector2(0.5f, 0f);
labelRect.offsetMin = new Vector2(4f, 2f);
labelRect.offsetMax = new Vector2(-4f, 32f);
label.raycastTarget = false;
}
_cardChoiceOverlay.SetAsLastSibling();
_cardChoiceOverlay.gameObject.SetActive(true);
_inputLock.Acquire("card-choice");
}
private void HideCardChoiceOverlay()
{
if (_cardChoiceOverlay == null)
return;
_cardChoiceKey = null;
_cardChoiceOverlay.gameObject.SetActive(false);
_inputLock.Release("card-choice");
}
private void BuildReconnectOverlay() private void BuildReconnectOverlay()
{ {
_reconnectOverlay = UiKit.CreatePanel( _reconnectOverlay = UiKit.CreatePanel(
@ -2346,7 +2526,9 @@ namespace Vanguard.UI
} }
else if (_view.Choice != null && _view.ActivePlayer == _view.You) else if (_view.Choice != null && _view.ActivePlayer == _view.You)
{ {
title = IsRearGuardChoice() ? "ВЫБОР КРУГА" : "ВЫБОР"; title = IsRearGuardChoice()
? "ВЫБОР КРУГА"
: IsCardChoice() ? "ВЫБОР КАРТЫ" : "ВЫБОР";
hint = _view.Choice.Prompt; hint = _view.Choice.Prompt;
} }
else if (_view.ActivePlayer != _view.You) else if (_view.ActivePlayer != _view.You)
@ -3116,7 +3298,8 @@ namespace Vanguard.UI
&& !IsPhaseIntent(intent) && !IsPhaseIntent(intent)
&& intent is not ActivateAbilityIntent && intent is not ActivateAbilityIntent
&& intent is not ResolveAbilityIntent && intent is not ResolveAbilityIntent
&& !(intent is ChooseIntent && IsRearGuardChoice())) && !(intent is ChooseIntent
&& (IsRearGuardChoice() || IsCardChoice())))
.ToList(); .ToList();
if (_pendingIntents.Count > 0) if (_pendingIntents.Count > 0)

View File

@ -385,6 +385,58 @@ namespace Vanguard.Tests
} }
} }
[Test]
public void Card_Choice_Shows_Overlay_With_Card_Tiles()
{
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("Card Choice Test Canvas");
try
{
var battle = new BattleView(canvas.transform, meta, () => { });
var match = new MatchView
{
You = 0,
ActivePlayer = 0,
Choice = new ChoiceView
{
Player = 0,
Prompt = "Найдите Future Knight, Llew или Flogal",
Options = new List<ChoiceOptionView>
{
new() { Slot = 0, Label = "Flogal (BT01/046EN)" },
new() { Slot = 1, Label = "Future Knight, Llew (BT01/045EN)" },
},
},
LegalIntents = new List<IntentPayload>(),
};
typeof(BattleView)
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(battle, match);
typeof(BattleView)
.GetMethod("SyncCardChoicePrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(battle, null);
var overlay = Resources.FindObjectsOfTypeAll<GameObject>()
.First(go => go.name == "CardChoiceOverlay");
Assert.IsTrue(overlay.activeSelf);
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
.Any(go => go.name == "CardChoice_1"));
}
finally
{
Object.DestroyImmediate(canvas.gameObject);
Cleanup();
}
}
[Test] [Test]
public void Action_Prompt_Shows_Current_Required_Action() public void Action_Prompt_Shows_Current_Required_Action()
{ {

View File

@ -1052,5 +1052,7 @@ Battle; Boot и общие сервисы остаются персистент
передний RC и Авангард (центральный задний круг). передний RC и Авангард (центральный задний круг).
- Barcgal: добавлена ACT-способность `[Rest]` — поиск Llew/Flogal в колоде, - Barcgal: добавлена ACT-способность `[Rest]` — поиск Llew/Flogal в колоде,
выбор круга и призыв; AUTO-призыв Barcgal из души после рейда сохранён. выбор круга и призыв; AUTO-призыв Barcgal из души после рейда сохранён.
Найденные карты показываются модальным оверлеем с карточными плитками
(арт + имя), выбор — кликом по карте, без списка кнопок.
- Проверки: сервер **144** правил + **45** серверных, `validate` и - Проверки: сервер **144** правил + **45** серверных, `validate` и
`translations check` OK; клиент EditMode **34/34**. `translations check` OK; клиент EditMode **35/35**.

View File

@ -161,7 +161,8 @@
после чего атаковать можно только поддерживаемой картой; передний круг → после чего атаковать можно только поддерживаемой картой; передний круг →
карта противника объявляет атаку. Авангард поддерживается центральным карта противника объявляет атаку. Авангард поддерживается центральным
задним кругом. Barcgal имеет ACT `[Rest]` — поиск Llew/Flogal в колоде с задним кругом. Barcgal имеет ACT `[Rest]` — поиск Llew/Flogal в колоде с
выбором круга призыва. выбором круга призыва; найденные карты показываются оверлеем с плитками
карт и выбираются кликом по карте.
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет - Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт: `Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
`tools/import_card_art.py`. `tools/import_card_art.py`.
@ -226,7 +227,7 @@ Standalone Windows:
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
``` ```
Клиентские проверки: **34/34** EditMode (сборка всех экранов, импорт арта, Клиентские проверки: **35/35** EditMode (сборка всех экранов, импорт арта,
перевод статических/динамических строк, live-переключение языка, рендер перевод статических/динамических строк, live-переключение языка, рендер
3D-доски с уроном и отдыхающей картой, геометрия поля, правила 3D-доски с уроном и отдыхающей картой, геометрия поля, правила
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона, перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,