Use board clicks for circle choice and record interaction rule
CI / test (push) Successful in 2m1s
Details
CI / test (push) Successful in 2m1s
Details
This commit is contained in:
parent
7bb6bcb975
commit
08c170e0d9
12
AGENTS.md
12
AGENTS.md
|
|
@ -19,6 +19,18 @@
|
|||
подробную историю — в `docs/HANDOFF.md`.
|
||||
- После блока: тесты, `validate`, `translations check`, коммит и push.
|
||||
|
||||
## UI и взаимодействие
|
||||
|
||||
- **Выбор делаем интерактивом по объектам:** клик/драг по карте, кругу,
|
||||
зоне или полю. Не заменяем выбор карты, круга или цели списком кнопок в
|
||||
интерфейсе.
|
||||
- Кнопки допустимы только для глобальных действий и подтверждений:
|
||||
завершить ход, сдаться, отмена, принять/пропустить опциональную
|
||||
способность. Если выбирается конкретная карта/круг/зона — это делается
|
||||
кликом по объекту.
|
||||
- Информационные оверлеи (`ActionPrompt`, подсказки) не должны перехватывать
|
||||
клики и не дублируют выбор кнопками.
|
||||
|
||||
## Технические ограничения
|
||||
|
||||
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1015,8 +1015,11 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
||||
убраны. После подтверждения призыва карта из души не ставится в первый
|
||||
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
|
||||
клиент показывает оверлей с кругами и подсвечивает свободные RC.
|
||||
Боковая панель пока оставлена для остальных действий.
|
||||
клиент подсвечивает свободные RC и принимает выбор кликом по кругу, без
|
||||
списка кнопок. Боковая панель пока оставлена для остальных действий.
|
||||
- Зафиксировано правило взаимодействия: выбор карты/круга/зоны/цели — клик
|
||||
или drag по объекту на поле; кнопки — только глобальные действия и
|
||||
подтверждения (`AGENTS.md`, `docs/client-visual-direction.md`).
|
||||
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
|
||||
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
|
||||
триггеров и опциональная способность. Промпт информационный, не
|
||||
|
|
|
|||
|
|
@ -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-стрелка,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,17 @@
|
|||
перенесены без изменения контрактов. `Boot` остаётся стартовой сценой,
|
||||
`VanguardApp` — персистентным composition root.
|
||||
|
||||
## Взаимодействие
|
||||
|
||||
- Выбор объектов — только интерактивом по сцене: карта, круг, зона, поле.
|
||||
Не показываем список кнопок «выбрать карту/круг» вместо клика по объекту.
|
||||
- Кнопки остаются для глобальных действий и подтверждений (завершить ход,
|
||||
сдаться, отмена, принять/пропустить опциональную способность).
|
||||
- Информационные оверлеи только объясняют, что требуется; они не
|
||||
перехватывают клики и не дублируют выбор кнопками.
|
||||
- `ActionPrompt` в бою — информационный слой; подсветка на 3D-поле
|
||||
показывает доступные карты/круги/зоны.
|
||||
|
||||
## Ограничения и следующие шаги
|
||||
|
||||
- Пока нет 3D-ассетов: карты показываются процедурными градиентными
|
||||
|
|
|
|||
Loading…
Reference in New Issue