Audit abilities and add Conroe auto call
CI / test (push) Successful in 1m46s
Details
CI / test (push) Successful in 1m46s
Details
This commit is contained in:
parent
d917448a5f
commit
228bc4ce8c
32
AGENTS.md
32
AGENTS.md
|
|
@ -31,6 +31,38 @@
|
||||||
- Информационные оверлеи (`ActionPrompt`, подсказки) не должны перехватывать
|
- Информационные оверлеи (`ActionPrompt`, подсказки) не должны перехватывать
|
||||||
клики и не дублируют выбор кнопками.
|
клики и не дублируют выбор кнопками.
|
||||||
|
|
||||||
|
## Способности
|
||||||
|
|
||||||
|
- Каждая текстовая способность карты должна иметь ровно одну реализацию:
|
||||||
|
`CardScript` (`Abilities`/`Continuous`/`Activated`), общий движок
|
||||||
|
(Sentinel, Heal/Trigger, Tejas) или флаг данных. Текст без реализации —
|
||||||
|
баг.
|
||||||
|
- **AUTO**: `AbilityDefinition` с `Trigger`, `Condition`, `Optional`,
|
||||||
|
`CanPay`/`Pay` и `Resolve`. Проверяй зону/источник через
|
||||||
|
`IsVanguardOf`/`IsRearGuardOf`/`IsInHand`; `OnPlace` не должен
|
||||||
|
срабатывать от VC, если текст требует RC.
|
||||||
|
- **ACT**: `ActivatedAbility`; флаги `RestsSelf`, `RetiresSelf`,
|
||||||
|
`RequiresDiscard`, `FromHand`, `FromDropZone`. `CanActivate` — только
|
||||||
|
проверка условий; оплата и эффект — в `Resolve` (или `Pay` для AUTO).
|
||||||
|
- **CONT**: `ContinuousAbility` для силы/критики; исключения вроде
|
||||||
|
«нельзя поддерживать» — через `CardScript.CanBeBoosted`, ограничения
|
||||||
|
передвижения/атаки — через `RestrictionType`.
|
||||||
|
- **Sentinel**: `IsSentinel` в данных, общий механизм Guard + сброс карты
|
||||||
|
того же клана; отдельный скрипт не нужен.
|
||||||
|
- **Trigger/Heal**: данные `trigger`; общий движок проверок и лечения.
|
||||||
|
- **Ключевые механики**: Tejas — `CanSnipeBackRow`; Restraint — флаг;
|
||||||
|
Boost/Intercept — `HasBoost`/`HasIntercept` и общий движок.
|
||||||
|
- **Выбор и цели**: только `AskUnitChoice`, `AskUnitMultiChoice`,
|
||||||
|
`AskHandCardChoice`, `AskEmptyRearGuardChoice`; клиент не выбирает
|
||||||
|
карты кнопками вместо объектов.
|
||||||
|
- **Клиент**: AUTO-подтверждение — оверлей; ACT — меню/оверлей карты;
|
||||||
|
стоимость способности (сброс/урон/поле) выбирается по объектам и
|
||||||
|
подтверждается кнопкой панели «Действия».
|
||||||
|
- После правок способностей проверять весь реестр `CardScripts.Default`,
|
||||||
|
тесты правил, `validate` и `translations check`.
|
||||||
|
- Полный аудит 66 карт и правила для новых карт —
|
||||||
|
`docs/ability-implementation.md`.
|
||||||
|
|
||||||
## Технические ограничения
|
## Технические ограничения
|
||||||
|
|
||||||
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на
|
- Не запускать `dotnet` и Docker на рабочем ПК; сервер проверять на
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Передача сессии
|
# Передача сессии
|
||||||
|
|
||||||
Состояние проекта на **23 октября 2026**. Документ нужен, чтобы новая сессия
|
Состояние проекта на **23 октября 2026**. Документ нужен, чтобы новая сессия
|
||||||
продолжила работу без повторного разбора истории. Краткая оперативная
|
продолжила работу без повторного разбора истории. Краткая оперативная
|
||||||
|
|
@ -26,7 +26,10 @@
|
||||||
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
в руке (Gancelot) доступна как `FromHand`-активация. Списки всех смен
|
||||||
убраны из панелей, смена/призыв — драгом.
|
убраны из панелей, смена/призыв — драгом.
|
||||||
- Защита из руки ограничена рангом авангарда.
|
- Защита из руки ограничена рангом авангарда.
|
||||||
- Тесты: сервер **146/146** правил + **45/45** серверных, клиент
|
- Проведён аудит всех 66 карт с текстом: `docs/ability-implementation.md`.
|
||||||
|
Исправлены Conroe AUTO, зоны у Maiden of Libra/Starlight/Hungry/Clay/
|
||||||
|
Joka/Rakshasa, запрет поддержки Alfred.
|
||||||
|
- Тесты: сервер **148/148** правил + **45/45** серверных, клиент
|
||||||
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
**38/38** EditMode; `SCENE_SMOKE` и `PLAYMAT_SMOKE` успешны.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -64,7 +67,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
||||||
|
|
||||||
| Проверка | Результат |
|
| Проверка | Результат |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Тесты сервера | **191/191** (146 правил/контрактов + 45 сервер) |
|
| Тесты сервера | **193/193** (148 правил/контрактов + 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 ошибок |
|
||||||
|
|
@ -97,7 +100,7 @@ dotnet run --project server\src\Vanguard.Cli -- stress 1000
|
||||||
Ожидаемо:
|
Ожидаемо:
|
||||||
|
|
||||||
```
|
```
|
||||||
Пройден! : не пройдено 0, пройдено 146 (Vanguard.Rules.Tests)
|
Пройден! : не пройдено 0, пройдено 148 (Vanguard.Rules.Tests)
|
||||||
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
||||||
OK: ошибок 0, предупреждений 0
|
OK: ошибок 0, предупреждений 0
|
||||||
Переведено: 94/94
|
Переведено: 94/94
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Vanguard ReMaster — состояние сессии
|
# Vanguard ReMaster — состояние сессии
|
||||||
|
|
||||||
**Дата:** 2026-10-23
|
**Дата:** 2026-10-23
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
матчмейкинг, WebSocket-матч, история, реплеи.
|
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||||
in-process реестр соединений.
|
in-process реестр соединений.
|
||||||
- Тесты: **146** правил + **45** серверных, CI зелёный.
|
- Тесты: **148** правил + **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`.
|
||||||
|
|
@ -94,6 +94,8 @@
|
||||||
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
Призыв разрешён и в занятый RC; прежний юнит уходит в Дроп. Правило
|
||||||
действует для обычного призыва, призыва из души/колоды и выбора круга
|
действует для обычного призыва, призыва из души/колоды и выбора круга
|
||||||
способностями.
|
способностями.
|
||||||
|
- Аудит всех 66 карт со способностями и правила реализации:
|
||||||
|
`docs/ability-implementation.md`. Conroe получил AUTO-call при ride-over.
|
||||||
- Способности: кнопка «Активировать способность» одна на карту; при
|
- Способности: кнопка «Активировать способность» одна на карту; при
|
||||||
нескольких вариантах открывается overlay выбора способности. Карта для
|
нескольких вариантах открывается overlay выбора способности. Карта для
|
||||||
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
сброса выбирается **кликом по карте в руке**, а не пунктом меню.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Реализация способностей карт
|
||||||
|
|
||||||
|
Актуально на **2026-10-24**. Источник правил — `content/sets/*.yaml`,
|
||||||
|
`textEn`; источник исполнения — `Vanguard.Rules`.
|
||||||
|
|
||||||
|
## Проверка реестра
|
||||||
|
|
||||||
|
Проверены все **66** карт с текстом способностей в BT01/TD01/TD02.
|
||||||
|
Для каждой есть одна реализация:
|
||||||
|
|
||||||
|
- `CardScript` — 59 карт в `CardScripts.Default`;
|
||||||
|
- `Generic: Sentinel` — 3 карты (`flash-shield-iseult`,
|
||||||
|
`wyvern-guard-barri`, `battle-sister-chocolat`): данные `sentinel` +
|
||||||
|
общий Guard/сброс;
|
||||||
|
- `Generic: Heal trigger` — 3 карты (`yggdrasil-maiden-elaine`,
|
||||||
|
`dragon-monk-genjo`, `ring-girl-clara`): данные `trigger: Heal`;
|
||||||
|
- `Data: CanSnipeBackRow` — `wyvern-strike-tejas`: флаг данных + общий
|
||||||
|
движок атаки заднего ряда.
|
||||||
|
|
||||||
|
## Полный список
|
||||||
|
|
||||||
|
| Карта | Клан | Ранг | Реализация |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `king-of-knights-alfred` | RoyalPaladin | 3 | CardScript |
|
||||||
|
| `blaster-blade` | RoyalPaladin | 2 | CardScript |
|
||||||
|
| `barcgal` | RoyalPaladin | 0 | CardScript |
|
||||||
|
| `dragonic-overlord` | Kagero | 3 | CardScript |
|
||||||
|
| `embodiment-of-victory-aleph` | Kagero | 3 | CardScript |
|
||||||
|
| `ceo-amaterasu` | OracleThinkTank | 3 | CardScript |
|
||||||
|
| `battle-sister-cocoa` | OracleThinkTank | 1 | CardScript |
|
||||||
|
| `asura-kaiser` | NovaGrappler | 3 | CardScript |
|
||||||
|
| `demon-slaying-knight-lohengrin` | RoyalPaladin | 3 | CardScript |
|
||||||
|
| `solitary-knight-gancelot` | RoyalPaladin | 3 | CardScript |
|
||||||
|
| `flash-shield-iseult` | RoyalPaladin | 1 | Generic: Sentinel |
|
||||||
|
| `future-knight-llew` | RoyalPaladin | 0 | CardScript |
|
||||||
|
| `vortex-dragon` | Kagero | 3 | CardScript |
|
||||||
|
| `dragon-knight-aleph` | Kagero | 2 | CardScript |
|
||||||
|
| `wyvern-guard-barri` | Kagero | 1 | Generic: Sentinel |
|
||||||
|
| `lizard-soldier-conroe` | Kagero | 0 | CardScript |
|
||||||
|
| `maiden-of-libra` | OracleThinkTank | 2 | CardScript |
|
||||||
|
| `battle-sister-mocha` | OracleThinkTank | 2 | CardScript |
|
||||||
|
| `battle-sister-chocolat` | OracleThinkTank | 1 | Generic: Sentinel |
|
||||||
|
| `juggernaut-maximum` | SpikeBrothers | 3 | CardScript |
|
||||||
|
| `wyvern-strike-tejas` | Kagero | 2 | Data: CanSnipeBackRow |
|
||||||
|
| `oracle-guardian-apollon` | OracleThinkTank | 3 | CardScript |
|
||||||
|
| `lozenge-magus` | OracleThinkTank | 0 | CardScript |
|
||||||
|
| `mr-invincible` | NovaGrappler | 3 | CardScript |
|
||||||
|
| `brutal-jack` | NovaGrappler | 2 | CardScript |
|
||||||
|
| `queen-of-heart` | NovaGrappler | 1 | CardScript |
|
||||||
|
| `battleraizer` | NovaGrappler | 0 | CardScript |
|
||||||
|
| `tyrant-deathrex` | Tachikaze | 3 | CardScript |
|
||||||
|
| `assault-dragon-blightops` | Tachikaze | 2 | CardScript |
|
||||||
|
| `stealth-dragon-voidmaster` | Nubatama | 3 | CardScript |
|
||||||
|
| `demon-eater` | DarkIrregulars | 3 | CardScript |
|
||||||
|
| `monster-frank` | Granblue | 3 | CardScript |
|
||||||
|
| `hell-spider` | Megacolony | 3 | CardScript |
|
||||||
|
| `covenant-knight-randolf` | RoyalPaladin | 2 | CardScript |
|
||||||
|
| `lake-maiden-lien` | RoyalPaladin | 1 | CardScript |
|
||||||
|
| `wingal` | RoyalPaladin | 1 | CardScript |
|
||||||
|
| `yggdrasil-maiden-elaine` | RoyalPaladin | 0 | Generic: Heal trigger |
|
||||||
|
| `dragon-monk-gojo` | Kagero | 1 | CardScript |
|
||||||
|
| `wyvern-strike-jarran` | Kagero | 1 | CardScript |
|
||||||
|
| `dragon-monk-genjo` | Kagero | 0 | Generic: Heal trigger |
|
||||||
|
| `weather-girl-milk` | OracleThinkTank | 1 | CardScript |
|
||||||
|
| `hungry-dumpty` | NovaGrappler | 2 | CardScript |
|
||||||
|
| `screamin-and-dancin-announcer-shout` | NovaGrappler | 1 | CardScript |
|
||||||
|
| `clay-doll-mechanic` | NovaGrappler | 1 | CardScript |
|
||||||
|
| `ring-girl-clara` | NovaGrappler | 0 | Generic: Heal trigger |
|
||||||
|
| `stealth-beast-chigasumi` | Nubatama | 2 | CardScript |
|
||||||
|
| `stealth-dragon-dreadmaster` | Nubatama | 1 | CardScript |
|
||||||
|
| `stealth-beast-hagakure` | Nubatama | 0 | CardScript |
|
||||||
|
| `blue-dust` | DarkIrregulars | 2 | CardScript |
|
||||||
|
| `nightmare-baby` | DarkIrregulars | 1 | CardScript |
|
||||||
|
| `rock-the-wall` | DarkIrregulars | 0 | CardScript |
|
||||||
|
| `highspeed-brakki` | SpikeBrothers | 2 | CardScript |
|
||||||
|
| `redshoe-milly` | SpikeBrothers | 0 | CardScript |
|
||||||
|
| `guiding-zombie` | Granblue | 0 | CardScript |
|
||||||
|
| `karma-queen` | Megacolony | 1 | CardScript |
|
||||||
|
| `crimson-butterfly-brigitte` | RoyalPaladin | 3 | CardScript |
|
||||||
|
| `knight-of-conviction-bors` | RoyalPaladin | 3 | CardScript |
|
||||||
|
| `knight-of-the-harp-tristan` | RoyalPaladin | 2 | CardScript |
|
||||||
|
| `starlight-unicorn` | RoyalPaladin | 1 | CardScript |
|
||||||
|
| `knight-of-rose-morgana` | RoyalPaladin | 1 | CardScript |
|
||||||
|
| `dragon-monk-goku` | Kagero | 3 | CardScript |
|
||||||
|
| `demonic-dragon-berserker-yaksha` | Kagero | 3 | CardScript |
|
||||||
|
| `berserk-dragon` | Kagero | 2 | CardScript |
|
||||||
|
| `flame-of-hope-aermo` | Kagero | 1 | CardScript |
|
||||||
|
| `demonic-dragon-madonna-joka` | Kagero | 1 | CardScript |
|
||||||
|
| `demonic-dragon-mage-rakshasa` | Kagero | 0 | CardScript |
|
||||||
|
|
||||||
|
## Исправлено при аудите 2026-10-24
|
||||||
|
|
||||||
|
- **Conroe**: добавлена недостающая `AUTO`: когда другой Kagero ride-over
|
||||||
|
эту карту, можно призвать Conroe из души в RC (`OnRideOver`).
|
||||||
|
- **Maiden of Libra**: добавлена проверка, что авангард — Oracle Think
|
||||||
|
Tank, как в тексте.
|
||||||
|
- **Starlight Unicorn**, **Hungry Dumpty**, **Clay Doll Mechanic**:
|
||||||
|
`OnPlace` теперь требует именно RC, а не срабатывает от VC.
|
||||||
|
- **Joka**, **Rakshasa**: бонус за retire соперника теперь требует
|
||||||
|
нахождения на RC, а не просто «не Vanguard» (включая руку).
|
||||||
|
- **Alfred**: добавлен CONT-запрет поддержки авангарда Alfred.
|
||||||
|
|
||||||
|
## Новые карты
|
||||||
|
|
||||||
|
Для новой карты с текстом:
|
||||||
|
|
||||||
|
1. Найти все `AUTO`/`ACT`/`CONT`/ключевые механики в `textEn`.
|
||||||
|
2. Реализовать `CardScript` или общий движок; не оставлять текст без
|
||||||
|
`AbilityDefinition`.
|
||||||
|
3. Проверить зону источника (`IsVanguardOf`, `IsRearGuardOf`,
|
||||||
|
`IsInHand`) и опциональность (`Optional`/`CanPay`/`Pay`).
|
||||||
|
4. Для выбора использовать только `Ask*Choice`, не кнопочные списки.
|
||||||
|
5. Добавить тест на каждый триггер/активацию и прогнать весь реестр.
|
||||||
|
|
@ -87,6 +87,12 @@ public abstract class CardScript
|
||||||
public virtual IEnumerable<AbilityDefinition> Abilities => Array.Empty<AbilityDefinition>();
|
public virtual IEnumerable<AbilityDefinition> Abilities => Array.Empty<AbilityDefinition>();
|
||||||
public virtual IEnumerable<ContinuousAbility> Continuous => Array.Empty<ContinuousAbility>();
|
public virtual IEnumerable<ContinuousAbility> Continuous => Array.Empty<ContinuousAbility>();
|
||||||
public virtual IEnumerable<ActivatedAbility> Activated => Array.Empty<ActivatedAbility>();
|
public virtual IEnumerable<ActivatedAbility> Activated => Array.Empty<ActivatedAbility>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CONT-исключения, которые нельзя выразить только бонусом силы,
|
||||||
|
/// например «ваши юниты не могут поддерживать этот юнит».
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool CanBeBoosted(AbilityContext context) => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Сработавшая способность, ожидающая выбора порядка или решения игрока.</summary>
|
/// <summary>Сработавшая способность, ожидающая выбора порядка или решения игрока.</summary>
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ public sealed class MaidenOfLibraScript : CardScript
|
||||||
{
|
{
|
||||||
Trigger = AbilityTrigger.OnAttackHit,
|
Trigger = AbilityTrigger.OnAttackHit,
|
||||||
Optional = true,
|
Optional = true,
|
||||||
|
Condition = ctx => ctx.Engine.VanguardClan(ctx.Owner) == Clan.OracleThinkTank,
|
||||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2),
|
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2),
|
||||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2),
|
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2),
|
||||||
Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 1),
|
Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 1),
|
||||||
|
|
@ -224,6 +225,25 @@ public sealed class ConroeScript : CardScript
|
||||||
{
|
{
|
||||||
public override string NameId => "lizard-soldier-conroe";
|
public override string NameId => "lizard-soldier-conroe";
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnRideOver,
|
||||||
|
Optional = true,
|
||||||
|
Condition = ctx =>
|
||||||
|
ctx.Other is not null &&
|
||||||
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.Kagero,
|
||||||
|
Resolve = ctx => ctx.Engine.AskEmptyRearGuardChoice(
|
||||||
|
ctx.Owner,
|
||||||
|
"Выберите круг для призыва Conroe",
|
||||||
|
slot => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner, slot)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override IEnumerable<ActivatedAbility> Activated
|
public override IEnumerable<ActivatedAbility> Activated
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -410,6 +430,9 @@ public sealed class AlfredScript : CardScript
|
||||||
{
|
{
|
||||||
public override string NameId => "king-of-knights-alfred";
|
public override string NameId => "king-of-knights-alfred";
|
||||||
|
|
||||||
|
public override bool CanBeBoosted(AbilityContext context) =>
|
||||||
|
!context.Engine.IsVanguardOf(context.Source, context.Owner);
|
||||||
|
|
||||||
public override IEnumerable<ContinuousAbility> Continuous
|
public override IEnumerable<ContinuousAbility> Continuous
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ public sealed class FlipDamageOnPlaceScript : CardScript
|
||||||
{
|
{
|
||||||
Trigger = AbilityTrigger.OnPlace,
|
Trigger = AbilityTrigger.OnPlace,
|
||||||
Condition = ctx =>
|
Condition = ctx =>
|
||||||
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
ctx.Engine.IsRearGuardOf(ctx.Source, ctx.Owner) &&
|
||||||
ctx.Engine.VanguardClan(ctx.Owner) == _clan,
|
ctx.Engine.VanguardClan(ctx.Owner) == _clan,
|
||||||
Resolve = ctx => ctx.Engine.TurnOneDamageFaceUp(ctx.Owner),
|
Resolve = ctx => ctx.Engine.TurnOneDamageFaceUp(ctx.Owner),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ public sealed class StarlightUnicornScript : CardScript
|
||||||
yield return new AbilityDefinition
|
yield return new AbilityDefinition
|
||||||
{
|
{
|
||||||
Trigger = AbilityTrigger.OnPlace,
|
Trigger = AbilityTrigger.OnPlace,
|
||||||
Condition = ctx => !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
Condition = ctx => ctx.Engine.IsRearGuardOf(ctx.Source, ctx.Owner),
|
||||||
Resolve = ctx =>
|
Resolve = ctx =>
|
||||||
{
|
{
|
||||||
var slots = new List<int>();
|
var slots = new List<int>();
|
||||||
|
|
@ -261,7 +261,7 @@ public sealed class OpponentRetireBonusScript : CardScript
|
||||||
{
|
{
|
||||||
Trigger = AbilityTrigger.OnOpponentRetire,
|
Trigger = AbilityTrigger.OnOpponentRetire,
|
||||||
Condition = ctx =>
|
Condition = ctx =>
|
||||||
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
ctx.Engine.IsRearGuardOf(ctx.Source, ctx.Owner) &&
|
||||||
ctx.Phase == Phase.Main,
|
ctx.Phase == Phase.Main,
|
||||||
Resolve = ctx => ctx.Engine.AddPower(
|
Resolve = ctx => ctx.Engine.AddPower(
|
||||||
ctx.Source,
|
ctx.Source,
|
||||||
|
|
|
||||||
|
|
@ -563,6 +563,25 @@ public sealed class GameEngine
|
||||||
.Sum(c => c.PowerBonus?.Invoke(context) ?? 0);
|
.Sum(c => c.PowerBonus?.Invoke(context) ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanBeBoosted(CardInstance unit)
|
||||||
|
{
|
||||||
|
var owner = OwnerOf(unit);
|
||||||
|
if (owner < 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
|
||||||
|
if (!_scripts.TryGetValue(nameId, out var script))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return script.CanBeBoosted(new AbilityContext
|
||||||
|
{
|
||||||
|
Engine = this,
|
||||||
|
Source = unit,
|
||||||
|
Owner = owner,
|
||||||
|
Phase = State.Phase,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public int OwnerOf(CardInstance unit)
|
public int OwnerOf(CardInstance unit)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
|
|
@ -580,6 +599,9 @@ public sealed class GameEngine
|
||||||
public bool IsVanguardOf(CardInstance unit, int player) =>
|
public bool IsVanguardOf(CardInstance unit, int player) =>
|
||||||
ReferenceEquals(State.Players[player].Vanguard, unit);
|
ReferenceEquals(State.Players[player].Vanguard, unit);
|
||||||
|
|
||||||
|
public bool IsRearGuardOf(CardInstance unit, int player) =>
|
||||||
|
Array.IndexOf(State.Players[player].RearGuards, unit) >= 0;
|
||||||
|
|
||||||
public int CountRearGuards(int player, Clan clan) =>
|
public int CountRearGuards(int player, Clan clan) =>
|
||||||
State.Players[player].RearGuards.Count(u =>
|
State.Players[player].RearGuards.Count(u =>
|
||||||
u is not null && State.Catalog.CardOf(u.PrintingId).Clan == clan);
|
u is not null && State.Catalog.CardOf(u.PrintingId).Clan == clan);
|
||||||
|
|
@ -1519,7 +1541,7 @@ public sealed class GameEngine
|
||||||
4 => 2,
|
4 => 2,
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
if (boosterSlot >= 0)
|
if (boosterSlot >= 0 && CanBeBoosted(attacker))
|
||||||
{
|
{
|
||||||
var booster = State.GetUnit(player, boosterSlot);
|
var booster = State.GetUnit(player, boosterSlot);
|
||||||
if (booster is not null && !booster.Rested && State.CanBoost(booster))
|
if (booster is not null && !booster.Rested && State.CanBoost(booster))
|
||||||
|
|
@ -2019,6 +2041,9 @@ public sealed class GameEngine
|
||||||
|
|
||||||
if (!State.CanBoost(booster))
|
if (!State.CanBoost(booster))
|
||||||
return CommandResult.Reject("Эта карта не может поддерживать.");
|
return CommandResult.Reject("Эта карта не может поддерживать.");
|
||||||
|
|
||||||
|
if (!CanBeBoosted(attacker))
|
||||||
|
return CommandResult.Reject("Этого юнита нельзя поддерживать.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var attackerPlayer = State.ActivePlayer;
|
var attackerPlayer = State.ActivePlayer;
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,22 @@ public class GameEngineTests
|
||||||
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Conroe_Offers_Optional_Call_When_Ridden_Over()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
SetVanguard(engine, 0, Conroe);
|
||||||
|
Give(engine, 0, Conroe);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new RideCommand(Conroe)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingAbility);
|
||||||
|
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingChoice);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||||||
|
Assert.Equal("BT01/016EN", engine.State.Players[0].RearGuards[0]?.PrintingId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Barcgal_Decline_Keeps_Card_In_Soul()
|
public void Barcgal_Decline_Keeps_Card_In_Soul()
|
||||||
{
|
{
|
||||||
|
|
@ -936,6 +952,23 @@ public class GameEngineTests
|
||||||
Assert.Equal(0, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
Assert.Equal(0, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Alfred_On_Vanguard_Cannot_Be_Boosted()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
SetVanguard(engine, 0, Alfred);
|
||||||
|
|
||||||
|
Give(engine, 0, Marron);
|
||||||
|
Assert.True(engine.Execute(new CallCommand(Marron, 1)).Accepted);
|
||||||
|
engine.State.Players[0].RearGuards[1]!.Rested = false;
|
||||||
|
|
||||||
|
SetVanguard(engine, 1, Gallatin);
|
||||||
|
Assert.True(engine.Execute(new EnterBattleCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.False(engine.Execute(new AttackCommand(-1, -1, 1)).Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Alfred_Gets_2000_Per_RoyalPaladin_RearGuard()
|
public void Alfred_Gets_2000_Per_RoyalPaladin_RearGuard()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue