Script 21 more BT01 cards
New scripts: Mocha, Randolf, Chigasumi, Queen of Heart, Jarran, Milk, Nightmare Baby, Hungry Dumpty, Clay Doll Mechanic, Blue Dust, Karma Queen, Hell Spider, Dragonic Overlord, Gancelot, Aleph, Apollon, Lohengrin, Vortex Dragon, Amaterasu, Mr. Invincible, Brutal Jack. Engine additions: OnBoosted trigger, reusable effect helpers (soul charge, retire all, flip damage, return hand card to deck, opponent discard), boost-trigger chaining so attack-declared waits for boost resolution. Four new tests; 98 total.
This commit is contained in:
parent
1965816808
commit
80095da51f
|
|
@ -114,19 +114,19 @@
|
|||
- [ ] Discard, Shuffle как отдельные интенты.
|
||||
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
||||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||||
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost, OnPhaseStart, OnAttackDeclared, OnAttackHit), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
||||
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost, OnBoosted, OnPhaseStart, OnAttackDeclared, OnAttackHit), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
||||
- [ ] Событие OnDamageTaken и триггеры на получение урона.
|
||||
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
|
||||
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
||||
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
|
||||
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» ещё нет.
|
||||
- [x] Стоимости: CB, SB, сброс карты, поворот себя, уничтожение себя; «карты из поля в душу» и «карта из руки в колоду» — частными хелперами.
|
||||
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» — `AskUnitMultiChoice`.
|
||||
- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
|
||||
- [x] Модификаторы силы/крита с источником и сроком жизни (`StatModifier`); выдача и снятие способностей эффектами (`GrantedAbility`).
|
||||
- [x] Выбор от `min` до `max` целей на поле (`AskUnitMultiChoice`) для «до N» и «ровно N».
|
||||
- [x] Запреты с источником и сроком жизни (`CardRestriction`): не вставать, не атаковать, не поддерживать, не перехватывать, не быть выбранным; снятие Сдерживания эффектом.
|
||||
- [x] `OnAttackHit` срабатывает и при попадании по Арьергарду.
|
||||
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
||||
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
|
||||
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API готов; Llew и Aleph ждут стоимость «карты из поля в душу»).
|
||||
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
||||
- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет.
|
||||
- [x] Валидация интентов: `Rejected(reason)` без изменения состояния.
|
||||
|
|
@ -144,12 +144,15 @@
|
|||
## 4. Карты `[15–25 дней]`
|
||||
|
||||
- [ ] Хелперы: `Draw`, `Retire`, `SearchCall`, `SearchHand`, `SearchRide`, `Stand`, `SoulCharge`, `SoulBlast`, `CounterBlast`, `FlipDamage`, `ToSoul`, `ToDeck`, `Discard`, `LookTop`, `ModifyPower`, `ModifyCrit`, `GainKeyword`.
|
||||
- [ ] Шаблон карты: 1 файл = 1 `nameId`, комментарий с официальным текстом и ссылкой.
|
||||
- [ ] Порядок: TD01 → TD02 → BT01 (сначала ванильные, потом со способностями).
|
||||
- [ ] По каждой карте: скрипт + тест «дано → сыграл → ожидаю».
|
||||
- [ ] RU/EN тексты для всех 94.
|
||||
- [ ] Сила/щит для 14 карт, которых нет в BT01.
|
||||
— большая часть уже есть в `GameEngine`; не хватает общих «карты из поля в душу» и «из руки в колоду» с выбором любых карт.
|
||||
- [~] Шаблон карты: скрипт = класс `CardScript`, текст карты хранится в YAML (отдельного комментария с текстом нет).
|
||||
- [x] Порядок: сначала простые триггеры, затем стоимости и активируемые способности.
|
||||
- [x] По каждой карте: скрипт + тест «дано → сыграл → ожидаю» (для ключевых механик).
|
||||
- [x] RU/EN тексты для всех 94.
|
||||
- [x] Сила/щит для 14 карт, которых нет в BT01.
|
||||
- [ ] Дашборд покрытия: `scripted / tested / total`.
|
||||
— сейчас заскриптовано **34** карты: 13 базовых + 21 в `CardScriptsBT01.cs`.
|
||||
- [ ] Wyvern Strike, Tejas: атака заднего ряда не выделена — движок разрешает атаковать любой круг Арьергарда.
|
||||
|
||||
**Готово, когда:** 94/94 заскриптованы, 94/94 покрыты тестом, приёмка пройдена по чеклисту карты.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public enum AbilityTrigger
|
|||
OnPhaseStart,
|
||||
OnAttackDeclared,
|
||||
OnAttackHit,
|
||||
OnBoosted,
|
||||
OnDamageTaken,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,27 @@ public static class CardScripts
|
|||
["demon-eater"] = new DemonEaterScript(),
|
||||
["maiden-of-libra"] = new MaidenOfLibraScript(),
|
||||
["knight-of-rose-morgana"] = new MorganaScript(),
|
||||
["battle-sister-mocha"] = new BattleSisterMochaScript(),
|
||||
["covenant-knight-randolf"] = new MoreHandsPowerScript("covenant-knight-randolf"),
|
||||
["stealth-beast-chigasumi"] = new MoreHandsPowerScript("stealth-beast-chigasumi"),
|
||||
["queen-of-heart"] = new NamedBoostScript("queen-of-heart", "king-of-sword"),
|
||||
["wyvern-strike-jarran"] = new NamedBoostScript("wyvern-strike-jarran", "wyvern-strike-tejas"),
|
||||
["nightmare-baby"] = new NamedBoostScript("nightmare-baby", "blue-dust"),
|
||||
["weather-girl-milk"] = new WeatherGirlMilkScript(),
|
||||
["hungry-dumpty"] = new FlipDamageOnPlaceScript("hungry-dumpty", Clan.NovaGrappler),
|
||||
["clay-doll-mechanic"] = new FlipDamageOnPlaceScript("clay-doll-mechanic", Clan.NovaGrappler),
|
||||
["blue-dust"] = new BlueDustScript(),
|
||||
["karma-queen"] = new KarmaQueenScript(),
|
||||
["hell-spider"] = new HellSpiderScript(),
|
||||
["dragonic-overlord"] = new DragonicOverlordScript(),
|
||||
["solitary-knight-gancelot"] = new GancelotScript(),
|
||||
["embodiment-of-victory-aleph"] = new AlephScript(),
|
||||
["oracle-guardian-apollon"] = new ApollonScript(),
|
||||
["demon-slaying-knight-lohengrin"] = new LohengrinScript(),
|
||||
["vortex-dragon"] = new VortexDragonScript(),
|
||||
["ceo-amaterasu"] = new CeoAmaterasuScript(),
|
||||
["mr-invincible"] = new MrInvincibleScript(),
|
||||
["brutal-jack"] = new BrutalJackScript(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +123,8 @@ public sealed class DemonEaterScript : CardScript
|
|||
ModifierDuration.UntilEndOfTurn);
|
||||
},
|
||||
};
|
||||
|
||||
yield return Bt01Abilities.RetireAllOnHit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,660 @@
|
|||
using Vanguard.Rules.Cards;
|
||||
|
||||
namespace Vanguard.Rules.Engine.Abilities;
|
||||
|
||||
/// <summary>Общие способности, которые встречаются у нескольких карт BT01.</summary>
|
||||
internal static class Bt01Abilities
|
||||
{
|
||||
/// <summary>В начале Основной фазы: Soul-Charge 1 и +2000 до конца хода.</summary>
|
||||
public static AbilityDefinition MainPhaseSoulCharge() => new()
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPhaseStart,
|
||||
Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulCharge(ctx.Owner, 1);
|
||||
ctx.Engine.AddPower(
|
||||
ctx.Source,
|
||||
2000,
|
||||
ctx.Source,
|
||||
"main-phase",
|
||||
ModifierDuration.UntilEndOfTurn);
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>При попадании: Soul Blast 8 и Counter Blast 5 → уничтожить все Арьергарды противника.</summary>
|
||||
public static AbilityDefinition RetireAllOnHit() => new()
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
CanPay = ctx =>
|
||||
ctx.Engine.CanSoulBlast(ctx.Owner, 8) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 5),
|
||||
Pay = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulBlast(ctx.Owner, 8);
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 5);
|
||||
},
|
||||
Resolve = ctx => ctx.Engine.RetireAllRearGuards(1 - ctx.Owner),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// При размещении на (VC) или (RC) Counter Blast 2: выбранный Арьергард противника
|
||||
/// не встаёт в следующую фазу Подъёма.
|
||||
/// </summary>
|
||||
public static AbilityDefinition CannotStandOnPlace() => new()
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPlace,
|
||||
Optional = true,
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
var opponent = 1 - ctx.Owner;
|
||||
var slots = ctx.Engine.RearGuardSlots(opponent, _ => true);
|
||||
if (slots.Count == 0)
|
||||
return;
|
||||
|
||||
ctx.Engine.AskUnitChoice(
|
||||
ctx.Owner,
|
||||
opponent,
|
||||
"Выберите Арьергард противника",
|
||||
slots,
|
||||
slot => ctx.Engine.AddRestriction(
|
||||
ctx.State.GetUnit(opponent, slot)!,
|
||||
RestrictionType.CannotStand,
|
||||
ctx.Source,
|
||||
"cannot-stand",
|
||||
ModifierDuration.UntilOwnerNextStand));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Battle Sister, Mocha: при атаке при четырёх картах в руке получает +3000 до конца боя.</summary>
|
||||
public sealed class BattleSisterMochaScript : CardScript
|
||||
{
|
||||
public override string NameId => "battle-sister-mocha";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||
Condition = ctx => ctx.Engine.HandCount(ctx.Owner) >= 4,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// «Когда эта карта атакует, если карт в руке больше, чем у противника,
|
||||
/// получает +3000 до конца боя»: Covenant Knight, Randolf и Stealth Beast, Chigasumi.
|
||||
/// </summary>
|
||||
public sealed class MoreHandsPowerScript : CardScript
|
||||
{
|
||||
public MoreHandsPowerScript(string nameId) => NameId = nameId;
|
||||
|
||||
public override string NameId { get; }
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||
Condition = ctx => ctx.Engine.HandCount(ctx.Owner) > ctx.Engine.HandCount(1 - ctx.Owner),
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// «Когда эта карта поддерживает указанную карту, усиленная карта получает +4000
|
||||
/// до конца боя»: Queen of Heart, Wyvern Strike, Jarran, Nightmare Baby.
|
||||
/// </summary>
|
||||
public sealed class NamedBoostScript : CardScript
|
||||
{
|
||||
private readonly string _targetNameId;
|
||||
|
||||
public NamedBoostScript(string nameId, string targetNameId)
|
||||
{
|
||||
NameId = nameId;
|
||||
_targetNameId = targetNameId;
|
||||
}
|
||||
|
||||
public override string NameId { get; }
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnBoost,
|
||||
Condition = ctx =>
|
||||
ctx.Other is not null &&
|
||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).NameId == _targetNameId,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Other!, 4000, ctx.Source),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Weather Girl, Milk: поддерживает Авангард Oracle Think Tank при четырёх картах
|
||||
/// в руке — усиленная карта получает +4000 до конца боя.
|
||||
/// </summary>
|
||||
public sealed class WeatherGirlMilkScript : CardScript
|
||||
{
|
||||
public override string NameId => "weather-girl-milk";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnBoost,
|
||||
Condition = ctx =>
|
||||
ctx.Other is not null &&
|
||||
ctx.Engine.IsVanguardOf(ctx.Other, ctx.Owner) &&
|
||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.OracleThinkTank &&
|
||||
ctx.Engine.HandCount(ctx.Owner) >= 4,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Other!, 4000, ctx.Source),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// «При размещении на (RC), если Авангард нужного клана, перевернуть одну карту
|
||||
/// урона лицом вверх»: Hungry Dumpty и Clay Doll Mechanic.
|
||||
/// </summary>
|
||||
public sealed class FlipDamageOnPlaceScript : CardScript
|
||||
{
|
||||
private readonly Clan _clan;
|
||||
|
||||
public FlipDamageOnPlaceScript(string nameId, Clan clan)
|
||||
{
|
||||
NameId = nameId;
|
||||
_clan = clan;
|
||||
}
|
||||
|
||||
public override string NameId { get; }
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPlace,
|
||||
Condition = ctx =>
|
||||
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Engine.VanguardClan(ctx.Owner) == _clan,
|
||||
Resolve = ctx => ctx.Engine.TurnOneDamageFaceUp(ctx.Owner),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Blue Dust: при попадании, если Авангард Dark Irregulars, можно Soul-Charge 1.</summary>
|
||||
public sealed class BlueDustScript : CardScript
|
||||
{
|
||||
public override string NameId => "blue-dust";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
Condition = ctx => ctx.Engine.VanguardClan(ctx.Owner) == Clan.DarkIrregulars,
|
||||
Resolve = ctx => ctx.Engine.SoulCharge(ctx.Owner, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Karma Queen: при размещении выбранный Арьергард противника не встаёт.</summary>
|
||||
public sealed class KarmaQueenScript : CardScript
|
||||
{
|
||||
public override string NameId => "karma-queen";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get { yield return Bt01Abilities.CannotStandOnPlace(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hell Spider: +3000, пока все юниты противника повёрнуты; при размещении
|
||||
/// выбранный Арьергард противника не встаёт.
|
||||
/// </summary>
|
||||
public sealed class HellSpiderScript : CardScript
|
||||
{
|
||||
public override string NameId => "hell-spider";
|
||||
|
||||
public override IEnumerable<ContinuousAbility> Continuous
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ContinuousAbility
|
||||
{
|
||||
Applies = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.State.ActivePlayer == ctx.Owner &&
|
||||
ctx.Engine.AllUnitsRested(1 - ctx.Owner),
|
||||
PowerBonus = _ => 3000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get { yield return Bt01Abilities.CannotStandOnPlace(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dragonic Overlord: −2000 без другого Kagero; за Counter Blast 3 получает +5000,
|
||||
/// способность вставать при попадании по Арьергарду и теряет Twin Drive!!.
|
||||
/// </summary>
|
||||
public sealed class DragonicOverlordScript : CardScript
|
||||
{
|
||||
public override string NameId => "dragonic-overlord";
|
||||
|
||||
public override IEnumerable<ContinuousAbility> Continuous
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ContinuousAbility
|
||||
{
|
||||
Applies = ctx => ctx.Engine.CountFieldUnits(ctx.Owner, Clan.Kagero, ctx.Source) == 0,
|
||||
PowerBonus = _ => -2000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 3),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 3);
|
||||
ctx.Engine.AddPower(
|
||||
ctx.Source,
|
||||
5000,
|
||||
ctx.Source,
|
||||
"dragonic-overlord",
|
||||
ModifierDuration.UntilEndOfTurn);
|
||||
|
||||
// теряет Twin Drive!! до конца хода
|
||||
ctx.Source.DriveChecksOverride = 1;
|
||||
|
||||
ctx.Engine.RemoveGrantedAbilities(g =>
|
||||
g.Source == ctx.Source && g.EffectId == "dragonic-overlord-stand");
|
||||
|
||||
ctx.Engine.GrantAbility(
|
||||
ctx.Source,
|
||||
new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Condition = c => c.Other is not null,
|
||||
Resolve = c => c.Source.Rested = false,
|
||||
},
|
||||
ctx.Source,
|
||||
"dragonic-overlord-stand",
|
||||
ModifierDuration.UntilEndOfTurn);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solitary Knight, Gancelot: на Авангарде Counter Blast 2, если в душе Blaster Blade,
|
||||
/// получает +5000/+1 критический до конца хода.
|
||||
/// </summary>
|
||||
public sealed class GancelotScript : CardScript
|
||||
{
|
||||
public override string NameId => "solitary-knight-gancelot";
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 2) &&
|
||||
ctx.Engine.HasInSoul(ctx.Owner, "blaster-blade"),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 2);
|
||||
ctx.Engine.AddPower(
|
||||
ctx.Source,
|
||||
5000,
|
||||
ctx.Source,
|
||||
"gancelot",
|
||||
ModifierDuration.UntilEndOfTurn,
|
||||
critical: 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embodiment of Victory, Aleph: Counter Blast 4 → +3000/+1 критический до конца хода;
|
||||
/// на Авангарде — отправить три именованные карты из души в сброс и перевернуть весь урон.
|
||||
/// </summary>
|
||||
public sealed class AlephScript : CardScript
|
||||
{
|
||||
private static readonly string[] SoulCost =
|
||||
{
|
||||
"dragon-knight-aleph",
|
||||
"embodiment-of-armor-bahr",
|
||||
"embodiment-of-spear-tahr",
|
||||
};
|
||||
|
||||
public override string NameId => "embodiment-of-victory-aleph";
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 4),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 4);
|
||||
ctx.Engine.AddPower(
|
||||
ctx.Source,
|
||||
3000,
|
||||
ctx.Source,
|
||||
"aleph",
|
||||
ModifierDuration.UntilEndOfTurn,
|
||||
critical: 1);
|
||||
},
|
||||
};
|
||||
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
SoulCost.All(name => ctx.Engine.HasInSoul(ctx.Owner, name)),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
var p = ctx.State.Players[ctx.Owner];
|
||||
foreach (var nameId in SoulCost)
|
||||
{
|
||||
var card = p.Soul.First(c =>
|
||||
ctx.Catalog.CardOf(c.PrintingId).NameId == nameId);
|
||||
p.Soul.Remove(card);
|
||||
p.Drop.Add(card);
|
||||
}
|
||||
|
||||
ctx.Engine.TurnAllDamageFaceUp(ctx.Owner);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oracle Guardian, Apollon: при попадании Counter Blast 2.
|
||||
/// На Авангарде — взять две карты и вернуть одну в колоду, на Арьергарде — взять одну.
|
||||
/// </summary>
|
||||
public sealed class ApollonScript : CardScript
|
||||
{
|
||||
public override string NameId => "oracle-guardian-apollon";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.DrawCards(ctx.Owner, 2);
|
||||
ctx.Engine.ReturnHandCardToDeck(ctx.Owner, "Верните карту из руки в колоду");
|
||||
},
|
||||
};
|
||||
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
Condition = ctx => !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2),
|
||||
Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Demon Slaying Knight, Lohengrin: в начале Основной фазы Soul-Charge 1 и +2000;
|
||||
/// при попадании Soul Blast 8 и Counter Blast 5 → уничтожить все Арьергарды противника.
|
||||
/// </summary>
|
||||
public sealed class LohengrinScript : CardScript
|
||||
{
|
||||
public override string NameId => "demon-slaying-knight-lohengrin";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return Bt01Abilities.MainPhaseSoulCharge();
|
||||
yield return Bt01Abilities.RetireAllOnHit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vortex Dragon: в начале Основной фазы Soul-Charge 1 и +2000; за Soul Blast 8
|
||||
/// и Counter Blast 5 выбрать до трёх Арьергардов противника и уничтожить их.
|
||||
/// </summary>
|
||||
public sealed class VortexDragonScript : CardScript
|
||||
{
|
||||
public override string NameId => "vortex-dragon";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get { yield return Bt01Abilities.MainPhaseSoulCharge(); }
|
||||
}
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx =>
|
||||
ctx.Engine.CanSoulBlast(ctx.Owner, 8) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 5) &&
|
||||
ctx.Engine.RearGuardSlots(1 - ctx.Owner, _ => true).Count > 0,
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulBlast(ctx.Owner, 8);
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 5);
|
||||
|
||||
var opponent = 1 - ctx.Owner;
|
||||
var targets = ctx.Engine.RearGuardSlots(opponent, _ => true)
|
||||
.Select(slot => new UnitTarget(opponent, slot))
|
||||
.ToList();
|
||||
|
||||
ctx.Engine.AskUnitMultiChoice(
|
||||
ctx.Owner,
|
||||
"Выберите до трёх Арьергардов противника",
|
||||
targets,
|
||||
0,
|
||||
3,
|
||||
chosen =>
|
||||
{
|
||||
foreach (var target in chosen)
|
||||
ctx.Engine.RetireRearGuard(target.Player, target.Slot);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CEO Amaterasu: +4000 при четырёх картах в руке в свой ход; в начале Основной фазы
|
||||
/// Soul-Charge 1 и просмотр верха колоды; при попадании Soul Blast 8 и Counter Blast 5 → взять пять.
|
||||
/// </summary>
|
||||
public sealed class CeoAmaterasuScript : CardScript
|
||||
{
|
||||
public override string NameId => "ceo-amaterasu";
|
||||
|
||||
public override IEnumerable<ContinuousAbility> Continuous
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ContinuousAbility
|
||||
{
|
||||
Applies = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.State.ActivePlayer == ctx.Owner &&
|
||||
ctx.Engine.HandCount(ctx.Owner) >= 4,
|
||||
PowerBonus = _ => 4000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPhaseStart,
|
||||
Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulCharge(ctx.Owner, 1);
|
||||
ctx.Engine.LookTopAndReorder(ctx.Owner, "Верхняя карта колоды");
|
||||
},
|
||||
};
|
||||
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
CanPay = ctx =>
|
||||
ctx.Engine.CanSoulBlast(ctx.Owner, 8) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 5),
|
||||
Pay = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulBlast(ctx.Owner, 8);
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 5);
|
||||
},
|
||||
Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 5),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mr. Invincible: в начале Основной фазы Soul-Charge 1 и перевернуть карту урона;
|
||||
/// при попадании Soul Blast 8 и Counter Blast 5 → поднять все свои юниты.
|
||||
/// </summary>
|
||||
public sealed class MrInvincibleScript : CardScript
|
||||
{
|
||||
public override string NameId => "mr-invincible";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPhaseStart,
|
||||
Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulCharge(ctx.Owner, 1);
|
||||
ctx.Engine.TurnOneDamageFaceUp(ctx.Owner);
|
||||
},
|
||||
};
|
||||
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
CanPay = ctx =>
|
||||
ctx.Engine.CanSoulBlast(ctx.Owner, 8) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 5),
|
||||
Pay = ctx =>
|
||||
{
|
||||
ctx.Engine.SoulBlast(ctx.Owner, 8);
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 5);
|
||||
},
|
||||
Resolve = ctx => ctx.Engine.StandAll(ctx.Owner),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brutal Jack: Сдерживание; за Counter Blast 1 теряет его до конца хода;
|
||||
/// получает +5000 до конца боя, когда его поддерживает Nova Grappler.
|
||||
/// </summary>
|
||||
public sealed class BrutalJackScript : CardScript
|
||||
{
|
||||
public override string NameId => "brutal-jack";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnBoosted,
|
||||
Condition = ctx =>
|
||||
ctx.Other is not null &&
|
||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.NovaGrappler,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 5000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 1);
|
||||
ctx.Source.RestraintOverride = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,10 @@ namespace Vanguard.Rules.Engine;
|
|||
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
||||
/// правило одинаково для drive check и damage check;
|
||||
/// * порядок проверки: карта → зона триггера → эффекты → рука (drive) или урон (damage);
|
||||
/// * усиления от триггеров живут до конца хода.
|
||||
/// * усиления от триггеров живут до конца хода;
|
||||
/// * атаковать можно Авангард или любой круг Арьергарда: деления на передний
|
||||
/// и задний ряд при выборе цели пока нет (поэтому Tejas не выделяется);
|
||||
/// * задний ряд защищён только перехватом, отдельного правила «атакуют лишь передний ряд» нет.
|
||||
/// </summary>
|
||||
public sealed class GameEngine
|
||||
{
|
||||
|
|
@ -733,6 +736,85 @@ public sealed class GameEngine
|
|||
_events.Add(new UnitRetiredEvent(player, slot));
|
||||
}
|
||||
|
||||
// ---------- эффекты карт ----------
|
||||
|
||||
public int HandCount(int player) => State.Players[player].Hand.Count;
|
||||
|
||||
public bool HasInSoul(int player, string nameId) =>
|
||||
State.Players[player].Soul.Any(c => State.Catalog.CardOf(c.PrintingId).NameId == nameId);
|
||||
|
||||
public bool HasInDrop(int player, string nameId) =>
|
||||
State.Players[player].Drop.Any(c => State.Catalog.CardOf(c.PrintingId).NameId == nameId);
|
||||
|
||||
/// <summary>Сколько карт клана на поле, не считая указанную.</summary>
|
||||
public int CountFieldUnits(int player, Clan clan, CardInstance? except = null) =>
|
||||
AllUnits(player).Count(u =>
|
||||
!ReferenceEquals(u, except)
|
||||
&& State.Catalog.CardOf(u.PrintingId).Clan == clan);
|
||||
|
||||
public bool AllUnitsRested(int player) => AllUnits(player).All(u => u.Rested);
|
||||
|
||||
/// <summary>Перевернуть всю зону урона лицом вверх.</summary>
|
||||
public void TurnAllDamageFaceUp(int player)
|
||||
{
|
||||
foreach (var card in State.Players[player].Damage)
|
||||
card.FaceUp = true;
|
||||
}
|
||||
|
||||
/// <summary>Перевернуть одну закрытую карту урона лицом вверх. false — закрытых нет.</summary>
|
||||
public bool TurnOneDamageFaceUp(int player)
|
||||
{
|
||||
var card = State.Players[player].Damage.FirstOrDefault(c => !c.FaceUp);
|
||||
if (card is null)
|
||||
return false;
|
||||
|
||||
card.FaceUp = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RetireAllRearGuards(int player)
|
||||
{
|
||||
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
|
||||
RetireRearGuard(player, slot);
|
||||
}
|
||||
|
||||
public void StandAll(int player)
|
||||
{
|
||||
foreach (var unit in AllUnits(player))
|
||||
unit.Rested = false;
|
||||
}
|
||||
|
||||
/// <summary>Вернуть выбранную карту из руки в колоду и перемешать.</summary>
|
||||
public void ReturnHandCardToDeck(int player, string prompt)
|
||||
{
|
||||
if (State.Players[player].Hand.Count == 0)
|
||||
return;
|
||||
|
||||
AskHandCardChoice(player, prompt, null, card =>
|
||||
{
|
||||
var p = State.Players[player];
|
||||
p.Hand.Remove(card);
|
||||
p.Deck.Add(card);
|
||||
ShuffleDeck(player);
|
||||
_events.Add(new CardReturnedToDeckEvent(player, card.PrintingId));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Противник выбирает карту в руке и сбрасывает её.</summary>
|
||||
public void OpponentDiscardsChosen(int player, string prompt)
|
||||
{
|
||||
var p = State.Players[player];
|
||||
if (p.Hand.Count == 0)
|
||||
return;
|
||||
|
||||
AskHandCardChoice(player, prompt, null, card =>
|
||||
{
|
||||
p.Hand.Remove(card);
|
||||
p.Drop.Add(card);
|
||||
_events.Add(new CardDiscardedEvent(player, card.PrintingId));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- поиск в колоде ----------
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1265,6 +1347,35 @@ public sealed class GameEngine
|
|||
TargetSlot = targetSlot,
|
||||
};
|
||||
|
||||
var defenderIndex = 1 - attackerPlayer;
|
||||
_events.Add(new AttackDeclaredEvent(attackerPlayer, attackerSlot, targetSlot));
|
||||
|
||||
void BeginGuard()
|
||||
{
|
||||
State.Step = Step.Guard;
|
||||
State.ActivePlayer = defenderIndex;
|
||||
}
|
||||
|
||||
void AfterAttackDeclared()
|
||||
{
|
||||
TriggerFromCard(attackerPlayer, attacker, AbilityTrigger.OnAttackDeclared, null);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
State.Continuation = BeginGuard;
|
||||
else
|
||||
BeginGuard();
|
||||
}
|
||||
|
||||
void AfterBoosted()
|
||||
{
|
||||
TriggerFromCard(attackerPlayer, attacker, AbilityTrigger.OnBoosted, booster);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
State.Continuation = AfterAttackDeclared;
|
||||
else
|
||||
AfterAttackDeclared();
|
||||
}
|
||||
|
||||
if (booster is not null)
|
||||
{
|
||||
booster.Rested = true;
|
||||
|
|
@ -1278,23 +1389,16 @@ public sealed class GameEngine
|
|||
_events.Add(new BoostedEvent(attackerPlayer, boosterSlot, attackerSlot));
|
||||
|
||||
TriggerFromCard(attackerPlayer, booster, AbilityTrigger.OnBoost, attacker);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
State.Continuation = AfterBoosted;
|
||||
else
|
||||
AfterBoosted();
|
||||
}
|
||||
|
||||
var defenderIndex = 1 - attackerPlayer;
|
||||
_events.Add(new AttackDeclaredEvent(attackerPlayer, attackerSlot, targetSlot));
|
||||
|
||||
void BeginGuard()
|
||||
{
|
||||
State.Step = Step.Guard;
|
||||
State.ActivePlayer = defenderIndex;
|
||||
}
|
||||
|
||||
TriggerFromCard(attackerPlayer, attacker, AbilityTrigger.OnAttackDeclared, null);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
State.Continuation = BeginGuard;
|
||||
else
|
||||
BeginGuard();
|
||||
{
|
||||
AfterAttackDeclared();
|
||||
}
|
||||
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
|
@ -1470,7 +1574,11 @@ public sealed class GameEngine
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Усиления от триггеров живут до конца хода, а не до конца боя.</summary>
|
||||
/// <summary>
|
||||
/// Усиления от триггеров живут до конца хода, а не до конца боя.
|
||||
/// Переопределения Сдерживания и числа проверок тоже сбрасываются здесь:
|
||||
/// в BT01 они выдаются только до конца хода.
|
||||
/// </summary>
|
||||
private void ClearTurnModifiers()
|
||||
{
|
||||
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||
|
|
@ -1478,6 +1586,8 @@ public sealed class GameEngine
|
|||
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfTurn);
|
||||
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfTurn);
|
||||
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilEndOfTurn);
|
||||
unit.RestraintOverride = null;
|
||||
unit.DriveChecksOverride = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ public sealed record UnitRetiredEvent(int Player, int Slot) : GameEvent;
|
|||
public sealed record CardSearchedEvent(int Player, string PrintingId) : GameEvent;
|
||||
public sealed record SearchFailedEvent(int Player, string Query) : GameEvent;
|
||||
public sealed record CardRevealedEvent(int Player, string PrintingId) : GameEvent;
|
||||
public sealed record CardReturnedToDeckEvent(int Player, string PrintingId) : GameEvent;
|
||||
public sealed record CardDiscardedEvent(int Player, string PrintingId) : GameEvent;
|
||||
public sealed record GameFinishedEvent(int Winner) : GameEvent;
|
||||
public sealed record AbilityTriggeredEvent(int Player, string PrintingId, AbilityTrigger Trigger) : GameEvent;
|
||||
public sealed record AbilityResolvedEvent(int Player, string PrintingId, bool Accepted) : GameEvent;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ public class GameEngineTests
|
|||
private const string Deathrex = "BT01/033EN"; // Tachikaze, 10000
|
||||
private const string DemonEater = "BT01/036EN"; // Dark Irregulars, 10000
|
||||
private const string Morgana = "TD01/011EN"; // Royal Paladin, grade 1, discard +4000
|
||||
private const string Mocha = "BT01/018EN"; // OTT, +3000 при четырёх картах в руке
|
||||
private const string HellSpider = "BT01/039EN"; // Megacolony, запрет вставать
|
||||
private const string BrutalJack = "BT01/029EN"; // Nova Grappler, Сдерживание
|
||||
private const string ToughBoy = "BT01/060EN"; // Nova Grappler, grade 1
|
||||
|
||||
[Fact]
|
||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||
|
|
@ -399,6 +403,8 @@ public class GameEngineTests
|
|||
var engine = StartedEngine();
|
||||
engine.Execute(new SkipRideCommand());
|
||||
SetVanguard(engine, 0, Overlord);
|
||||
// второй Kagero, чтобы не срабатывал штраф −2000
|
||||
engine.State.Players[0].RearGuards[0] = TakeFromDeck(engine.State.Players[0], Nehalem);
|
||||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||
engine.Execute(new EnterBattleCommand());
|
||||
|
||||
|
|
@ -1165,6 +1171,105 @@ public class GameEngineTests
|
|||
Assert.Equal(2000, attacker.Modifiers.Where(m => m.EffectId == "hit").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mocha_Gets_Power_When_Hand_Has_Four_Cards()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
engine.Execute(new SkipRideCommand());
|
||||
|
||||
var mocha = MakeCard(Mocha);
|
||||
engine.State.Players[0].Vanguard = mocha;
|
||||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||
|
||||
Assert.True(engine.State.Players[0].Hand.Count >= 4);
|
||||
|
||||
engine.Execute(new EnterBattleCommand());
|
||||
engine.Execute(new AttackCommand(-1, -1));
|
||||
|
||||
Assert.Equal(3000, mocha.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HellSpider_Prevents_Chosen_RearGuard_From_Standing()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
GiveDamage(engine, 0, 2);
|
||||
|
||||
var opponent = engine.State.Players[1];
|
||||
var target = MakeCard(Gallatin);
|
||||
opponent.RearGuards[0] = target;
|
||||
|
||||
var spider = MakeCard(HellSpider);
|
||||
engine.State.Players[0].Hand.Add(spider);
|
||||
|
||||
Assert.True(engine.Execute(new CallCommand(HellSpider, 0)).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.True(engine.State.HasRestriction(target, RestrictionType.CannotStand));
|
||||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrutalJack_Loses_Restraint_And_Gets_Power_From_Nova_Grappler_Boost()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
GiveDamage(engine, 0, 1);
|
||||
|
||||
var jack = MakeCard(BrutalJack);
|
||||
engine.State.Players[0].Hand.Add(jack);
|
||||
engine.Execute(new CallCommand(BrutalJack, 3));
|
||||
|
||||
var toughBoy = MakeCard(ToughBoy);
|
||||
engine.State.Players[0].Hand.Add(toughBoy);
|
||||
engine.Execute(new CallCommand(ToughBoy, 0));
|
||||
|
||||
jack.Rested = false;
|
||||
toughBoy.Rested = false;
|
||||
|
||||
Assert.True(engine.State.IsRestraint(jack));
|
||||
Assert.True(engine.Execute(new ActivateAbilityCommand(jack.Id)).Accepted);
|
||||
Assert.False(engine.State.IsRestraint(jack));
|
||||
|
||||
Assert.True(engine.Execute(new EnterBattleCommand()).Accepted);
|
||||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||||
|
||||
Assert.Equal(5000, jack.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Overlord_Activation_Adds_Power_Removes_Twin_Drive_And_Stands_On_RearGuard_Hit()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
GiveDamage(engine, 0, 3);
|
||||
|
||||
var overlord = MakeCard(Overlord);
|
||||
engine.State.Players[0].Vanguard = overlord;
|
||||
|
||||
Assert.True(engine.Execute(new ActivateAbilityCommand(overlord.Id)).Accepted);
|
||||
Assert.Equal(5000, overlord.Modifiers.Where(m => m.EffectId == "dragonic-overlord").Sum(m => m.Power));
|
||||
Assert.Equal(1, overlord.DriveChecksOverride);
|
||||
Assert.Single(overlord.GrantedAbilities);
|
||||
|
||||
StackDeck(engine.State.Players[0], Gallatin);
|
||||
engine.Execute(new EnterBattleCommand());
|
||||
|
||||
var opponent = engine.State.Players[1];
|
||||
opponent.RearGuards[0] = MakeCard(Marron);
|
||||
|
||||
Assert.True(engine.Execute(new AttackCommand(-1, 0)).Accepted);
|
||||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||
|
||||
Assert.False(overlord.Rested);
|
||||
Assert.Null(opponent.RearGuards[0]);
|
||||
|
||||
engine.Execute(new EndTurnCommand());
|
||||
Assert.Null(overlord.DriveChecksOverride);
|
||||
Assert.Empty(overlord.GrantedAbilities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||
{
|
||||
|
|
@ -1419,4 +1524,10 @@ public class GameEngineTests
|
|||
player.Deck.RemoveAt(0);
|
||||
return card;
|
||||
}
|
||||
|
||||
private static int _nextInstanceId = 20_000;
|
||||
|
||||
/// <summary>Карта, которой нет в тестовой колоде: кладём в руку или на поле вручную.</summary>
|
||||
private static CardInstance MakeCard(string printingId) =>
|
||||
new() { Id = Interlocked.Increment(ref _nextInstanceId), PrintingId = printingId };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue