Add unified required-action prompt overlay
CI / test (push) Successful in 2m6s Details

This commit is contained in:
MpakobecEx 2026-10-23 18:54:58 +05:00
parent ef2d8e8a42
commit 7bb6bcb975
4 changed files with 201 additions and 2 deletions

View File

@ -111,6 +111,10 @@ namespace Vanguard.UI
private RectTransform _phasePanel; private RectTransform _phasePanel;
private RectTransform _combatStats; private RectTransform _combatStats;
private Text _combatStatsText; private Text _combatStatsText;
private RectTransform _actionPrompt;
private Text _actionPromptTitle;
private Text _actionPromptHint;
private string _actionPromptKey;
private bool _autoFinishMulligan; private bool _autoFinishMulligan;
private float _autoFinishMulliganAt; private float _autoFinishMulliganAt;
private bool _autoEndBattleArmed; private bool _autoEndBattleArmed;
@ -168,6 +172,8 @@ namespace Vanguard.UI
_concedeLabel = _concede.GetComponentInChildren<Text>(); _concedeLabel = _concede.GetComponentInChildren<Text>();
UiKit.SetPreferredHeight(_concede, 44f, 0f); UiKit.SetPreferredHeight(_concede, 44f, 0f);
BuildActionPrompt();
var right = UiKit.CreatePanel(_root, "IntentPanel", Panel); var right = UiKit.CreatePanel(_root, "IntentPanel", Panel);
_intentPanel = right; _intentPanel = right;
right.anchorMin = new Vector2(1f, 0f); right.anchorMin = new Vector2(1f, 0f);
@ -266,6 +272,7 @@ namespace Vanguard.UI
_reconnect.gameObject.SetActive(false); _reconnect.gameObject.SetActive(false);
_result.gameObject.SetActive(false); _result.gameObject.SetActive(false);
_pendingIntents.Clear(); _pendingIntents.Clear();
HideActionPrompt();
_discardChoiceActive = false; _discardChoiceActive = false;
_discardAbilityChoices.Clear(); _discardAbilityChoices.Clear();
_discardGuardChoices.Clear(); _discardGuardChoices.Clear();
@ -332,6 +339,7 @@ namespace Vanguard.UI
} }
_view = null; _view = null;
HideActionPrompt();
_root.gameObject.SetActive(false); _root.gameObject.SetActive(false);
_result.gameObject.SetActive(false); _result.gameObject.SetActive(false);
_feedback.gameObject.SetActive(false); _feedback.gameObject.SetActive(false);
@ -357,6 +365,7 @@ namespace Vanguard.UI
UpdateClock(); UpdateClock();
UpdateFeedback(); UpdateFeedback();
RefreshActionPrompt();
SanitizeInputLocks(); SanitizeInputLocks();
SyncAbilityPrompt(); SyncAbilityPrompt();
SyncChoicePrompt(); SyncChoicePrompt();
@ -2309,6 +2318,139 @@ namespace Vanguard.UI
OpenZone(BoardZone.Deck, pile.Player); OpenZone(BoardZone.Deck, pile.Player);
} }
private void BuildActionPrompt()
{
_actionPrompt = UiKit.CreatePanel(
_root,
"ActionPrompt",
new Color(0.025f, 0.04f, 0.065f, 0.88f));
_actionPrompt.anchorMin = new Vector2(0.5f, 1f);
_actionPrompt.anchorMax = new Vector2(0.5f, 1f);
_actionPrompt.pivot = new Vector2(0.5f, 1f);
_actionPrompt.anchoredPosition = new Vector2(0f, -98f);
_actionPrompt.sizeDelta = new Vector2(760f, 72f);
var image = _actionPrompt.GetComponent<Image>();
if (image != null)
image.raycastTarget = false;
UiKit.AddVerticalLayout(_actionPrompt, 8, 2);
_actionPromptTitle = UiKit.CreateText(
_actionPrompt,
"ActionPromptTitle",
string.Empty,
16,
new Color(1f, 0.80f, 0.42f),
TextAnchor.MiddleCenter);
_actionPromptTitle.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(_actionPromptTitle, 24f);
_actionPromptHint = UiKit.CreateText(
_actionPrompt,
"ActionPromptHint",
string.Empty,
14,
new Color(0.78f, 0.86f, 0.98f),
TextAnchor.MiddleCenter);
UiKit.SetPreferredHeight(_actionPromptHint, 24f);
_actionPrompt.gameObject.SetActive(false);
}
private void RefreshActionPrompt()
{
if (_actionPrompt == null || _view == null || _view.IsFinished)
{
HideActionPrompt();
return;
}
string title = null;
string hint = null;
if (_discardChoiceActive)
{
title = "СБРОС КАРТЫ";
hint = string.IsNullOrWhiteSpace(_discardPrompt)
? "Выберите карту в руке для сброса."
: _discardPrompt;
}
else if (_pendingIntents.Count > 0)
{
title = "ВЫБОР ЦЕЛИ";
hint = _pendingIntents.Any(intent => intent is ResolveTriggerIntent)
? "Выберите цель силы или эффекта триггера."
: "Выберите цель на поле.";
}
else if (_view.Ability != null && _view.ActivePlayer == _view.You)
{
title = "СПОСОБНОСТЬ";
hint = "Разыграть способность «" + CardName(_view.Ability.PrintingId) + "»?";
}
else if (_view.Order != null && _view.ActivePlayer == _view.You)
{
title = "ПОРЯДОК ТРИГГЕРОВ";
hint = "Выберите, какой эффект разрешить первым.";
}
else if (_view.Trigger != null && _view.ActivePlayer == _view.You)
{
title = "ТРИГГЕР";
hint = "Выберите, как применить эффект триггера.";
}
else if (_view.Choice != null && _view.ActivePlayer == _view.You)
{
title = IsRearGuardChoice() ? "ВЫБОР КРУГА" : "ВЫБОР";
hint = _view.Choice.Prompt;
}
else if (_view.ActivePlayer != _view.You)
{
HideActionPrompt();
return;
}
else if (_view.Phase == GamePhase.Mulligan)
{
title = "МУЛИГАН";
hint = "Выберите карты в руке, затем замените их или закончите муллиган.";
}
else if (_view.Phase == GamePhase.Ride)
{
title = "СМЕНА";
hint = "Выберите карту для смены или пропустите смену.";
}
else if (_view.Step == GameStep.Guard)
{
title = "ЗАЩИТА";
hint = "Выберите защиту или не защищайтесь.";
}
else if (_view.Phase == GamePhase.Battle)
{
title = "БОЙ";
hint = "Выберите атакующего или завершите ход.";
}
else
{
HideActionPrompt();
return;
}
var key = (title ?? string.Empty) + "\n" + (hint ?? string.Empty);
if (string.Equals(_actionPromptKey, key, StringComparison.Ordinal)
&& _actionPrompt.gameObject.activeSelf)
return;
_actionPromptKey = key;
_actionPromptTitle.text = title ?? string.Empty;
_actionPromptHint.text = hint ?? string.Empty;
_actionPrompt.gameObject.SetActive(true);
}
private void HideActionPrompt()
{
if (_actionPrompt == null)
return;
_actionPromptKey = null;
_actionPrompt.gameObject.SetActive(false);
}
private void BuildPhasePanel() private void BuildPhasePanel()
{ {
_phasePanel = UiKit.CreatePanel( _phasePanel = UiKit.CreatePanel(

View File

@ -380,6 +380,54 @@ namespace Vanguard.Tests
} }
} }
[Test]
public void Action_Prompt_Shows_Current_Required_Action()
{
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("Action Prompt Test Canvas");
try
{
var battle = new BattleView(canvas.transform, meta, () => { });
var match = new MatchView
{
You = 0,
ActivePlayer = 0,
TurnNumber = 2,
Phase = GamePhase.Ride,
LegalIntents = new List<IntentPayload>(),
};
typeof(BattleView)
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(battle, match);
var refresh = typeof(BattleView)
.GetMethod("RefreshActionPrompt", BindingFlags.Instance | BindingFlags.NonPublic)!;
refresh.Invoke(battle, null);
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);
match.Phase = GamePhase.Main;
refresh.Invoke(battle, null);
Assert.IsFalse(prompt.activeSelf);
}
finally
{
Object.DestroyImmediate(canvas.gameObject);
Cleanup();
}
}
private static RectTransform FindRect(string name) => private static RectTransform FindRect(string name) =>
Resources.FindObjectsOfTypeAll<GameObject>() Resources.FindObjectsOfTypeAll<GameObject>()
.Where(go => go.scene.IsValid() && go.name == name) .Where(go => go.scene.IsValid() && go.name == name)

View File

@ -1017,6 +1017,11 @@ Battle; Boot и общие сервисы остаются персистент
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`, свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
клиент показывает оверлей с кругами и подсвечивает свободные RC. клиент показывает оверлей с кругами и подсвечивает свободные RC.
Боковая панель пока оставлена для остальных действий. Боковая панель пока оставлена для остальных действий.
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
триггеров и опциональная способность. Промпт информационный, не
перехватывает клики; обновляется из снапшота и не показывается в ход
противника.
- Trigger-зона противника зеркальна: над его колодой в сторону центра, - Trigger-зона противника зеркальна: над его колодой в сторону центра,
а не за дальним краем. а не за дальним краем.
- Карты на столе и в руке масштабируются общим `cardScale = 1.25` - Карты на столе и в руке масштабируются общим `cardScale = 1.25`
@ -1039,4 +1044,4 @@ Battle; Boot и общие сервисы остаются персистент
(карты, которые сервер должен предложить в фазе Ride). Смотреть: (карты, которые сервер должен предложить в фазе Ride). Смотреть:
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`. `ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
- Проверки: сервер **142** правил + **45** серверных, `validate` и - Проверки: сервер **142** правил + **45** серверных, `validate` и
`translations check` OK; клиент EditMode **33/33**. `translations check` OK; клиент EditMode **34/34**.

View File

@ -141,6 +141,10 @@
`AskEmptyRearGuardChoice`, клиент показывает оверлей с кругами и `AskEmptyRearGuardChoice`, клиент показывает оверлей с кругами и
подсвечивает свободные RC — первый круг больше не выбирается подсвечивает свободные RC — первый круг больше не выбирается
автоматически. Сама панель пока оставлена. автоматически. Сама панель пока оставлена.
- Общий верхний `ActionPrompt` показывает, что сейчас требуется: муллиган,
смена, защита, бой, выбор карты/круга/цели, триггер, порядок триггеров,
опциональная способность. Он не перехватывает клики и скрыт в ход
противника.
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против - Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка, том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
@ -214,7 +218,7 @@ Standalone Windows:
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
``` ```
Клиентские проверки: **33/33** EditMode (сборка всех экранов, импорт арта, Клиентские проверки: **34/34** EditMode (сборка всех экранов, импорт арта,
перевод статических/динамических строк, live-переключение языка, рендер перевод статических/динамических строк, live-переключение языка, рендер
3D-доски с уроном и отдыхающей картой, геометрия поля, правила 3D-доски с уроном и отдыхающей картой, геометрия поля, правила
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона, перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,