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 _combatStats;
|
||||
private Text _combatStatsText;
|
||||
private RectTransform _actionPrompt;
|
||||
private Text _actionPromptTitle;
|
||||
private Text _actionPromptHint;
|
||||
private string _actionPromptKey;
|
||||
private bool _autoFinishMulligan;
|
||||
private float _autoFinishMulliganAt;
|
||||
private bool _autoEndBattleArmed;
|
||||
|
|
@ -168,6 +172,8 @@ namespace Vanguard.UI
|
|||
_concedeLabel = _concede.GetComponentInChildren<Text>();
|
||||
UiKit.SetPreferredHeight(_concede, 44f, 0f);
|
||||
|
||||
BuildActionPrompt();
|
||||
|
||||
var right = UiKit.CreatePanel(_root, "IntentPanel", Panel);
|
||||
_intentPanel = right;
|
||||
right.anchorMin = new Vector2(1f, 0f);
|
||||
|
|
@ -266,6 +272,7 @@ namespace Vanguard.UI
|
|||
_reconnect.gameObject.SetActive(false);
|
||||
_result.gameObject.SetActive(false);
|
||||
_pendingIntents.Clear();
|
||||
HideActionPrompt();
|
||||
_discardChoiceActive = false;
|
||||
_discardAbilityChoices.Clear();
|
||||
_discardGuardChoices.Clear();
|
||||
|
|
@ -332,6 +339,7 @@ namespace Vanguard.UI
|
|||
}
|
||||
|
||||
_view = null;
|
||||
HideActionPrompt();
|
||||
_root.gameObject.SetActive(false);
|
||||
_result.gameObject.SetActive(false);
|
||||
_feedback.gameObject.SetActive(false);
|
||||
|
|
@ -357,6 +365,7 @@ namespace Vanguard.UI
|
|||
|
||||
UpdateClock();
|
||||
UpdateFeedback();
|
||||
RefreshActionPrompt();
|
||||
SanitizeInputLocks();
|
||||
SyncAbilityPrompt();
|
||||
SyncChoicePrompt();
|
||||
|
|
@ -2309,6 +2318,139 @@ namespace Vanguard.UI
|
|||
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()
|
||||
{
|
||||
_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) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Where(go => go.scene.IsValid() && go.name == name)
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,11 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
свободный круг автоматически: сервер предлагает `AskEmptyRearGuardChoice`,
|
||||
клиент показывает оверлей с кругами и подсвечивает свободные RC.
|
||||
Боковая панель пока оставлена для остальных действий.
|
||||
- Добавлен общий верхний `ActionPrompt` с текущим обязательным действием:
|
||||
муллиган, смена, защита, бой, выбор карты/круга/цели, триггер, порядок
|
||||
триггеров и опциональная способность. Промпт информационный, не
|
||||
перехватывает клики; обновляется из снапшота и не показывается в ход
|
||||
противника.
|
||||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||||
а не за дальним краем.
|
||||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||||
|
|
@ -1039,4 +1044,4 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **33/33**.
|
||||
`translations check` OK; клиент EditMode **34/34**.
|
||||
|
|
|
|||
|
|
@ -141,6 +141,10 @@
|
|||
`AskEmptyRearGuardChoice`, клиент показывает оверлей с кругами и
|
||||
подсвечивает свободные RC — первый круг больше не выбирается
|
||||
автоматически. Сама панель пока оставлена.
|
||||
- Общий верхний `ActionPrompt` показывает, что сейчас требуется: муллиган,
|
||||
смена, защита, бой, выбор карты/круга/цели, триггер, порядок триггеров,
|
||||
опциональная способность. Он не перехватывает клики и скрыт в ход
|
||||
противника.
|
||||
- Бой: динамическая плашка `POWER X vs SHIELD Y` (Power атакующего против
|
||||
Power защищающегося + GuardShield), пересчитывается на каждом снимке, в
|
||||
том числе после Guard. От атакующего к цели идёт затухающая 3D-стрелка,
|
||||
|
|
@ -214,7 +218,7 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **33/33** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **34/34** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
|
|
|
|||
Loading…
Reference in New Issue