Fix draw and heal trigger targets, Tejas sniping, and single moves
CI / test (push) Successful in 1m50s
Details
CI / test (push) Successful in 1m50s
Details
This commit is contained in:
parent
e2d35017ae
commit
0d5f7b933d
|
|
@ -255,3 +255,16 @@ Smoke: `Vanguard.Editor.SceneSmoke.Run` и `RunPlaymat`. Windows build:
|
||||||
`Vanguard.Editor.StandaloneBuild.BuildWindowsCli`.
|
`Vanguard.Editor.StandaloneBuild.BuildWindowsCli`.
|
||||||
|
|
||||||
После блока: тесты, `validate`, `translations check`, коммит и push.
|
После блока: тесты, `validate`, `translations check`, коммит и push.
|
||||||
|
|
||||||
|
## 8. Триггеры и особые случаи
|
||||||
|
|
||||||
|
- После выбора цели силы у **Draw** дополнительный эффект (draw 1)
|
||||||
|
разрешается сразу: второй цели на поле нет.
|
||||||
|
- **Heal** после цели силы просит карту только в **своей** зоне урона и
|
||||||
|
только если серверные условия выполнены (damage ≥ opponent и есть карты).
|
||||||
|
Нет карт/условий — дополнительный эффект не активируется.
|
||||||
|
- Tejas бьёт задний ряд своей колонки и с RC, и с VC (центральный задний
|
||||||
|
ряд); проверять `snipeBackRow` + `SameColumn`.
|
||||||
|
- Если у RC доступен только один круг перемещения в колонке, клиент
|
||||||
|
выполняет перемещение сразу, без выбора цели.
|
||||||
|
- Подсветка урона при выборе карты ограничена зоной урона локального игрока.
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,9 @@ namespace Vanguard.Board3D
|
||||||
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
public HashSet<int> SelectedHandInstanceIds { get; } = new HashSet<int>();
|
||||||
public bool MulliganSelecting { get; set; }
|
public bool MulliganSelecting { get; set; }
|
||||||
public bool DamageChoice { get; set; }
|
public bool DamageChoice { get; set; }
|
||||||
|
public int DamageChoicePlayer { get; set; } = -1;
|
||||||
public bool AbilityDamageTargeting { get; set; }
|
public bool AbilityDamageTargeting { get; set; }
|
||||||
|
public int AbilityDamagePlayer { get; set; } = -1;
|
||||||
public int AbilityDamageIndex { get; set; } = -1;
|
public int AbilityDamageIndex { get; set; } = -1;
|
||||||
public bool MatchFinished { get; set; }
|
public bool MatchFinished { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -755,7 +755,7 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
var isUp = i < faceUp.Count;
|
var isUp = i < faceUp.Count;
|
||||||
var card = isUp ? faceUp[i] : null;
|
var card = isUp ? faceUp[i] : null;
|
||||||
var highlight = DamageHighlight(state, i);
|
var highlight = DamageHighlight(state, player.Index, i);
|
||||||
specs.Add(new CardSpec
|
specs.Add(new CardSpec
|
||||||
{
|
{
|
||||||
Key = "D:" + player.Index + ":" + i,
|
Key = "D:" + player.Index + ":" + i,
|
||||||
|
|
@ -802,16 +802,20 @@ namespace Vanguard.Board3D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color? DamageHighlight(BoardRenderState state, int index)
|
private Color? DamageHighlight(
|
||||||
|
BoardRenderState state,
|
||||||
|
int player,
|
||||||
|
int index)
|
||||||
{
|
{
|
||||||
if (!state.DamageChoice && !state.AbilityDamageTargeting)
|
var damageChoice = state.DamageChoice
|
||||||
|
&& player == state.DamageChoicePlayer;
|
||||||
|
var abilityCost = state.AbilityDamageTargeting
|
||||||
|
&& player == state.AbilityDamagePlayer;
|
||||||
|
if (!damageChoice && !abilityCost)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (state.AbilityDamageTargeting
|
if (abilityCost && state.AbilityDamageIndex == index)
|
||||||
&& state.AbilityDamageIndex == index)
|
|
||||||
{
|
|
||||||
return _config.selectedColor;
|
return _config.selectedColor;
|
||||||
}
|
|
||||||
|
|
||||||
return _config.highlightColor;
|
return _config.highlightColor;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1681,10 +1681,10 @@ namespace Vanguard.UI
|
||||||
var state = new BoardRenderState
|
var state = new BoardRenderState
|
||||||
{
|
{
|
||||||
MulliganSelecting = IsMulliganSelecting(),
|
MulliganSelecting = IsMulliganSelecting(),
|
||||||
// Damage cards are highlighted only while an ability cost is
|
DamageChoice = IsDamageChoice(),
|
||||||
// being paid, not for ordinary damage choices.
|
DamageChoicePlayer = _view?.You ?? -1,
|
||||||
DamageChoice = false,
|
|
||||||
AbilityDamageTargeting = IsAbilityDamageTargeting(),
|
AbilityDamageTargeting = IsAbilityDamageTargeting(),
|
||||||
|
AbilityDamagePlayer = _view?.You ?? -1,
|
||||||
AbilityDamageIndex = IsAbilityDamageTargeting()
|
AbilityDamageIndex = IsAbilityDamageTargeting()
|
||||||
? _abilityDamageIndex
|
? _abilityDamageIndex
|
||||||
: -1,
|
: -1,
|
||||||
|
|
@ -1955,7 +1955,7 @@ namespace Vanguard.UI
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BoardZone.Damage:
|
case BoardZone.Damage:
|
||||||
OnDamageClicked(card.Index);
|
OnDamageClicked(card.Player, card.Index);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BoardZone.Soul:
|
case BoardZone.Soul:
|
||||||
|
|
@ -2366,10 +2366,22 @@ namespace Vanguard.UI
|
||||||
var moves = sources.OfType<MoveIntent>().ToList();
|
var moves = sources.OfType<MoveIntent>().ToList();
|
||||||
if (moves.Count > 0)
|
if (moves.Count > 0)
|
||||||
{
|
{
|
||||||
actions.Add(new CardActionMenuItem("Переместить", () =>
|
if (moves.Count == 1)
|
||||||
{
|
{
|
||||||
BeginTargetSelection(moves, "Выберите круг для перемещения.");
|
var move = moves[0];
|
||||||
}));
|
actions.Add(new CardActionMenuItem(
|
||||||
|
"Переместить",
|
||||||
|
() => OnIntentClicked(move)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actions.Add(new CardActionMenuItem("Переместить", () =>
|
||||||
|
{
|
||||||
|
BeginTargetSelection(
|
||||||
|
moves,
|
||||||
|
"Выберите круг для перемещения.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var calls = sources.OfType<CallIntent>().ToList();
|
var calls = sources.OfType<CallIntent>().ToList();
|
||||||
|
|
@ -3488,14 +3500,14 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private CardCatalogItemDto ResolveCatalogItem(string printingId) =>
|
private CardCatalogItemDto ResolveCatalogItem(string printingId) =>
|
||||||
_catalog.TryGetValue(printingId, out var item) ? item : null;
|
_catalog.TryGetValue(printingId, out var item) ? item : null;
|
||||||
private void OnDamageClicked(int displayIndex)
|
private void OnDamageClicked(int player, int displayIndex)
|
||||||
{
|
{
|
||||||
if (_inputLock.IsLocked)
|
if (_inputLock.IsLocked || _view == null || player != _view.You)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (IsDamageChoice())
|
if (IsDamageChoice())
|
||||||
{
|
{
|
||||||
if (_view?.Choice?.Options == null)
|
if (_view.Choice?.Options == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (var i = 0; i < _view.Choice.Options.Count; i++)
|
for (var i = 0; i < _view.Choice.Options.Count; i++)
|
||||||
|
|
@ -3769,6 +3781,18 @@ namespace Vanguard.UI
|
||||||
_triggerPowerSlot = target.Slot;
|
_triggerPowerSlot = target.Slot;
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_pendingIntents.AddRange(powerTargets);
|
_pendingIntents.AddRange(powerTargets);
|
||||||
|
|
||||||
|
// Draw and Heal have no second board target: Draw resolves
|
||||||
|
// immediately, Heal asks for a damage card on the server only
|
||||||
|
// when its conditions are met.
|
||||||
|
if (_view.Trigger.Trigger is TriggerKind.Draw or TriggerKind.Heal)
|
||||||
|
{
|
||||||
|
var trigger = powerTargets[0];
|
||||||
|
ClearSelection(rebuild: false);
|
||||||
|
OnIntentClicked(trigger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
RebuildHighlights();
|
RebuildHighlights();
|
||||||
RebuildIntents(_view);
|
RebuildIntents(_view);
|
||||||
SetStatus(
|
SetStatus(
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,14 @@
|
||||||
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
||||||
убраны из панелей, смена/призыв — драгом.
|
убраны из панелей, смена/призыв — драгом.
|
||||||
- Защита из руки ограничена рангом авангарда.
|
- Защита из руки ограничена рангом авангарда.
|
||||||
|
- Draw-триггер после выбора цели силы сразу тянет карту; Heal после цели
|
||||||
|
силы просит карту только в своей зоне урона и только при выполнении условий.
|
||||||
|
- Tejas может атаковать задний ряд не только с RC, но и с VC (центр).
|
||||||
|
- Перемещение RC с единственным доступным кругом выполняется без выбора цели.
|
||||||
- Проведён аудит всех 66 карт с текстом: `docs/ability-implementation.md`.
|
- Проведён аудит всех 66 карт с текстом: `docs/ability-implementation.md`.
|
||||||
Исправлены Conroe AUTO, зоны у Maiden of Libra/Starlight/Hungry/Clay/
|
Исправлены Conroe AUTO, зоны у Maiden of Libra/Starlight/Hungry/Clay/
|
||||||
Joka/Rakshasa, запрет поддержки Alfred.
|
Joka/Rakshasa, запрет поддержки Alfred.
|
||||||
- Тесты: сервер **148/148** правил + **45/45** серверных, клиент
|
- Тесты: сервер **149/149** правил + **45/45** серверных, клиент
|
||||||
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -67,7 +71,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
||||||
|
|
||||||
| Проверка | Результат |
|
| Проверка | Результат |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Тесты сервера | **193/193** (148 правил/контрактов + 45 сервер) |
|
| Тесты сервера | **194/194** (149 правил/контрактов + 45 сервер) |
|
||||||
| `validate` | 0 ошибок, 0 предупреждений |
|
| `validate` | 0 ошибок, 0 предупреждений |
|
||||||
| `translations check` | 94/94 |
|
| `translations check` | 94/94 |
|
||||||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||||||
|
|
@ -100,7 +104,7 @@ dotnet run --project server\src\Vanguard.Cli -- stress 1000
|
||||||
Ожидаемо:
|
Ожидаемо:
|
||||||
|
|
||||||
```
|
```
|
||||||
Пройден! : не пройдено 0, пройдено 148 (Vanguard.Rules.Tests)
|
Пройден! : не пройдено 0, пройдено 149 (Vanguard.Rules.Tests)
|
||||||
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
||||||
OK: ошибок 0, предупреждений 0
|
OK: ошибок 0, предупреждений 0
|
||||||
Переведено: 94/94
|
Переведено: 94/94
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
матчмейкинг, WebSocket-матч, история, реплеи.
|
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||||
in-process реестр соединений.
|
in-process реестр соединений.
|
||||||
- Тесты: **148** правил + **45** серверных, CI зелёный.
|
- Тесты: **149** правил + **45** серверных, CI зелёный.
|
||||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||||
- Диагностика Ride в `MatchGrain`: входящие интенты, результат, события,
|
- Диагностика Ride в `MatchGrain`: входящие интенты, результат, события,
|
||||||
снапшоты и `rideReady` по руке/рангам; логи через `docker compose logs`.
|
снапшоты и `rideReady` по руке/рангам; логи через `docker compose logs`.
|
||||||
|
|
@ -91,6 +91,8 @@
|
||||||
убраны; действия доступны из меню выбранной карты в руке.
|
убраны; действия доступны из меню выбранной карты в руке.
|
||||||
- Призыв: клик по карте в руке открывает меню действий (Призвать / Сменить /
|
- Призыв: клик по карте в руке открывает меню действий (Призвать / Сменить /
|
||||||
Активировать способность / Подробнее), drag в RC/VC выполняет призыв/смену.
|
Активировать способность / Подробнее), drag в RC/VC выполняет призыв/смену.
|
||||||
|
Если у RC доступен единственный круг перемещения, перемещение выполняется
|
||||||
|
сразу, без выбора цели.
|
||||||
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
||||||
действует для обычного призыва, призыва из души/колоды и выбора круга
|
действует для обычного призыва, призыва из души/колоды и выбора круга
|
||||||
способностями.
|
способностями.
|
||||||
|
|
@ -102,6 +104,9 @@
|
||||||
Perfect Guard также просит выбрать карту сброса в руке. Клик по карте в
|
Perfect Guard также просит выбрать карту сброса в руке. Клик по карте в
|
||||||
руке с активируемой способностью открывает меню действий; пункт активации
|
руке с активируемой способностью открывает меню действий; пункт активации
|
||||||
показывается, если фаза и условия позволяют способность.
|
показывается, если фаза и условия позволяют способность.
|
||||||
|
- Триггеры: Draw после выбора цели силы сразу тянет карту; Heal выбирает
|
||||||
|
карту только в своей зоне урона и только при выполнении условий, иначе
|
||||||
|
дополнительный эффект не активируется.
|
||||||
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
||||||
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
||||||
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
||||||
|
|
@ -176,6 +181,8 @@
|
||||||
душа. Guardian-карты повёрнуты на 90° и уложены с перекрытием ~30%,
|
душа. Guardian-карты повёрнуты на 90° и уложены с перекрытием ~30%,
|
||||||
новые сверху; Trigger-зона зеркальна у противника. 3D-подписи зон скрыты,
|
новые сверху; Trigger-зона зеркальна у противника. 3D-подписи зон скрыты,
|
||||||
включаются флагом `showZoneLabels`.
|
включаются флагом `showZoneLabels`.
|
||||||
|
- Tejas может атаковать задний ряд противника своей колонки как с RC, так и
|
||||||
|
с VC (центральный задний ряд).
|
||||||
- Drag в бою: задний ряд → свой передний круг/Авангард объявляет поддержку,
|
- Drag в бою: задний ряд → свой передний круг/Авангард объявляет поддержку,
|
||||||
после чего атаковать можно только поддерживаемой картой; передний круг →
|
после чего атаковать можно только поддерживаемой картой; передний круг →
|
||||||
карта противника объявляет атаку. Авангард поддерживается центральным
|
карта противника объявляет атаку. Авангард поддерживается центральным
|
||||||
|
|
|
||||||
|
|
@ -1527,9 +1527,7 @@ public sealed class GameEngine
|
||||||
|
|
||||||
targets.AddRange(UnitSlots(1 - player).Where(slot => slot >= 0
|
targets.AddRange(UnitSlots(1 - player).Where(slot => slot >= 0
|
||||||
&& (slot is 3 or 4
|
&& (slot is 3 or 4
|
||||||
|| (canSnipe
|
|| (canSnipe && SameColumn(attackerSlot, slot)))));
|
||||||
&& attackerSlot is 3 or 4
|
|
||||||
&& SameColumn(attackerSlot, slot)))));
|
|
||||||
|
|
||||||
foreach (var target in targets)
|
foreach (var target in targets)
|
||||||
commands.Add(new AttackCommand(attackerSlot, target));
|
commands.Add(new AttackCommand(attackerSlot, target));
|
||||||
|
|
@ -1951,9 +1949,10 @@ public sealed class GameEngine
|
||||||
return CommandResult.Ok();
|
return CommandResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Колонки: 0↔3 (левая), 2↔4 (правая). У центрального заднего ряда пары нет.</summary>
|
/// <summary>Колонки: -1↔1 (центр), 0↔3 (левая), 2↔4 (правая).</summary>
|
||||||
private static bool SameColumn(int fromSlot, int toSlot) =>
|
private static bool SameColumn(int fromSlot, int toSlot) =>
|
||||||
(fromSlot, toSlot) is (0, 3) or (3, 0) or (2, 4) or (4, 2);
|
(fromSlot, toSlot) is (0, 3) or (3, 0) or (2, 4) or (4, 2)
|
||||||
|
or (-1, 1) or (1, -1);
|
||||||
|
|
||||||
private CommandResult EnterBattle()
|
private CommandResult EnterBattle()
|
||||||
{
|
{
|
||||||
|
|
@ -2011,7 +2010,7 @@ public sealed class GameEngine
|
||||||
if (!State.Catalog.CardOf(attacker.PrintingId).CanSnipeBackRow)
|
if (!State.Catalog.CardOf(attacker.PrintingId).CanSnipeBackRow)
|
||||||
return CommandResult.Reject("Арьергард в заднем ряду нельзя атаковать без способности.");
|
return CommandResult.Reject("Арьергард в заднем ряду нельзя атаковать без способности.");
|
||||||
|
|
||||||
if (attackerSlot is not (3 or 4) || !SameColumn(attackerSlot, targetSlot))
|
if (!SameColumn(attackerSlot, targetSlot))
|
||||||
return CommandResult.Reject("Атаковать задний ряд можно только из своей колонки.");
|
return CommandResult.Reject("Атаковать задний ряд можно только из своей колонки.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1965,6 +1965,23 @@ public class GameEngineTests
|
||||||
Assert.Null(defender.RearGuards[0]);
|
Assert.Null(defender.RearGuards[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tejas_On_Vanguard_Attacks_Back_Row_Center()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
engine.State.Players[0].Vanguard = MakeCard(Tejas);
|
||||||
|
|
||||||
|
var defender = engine.State.Players[1];
|
||||||
|
defender.RearGuards[1] = MakeCard(Marron);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new EnterBattleCommand()).Accepted);
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, 1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(defender.RearGuards[1]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Yaksha_Rides_From_Hand_When_Opponent_RearGuard_Is_Retired()
|
public void Yaksha_Rides_From_Hand_When_Opponent_RearGuard_Is_Retired()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue