Restore drag actions and hand action menu
CI / test (push) Successful in 2m5s
Details
CI / test (push) Successful in 2m5s
Details
This commit is contained in:
parent
14717a3dff
commit
d917448a5f
|
|
@ -12,8 +12,9 @@ namespace Vanguard.Board3D
|
|||
|
||||
public static bool CanCall(int slot, bool occupied) =>
|
||||
slot >= 0
|
||||
&& slot < PlaymatLayout.RearGuardCount
|
||||
&& !occupied;
|
||||
&& slot < PlaymatLayout.RearGuardCount;
|
||||
// Occupied circles are valid call targets: the previous unit is
|
||||
// moved to the drop zone.
|
||||
|
||||
public static bool CanGuard(BoardZone zone) =>
|
||||
zone == BoardZone.Guardian;
|
||||
|
|
|
|||
|
|
@ -1724,6 +1724,12 @@ namespace Vanguard.UI
|
|||
state.DropTargets.Add(new BoardSlotKey(
|
||||
1 - _view.You,
|
||||
attack.TargetSlot));
|
||||
if (attack.BoosterSlot >= 0)
|
||||
{
|
||||
state.DropTargets.Add(new BoardSlotKey(
|
||||
_view.You,
|
||||
attack.AttackerSlot));
|
||||
}
|
||||
break;
|
||||
case GuardIntent _:
|
||||
case InterceptIntent _:
|
||||
|
|
@ -1917,6 +1923,12 @@ namespace Vanguard.UI
|
|||
break;
|
||||
}
|
||||
|
||||
if (!targetSelection)
|
||||
{
|
||||
OpenCardMenu(card);
|
||||
break;
|
||||
}
|
||||
|
||||
OnBoardTargetClicked(new BoardTarget(
|
||||
card.Player, -2, card.PrintingId, card.InstanceId, true));
|
||||
break;
|
||||
|
|
@ -2191,17 +2203,6 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private bool HasActivatedAbility(string printingId)
|
||||
{
|
||||
var meta = ResolveCatalogItem(printingId);
|
||||
if (meta == null)
|
||||
return false;
|
||||
|
||||
var text = ((meta.TextEn ?? string.Empty) + " "
|
||||
+ (meta.TextRu ?? string.Empty)).ToLowerInvariant();
|
||||
return text.Contains("[act]") || text.Contains("{act}");
|
||||
}
|
||||
|
||||
private bool RequiresCounterBlast(ActivateAbilityIntent intent)
|
||||
{
|
||||
if (intent == null || _view == null)
|
||||
|
|
@ -2339,7 +2340,7 @@ namespace Vanguard.UI
|
|||
card.Zone == BoardZone.Vanguard ? -1 : card.Slot,
|
||||
card.PrintingId,
|
||||
card.InstanceId,
|
||||
false);
|
||||
card.Zone == BoardZone.Hand);
|
||||
|
||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||
var sources = legal
|
||||
|
|
@ -2371,9 +2372,26 @@ namespace Vanguard.UI
|
|||
}));
|
||||
}
|
||||
|
||||
var calls = sources.OfType<CallIntent>().ToList();
|
||||
if (calls.Count > 0)
|
||||
{
|
||||
actions.Add(new CardActionMenuItem("Призвать", () =>
|
||||
{
|
||||
BeginTargetSelection(calls, "Выберите круг для призыва.");
|
||||
}));
|
||||
}
|
||||
|
||||
var rides = sources.OfType<RideIntent>().ToList();
|
||||
if (rides.Count > 0)
|
||||
{
|
||||
var ride = rides[0];
|
||||
actions.Add(new CardActionMenuItem(
|
||||
"Сменить: " + CardName(ride.PrintingId),
|
||||
() => OnIntentClicked(ride)));
|
||||
}
|
||||
|
||||
var abilities = sources.OfType<ActivateAbilityIntent>().ToList();
|
||||
if (abilities.Count == 0
|
||||
&& card.Zone is BoardZone.Vanguard or BoardZone.RearGuard)
|
||||
if (abilities.Count == 0)
|
||||
{
|
||||
// Fallback: never hide an activated ability because source
|
||||
// target resolution failed for an unusual card placement.
|
||||
|
|
@ -2391,7 +2409,8 @@ namespace Vanguard.UI
|
|||
|
||||
foreach (var intent in sources)
|
||||
{
|
||||
if (intent is AttackIntent or MoveIntent or ActivateAbilityIntent)
|
||||
if (intent is AttackIntent or MoveIntent or ActivateAbilityIntent
|
||||
or CallIntent or RideIntent)
|
||||
continue;
|
||||
|
||||
var captured = intent;
|
||||
|
|
@ -3256,22 +3275,6 @@ namespace Vanguard.UI
|
|||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -3595,25 +3598,6 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
if (target.IsHand
|
||||
&& target.Player == _view.You
|
||||
&& _pendingIntents.Count == 0
|
||||
&& HasActivatedAbility(target.PrintingId))
|
||||
{
|
||||
var handIndex = HandIndexOf(target.InstanceId);
|
||||
var hand = _view.Players
|
||||
.FirstOrDefault(player => player.Index == _view.You)
|
||||
?.Hand;
|
||||
if (hand != null && handIndex >= 0 && handIndex < hand.Count)
|
||||
{
|
||||
OpenCardDetails(BoardCardRef.Hand(
|
||||
_view.You,
|
||||
handIndex,
|
||||
hand[handIndex]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||
|
||||
if (_pendingIntents.Any(intent => intent is ResolveTriggerIntent)
|
||||
|
|
@ -3869,6 +3853,21 @@ namespace Vanguard.UI
|
|||
return true;
|
||||
case ActivateAbilityIntent activate:
|
||||
source = InstanceTarget(_view.You, activate.InstanceId);
|
||||
if (source.InstanceId == 0)
|
||||
{
|
||||
var hand = _view.Players
|
||||
.FirstOrDefault(player => player.Index == _view.You)
|
||||
?.Hand;
|
||||
var handCard = hand?.FirstOrDefault(
|
||||
card => card.InstanceId == activate.InstanceId);
|
||||
if (handCard != null)
|
||||
{
|
||||
source = new BoardTarget(
|
||||
_view.You, -2, handCard.PrintingId,
|
||||
handCard.InstanceId, true);
|
||||
}
|
||||
}
|
||||
|
||||
return source.InstanceId != 0;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -3973,7 +3972,6 @@ namespace Vanguard.UI
|
|||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||
var abilities = playable.OfType<ActivateAbilityIntent>().ToList();
|
||||
var rides = playable.OfType<RideIntent>().ToList();
|
||||
var board = playable
|
||||
.Where(intent => IsBoardIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent
|
||||
|
|
@ -4086,9 +4084,6 @@ namespace Vanguard.UI
|
|||
if (abilities.Count > 0 && _pendingAbilityCost == null)
|
||||
AddAbilityButton(abilities);
|
||||
|
||||
if (rides.Count > 0)
|
||||
AddRideButtons(rides);
|
||||
|
||||
foreach (var intent in global)
|
||||
AddIntentButton(intent);
|
||||
|
||||
|
|
@ -4254,7 +4249,10 @@ namespace Vanguard.UI
|
|||
{
|
||||
return IsHand && other.IsHand
|
||||
&& Player == other.Player
|
||||
&& string.Equals(PrintingId, other.PrintingId, StringComparison.Ordinal);
|
||||
&& string.Equals(PrintingId, other.PrintingId, StringComparison.Ordinal)
|
||||
&& (InstanceId == 0
|
||||
|| other.InstanceId == 0
|
||||
|| InstanceId == other.InstanceId);
|
||||
}
|
||||
|
||||
return Player == other.Player && Slot == other.Slot;
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ namespace Vanguard.Tests
|
|||
{
|
||||
Assert.IsTrue(BoardPlacementRules.CanCall(0, occupied: false));
|
||||
Assert.IsTrue(BoardPlacementRules.CanCall(4, occupied: false));
|
||||
Assert.IsFalse(BoardPlacementRules.CanCall(0, occupied: true));
|
||||
Assert.IsTrue(BoardPlacementRules.CanCall(0, occupied: true));
|
||||
Assert.IsFalse(BoardPlacementRules.CanCall(5, occupied: false));
|
||||
|
||||
Assert.IsTrue(BoardPlacementRules.CanRide(BoardZone.Vanguard, -1));
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@
|
|||
Урон больше не подсвечивается вне режима оплаты. Правый клик отменяет выбор.
|
||||
- Призыв в занятый RC заменяет юнита: прежний уходит в Дроп; правило
|
||||
распространено на призыв из руки, души и колоды. Сервер задеплоен.
|
||||
- Клик по карте в руке с `[ACT]`/`{act}` открывает оверлей карты.
|
||||
- Тесты: сервер **190/190**, клиент **38/38** EditMode; `SCENE_SMOKE` и
|
||||
`PLAYMAT_SMOKE` успешны.
|
||||
- Клик по карте в руке открывает меню действий с валидными пунктами
|
||||
(Призвать / Сменить / Активировать способность / Подробнее); способность
|
||||
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
||||
убраны из панелей, смена/призыв — драгом.
|
||||
- Защита из руки ограничена рангом авангарда.
|
||||
- Тесты: сервер **146/146** правил + **45/45** серверных, клиент
|
||||
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -60,7 +64,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Тесты сервера | **190/190** (145 правил/контрактов + 45 сервер) |
|
||||
| Тесты сервера | **191/191** (146 правил/контрактов + 45 сервер) |
|
||||
| `validate` | 0 ошибок, 0 предупреждений |
|
||||
| `translations check` | 94/94 |
|
||||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||||
|
|
@ -93,7 +97,7 @@ dotnet run --project server\src\Vanguard.Cli -- stress 1000
|
|||
Ожидаемо:
|
||||
|
||||
```
|
||||
Пройден! : не пройдено 0, пройдено 145 (Vanguard.Rules.Tests)
|
||||
Пройден! : не пройдено 0, пройдено 146 (Vanguard.Rules.Tests)
|
||||
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
||||
OK: ошибок 0, предупреждений 0
|
||||
Переведено: 94/94
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||
in-process реестр соединений.
|
||||
- Тесты: **145** правил + **45** серверных, CI зелёный.
|
||||
- Тесты: **146** правил + **45** серверных, CI зелёный.
|
||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||
- Диагностика Ride в `MatchGrain`: входящие интенты, результат, события,
|
||||
снапшоты и `rideReady` по руке/рангам; логи через `docker compose logs`.
|
||||
|
|
@ -87,21 +87,26 @@
|
|||
`P/C`.
|
||||
- Смена: в фазе Ride доступны карты того же ранга и на 1 выше. На первом
|
||||
ходу переход в Battle не предлагается; атака по-прежнему запрещена.
|
||||
Кнопки «Сменить: карта» есть и в панели действий, и в панели «Действия».
|
||||
- Призыв: разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
||||
Смена — драг карты из руки на VC. Списки всех возможных смен в панелях
|
||||
убраны; действия доступны из меню выбранной карты в руке.
|
||||
- Призыв: клик по карте в руке открывает меню действий (Призвать / Сменить /
|
||||
Активировать способность / Подробнее), drag в RC/VC выполняет призыв/смену.
|
||||
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
||||
действует для обычного призыва, призыва из души/колоды и выбора круга
|
||||
способностями.
|
||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||
нескольких вариантах открывается overlay выбора способности. Карта для
|
||||
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
||||
Perfect Guard также просит выбрать карту сброса в руке. Клик по карте в
|
||||
руке с активируемой способностью открывает оверлей карты, не призывая её.
|
||||
руке с активируемой способностью открывает меню действий; пункт активации
|
||||
показывается, если фаза и условия позволяют способность.
|
||||
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
||||
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
||||
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
||||
отменяет выбор.
|
||||
- Защита: в панели «Действия» одна кнопка — «Не защищаться» без выбора и
|
||||
«Защищаться» с выбранными картами. Карты защиты и перехвата выбираются
|
||||
«Защищаться» с выбранными картами. Защитник из руки должен быть не выше
|
||||
ранга своего авангарда. Карты защиты и перехвата выбираются
|
||||
кликом/драгом по объектам; допустимые подсвечиваются, выбранные — зелёным.
|
||||
Щит выбранных карт сразу учитывается в счётчике; после подтверждения карты
|
||||
уходят в зону защиты, шаг защиты завершается. Дубли карт в Guardian
|
||||
|
|
|
|||
|
|
@ -185,9 +185,10 @@ G-эра, в Classic не используется: выбор воина нас
|
|||
уходит в rest. Цель — воин соперника в переднем ряду (или авангард).
|
||||
2. **Boost.** Стоящий воин 0–1 ранга позади атакующего может уйти в rest;
|
||||
его сила добавляется атакующему до конца битвы.
|
||||
3. **Guard.** Защищающийся может призвать защитников из руки. Щит
|
||||
защитников/перехватчиков добавляется к силе защищающегося до конца битвы.
|
||||
У воинов 3+ ранга щита нет, кроме стражей наследия.
|
||||
3. **Guard.** Защищающийся может призвать защитников из руки. Ранг защитника
|
||||
не выше ранга своего авангарда. Щит защитников/перехватчиков добавляется к
|
||||
силе защищающегося до конца битвы. У воинов 3+ ранга щита нет, кроме
|
||||
стражей наследия.
|
||||
4. **Intercept.** Воин 2 ранга из переднего ряда арьергарда может выйти на
|
||||
перехват; его щит учитывается, после боя он уходит в дроп.
|
||||
5. **Drive Check.** Только если атакующий — авангард. Верхняя карта колоды
|
||||
|
|
|
|||
|
|
@ -1537,9 +1537,13 @@ public sealed class GameEngine
|
|||
if (State.Step != Step.Guard || State.Battle is null)
|
||||
return;
|
||||
|
||||
var defenderGrade = VanguardGrade(1 - State.Battle.AttackerPlayer);
|
||||
foreach (var printingId in p.Hand.Select(c => c.PrintingId).Distinct())
|
||||
{
|
||||
var def = State.Catalog.CardOf(printingId);
|
||||
if (def.Grade > defenderGrade)
|
||||
continue;
|
||||
|
||||
if (def.IsSentinel)
|
||||
{
|
||||
commands.Add(new GuardCommand(printingId));
|
||||
|
|
@ -2101,6 +2105,9 @@ public sealed class GameEngine
|
|||
var card = defender.Hand[index];
|
||||
var def = State.Catalog.CardOf(card.PrintingId);
|
||||
|
||||
if (def.Grade > VanguardGrade(defenderIndex))
|
||||
return CommandResult.Reject("Ранг карты защиты выше ранга авангарда.");
|
||||
|
||||
if (!def.IsSentinel && discardPrintingId is not null)
|
||||
return CommandResult.Reject("Эта карта не требует оплаты стоимости.");
|
||||
|
||||
|
|
|
|||
|
|
@ -535,10 +535,22 @@ public class GameEngineTests
|
|||
Assert.False(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Guard_Rejects_Unit_With_Grade_Above_Vanguard()
|
||||
{
|
||||
var engine = ReadyToAttack();
|
||||
SetVanguard(engine, 1, Marron);
|
||||
Give(engine, 1, Gallatin);
|
||||
|
||||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||
Assert.False(engine.Execute(new GuardCommand(Gallatin)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Perfect_Guard_Nullifies_Attack_When_Cost_Paid()
|
||||
{
|
||||
var engine = ReadyToAttack();
|
||||
SetVanguard(engine, 1, Marron);
|
||||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||
Give(engine, 1, Iseult);
|
||||
Give(engine, 1, Marron);
|
||||
|
|
@ -556,6 +568,7 @@ public class GameEngineTests
|
|||
public void Perfect_Guard_Without_Cost_Adds_No_Shield()
|
||||
{
|
||||
var engine = ReadyToAttack();
|
||||
SetVanguard(engine, 1, Marron);
|
||||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||
Give(engine, 1, Iseult);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue