Fix first-turn battle, same-grade ride, ability and guard choices, center camera
CI / test (push) Successful in 1m53s
Details
CI / test (push) Successful in 1m53s
Details
This commit is contained in:
parent
dfd766eaad
commit
30aed3b25d
|
|
@ -54,12 +54,12 @@ namespace Vanguard.App
|
|||
camera.fieldOfView = theme == SceneTheme.Battle ? 46f : 52f;
|
||||
camera.transform.position = theme switch
|
||||
{
|
||||
SceneTheme.Battle => new Vector3(1.4f, 5.8f, -8f),
|
||||
SceneTheme.Battle => new Vector3(0f, 4.2f, -8f),
|
||||
SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f),
|
||||
_ => new Vector3(0f, 3.2f, -10f),
|
||||
};
|
||||
camera.transform.rotation = theme == SceneTheme.Battle
|
||||
? Quaternion.Euler(35.9f, 0f, 0f)
|
||||
? Quaternion.Euler(27.7f, 0f, 0f)
|
||||
: Quaternion.Euler(8f, 0f, 0f);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ MonoBehaviour:
|
|||
damageX: 3
|
||||
pileX: 2.95
|
||||
showZoneLabels: 0
|
||||
cameraPosition: {x: 1.4, y: 5.8, z: -8}
|
||||
cameraTarget: {x: 1.4, y: 0, z: 0}
|
||||
cameraPosition: {x: 0, y: 4.2, z: -8}
|
||||
cameraTarget: {x: 0, y: 0, z: 0}
|
||||
cameraFov: 46
|
||||
tableColor: {r: 0.045, g: 0.04, b: 0.055, a: 1}
|
||||
matSelfColor: {r: 0.055, g: 0.095, b: 0.16, a: 1}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ namespace Vanguard.Board3D
|
|||
|
||||
[Header("Камера")]
|
||||
[Tooltip("Фиксированная камера над полем. Не двигается игроком.")]
|
||||
public Vector3 cameraPosition = new Vector3(1.4f, 5.8f, -8.0f);
|
||||
public Vector3 cameraTarget = new Vector3(1.4f, 0f, 0f);
|
||||
public Vector3 cameraPosition = new Vector3(0f, 4.2f, -8f);
|
||||
public Vector3 cameraTarget = new Vector3(0f, 0f, 0f);
|
||||
public float cameraFov = 46f;
|
||||
|
||||
[Header("Палитра")]
|
||||
|
|
|
|||
|
|
@ -1086,8 +1086,7 @@ namespace Vanguard.UI
|
|||
}
|
||||
|
||||
private void OpenCardMenu(BoardCardRef card)
|
||||
{
|
||||
if (_view == null || _board3D == null)
|
||||
{ if (_view == null || _board3D == null)
|
||||
return;
|
||||
|
||||
var target = new BoardTarget(
|
||||
|
|
@ -1127,9 +1126,17 @@ namespace Vanguard.UI
|
|||
}));
|
||||
}
|
||||
|
||||
var abilities = sources.OfType<ActivateAbilityIntent>().ToList();
|
||||
if (abilities.Count > 0)
|
||||
{
|
||||
actions.Add(new CardActionMenuItem(
|
||||
"Активировать способность",
|
||||
() => OpenAbilitySelector(abilities)));
|
||||
}
|
||||
|
||||
foreach (var intent in sources)
|
||||
{
|
||||
if (intent is AttackIntent or MoveIntent)
|
||||
if (intent is AttackIntent or MoveIntent or ActivateAbilityIntent)
|
||||
continue;
|
||||
|
||||
var captured = intent;
|
||||
|
|
@ -1225,6 +1232,159 @@ namespace Vanguard.UI
|
|||
OnBoardTargetClicked(new BoardTarget(slot.Player, slot.Slot, null, 0, false));
|
||||
}
|
||||
|
||||
private void AddAbilityButton(IReadOnlyList<ActivateAbilityIntent> abilities)
|
||||
{
|
||||
if (abilities == null || abilities.Count == 0)
|
||||
return;
|
||||
|
||||
var button = UiKit.CreateButton(
|
||||
_intents,
|
||||
"ActivateAbility",
|
||||
"Активировать способность",
|
||||
() => OpenAbilitySelector(abilities),
|
||||
16);
|
||||
button.interactable = !_inputLock.IsLocked;
|
||||
UiKit.SetPreferredHeight(button, 46f);
|
||||
}
|
||||
|
||||
private void OpenAbilitySelector(IReadOnlyList<ActivateAbilityIntent> abilities)
|
||||
{
|
||||
var groups = abilities
|
||||
.GroupBy(ability => ability.InstanceId)
|
||||
.ToList();
|
||||
if (groups.Count == 0)
|
||||
return;
|
||||
|
||||
if (groups.Count == 1)
|
||||
{
|
||||
ShowAbilityVariantChoice(groups[0].ToList());
|
||||
return;
|
||||
}
|
||||
|
||||
var items = groups
|
||||
.Select(group => new CardActionMenuItem(
|
||||
"Активировать: " + CardNameForInstance(group.Key),
|
||||
() => ShowAbilityVariantChoice(group.ToList())))
|
||||
.ToList();
|
||||
ShowChoiceMenu("Способности", items);
|
||||
}
|
||||
|
||||
private void ShowAbilityVariantChoice(
|
||||
IReadOnlyList<ActivateAbilityIntent> variants)
|
||||
{
|
||||
var distinct = variants
|
||||
.GroupBy(variant => variant.DiscardPrintingId ?? string.Empty)
|
||||
.Select(group => group.First())
|
||||
.ToList();
|
||||
if (distinct.Count == 0)
|
||||
return;
|
||||
|
||||
if (distinct.Count == 1)
|
||||
{
|
||||
OnIntentClicked(distinct[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = distinct
|
||||
.Select(variant => new CardActionMenuItem(
|
||||
string.IsNullOrEmpty(variant.DiscardPrintingId)
|
||||
? "Без сброса"
|
||||
: "Сбросить: " + CardName(variant.DiscardPrintingId),
|
||||
() => OnIntentClicked(variant)))
|
||||
.ToList();
|
||||
ShowChoiceMenu("Сброс для способности", items);
|
||||
}
|
||||
|
||||
private void ShowGuardDiscardChoice(
|
||||
IReadOnlyList<GuardIntent> guards,
|
||||
Action onClosed = null)
|
||||
{
|
||||
var distinct = guards
|
||||
.GroupBy(guard => guard.DiscardPrintingId ?? string.Empty)
|
||||
.Select(group => group.First())
|
||||
.ToList();
|
||||
if (distinct.Count == 0)
|
||||
return;
|
||||
|
||||
if (distinct.Count > 1)
|
||||
{
|
||||
distinct = distinct
|
||||
.Where(guard => !string.IsNullOrEmpty(guard.DiscardPrintingId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (distinct.Count == 1)
|
||||
{
|
||||
OnIntentClicked(distinct[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = distinct
|
||||
.Select(guard => new CardActionMenuItem(
|
||||
string.IsNullOrEmpty(guard.DiscardPrintingId)
|
||||
? "Guard без сброса"
|
||||
: "Perfect Guard: сбросить " + CardName(guard.DiscardPrintingId),
|
||||
() => OnIntentClicked(guard)))
|
||||
.ToList();
|
||||
ShowChoiceMenu("Guard", items, onClosed);
|
||||
}
|
||||
|
||||
private void ShowChoiceMenu(
|
||||
string title,
|
||||
IReadOnlyList<CardActionMenuItem> items,
|
||||
Action onClosed = null)
|
||||
{
|
||||
if (_cardMenu == null || items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
_inputLock.Acquire("card-menu");
|
||||
_cardMenu.Show(
|
||||
new Vector2(Screen.width * 0.5f, Screen.height * 0.5f),
|
||||
title,
|
||||
items,
|
||||
() =>
|
||||
{
|
||||
_inputLock.Release("card-menu");
|
||||
onClosed?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private string CardNameForInstance(int instanceId)
|
||||
{
|
||||
if (_view == null)
|
||||
return "карта #" + instanceId;
|
||||
|
||||
foreach (var player in _view.Players)
|
||||
{
|
||||
var cards = new List<CardView>();
|
||||
if (player.Vanguard != null)
|
||||
cards.Add(player.Vanguard);
|
||||
if (player.RearGuards != null)
|
||||
{
|
||||
foreach (var card in player.RearGuards)
|
||||
{
|
||||
if (card != null)
|
||||
cards.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
if (player.Hand != null)
|
||||
cards.AddRange(player.Hand);
|
||||
if (player.Drop != null)
|
||||
cards.AddRange(player.Drop);
|
||||
if (player.Soul != null)
|
||||
cards.AddRange(player.Soul);
|
||||
if (player.Guardian != null)
|
||||
cards.AddRange(player.Guardian);
|
||||
|
||||
var found = cards.FirstOrDefault(card => card.InstanceId == instanceId);
|
||||
if (found != null)
|
||||
return CardName(found.PrintingId);
|
||||
}
|
||||
|
||||
return "карта #" + instanceId;
|
||||
}
|
||||
|
||||
private void OnBoard3DCardDropped(BoardCardRef source, BoardCardRef target)
|
||||
{
|
||||
if (_view == null || _sending || _inputLock.IsLocked)
|
||||
|
|
@ -1237,8 +1397,16 @@ namespace Vanguard.UI
|
|||
{
|
||||
if (target.Zone == BoardZone.Guardian)
|
||||
{
|
||||
intent = legal.OfType<GuardIntent>()
|
||||
.FirstOrDefault(item => item.PrintingId == source.PrintingId);
|
||||
var guards = legal.OfType<GuardIntent>()
|
||||
.Where(item => item.PrintingId == source.PrintingId)
|
||||
.ToList();
|
||||
if (guards.Count > 1)
|
||||
{
|
||||
ShowGuardDiscardChoice(guards, Render);
|
||||
return;
|
||||
}
|
||||
|
||||
intent = guards.FirstOrDefault();
|
||||
}
|
||||
else if (target.Player == _view.You && target.Zone == BoardZone.Vanguard)
|
||||
{
|
||||
|
|
@ -1354,6 +1522,7 @@ namespace Vanguard.UI
|
|||
|
||||
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||
.Where(IsPhaseIntent)
|
||||
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
||||
.ToList();
|
||||
if (intents.Count == 0)
|
||||
{
|
||||
|
|
@ -1696,6 +1865,13 @@ namespace Vanguard.UI
|
|||
.Where(intent => TryGetSource(intent, out var source) && source.Matches(target))
|
||||
.ToList();
|
||||
|
||||
var guards = sources.OfType<GuardIntent>().ToList();
|
||||
if (guards.Count > 0 && guards.Count == sources.Count)
|
||||
{
|
||||
ShowGuardDiscardChoice(guards);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sources.Count == 0)
|
||||
{
|
||||
SetStatus("Для этой карты сейчас нет доступного действия.", Muted);
|
||||
|
|
@ -1713,6 +1889,28 @@ namespace Vanguard.UI
|
|||
.Where(intent => TryGetTarget(intent, out _))
|
||||
.ToList();
|
||||
|
||||
if (withTargets.Count == 0 && sources.Count > 1)
|
||||
{
|
||||
var abilities = sources.OfType<ActivateAbilityIntent>().ToList();
|
||||
var other = sources
|
||||
.Where(intent => intent is not ActivateAbilityIntent)
|
||||
.ToList();
|
||||
var items = other
|
||||
.Select(intent => new CardActionMenuItem(
|
||||
Label(intent),
|
||||
() => OnIntentClicked(intent)))
|
||||
.ToList();
|
||||
if (abilities.Count > 0)
|
||||
{
|
||||
items.Add(new CardActionMenuItem(
|
||||
"Активировать способность",
|
||||
() => OpenAbilitySelector(abilities)));
|
||||
}
|
||||
|
||||
ShowChoiceMenu("Действия", items);
|
||||
return;
|
||||
}
|
||||
|
||||
if (withTargets.Count == 0)
|
||||
{
|
||||
ClearSelection(rebuild: false);
|
||||
|
|
@ -1956,9 +2154,14 @@ namespace Vanguard.UI
|
|||
|
||||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||
var board = playable.Where(IsBoardIntent).ToList();
|
||||
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
||||
var board = playable
|
||||
.Where(intent => IsBoardIntent(intent) && intent is not ActivateAbilityIntent)
|
||||
.ToList();
|
||||
var global = playable
|
||||
.Where(intent => !IsBoardIntent(intent) && !IsPhaseIntent(intent))
|
||||
.Where(intent => !IsBoardIntent(intent)
|
||||
&& !IsPhaseIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent)
|
||||
.ToList();
|
||||
|
||||
if (_pendingIntents.Count > 0)
|
||||
|
|
@ -2065,6 +2268,9 @@ namespace Vanguard.UI
|
|||
UiKit.SetPreferredHeight(toggle, 38f);
|
||||
}
|
||||
|
||||
if (abilities.Count > 0)
|
||||
AddAbilityButton(abilities);
|
||||
|
||||
foreach (var intent in global)
|
||||
AddIntentButton(intent);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ namespace Vanguard.UI
|
|||
["Атака "] = "Attack ",
|
||||
["Атака"] = "Attack",
|
||||
["Атака с поддержкой: "] = "Boosted attack: ",
|
||||
["Активировать способность"] = "Activate ability",
|
||||
["Активировать: "] = "Activate: ",
|
||||
["Сброс для способности"] = "Discard for ability",
|
||||
["Сбросить: "] = "Discard: ",
|
||||
["Без сброса"] = "No discard",
|
||||
["Guard без сброса"] = "Guard without discard",
|
||||
["Perfect Guard: сбросить "] = "Perfect Guard: discard ",
|
||||
["Действия"] = "Actions",
|
||||
["Переместить"] = "Move",
|
||||
["Подробнее"] = "Details",
|
||||
["Нет доступных действий"] = "No available actions",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ namespace Vanguard.Tests
|
|||
{
|
||||
public sealed class PlaymatLayoutTests
|
||||
{
|
||||
[Test]
|
||||
public void Camera_Is_Centered_And_Only_Pitches_Down()
|
||||
{
|
||||
var config = PlaymatLayoutConfig.CreateDefault();
|
||||
Assert.AreEqual(0f, config.cameraPosition.x, 0.0001f);
|
||||
Assert.AreEqual(0f, config.cameraTarget.x, 0.0001f);
|
||||
Assert.AreEqual(config.cameraPosition.x, config.cameraTarget.x, 0.0001f);
|
||||
Assert.Greater(config.cameraPosition.y, config.cameraTarget.y);
|
||||
Assert.AreEqual(0f, config.cameraTarget.z, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Default_Playmat_Has_Six_Circles_Per_Side()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Тесты сервера | **184/184** (139 правил/контрактов + 45 сервер) |
|
||||
| Тесты сервера | **186/186** (141 правил/контрактов + 45 сервер) |
|
||||
| `validate` | 0 ошибок, 0 предупреждений |
|
||||
| `translations check` | 94/94 |
|
||||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||||
|
|
@ -858,8 +858,9 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики,
|
||||
drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и
|
||||
индикатором точки броска. Ошибка интента возвращает карту на место.
|
||||
- Камера фиксирована над полем: позиция `(1.4, 5.8, -8)`, цель
|
||||
`(1.4, 0, 0)`, FOV 46; игрок её не двигает. Рука не лежит на столе,
|
||||
- Камера фиксирована по центру поля: позиция `(0, 4.2, -8)`, цель
|
||||
`(0, 0, 0)`, FOV 46; наклон только по X, без бокового смещения, игрок её
|
||||
не двигает. Рука не лежит на столе,
|
||||
а закреплена в экране снизу по центру (`handViewportX/Y`,
|
||||
`handViewDistance`) и всегда развёрнута лицом к игроку; уменьшена, чтобы
|
||||
не перекрывать задний ряд RC. Драг из руки уходит на стол.
|
||||
|
|
@ -958,7 +959,7 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- Добавлен `Battle_Layout_Fits_Reference_Resolution`: проверяет блоки зон,
|
||||
панель фаз и боевую плашку на 1920×1080 и отсутствие пересечений; при
|
||||
другом разрешении CanvasScaler масштабирует reference-раскладку.
|
||||
- Проверки: клиент EditMode **28/28**, `SCENE_SMOKE success=True`,
|
||||
- Проверки: клиент EditMode **29/29**, `SCENE_SMOKE success=True`,
|
||||
`PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
- Дальше: только 3D-модели/наполнение сцен, анимации retire/ghost и
|
||||
декбилдер; prefab-оверлеи и раскладка закрыты.
|
||||
|
|
@ -978,3 +979,21 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
не показателен: runtime-материалы требуют графического устройства.
|
||||
- Сервер для standalone по умолчанию — `http://192.168.31.158:18080`;
|
||||
его нужно поднять/держать доступным.
|
||||
|
||||
## 2026-10-23 — Исправления боя и камера
|
||||
|
||||
- Первый ход: `EnterBattleCommand` больше не попадает в легальные интенты,
|
||||
пока действует правило первого хода; кнопка «В бой» не появляется.
|
||||
Атака по-прежнему запрещена.
|
||||
- Смена: в фазе Ride предлагается и карта того же ранга, и следующего
|
||||
(раньше legal-команды содержали только `+1`, хотя движок разрешал same
|
||||
grade).
|
||||
- Способности: `ActivateAbilityIntent` группируются по карте; в UI одна
|
||||
кнопка «Активировать способность». Если целей/вариантов несколько —
|
||||
отдельный overlay, затем выбор карты для сброса (без duplicate-кнопок).
|
||||
- Guard: варианты Perfect Guard/сброса показываются отдельным overlay;
|
||||
случайный первый интент больше не выбирается.
|
||||
- Камера боя: `(0, 4.2, -8)` → цель `(0, 0, 0)`, только наклон по X;
|
||||
поле по центру, без бокового смещения.
|
||||
- Проверки: сервер **141** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **29/29**.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||
in-process реестр соединений.
|
||||
- Тесты: **139** правил + **45** серверных, CI зелёный.
|
||||
- Тесты: **141** правил + **45** серверных, CI зелёный.
|
||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||
- Документация: `docs/server-api.md`, `docs/protocol.md`,
|
||||
`docs/game-rules.md`, `docs/HANDOFF.md`.
|
||||
|
|
@ -68,10 +68,10 @@
|
|||
Рубашка — стандартная VF Vanguard (`Resources/CardArt/vanguard_back.png`),
|
||||
общая для 2D и 3D. Рука закреплена в поле зрения камеры снизу по центру
|
||||
и всегда смотрит на игрока, а не лежит на столе; уменьшена, чтобы не
|
||||
закрывать задний ряд RC. Камера фиксирована над полем под небольшим
|
||||
углом и не двигается игроком: `(1.4, 5.8, -8)`, цель `(1.4, 0, 0)`,
|
||||
FOV 46. Драг-н-дроп на плоскость стола с подсветкой целей и плавным
|
||||
снапом.
|
||||
закрывать задний ряд RC. Камера фиксирована по центру поля под небольшим
|
||||
углом к горизонту и не двигается игроком: `(0, 4.2, -8)`, цель
|
||||
`(0, 0, 0)`, FOV 46; наклон только по X, без бокового смещения. Драг-н-дроп
|
||||
на плоскость стола с подсветкой целей и плавным снапом.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||
противника зеркально; между полями общий круг защиты; колода, дроп,
|
||||
триггер-зона и урон — 3D-зоны. Локальный игрок всегда снизу, даже если
|
||||
|
|
@ -79,6 +79,12 @@
|
|||
лицом вверх; урон — один вертикальный ряд, карта входит открытой.
|
||||
Над колодой — зона проверки триггера. Под картами V/R — динамические
|
||||
`P/C`.
|
||||
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
||||
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||
нескольких вариантах открывается overlay выбора, затем — выбор карты
|
||||
для сброса. Perfect Guard также спрашивает карту для сброса отдельным
|
||||
overlay, а не выбирает случайную.
|
||||
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
||||
противника — сверху справа. Блоки `Рука/Колода/Дроп/Душа/Урон/Защита/Триггер`
|
||||
кликабельны и открывают общий горизонтальный `CardListView`; из списка
|
||||
|
|
@ -175,7 +181,7 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **28/28** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **29/29** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@
|
|||
|
||||
1. Каждый игрок выбирает карту 0 ранга и кладёт её **закрытой** в круг
|
||||
авангарда — это стартовый авангард.
|
||||
2. Случайно определяется первый игрок. **В первый ход атаковать нельзя.**
|
||||
2. Случайно определяется первый игрок. **В первый ход атаковать нельзя**;
|
||||
переход в Battle на первом ходу также недоступен.
|
||||
3. Оба игрока перемешивают колоду и берут по **5 карт**.
|
||||
4. **Вторая попытка (mulligan):** игрок может вернуть **любое количество**
|
||||
карт из руки в низ колоды, взять столько же карт сверху, после чего колода
|
||||
|
|
|
|||
|
|
@ -1412,9 +1412,11 @@ public sealed class GameEngine
|
|||
break;
|
||||
|
||||
case Phase.Ride:
|
||||
var rideGrade = VanguardGrade(player);
|
||||
foreach (var printingId in p.Hand.Select(c => c.PrintingId).Distinct())
|
||||
{
|
||||
if (State.Catalog.CardOf(printingId).Grade == VanguardGrade(player) + 1)
|
||||
var grade = State.Catalog.CardOf(printingId).Grade;
|
||||
if (grade == rideGrade || grade == rideGrade + 1)
|
||||
commands.Add(new RideCommand(printingId));
|
||||
}
|
||||
|
||||
|
|
@ -1471,7 +1473,8 @@ public sealed class GameEngine
|
|||
foreach (var card in p.Drop)
|
||||
AddActivatedCommands(commands, card, player, fromHand: false, fromDrop: true);
|
||||
|
||||
commands.Add(new EnterBattleCommand());
|
||||
if (!_enforceFirstTurnAttackRule || State.TurnNumber != 1)
|
||||
commands.Add(new EnterBattleCommand());
|
||||
commands.Add(new EndTurnCommand());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,43 @@ public class GameEngineTests
|
|||
Assert.False(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void First_Turn_Does_Not_Offer_Battle_Phase()
|
||||
{
|
||||
var engine = new GameEngine(
|
||||
_catalog,
|
||||
7,
|
||||
BuildDeck(),
|
||||
BuildDeck(),
|
||||
requireLegalDeckTriggers: false);
|
||||
engine.Start(firstPlayer: 0);
|
||||
engine.Execute(new FinishMulliganCommand());
|
||||
engine.Execute(new FinishMulliganCommand());
|
||||
|
||||
Assert.DoesNotContain(
|
||||
engine.LegalCommands(),
|
||||
command => command is EnterBattleCommand);
|
||||
|
||||
engine.Execute(new SkipRideCommand());
|
||||
|
||||
Assert.DoesNotContain(
|
||||
engine.LegalCommands(),
|
||||
command => command is EnterBattleCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ride_Can_Use_Same_Grade()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
Give(engine, 0, Conroe);
|
||||
|
||||
Assert.Contains(
|
||||
engine.LegalCommands(),
|
||||
command => command is RideCommand ride && ride.PrintingId == Conroe);
|
||||
|
||||
Assert.True(engine.Execute(new RideCommand(Conroe)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Both_Players_Mulligan_Once_Then_First_Player_Starts()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue