Select discard cards from hand and expose ride in phase panel
CI / test (push) Successful in 2m43s Details

This commit is contained in:
MpakobecEx 2026-10-23 17:16:51 +05:00
parent e81b966867
commit 6d8882ba2f
4 changed files with 175 additions and 63 deletions

View File

@ -78,6 +78,12 @@ namespace Vanguard.UI
private long _clockServerMs;
private long _clockReceivedMs;
private readonly List<IntentPayload> _pendingIntents = new List<IntentPayload>();
private readonly List<ActivateAbilityIntent> _discardAbilityChoices =
new List<ActivateAbilityIntent>();
private readonly List<GuardIntent> _discardGuardChoices = new List<GuardIntent>();
private bool _discardChoiceActive;
private string _discardPrompt;
private Action _discardCancelAction;
private int _pendingBoosterSlot = -1;
private int? _triggerPowerSlot;
private readonly HashSet<int> _mulliganSelected = new HashSet<int>();
@ -249,6 +255,11 @@ namespace Vanguard.UI
_reconnect.gameObject.SetActive(false);
_result.gameObject.SetActive(false);
_pendingIntents.Clear();
_discardChoiceActive = false;
_discardAbilityChoices.Clear();
_discardGuardChoices.Clear();
_discardCancelAction = null;
_discardPrompt = null;
_highlightSlots.Clear();
_mulliganSelected.Clear();
_mulliganUsed = false;
@ -984,6 +995,9 @@ namespace Vanguard.UI
if (_inputLock.IsLocked && intent is not ConcedeIntent)
return;
if (_discardChoiceActive)
return;
if (_abilityDamageIndex >= 0)
{
if (intent is ActivateAbilityIntent activate)
@ -1276,6 +1290,12 @@ namespace Vanguard.UI
if (_view == null || _sending || _inputLock.IsLocked)
return;
if (_discardChoiceActive)
{
HandleDiscardCardClicked(card);
return;
}
var targetSelection = _pendingIntents.Count > 0
|| IsMulliganSelecting()
|| IsDamageChoice()
@ -1472,7 +1492,7 @@ namespace Vanguard.UI
private void OnBoard3DSlotClicked(BoardCardRef slot)
{
if (_view == null || _sending || _inputLock.IsLocked)
if (_view == null || _sending || _inputLock.IsLocked || _discardChoiceActive)
return;
if (slot.Zone == BoardZone.Guardian)
@ -1604,34 +1624,29 @@ namespace Vanguard.UI
return;
}
var items = new List<CardActionMenuItem>();
if (free != null)
items.Add(new CardActionMenuItem("Без сброса", () => OnIntentClicked(free)));
var hand = PlayerHand();
foreach (var card in hand
.GroupBy(item => item.PrintingId)
.Select(group => group.First()))
{
var variant = discardVariants.FirstOrDefault(
item => item.DiscardPrintingId == card.PrintingId);
if (variant == null)
continue;
items.Add(new CardActionMenuItem(
"Сбросить: " + CardName(card.PrintingId),
() => OnIntentClicked(variant)));
}
if (items.Count == 0)
ShowChoiceMenu(
"Способность",
new List<CardActionMenuItem>
{
items.AddRange(discardVariants.Select(variant =>
new CardActionMenuItem(
"Сбросить: " + CardName(variant.DiscardPrintingId),
() => OnIntentClicked(variant))));
"Без сброса",
() => OnIntentClicked(free)),
new CardActionMenuItem(
"Сбросить карту в руке",
() => StartDiscardSelection(
"Выберите карту в руке для сброса.",
discardVariants,
null)),
});
return;
}
ShowChoiceMenu("Сброс для способности", items);
StartDiscardSelection(
"Выберите карту в руке для сброса.",
discardVariants,
null);
}
private void ShowGuardDiscardChoice(
@ -1654,37 +1669,108 @@ namespace Vanguard.UI
return;
}
var items = new List<CardActionMenuItem>();
var hand = PlayerHand();
foreach (var card in hand
.GroupBy(item => item.PrintingId)
.Select(group => group.First()))
{
var variant = discardVariants.FirstOrDefault(
guard => guard.DiscardPrintingId == card.PrintingId);
if (variant == null)
continue;
items.Add(new CardActionMenuItem(
"Perfect Guard: сбросить " + CardName(card.PrintingId),
() => OnIntentClicked(variant)));
StartDiscardSelection(
"Perfect Guard: выберите карту в руке для сброса.",
null,
discardVariants,
onClosed);
}
if (items.Count == 0)
private void HandleDiscardCardClicked(BoardCardRef card)
{
items.AddRange(discardVariants.Select(guard =>
new CardActionMenuItem(
"Perfect Guard: сбросить " + CardName(guard.DiscardPrintingId),
() => OnIntentClicked(guard))));
if (_view == null || card.Zone != BoardZone.Hand || card.Player != _view.You)
{
SetStatus("Выберите карту в руке для сброса.", new Color(0.98f, 0.82f, 0.42f));
return;
}
ShowChoiceMenu("Guard", items, onClosed);
var printing = card.PrintingId;
var ability = _discardAbilityChoices.FirstOrDefault(
item => item.DiscardPrintingId == printing);
if (ability != null)
{
FinishDiscardSelection();
OnIntentClicked(ability);
return;
}
private List<CardView> PlayerHand()
var guard = _discardGuardChoices.FirstOrDefault(
item => item.DiscardPrintingId == printing);
if (guard != null)
{
var player = _view?.Players.FirstOrDefault(item => item.Index == _view.You);
return player?.Hand ?? new List<CardView>();
FinishDiscardSelection();
OnIntentClicked(guard);
return;
}
SetStatus("Эта карта не подходит для сброса.", new Color(1f, 0.55f, 0.45f));
}
private void StartDiscardSelection(
string prompt,
IReadOnlyList<ActivateAbilityIntent> abilities,
IReadOnlyList<GuardIntent> guards,
Action onCancel = null)
{
_discardChoiceActive = true;
_discardPrompt = prompt;
_discardCancelAction = onCancel;
_discardAbilityChoices.Clear();
if (abilities != null)
_discardAbilityChoices.AddRange(abilities);
_discardGuardChoices.Clear();
if (guards != null)
_discardGuardChoices.AddRange(guards);
SetStatus(prompt, new Color(0.98f, 0.82f, 0.42f));
RebuildIntents(_view);
}
private void FinishDiscardSelection()
{
_discardChoiceActive = false;
_discardAbilityChoices.Clear();
_discardGuardChoices.Clear();
_discardCancelAction = null;
_discardPrompt = null;
}
private void CancelDiscardSelection()
{
if (!_discardChoiceActive)
return;
var cancel = _discardCancelAction;
FinishDiscardSelection();
if (_view != null)
{
RebuildIntents(_view);
SetStatus("Выбор сброса отменён.", Muted);
}
cancel?.Invoke();
}
private void BuildDiscardPrompt()
{
var prompt = UiKit.CreateText(
_intents,
"DiscardPrompt",
string.IsNullOrEmpty(_discardPrompt)
? "Выберите карту в руке для сброса."
: _discardPrompt,
16,
new Color(0.98f, 0.82f, 0.42f),
TextAnchor.UpperLeft);
prompt.horizontalOverflow = HorizontalWrapMode.Wrap;
UiKit.SetPreferredHeight(prompt, 72f);
var cancel = UiKit.CreateButton(
_intents,
"DiscardCancel",
"Отмена",
CancelDiscardSelection,
16);
UiKit.SetPreferredHeight(cancel, 44f);
}
private void ShowChoiceMenu(
@ -1898,6 +1984,22 @@ namespace Vanguard.UI
title.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(title, 22f);
foreach (var ride in (view.LegalIntents ?? new List<IntentPayload>())
.OfType<RideIntent>()
.GroupBy(item => item.PrintingId)
.Select(group => group.First()))
{
var captured = ride;
var rideButton = UiKit.CreateButton(
_phasePanel,
"Phase_Ride_" + captured.PrintingId,
"Сменить: " + CardName(captured.PrintingId),
() => OnIntentClicked(captured),
14);
rideButton.interactable = !_inputLock.IsLocked;
UiKit.SetPreferredHeight(rideButton, 40f);
}
foreach (var intent in intents)
{
var captured = intent;
@ -2523,6 +2625,12 @@ namespace Vanguard.UI
{
ClearChildren(_intents);
if (_discardChoiceActive)
{
BuildDiscardPrompt();
return;
}
var legal = view.LegalIntents ?? new List<IntentPayload>();
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();

View File

@ -68,6 +68,10 @@ namespace Vanguard.UI
["Сброс для способности"] = "Discard for ability",
["Сбросить: "] = "Discard: ",
["Без сброса"] = "No discard",
["Сбросить карту в руке"] = "Discard a card from hand",
["Выберите карту в руке для сброса."] = "Choose a card in hand to discard.",
["Perfect Guard: выберите карту в руке для сброса."] = "Perfect Guard: choose a card in hand to discard.",
["Способность"] = "Ability",
["Guard без сброса"] = "Guard without discard",
["Perfect Guard: сбросить "] = "Perfect Guard: discard ",
["Защититься"] = "Guard",

View File

@ -987,16 +987,16 @@ Battle; Boot и общие сервисы остаются персистент
Атака по-прежнему запрещена.
- Смена: в фазе Ride предлагается и карта того же ранга, и следующего
(раньше legal-команды содержали только `+1`, хотя движок разрешал same
grade). В панели действий есть явная кнопка «Сменить: карта».
grade). Кнопки «Сменить: карта» есть в панели действий и в «Фазах».
- Маллиган: кнопка динамическая — без выбора «Закончить муллиган», с
выбором «Заменить X карт».
- Способности: `ActivateAbilityIntent` группируются по карте; в UI одна
кнопка «Активировать способность». Если целей/вариантов несколько —
отдельный overlay, затем выбор карты для сброса (без duplicate-кнопок).
кнопка «Активировать способность». Если вариантов несколько — overlay
выбора способности; карта для сброса выбирается кликом по руке.
- Guard: варианты Perfect Guard/сброса показываются отдельным overlay;
случайный первый интент больше не выбирается. В фазе защиты есть
«Защититься» (при наличии допустимых карт) и «Не защищаться». Сброс
выбирается из карт в руке, а не по первому интенту.
«Защититься» (при наличии допустимых карт) и «Не защищаться». Карта
сброса выбирается кликом по карте в руке.
- Правую панель действий можно временно скрыть/показать кнопкой в верхней
строке, чтобы видеть поле. При скрытии панель фаз и UI противника
сдвигаются вправо.

View File

@ -84,13 +84,13 @@
`P/C`.
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
В панели действий отображается явная кнопка «Сменить: карта».
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Фазы».
- Способности: кнопка «Активировать способность» одна на карту; при
нескольких вариантах открывается overlay выбора, затем — выбор карты
для сброса. Perfect Guard также спрашивает карту для сброса отдельным
overlay, а не выбирает случайную.
нескольких вариантах открывается overlay выбора способности. Карта для
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
Perfect Guard также просит выбрать карту сброса в руке.
- В фазе защиты доступны «Защититься» (если есть допустимые карты) и
«Не защищаться»; выбор карты защиты и сброса идёт через overlay.
«Не защищаться»; карта защиты и карта сброса выбираются в руке.
- Локальный standalone работает в фоне (`runInBackground`), чтобы heartbeat
WebSocket не останавливался при потере фокуса.
- При потере соединения весь бой блокируется полноэкранным overlay