Add card-tile overlay for deck search choices
CI / test (push) Has been cancelled
Details
CI / test (push) Has been cancelled
Details
This commit is contained in:
parent
f1ad0a75d3
commit
bf41fbe6d0
|
|
@ -71,6 +71,10 @@ namespace Vanguard.UI
|
|||
private Text _abilityOverlayName;
|
||||
private RectTransform _abilityOverlayButtons;
|
||||
private int _abilityPromptInstanceId;
|
||||
private RectTransform _cardChoiceOverlay;
|
||||
private Text _cardChoiceTitle;
|
||||
private RectTransform _cardChoiceContent;
|
||||
private string _cardChoiceKey;
|
||||
private MatchConnectionInfo _info;
|
||||
private MatchView _view;
|
||||
private bool _connecting;
|
||||
|
|
@ -238,6 +242,7 @@ namespace Vanguard.UI
|
|||
BuildPhasePanel();
|
||||
BuildCombatStats();
|
||||
BuildAbilityOverlay();
|
||||
BuildCardChoiceOverlay();
|
||||
BuildReconnectOverlay();
|
||||
_cardMenu = new CardActionMenuView();
|
||||
_cardMenu.Build(_root);
|
||||
|
|
@ -310,6 +315,7 @@ namespace Vanguard.UI
|
|||
_board3D?.SetVisible(true);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
HideCardChoiceOverlay();
|
||||
_selfZones?.Update(null, self: true);
|
||||
_opponentZones?.Update(null, self: false);
|
||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||
|
|
@ -343,6 +349,7 @@ namespace Vanguard.UI
|
|||
_skipFeedback.gameObject.SetActive(false);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
HideCardChoiceOverlay();
|
||||
_cardMenu?.Hide();
|
||||
_combatStats?.gameObject.SetActive(false);
|
||||
_board3D?.SetVisible(false);
|
||||
|
|
@ -363,6 +370,7 @@ namespace Vanguard.UI
|
|||
RefreshActionPrompt();
|
||||
SanitizeInputLocks();
|
||||
SyncAbilityPrompt();
|
||||
SyncCardChoicePrompt();
|
||||
UpdateAutoPhases();
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +393,8 @@ namespace Vanguard.UI
|
|||
_inputLock.Release("card-menu");
|
||||
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
||||
_inputLock.Release("ability");
|
||||
if (_cardChoiceOverlay == null || !_cardChoiceOverlay.gameObject.activeSelf)
|
||||
_inputLock.Release("card-choice");
|
||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||
{
|
||||
_feedbackLockHeld = false;
|
||||
|
|
@ -747,6 +757,7 @@ namespace Vanguard.UI
|
|||
|
||||
Render();
|
||||
SyncAbilityPrompt();
|
||||
SyncCardChoicePrompt();
|
||||
}
|
||||
|
||||
private void OnEvents(ServerEvents events)
|
||||
|
|
@ -1307,6 +1318,175 @@ namespace Vanguard.UI
|
|||
|| 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()
|
||||
{
|
||||
_reconnectOverlay = UiKit.CreatePanel(
|
||||
|
|
@ -2346,7 +2526,9 @@ namespace Vanguard.UI
|
|||
}
|
||||
else if (_view.Choice != null && _view.ActivePlayer == _view.You)
|
||||
{
|
||||
title = IsRearGuardChoice() ? "ВЫБОР КРУГА" : "ВЫБОР";
|
||||
title = IsRearGuardChoice()
|
||||
? "ВЫБОР КРУГА"
|
||||
: IsCardChoice() ? "ВЫБОР КАРТЫ" : "ВЫБОР";
|
||||
hint = _view.Choice.Prompt;
|
||||
}
|
||||
else if (_view.ActivePlayer != _view.You)
|
||||
|
|
@ -3116,7 +3298,8 @@ namespace Vanguard.UI
|
|||
&& !IsPhaseIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent
|
||||
&& intent is not ResolveAbilityIntent
|
||||
&& !(intent is ChooseIntent && IsRearGuardChoice()))
|
||||
&& !(intent is ChooseIntent
|
||||
&& (IsRearGuardChoice() || IsCardChoice())))
|
||||
.ToList();
|
||||
|
||||
if (_pendingIntents.Count > 0)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
public void Action_Prompt_Shows_Current_Required_Action()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1052,5 +1052,7 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
передний RC и Авангард (центральный задний круг).
|
||||
- Barcgal: добавлена ACT-способность `[Rest]` — поиск Llew/Flogal в колоде,
|
||||
выбор круга и призыв; AUTO-призыв Barcgal из души после рейда сохранён.
|
||||
Найденные карты показываются модальным оверлеем с карточными плитками
|
||||
(арт + имя), выбор — кликом по карте, без списка кнопок.
|
||||
- Проверки: сервер **144** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **34/34**.
|
||||
`translations check` OK; клиент EditMode **35/35**.
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@
|
|||
после чего атаковать можно только поддерживаемой картой; передний круг →
|
||||
карта противника объявляет атаку. Авангард поддерживается центральным
|
||||
задним кругом. Barcgal имеет ACT `[Rest]` — поиск Llew/Flogal в колоде с
|
||||
выбором круга призыва.
|
||||
выбором круга призыва; найденные карты показываются оверлеем с плитками
|
||||
карт и выбираются кликом по карте.
|
||||
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
||||
`tools/import_card_art.py`.
|
||||
|
|
@ -226,7 +227,7 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **34/34** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **35/35** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
|
|
|
|||
Loading…
Reference in New Issue