Use board clicks for circle choice and record interaction rule
CI / test (push) Successful in 2m1s Details

This commit is contained in:
MpakobecEx 2026-10-23 19:00:50 +05:00
parent 7bb6bcb975
commit 08c170e0d9
6 changed files with 45 additions and 124 deletions

View File

@ -19,6 +19,18 @@
подробную историю — в `docs/HANDOFF.md`.
- После блока: тесты, `validate`, `translations check`, коммит и push.
## UI и взаимодействие
- **Выбор делаем интерактивом по объектам:** клик/драг по карте, кругу,
зоне или полю. Не заменяем выбор карты, круга или цели списком кнопок в
интерфейсе.
- Кнопки допустимы только для глобальных действий и подтверждений:
завершить ход, сдаться, отмена, принять/пропустить опциональную
способность. Если выбирается конкретная карта/круг/зона — это делается
кликом по объекту.
- Информационные оверлеи (`ActionPrompt`, подсказки) не должны перехватывать
клики и не дублируют выбор кнопками.
## Технические ограничения
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на

View File

@ -71,10 +71,6 @@ namespace Vanguard.UI
private Text _abilityOverlayName;
private RectTransform _abilityOverlayButtons;
private int _abilityPromptInstanceId;
private RectTransform _choiceOverlay;
private Text _choiceOverlayTitle;
private RectTransform _choiceOverlayButtons;
private string _choicePromptKey;
private MatchConnectionInfo _info;
private MatchView _view;
private bool _connecting;
@ -241,7 +237,6 @@ namespace Vanguard.UI
BuildPhasePanel();
BuildCombatStats();
BuildAbilityOverlay();
BuildChoiceOverlay();
BuildReconnectOverlay();
_cardMenu = new CardActionMenuView();
_cardMenu.Build(_root);
@ -313,7 +308,6 @@ namespace Vanguard.UI
_board3D?.SetVisible(true);
_overlays?.CloseAll();
HideAbilityOverlay();
HideChoiceOverlay();
_selfZones?.Update(null, self: true);
_opponentZones?.Update(null, self: false);
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
@ -347,7 +341,6 @@ namespace Vanguard.UI
_skipFeedback.gameObject.SetActive(false);
_overlays?.CloseAll();
HideAbilityOverlay();
HideChoiceOverlay();
_cardMenu?.Hide();
_combatStats?.gameObject.SetActive(false);
_board3D?.SetVisible(false);
@ -368,7 +361,6 @@ namespace Vanguard.UI
RefreshActionPrompt();
SanitizeInputLocks();
SyncAbilityPrompt();
SyncChoicePrompt();
UpdateAutoPhases();
}
@ -391,8 +383,6 @@ namespace Vanguard.UI
_inputLock.Release("card-menu");
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
_inputLock.Release("ability");
if (_choiceOverlay == null || !_choiceOverlay.gameObject.activeSelf)
_inputLock.Release("choice");
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
{
_feedbackLockHeld = false;
@ -755,7 +745,6 @@ namespace Vanguard.UI
Render();
SyncAbilityPrompt();
SyncChoicePrompt();
}
private void OnEvents(ServerEvents events)
@ -1307,45 +1296,6 @@ namespace Vanguard.UI
_inputLock.Release("ability");
}
private void BuildChoiceOverlay()
{
_choiceOverlay = UiKit.CreatePanel(
_root,
"ChoiceOverlay",
new Color(0f, 0f, 0f, 0.72f));
var card = UiKit.CreateCenteredCard(
_choiceOverlay,
"ChoiceCard",
new Vector2(760f, 300f),
Panel);
UiKit.AddVerticalLayout(card, 22, 10);
var title = UiKit.CreateText(
card,
"ChoiceTitle",
"Выбор",
22,
new Color(1f, 0.78f, 0.42f),
TextAnchor.MiddleCenter);
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 34f);
_choiceOverlayTitle = UiKit.CreateText(
card,
"ChoicePrompt",
string.Empty,
18,
Color.white,
TextAnchor.MiddleCenter);
UiKit.SetPreferredHeight(_choiceOverlayTitle, 52f);
_choiceOverlayButtons = UiKit.CreatePanel(
card, "ChoiceButtons", Color.clear);
UiKit.AddHorizontalLayout(_choiceOverlayButtons, 0, 8);
UiKit.SetPreferredHeight(_choiceOverlayButtons, 52f);
_choiceOverlay.gameObject.SetActive(false);
}
private bool IsRearGuardChoice()
{
var prompt = _view?.Choice?.Prompt;
@ -1354,68 +1304,6 @@ namespace Vanguard.UI
|| prompt.IndexOf("rear", StringComparison.OrdinalIgnoreCase) >= 0);
}
private void SyncChoicePrompt()
{
if (_choiceOverlay == null || _sending)
return;
var choice = _view?.Choice;
if (choice == null
|| _view == null
|| _view.ActivePlayer != _view.You
|| !IsRearGuardChoice())
{
HideChoiceOverlay();
return;
}
var key = (choice.Prompt ?? string.Empty) + "|" +
string.Join(",", choice.Options.Select(option => option.Slot));
if (_choiceOverlay.gameObject.activeSelf
&& string.Equals(_choicePromptKey, key, StringComparison.Ordinal))
return;
_choicePromptKey = key;
_choiceOverlayTitle.text = choice.Prompt ?? "Выберите круг";
UiKit.MakeLiteral(_choiceOverlayTitle);
ClearChildren(_choiceOverlayButtons);
for (var i = 0; i < choice.Options.Count; i++)
{
var index = i;
var option = choice.Options[i];
var button = UiKit.CreateButton(
_choiceOverlayButtons,
"Choice_" + option.Slot,
string.IsNullOrWhiteSpace(option.Label)
? "Круг " + (option.Slot + 1)
: option.Label,
() =>
{
HideChoiceOverlay();
OnIntentClicked(
new ChooseIntent { Index = index },
ignoreInputLock: true);
},
16);
UiKit.SetPreferredHeight(button, 46f);
}
_choiceOverlay.SetAsLastSibling();
_choiceOverlay.gameObject.SetActive(true);
_inputLock.Acquire("choice");
}
private void HideChoiceOverlay()
{
if (_choiceOverlay == null)
return;
_choicePromptKey = null;
_choiceOverlay.gameObject.SetActive(false);
_inputLock.Release("choice");
}
private void BuildReconnectOverlay()
{
_reconnectOverlay = UiKit.CreatePanel(
@ -1865,7 +1753,6 @@ namespace Vanguard.UI
option => option.Slot == slot.Slot);
if (index >= 0)
{
HideChoiceOverlay();
OnIntentClicked(
new ChooseIntent { Index = index },
ignoreInputLock: true);

View File

@ -326,7 +326,7 @@ namespace Vanguard.Tests
}
[Test]
public void Rear_Guard_Choice_Shows_Overlay_With_Circles()
public void Rear_Guard_Choice_Is_Shown_Without_Buttons()
{
var config = new AppConfig();
config.Load();
@ -364,13 +364,18 @@ namespace Vanguard.Tests
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(battle, match);
typeof(BattleView)
.GetMethod("SyncChoicePrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetMethod("RefreshActionPrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(battle, null);
var overlay = Resources.FindObjectsOfTypeAll<GameObject>()
.First(go => go.name == "ChoiceOverlay");
Assert.IsTrue(overlay.activeSelf);
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
var prompt = Resources.FindObjectsOfTypeAll<GameObject>()
.First(go => go.name == "ActionPrompt");
Assert.IsTrue(prompt.activeSelf);
var title = Resources.FindObjectsOfTypeAll<Text>()
.First(text => text.name == "ActionPromptTitle");
Assert.AreEqual("ВЫБОР КРУГА", title.text);
Assert.IsFalse(Resources.FindObjectsOfTypeAll<GameObject>()
.Any(go => go.name == "ChoiceOverlay"));
Assert.IsFalse(Resources.FindObjectsOfTypeAll<GameObject>()
.Any(go => go.name == "Choice_3"));
}
finally

View File

@ -1015,8 +1015,11 @@ Battle; Boot и общие сервисы остаются персистент
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
убраны. После подтверждения призыва карта из души не ставится в первый
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
клиент показывает оверлей с кругами и подсвечивает свободные RC.
Боковая панель пока оставлена для остальных действий.
клиент подсвечивает свободные RC и принимает выбор кликом по кругу, без
списка кнопок. Боковая панель пока оставлена для остальных действий.
- Зафиксировано правило взаимодействия: выбор карты/круга/зоны/цели — клик
или drag по объекту на поле; кнопки — только глобальные действия и
подтверждения (`AGENTS.md`, `docs/client-visual-direction.md`).
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
триггеров и опциональная способность. Промпт информационный, не

View File

@ -138,13 +138,16 @@
- Опциональные AUTO-способности (Barcgal после рейда Marron) открывают
модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой
панели этих кнопок нет. После подтверждения призыва сервер предлагает
`AskEmptyRearGuardChoice`, клиент показывает оверлей с кругами и
подсвечивает свободные RC — первый круг больше не выбирается
автоматически. Сама панель пока оставлена.
`AskEmptyRearGuardChoice`, клиент подсвечивает свободные RC и принимает
выбор кликом по кругу — первого круга по умолчанию нет и отдельных
кнопок выбора тоже. Сама панель пока оставлена.
- Общий верхний `ActionPrompt` показывает, что сейчас требуется: муллиган,
смена, защита, бой, выбор карты/круга/цели, триггер, порядок триггеров,
опциональная способность. Он не перехватывает клики и скрыт в ход
противника.
- Правило: выбор карты/круга/зоны/цели делается кликом/драгом по объекту
на поле, не кнопками; кнопки — только глобальные действия/подтверждения.
Зафиксировано в `AGENTS.md` и `docs/client-visual-direction.md`.
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,

View File

@ -54,6 +54,17 @@
перенесены без изменения контрактов. `Boot` остаётся стартовой сценой,
`VanguardApp` — персистентным composition root.
## Взаимодействие
- Выбор объектов — только интерактивом по сцене: карта, круг, зона, поле.
Не показываем список кнопок «выбрать карту/круг» вместо клика по объекту.
- Кнопки остаются для глобальных действий и подтверждений (завершить ход,
сдаться, отмена, принять/пропустить опциональную способность).
- Информационные оверлеи только объясняют, что требуется; они не
перехватывают клики и не дублируют выбор кнопками.
- `ActionPrompt` в бою — информационный слой; подсветка на 3D-поле
показывает доступные карты/круги/зоны.
## Ограничения и следующие шаги
- Пока нет 3D-ассетов: карты показываются процедурными градиентными