Add explicit ride controls, dynamic mulligan and hideable action panel
CI / test (push) Successful in 1m55s
Details
CI / test (push) Successful in 1m55s
Details
This commit is contained in:
parent
a3d2157925
commit
489d4eb394
|
|
@ -83,6 +83,9 @@ namespace Vanguard.UI
|
|||
private ZoneBlockStripView _selfZones;
|
||||
private ZoneBlockStripView _opponentZones;
|
||||
private CardActionMenuView _cardMenu;
|
||||
private RectTransform _intentPanel;
|
||||
private Text _intentPanelToggleLabel;
|
||||
private bool _intentPanelVisible = true;
|
||||
private RectTransform _phasePanel;
|
||||
private RectTransform _combatStats;
|
||||
private Text _combatStatsText;
|
||||
|
|
@ -128,11 +131,23 @@ namespace Vanguard.UI
|
|||
top, "Clock", "—", 22, Color.white, TextAnchor.MiddleRight);
|
||||
UiKit.SetPreferredHeight(_clock, 44f, 0f);
|
||||
|
||||
var toggle = UiKit.CreateButton(
|
||||
top,
|
||||
"ToggleIntentPanel",
|
||||
"Скрыть панель",
|
||||
ToggleIntentPanel,
|
||||
16);
|
||||
var toggleElement = UiKit.SetPreferredHeight(toggle, 44f, 0f);
|
||||
toggleElement.preferredWidth = 168f;
|
||||
_intentPanelToggleLabel = toggle.GetComponentInChildren<Text>();
|
||||
UiKit.MakeLiteral(_intentPanelToggleLabel);
|
||||
|
||||
_concede = UiKit.CreateButton(top, "Concede", "Сдаться", OnConcedeClicked, 18);
|
||||
_concedeLabel = _concede.GetComponentInChildren<Text>();
|
||||
UiKit.SetPreferredHeight(_concede, 44f, 0f);
|
||||
|
||||
var right = UiKit.CreatePanel(_root, "IntentPanel", Panel);
|
||||
_intentPanel = right;
|
||||
right.anchorMin = new Vector2(1f, 0f);
|
||||
right.anchorMax = new Vector2(1f, 1f);
|
||||
right.pivot = new Vector2(1f, 0.5f);
|
||||
|
|
@ -236,6 +251,14 @@ namespace Vanguard.UI
|
|||
_autoFinishMulliganAt = 0f;
|
||||
_autoEndBattleArmed = false;
|
||||
_autoEndBattleAt = 0f;
|
||||
_intentPanelVisible = true;
|
||||
if (_intentPanel != null)
|
||||
_intentPanel.gameObject.SetActive(true);
|
||||
if (_intentPanelToggleLabel != null)
|
||||
{
|
||||
_intentPanelToggleLabel.text = "Скрыть панель";
|
||||
UiKit.MakeLiteral(_intentPanelToggleLabel);
|
||||
}
|
||||
_inputLock.ReleaseAll();
|
||||
_feedbackLockHeld = false;
|
||||
_logLines.Clear();
|
||||
|
|
@ -847,6 +870,20 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private void ToggleIntentPanel()
|
||||
{
|
||||
_intentPanelVisible = !_intentPanelVisible;
|
||||
if (_intentPanel != null)
|
||||
_intentPanel.gameObject.SetActive(_intentPanelVisible);
|
||||
if (_intentPanelToggleLabel != null)
|
||||
{
|
||||
_intentPanelToggleLabel.text = _intentPanelVisible
|
||||
? "Скрыть панель"
|
||||
: "Показать панель";
|
||||
UiKit.MakeLiteral(_intentPanelToggleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConcedeClicked()
|
||||
{
|
||||
if (_view == null || _finished)
|
||||
|
|
@ -1247,9 +1284,26 @@ namespace Vanguard.UI
|
|||
UiKit.SetPreferredHeight(button, 46f);
|
||||
}
|
||||
|
||||
private void AddGuardButton(IReadOnlyList<GuardIntent> guards)
|
||||
private void AddRideButtons(IReadOnlyList<RideIntent> rides)
|
||||
{
|
||||
if (guards == null || guards.Count == 0)
|
||||
foreach (var ride in rides
|
||||
.GroupBy(item => item.PrintingId)
|
||||
.Select(group => group.First()))
|
||||
{
|
||||
var captured = ride;
|
||||
var button = UiKit.CreateButton(
|
||||
_intents,
|
||||
"Ride_" + captured.PrintingId,
|
||||
"Сменить: " + CardName(captured.PrintingId),
|
||||
() => OnIntentClicked(captured),
|
||||
16);
|
||||
button.interactable = !_inputLock.IsLocked;
|
||||
UiKit.SetPreferredHeight(button, 46f);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddGuardButton(IReadOnlyList<GuardIntent> guards)
|
||||
{ if (guards == null || guards.Count == 0)
|
||||
return;
|
||||
|
||||
var button = UiKit.CreateButton(
|
||||
|
|
@ -1327,19 +1381,44 @@ namespace Vanguard.UI
|
|||
if (distinct.Count == 0)
|
||||
return;
|
||||
|
||||
if (distinct.Count == 1)
|
||||
var free = distinct.FirstOrDefault(
|
||||
variant => string.IsNullOrEmpty(variant.DiscardPrintingId));
|
||||
var discardVariants = distinct
|
||||
.Where(variant => !string.IsNullOrEmpty(variant.DiscardPrintingId))
|
||||
.ToList();
|
||||
if (discardVariants.Count == 0)
|
||||
{
|
||||
OnIntentClicked(distinct[0]);
|
||||
OnIntentClicked(free ?? distinct[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = distinct
|
||||
.Select(variant => new CardActionMenuItem(
|
||||
string.IsNullOrEmpty(variant.DiscardPrintingId)
|
||||
? "Без сброса"
|
||||
: "Сбросить: " + CardName(variant.DiscardPrintingId),
|
||||
() => OnIntentClicked(variant)))
|
||||
.ToList();
|
||||
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)
|
||||
{
|
||||
items.AddRange(discardVariants.Select(variant =>
|
||||
new CardActionMenuItem(
|
||||
"Сбросить: " + CardName(variant.DiscardPrintingId),
|
||||
() => OnIntentClicked(variant))));
|
||||
}
|
||||
|
||||
ShowChoiceMenu("Сброс для способности", items);
|
||||
}
|
||||
|
||||
|
|
@ -1354,29 +1433,48 @@ namespace Vanguard.UI
|
|||
if (distinct.Count == 0)
|
||||
return;
|
||||
|
||||
if (distinct.Count > 1)
|
||||
{
|
||||
distinct = distinct
|
||||
var discardVariants = distinct
|
||||
.Where(guard => !string.IsNullOrEmpty(guard.DiscardPrintingId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (distinct.Count == 1)
|
||||
if (discardVariants.Count == 0)
|
||||
{
|
||||
OnIntentClicked(distinct[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = distinct
|
||||
.Select(guard => new CardActionMenuItem(
|
||||
string.IsNullOrEmpty(guard.DiscardPrintingId)
|
||||
? "Guard без сброса"
|
||||
: "Perfect Guard: сбросить " + CardName(guard.DiscardPrintingId),
|
||||
() => OnIntentClicked(guard)))
|
||||
.ToList();
|
||||
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)));
|
||||
}
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
items.AddRange(discardVariants.Select(guard =>
|
||||
new CardActionMenuItem(
|
||||
"Perfect Guard: сбросить " + CardName(guard.DiscardPrintingId),
|
||||
() => OnIntentClicked(guard))));
|
||||
}
|
||||
|
||||
ShowChoiceMenu("Guard", items, onClosed);
|
||||
}
|
||||
|
||||
private List<CardView> PlayerHand()
|
||||
{
|
||||
var player = _view?.Players.FirstOrDefault(item => item.Index == _view.You);
|
||||
return player?.Hand ?? new List<CardView>();
|
||||
}
|
||||
|
||||
private void ShowChoiceMenu(
|
||||
string title,
|
||||
IReadOnlyList<CardActionMenuItem> items,
|
||||
|
|
@ -2204,10 +2302,12 @@ namespace Vanguard.UI
|
|||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
||||
var guards = playable.OfType<GuardIntent>().ToList();
|
||||
var rides = playable.OfType<RideIntent>().ToList();
|
||||
var board = playable
|
||||
.Where(intent => IsBoardIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent
|
||||
&& intent is not GuardIntent)
|
||||
&& intent is not GuardIntent
|
||||
&& intent is not RideIntent)
|
||||
.ToList();
|
||||
var global = playable
|
||||
.Where(intent => !IsBoardIntent(intent)
|
||||
|
|
@ -2274,11 +2374,27 @@ namespace Vanguard.UI
|
|||
var exchange = UiKit.CreateButton(
|
||||
_intents,
|
||||
"MulliganExchange",
|
||||
"Маллиган: " + _mulliganSelected.Count + " карт",
|
||||
"Заменить " + _mulliganSelected.Count + " карт",
|
||||
SendMulligan,
|
||||
16);
|
||||
UiKit.SetPreferredHeight(exchange, 44f);
|
||||
}
|
||||
else
|
||||
{
|
||||
var finishMulligan = (view.LegalIntents ?? new List<IntentPayload>())
|
||||
.OfType<FinishMulliganIntent>()
|
||||
.FirstOrDefault();
|
||||
if (finishMulligan != null)
|
||||
{
|
||||
var finishButton = UiKit.CreateButton(
|
||||
_intents,
|
||||
"MulliganFinish",
|
||||
"Закончить муллиган",
|
||||
() => OnIntentClicked(finishMulligan),
|
||||
16);
|
||||
UiKit.SetPreferredHeight(finishButton, 44f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAbilityDamageTargeting())
|
||||
|
|
@ -2325,6 +2441,9 @@ namespace Vanguard.UI
|
|||
if (guards.Count > 0)
|
||||
AddGuardButton(guards);
|
||||
|
||||
if (rides.Count > 0)
|
||||
AddRideButtons(rides);
|
||||
|
||||
foreach (var intent in global)
|
||||
AddIntentButton(intent);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ namespace Vanguard.UI
|
|||
["Защититься: "] = "Guard: ",
|
||||
["Защита"] = "Guard",
|
||||
["Не защищаться"] = "Do not guard",
|
||||
["Скрыть панель"] = "Hide panel",
|
||||
["Показать панель"] = "Show panel",
|
||||
["Сменить: "] = "Ride: ",
|
||||
["Заменить "] = "Replace ",
|
||||
["Закончить муллиган"] = "Finish mulligan",
|
||||
["Действия"] = "Actions",
|
||||
["Переместить"] = "Move",
|
||||
["Подробнее"] = "Details",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Тесты сервера | **186/186** (141 правил/контрактов + 45 сервер) |
|
||||
| Тесты сервера | **187/187** (142 правил/контрактов + 45 сервер) |
|
||||
| `validate` | 0 ошибок, 0 предупреждений |
|
||||
| `translations check` | 94/94 |
|
||||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||||
|
|
@ -987,17 +987,22 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
Атака по-прежнему запрещена.
|
||||
- Смена: в фазе Ride предлагается и карта того же ранга, и следующего
|
||||
(раньше legal-команды содержали только `+1`, хотя движок разрешал same
|
||||
grade).
|
||||
grade). В панели действий есть явная кнопка «Сменить: карта».
|
||||
- Маллиган: кнопка динамическая — без выбора «Закончить муллиган», с
|
||||
выбором «Заменить X карт».
|
||||
- Способности: `ActivateAbilityIntent` группируются по карте; в UI одна
|
||||
кнопка «Активировать способность». Если целей/вариантов несколько —
|
||||
отдельный overlay, затем выбор карты для сброса (без duplicate-кнопок).
|
||||
- Guard: варианты Perfect Guard/сброса показываются отдельным overlay;
|
||||
случайный первый интент больше не выбирается. В фазе защиты есть
|
||||
«Защититься» (при наличии допустимых карт) и «Не защищаться».
|
||||
«Защититься» (при наличии допустимых карт) и «Не защищаться». Сброс
|
||||
выбирается из карт в руке, а не по первому интенту.
|
||||
- Правую панель действий можно временно скрыть/показать кнопкой в верхней
|
||||
строке, чтобы видеть поле.
|
||||
- Камера боя: `(0, 4.2, -8)` → цель `(0, 0, 0)`, только наклон по X;
|
||||
поле по центру, без бокового смещения.
|
||||
- UI: reference-разрешение **1920×1080**, CanvasScaler `matchWidthOrHeight
|
||||
= 0.5`; standalone принудительно `1920×1080 FullScreenWindow`,
|
||||
`defaultIsNativeResolution = 0`.
|
||||
- Проверки: сервер **141** правил + **45** серверных, `validate` и
|
||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **30/30**.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||
in-process реестр соединений.
|
||||
- Тесты: **141** правил + **45** серверных, CI зелёный.
|
||||
- Тесты: **142** правил + **45** серверных, CI зелёный.
|
||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||
- Документация: `docs/server-api.md`, `docs/protocol.md`,
|
||||
`docs/game-rules.md`, `docs/HANDOFF.md`.
|
||||
|
|
@ -84,12 +84,15 @@
|
|||
`P/C`.
|
||||
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
||||
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
||||
В панели действий отображается явная кнопка «Сменить: карта».
|
||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||
нескольких вариантах открывается overlay выбора, затем — выбор карты
|
||||
для сброса. Perfect Guard также спрашивает карту для сброса отдельным
|
||||
overlay, а не выбирает случайную.
|
||||
- В фазе защиты доступны «Защититься» (если есть допустимые карты) и
|
||||
«Не защищаться»; выбор карты защиты и сброса идёт через overlay.
|
||||
- Правую панель действий можно временно скрыть кнопкой «Скрыть панель»
|
||||
в верхней строке; поле остаётся видимым, вернуть — «Показать панель».
|
||||
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
||||
противника — сверху справа. Блоки `Рука/Колода/Дроп/Душа/Урон/Защита/Триггер`
|
||||
кликабельны и открывают общий горизонтальный `CardListView`; из списка
|
||||
|
|
@ -105,7 +108,9 @@
|
|||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||
Counter Blast — открытую карту до активации способности.
|
||||
- Маллиган: один раз на игрока, оба игрока; выбранные карты уходят в низ
|
||||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
||||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты;
|
||||
кнопка динамическая: без выбора — «Закончить муллиган», с выбором —
|
||||
«Заменить X карт».
|
||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||
стола; ввод заблокирован до завершения через `InputLockService`, очередь
|
||||
событий + кнопка «Пропустить анимации». Длительности и скорости карт —
|
||||
|
|
|
|||
|
|
@ -131,6 +131,28 @@ public class GameEngineTests
|
|||
Assert.True(engine.Execute(new RideCommand(Conroe)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ride_Can_Reach_Second_Grade_On_Next_Turn()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
Give(engine, 0, Marron);
|
||||
Assert.True(engine.Execute(new RideCommand(Marron)).Accepted);
|
||||
|
||||
engine.Execute(new EndTurnCommand());
|
||||
engine.Execute(new SkipRideCommand());
|
||||
engine.Execute(new EndTurnCommand());
|
||||
|
||||
Give(engine, 0, Gallatin);
|
||||
Assert.Contains(
|
||||
engine.LegalCommands(),
|
||||
command => command is RideCommand ride && ride.PrintingId == Gallatin);
|
||||
|
||||
Assert.True(engine.Execute(new RideCommand(Gallatin)).Accepted);
|
||||
Assert.Equal(
|
||||
2,
|
||||
engine.State.Catalog.CardOf(engine.State.Players[0].Vanguard!.PrintingId).Grade);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Both_Players_Mulligan_Once_Then_First_Player_Starts()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue