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`.
|
подробную историю — в `docs/HANDOFF.md`.
|
||||||
- После блока: тесты, `validate`, `translations check`, коммит и push.
|
- После блока: тесты, `validate`, `translations check`, коммит и push.
|
||||||
|
|
||||||
|
## UI и взаимодействие
|
||||||
|
|
||||||
|
- **Выбор делаем интерактивом по объектам:** клик/драг по карте, кругу,
|
||||||
|
зоне или полю. Не заменяем выбор карты, круга или цели списком кнопок в
|
||||||
|
интерфейсе.
|
||||||
|
- Кнопки допустимы только для глобальных действий и подтверждений:
|
||||||
|
завершить ход, сдаться, отмена, принять/пропустить опциональную
|
||||||
|
способность. Если выбирается конкретная карта/круг/зона — это делается
|
||||||
|
кликом по объекту.
|
||||||
|
- Информационные оверлеи (`ActionPrompt`, подсказки) не должны перехватывать
|
||||||
|
клики и не дублируют выбор кнопками.
|
||||||
|
|
||||||
## Технические ограничения
|
## Технические ограничения
|
||||||
|
|
||||||
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на
|
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,6 @@ namespace Vanguard.UI
|
||||||
private Text _abilityOverlayName;
|
private Text _abilityOverlayName;
|
||||||
private RectTransform _abilityOverlayButtons;
|
private RectTransform _abilityOverlayButtons;
|
||||||
private int _abilityPromptInstanceId;
|
private int _abilityPromptInstanceId;
|
||||||
private RectTransform _choiceOverlay;
|
|
||||||
private Text _choiceOverlayTitle;
|
|
||||||
private RectTransform _choiceOverlayButtons;
|
|
||||||
private string _choicePromptKey;
|
|
||||||
private MatchConnectionInfo _info;
|
private MatchConnectionInfo _info;
|
||||||
private MatchView _view;
|
private MatchView _view;
|
||||||
private bool _connecting;
|
private bool _connecting;
|
||||||
|
|
@ -241,7 +237,6 @@ namespace Vanguard.UI
|
||||||
BuildPhasePanel();
|
BuildPhasePanel();
|
||||||
BuildCombatStats();
|
BuildCombatStats();
|
||||||
BuildAbilityOverlay();
|
BuildAbilityOverlay();
|
||||||
BuildChoiceOverlay();
|
|
||||||
BuildReconnectOverlay();
|
BuildReconnectOverlay();
|
||||||
_cardMenu = new CardActionMenuView();
|
_cardMenu = new CardActionMenuView();
|
||||||
_cardMenu.Build(_root);
|
_cardMenu.Build(_root);
|
||||||
|
|
@ -313,7 +308,6 @@ namespace Vanguard.UI
|
||||||
_board3D?.SetVisible(true);
|
_board3D?.SetVisible(true);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
HideAbilityOverlay();
|
HideAbilityOverlay();
|
||||||
HideChoiceOverlay();
|
|
||||||
_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;
|
||||||
|
|
@ -347,7 +341,6 @@ namespace Vanguard.UI
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
HideAbilityOverlay();
|
HideAbilityOverlay();
|
||||||
HideChoiceOverlay();
|
|
||||||
_cardMenu?.Hide();
|
_cardMenu?.Hide();
|
||||||
_combatStats?.gameObject.SetActive(false);
|
_combatStats?.gameObject.SetActive(false);
|
||||||
_board3D?.SetVisible(false);
|
_board3D?.SetVisible(false);
|
||||||
|
|
@ -368,7 +361,6 @@ namespace Vanguard.UI
|
||||||
RefreshActionPrompt();
|
RefreshActionPrompt();
|
||||||
SanitizeInputLocks();
|
SanitizeInputLocks();
|
||||||
SyncAbilityPrompt();
|
SyncAbilityPrompt();
|
||||||
SyncChoicePrompt();
|
|
||||||
UpdateAutoPhases();
|
UpdateAutoPhases();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,8 +383,6 @@ 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 (_choiceOverlay == null || !_choiceOverlay.gameObject.activeSelf)
|
|
||||||
_inputLock.Release("choice");
|
|
||||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||||
{
|
{
|
||||||
_feedbackLockHeld = false;
|
_feedbackLockHeld = false;
|
||||||
|
|
@ -755,7 +745,6 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
Render();
|
Render();
|
||||||
SyncAbilityPrompt();
|
SyncAbilityPrompt();
|
||||||
SyncChoicePrompt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEvents(ServerEvents events)
|
private void OnEvents(ServerEvents events)
|
||||||
|
|
@ -1307,45 +1296,6 @@ namespace Vanguard.UI
|
||||||
_inputLock.Release("ability");
|
_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()
|
private bool IsRearGuardChoice()
|
||||||
{
|
{
|
||||||
var prompt = _view?.Choice?.Prompt;
|
var prompt = _view?.Choice?.Prompt;
|
||||||
|
|
@ -1354,68 +1304,6 @@ namespace Vanguard.UI
|
||||||
|| prompt.IndexOf("rear", StringComparison.OrdinalIgnoreCase) >= 0);
|
|| 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()
|
private void BuildReconnectOverlay()
|
||||||
{
|
{
|
||||||
_reconnectOverlay = UiKit.CreatePanel(
|
_reconnectOverlay = UiKit.CreatePanel(
|
||||||
|
|
@ -1865,7 +1753,6 @@ namespace Vanguard.UI
|
||||||
option => option.Slot == slot.Slot);
|
option => option.Slot == slot.Slot);
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
{
|
{
|
||||||
HideChoiceOverlay();
|
|
||||||
OnIntentClicked(
|
OnIntentClicked(
|
||||||
new ChooseIntent { Index = index },
|
new ChooseIntent { Index = index },
|
||||||
ignoreInputLock: true);
|
ignoreInputLock: true);
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Rear_Guard_Choice_Shows_Overlay_With_Circles()
|
public void Rear_Guard_Choice_Is_Shown_Without_Buttons()
|
||||||
{
|
{
|
||||||
var config = new AppConfig();
|
var config = new AppConfig();
|
||||||
config.Load();
|
config.Load();
|
||||||
|
|
@ -364,13 +364,18 @@ namespace Vanguard.Tests
|
||||||
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
.SetValue(battle, match);
|
.SetValue(battle, match);
|
||||||
typeof(BattleView)
|
typeof(BattleView)
|
||||||
.GetMethod("SyncChoicePrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
|
.GetMethod("RefreshActionPrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
.Invoke(battle, null);
|
.Invoke(battle, null);
|
||||||
|
|
||||||
var overlay = Resources.FindObjectsOfTypeAll<GameObject>()
|
var prompt = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
.First(go => go.name == "ChoiceOverlay");
|
.First(go => go.name == "ActionPrompt");
|
||||||
Assert.IsTrue(overlay.activeSelf);
|
Assert.IsTrue(prompt.activeSelf);
|
||||||
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
|
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"));
|
.Any(go => go.name == "Choice_3"));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -1015,8 +1015,11 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
кнопками «Активировать» / «Пропустить»; из боковой панели их кнопки
|
||||||
убраны. После подтверждения призыва карта из души не ставится в первый
|
убраны. После подтверждения призыва карта из души не ставится в первый
|
||||||
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
|
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
|
||||||
клиент показывает оверлей с кругами и подсвечивает свободные RC.
|
клиент подсвечивает свободные RC и принимает выбор кликом по кругу, без
|
||||||
Боковая панель пока оставлена для остальных действий.
|
списка кнопок. Боковая панель пока оставлена для остальных действий.
|
||||||
|
- Зафиксировано правило взаимодействия: выбор карты/круга/зоны/цели — клик
|
||||||
|
или drag по объекту на поле; кнопки — только глобальные действия и
|
||||||
|
подтверждения (`AGENTS.md`, `docs/client-visual-direction.md`).
|
||||||
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
|
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
|
||||||
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
|
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
|
||||||
триггеров и опциональная способность. Промпт информационный, не
|
триггеров и опциональная способность. Промпт информационный, не
|
||||||
|
|
|
||||||
|
|
@ -138,13 +138,16 @@
|
||||||
- Опциональные AUTO-способности (Barcgal после рейда Marron) открывают
|
- Опциональные AUTO-способности (Barcgal после рейда Marron) открывают
|
||||||
модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой
|
модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой
|
||||||
панели этих кнопок нет. После подтверждения призыва сервер предлагает
|
панели этих кнопок нет. После подтверждения призыва сервер предлагает
|
||||||
`AskEmptyRearGuardChoice`, клиент показывает оверлей с кругами и
|
`AskEmptyRearGuardChoice`, клиент подсвечивает свободные RC и принимает
|
||||||
подсвечивает свободные RC — первый круг больше не выбирается
|
выбор кликом по кругу — первого круга по умолчанию нет и отдельных
|
||||||
автоматически. Сама панель пока оставлена.
|
кнопок выбора тоже. Сама панель пока оставлена.
|
||||||
- Общий верхний `ActionPrompt` показывает, что сейчас требуется: муллиган,
|
- Общий верхний `ActionPrompt` показывает, что сейчас требуется: муллиган,
|
||||||
смена, защита, бой, выбор карты/круга/цели, триггер, порядок триггеров,
|
смена, защита, бой, выбор карты/круга/цели, триггер, порядок триггеров,
|
||||||
опциональная способность. Он не перехватывает клики и скрыт в ход
|
опциональная способность. Он не перехватывает клики и скрыт в ход
|
||||||
противника.
|
противника.
|
||||||
|
- Правило: выбор карты/круга/зоны/цели делается кликом/драгом по объекту
|
||||||
|
на поле, не кнопками; кнопки — только глобальные действия/подтверждения.
|
||||||
|
Зафиксировано в `AGENTS.md` и `docs/client-visual-direction.md`.
|
||||||
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
||||||
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
||||||
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@
|
||||||
перенесены без изменения контрактов. `Boot` остаётся стартовой сценой,
|
перенесены без изменения контрактов. `Boot` остаётся стартовой сценой,
|
||||||
`VanguardApp` — персистентным composition root.
|
`VanguardApp` — персистентным composition root.
|
||||||
|
|
||||||
|
## Взаимодействие
|
||||||
|
|
||||||
|
- Выбор объектов — только интерактивом по сцене: карта, круг, зона, поле.
|
||||||
|
Не показываем список кнопок «выбрать карту/круг» вместо клика по объекту.
|
||||||
|
- Кнопки остаются для глобальных действий и подтверждений (завершить ход,
|
||||||
|
сдаться, отмена, принять/пропустить опциональную способность).
|
||||||
|
- Информационные оверлеи только объясняют, что требуется; они не
|
||||||
|
перехватывают клики и не дублируют выбор кнопками.
|
||||||
|
- `ActionPrompt` в бою — информационный слой; подсветка на 3D-поле
|
||||||
|
показывает доступные карты/круги/зоны.
|
||||||
|
|
||||||
## Ограничения и следующие шаги
|
## Ограничения и следующие шаги
|
||||||
|
|
||||||
- Пока нет 3D-ассетов: карты показываются процедурными градиентными
|
- Пока нет 3D-ассетов: карты показываются процедурными градиентными
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue