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`.
|
||||
|
||||
После блока: тесты, `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 bool MulliganSelecting { get; set; }
|
||||
public bool DamageChoice { get; set; }
|
||||
public int DamageChoicePlayer { get; set; } = -1;
|
||||
public bool AbilityDamageTargeting { get; set; }
|
||||
public int AbilityDamagePlayer { get; set; } = -1;
|
||||
public int AbilityDamageIndex { get; set; } = -1;
|
||||
public bool MatchFinished { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -755,7 +755,7 @@ namespace Vanguard.Board3D
|
|||
{
|
||||
var isUp = i < faceUp.Count;
|
||||
var card = isUp ? faceUp[i] : null;
|
||||
var highlight = DamageHighlight(state, i);
|
||||
var highlight = DamageHighlight(state, player.Index, i);
|
||||
specs.Add(new CardSpec
|
||||
{
|
||||
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;
|
||||
|
||||
if (state.AbilityDamageTargeting
|
||||
&& state.AbilityDamageIndex == index)
|
||||
{
|
||||
if (abilityCost && state.AbilityDamageIndex == index)
|
||||
return _config.selectedColor;
|
||||
}
|
||||
|
||||
return _config.highlightColor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1681,10 +1681,10 @@ namespace Vanguard.UI
|
|||
var state = new BoardRenderState
|
||||
{
|
||||
MulliganSelecting = IsMulliganSelecting(),
|
||||
// Damage cards are highlighted only while an ability cost is
|
||||
// being paid, not for ordinary damage choices.
|
||||
DamageChoice = false,
|
||||
DamageChoice = IsDamageChoice(),
|
||||
DamageChoicePlayer = _view?.You ?? -1,
|
||||
AbilityDamageTargeting = IsAbilityDamageTargeting(),
|
||||
AbilityDamagePlayer = _view?.You ?? -1,
|
||||
AbilityDamageIndex = IsAbilityDamageTargeting()
|
||||
? _abilityDamageIndex
|
||||
: -1,
|
||||
|
|
@ -1955,7 +1955,7 @@ namespace Vanguard.UI
|
|||
break;
|
||||
|
||||
case BoardZone.Damage:
|
||||
OnDamageClicked(card.Index);
|
||||
OnDamageClicked(card.Player, card.Index);
|
||||
break;
|
||||
|
||||
case BoardZone.Soul:
|
||||
|
|
@ -2366,10 +2366,22 @@ namespace Vanguard.UI
|
|||
var moves = sources.OfType<MoveIntent>().ToList();
|
||||
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();
|
||||
|
|
@ -3488,14 +3500,14 @@ namespace Vanguard.UI
|
|||
|
||||
private CardCatalogItemDto ResolveCatalogItem(string printingId) =>
|
||||
_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;
|
||||
|
||||
if (IsDamageChoice())
|
||||
{
|
||||
if (_view?.Choice?.Options == null)
|
||||
if (_view.Choice?.Options == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _view.Choice.Options.Count; i++)
|
||||
|
|
@ -3769,6 +3781,18 @@ namespace Vanguard.UI
|
|||
_triggerPowerSlot = target.Slot;
|
||||
_pendingIntents.Clear();
|
||||
_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();
|
||||
RebuildIntents(_view);
|
||||
SetStatus(
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@
|
|||
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
||||
убраны из панелей, смена/призыв — драгом.
|
||||
- Защита из руки ограничена рангом авангарда.
|
||||
- Draw-триггер после выбора цели силы сразу тянет карту; Heal после цели
|
||||
силы просит карту только в своей зоне урона и только при выполнении условий.
|
||||
- Tejas может атаковать задний ряд не только с RC, но и с VC (центр).
|
||||
- Перемещение RC с единственным доступным кругом выполняется без выбора цели.
|
||||
- Проведён аудит всех 66 карт с текстом: `docs/ability-implementation.md`.
|
||||
Исправлены Conroe AUTO, зоны у Maiden of Libra/Starlight/Hungry/Clay/
|
||||
Joka/Rakshasa, запрет поддержки Alfred.
|
||||
- Тесты: сервер **148/148** правил + **45/45** серверных, клиент
|
||||
- Тесты: сервер **149/149** правил + **45/45** серверных, клиент
|
||||
**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 предупреждений |
|
||||
| `translations check` | 94/94 |
|
||||
| `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)
|
||||
OK: ошибок 0, предупреждений 0
|
||||
Переведено: 94/94
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||
in-process реестр соединений.
|
||||
- Тесты: **148** правил + **45** серверных, CI зелёный.
|
||||
- Тесты: **149** правил + **45** серверных, CI зелёный.
|
||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||
- Диагностика Ride в `MatchGrain`: входящие интенты, результат, события,
|
||||
снапшоты и `rideReady` по руке/рангам; логи через `docker compose logs`.
|
||||
|
|
@ -91,6 +91,8 @@
|
|||
убраны; действия доступны из меню выбранной карты в руке.
|
||||
- Призыв: клик по карте в руке открывает меню действий (Призвать / Сменить /
|
||||
Активировать способность / Подробнее), drag в RC/VC выполняет призыв/смену.
|
||||
Если у RC доступен единственный круг перемещения, перемещение выполняется
|
||||
сразу, без выбора цели.
|
||||
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
||||
действует для обычного призыва, призыва из души/колоды и выбора круга
|
||||
способностями.
|
||||
|
|
@ -102,6 +104,9 @@
|
|||
Perfect Guard также просит выбрать карту сброса в руке. Клик по карте в
|
||||
руке с активируемой способностью открывает меню действий; пункт активации
|
||||
показывается, если фаза и условия позволяют способность.
|
||||
- Триггеры: Draw после выбора цели силы сразу тянет карту; Heal выбирает
|
||||
карту только в своей зоне урона и только при выполнении условий, иначе
|
||||
дополнительный эффект не активируется.
|
||||
- Оплата способности: выбранные карты цены подсвечиваются в руке, а для
|
||||
Counter Blast — доступные и выбранные карты в зоне урона. Подтверждение
|
||||
оплаты — кнопка «Подтвердить оплату» в панели «Действия»; правый клик
|
||||
|
|
@ -176,6 +181,8 @@
|
|||
душа. Guardian-карты повёрнуты на 90° и уложены с перекрытием ~30%,
|
||||
новые сверху; Trigger-зона зеркальна у противника. 3D-подписи зон скрыты,
|
||||
включаются флагом `showZoneLabels`.
|
||||
- Tejas может атаковать задний ряд противника своей колонки как с RC, так и
|
||||
с VC (центральный задний ряд).
|
||||
- Drag в бою: задний ряд → свой передний круг/Авангард объявляет поддержку,
|
||||
после чего атаковать можно только поддерживаемой картой; передний круг →
|
||||
карта противника объявляет атаку. Авангард поддерживается центральным
|
||||
|
|
|
|||
|
|
@ -1527,9 +1527,7 @@ public sealed class GameEngine
|
|||
|
||||
targets.AddRange(UnitSlots(1 - player).Where(slot => slot >= 0
|
||||
&& (slot is 3 or 4
|
||||
|| (canSnipe
|
||||
&& attackerSlot is 3 or 4
|
||||
&& SameColumn(attackerSlot, slot)))));
|
||||
|| (canSnipe && SameColumn(attackerSlot, slot)))));
|
||||
|
||||
foreach (var target in targets)
|
||||
commands.Add(new AttackCommand(attackerSlot, target));
|
||||
|
|
@ -1951,9 +1949,10 @@ public sealed class GameEngine
|
|||
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) =>
|
||||
(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()
|
||||
{
|
||||
|
|
@ -2011,7 +2010,7 @@ public sealed class GameEngine
|
|||
if (!State.Catalog.CardOf(attacker.PrintingId).CanSnipeBackRow)
|
||||
return CommandResult.Reject("Арьергард в заднем ряду нельзя атаковать без способности.");
|
||||
|
||||
if (attackerSlot is not (3 or 4) || !SameColumn(attackerSlot, targetSlot))
|
||||
if (!SameColumn(attackerSlot, targetSlot))
|
||||
return CommandResult.Reject("Атаковать задний ряд можно только из своей колонки.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1965,6 +1965,23 @@ public class GameEngineTests
|
|||
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]
|
||||
public void Yaksha_Rides_From_Hand_When_Opponent_RearGuard_Is_Retired()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue