Add unified required-action prompt overlay
CI / test (push) Successful in 2m6s
Details
CI / test (push) Successful in 2m6s
Details
This commit is contained in:
parent
ef2d8e8a42
commit
7bb6bcb975
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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**.
|
||||||
|
|
|
||||||
|
|
@ -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-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue