Target selection and Activated abilities

PendingChoice/ChooseCommand let effects ask the player to pick a unit. Added Counter Blast and Soul Blast cost helpers. Blaster Blade implemented for both zones with its CB2 cost and rear-guard target. Activated abilities pay rest-self and discard; Lake Maiden Lien, Gojo and Shout draw a card. Seven tests.
This commit is contained in:
MpakobecEx 2026-10-21 13:14:16 +05:00
parent 5262b6b93c
commit a1e7c250bf
8 changed files with 479 additions and 14 deletions

View File

@ -114,9 +114,9 @@
- [ ] Discard, Shuffle как отдельные интенты. - [ ] Discard, Shuffle как отдельные интенты.
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную). - [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
- [x] Результат боя: `Hit` / `Guarded` как событие. - [x] Результат боя: `Hit` / `Guarded` как событие.
- [x] Событийная шина: `Triggered` (OnRideOver, OnBoost), `Continuous` с пересчётом при запросе; `Activated` ещё нет. - [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности. - [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
- [ ] `Activated`-способности и выбор целей игроком. - [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck. - [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
- [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N». - [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N».
- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет. - [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет.

View File

@ -5,6 +5,7 @@ namespace Vanguard.Rules.Engine.Abilities;
public enum AbilityTrigger public enum AbilityTrigger
{ {
OnRideOver, OnRideOver,
OnPlace,
OnBoost, OnBoost,
OnPhaseStart, OnPhaseStart,
OnAttackDeclared, OnAttackDeclared,
@ -33,6 +34,12 @@ public sealed class AbilityDefinition
public Func<AbilityContext, bool>? Condition { get; init; } public Func<AbilityContext, bool>? Condition { get; init; }
public required Action<AbilityContext> Resolve { get; init; } public required Action<AbilityContext> Resolve { get; init; }
/// <summary>Проверка, что стоимость можно оплатить.</summary>
public Func<AbilityContext, bool>? CanPay { get; init; }
/// <summary>Оплата стоимости перед разрешением.</summary>
public Action<AbilityContext>? Pay { get; init; }
/// <summary>«Вы можете» — движок спрашивает игрока перед разрешением.</summary> /// <summary>«Вы можете» — движок спрашивает игрока перед разрешением.</summary>
public bool Optional { get; init; } public bool Optional { get; init; }
} }
@ -45,11 +52,24 @@ public sealed class ContinuousAbility
public Func<AbilityContext, int>? CriticalBonus { get; init; } public Func<AbilityContext, int>? CriticalBonus { get; init; }
} }
/// <summary>
/// Активируемая способность: игрок сам объявляет её в Основной фазе.
/// Стоимость (повернуть себя, сбросить карту) оплачивает движок по описанию.
/// </summary>
public sealed class ActivatedAbility
{
public required Func<AbilityContext, bool> CanActivate { get; init; }
public bool RestsSelf { get; init; }
public bool RequiresDiscard { get; init; }
public required Action<AbilityContext> Resolve { get; init; }
}
public abstract class CardScript public abstract class CardScript
{ {
public abstract string NameId { get; } public abstract string NameId { get; }
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>();
} }
/// <summary>Сработавшая способность, ожидающая выбора порядка или решения игрока.</summary> /// <summary>Сработавшая способность, ожидающая выбора порядка или решения игрока.</summary>

View File

@ -11,9 +11,101 @@ public static class CardScripts
["barcgal"] = new BarcgalScript(), ["barcgal"] = new BarcgalScript(),
["wingal"] = new WingalScript(), ["wingal"] = new WingalScript(),
["king-of-knights-alfred"] = new AlfredScript(), ["king-of-knights-alfred"] = new AlfredScript(),
["blaster-blade"] = new BlasterBladeScript(),
["lake-maiden-lien"] = new DrawOneScript("lake-maiden-lien"),
["dragon-monk-gojo"] = new DrawOneScript("dragon-monk-gojo"),
["screamin-and-dancin-announcer-shout"] = new DrawOneScript("screamin-and-dancin-announcer-shout"),
}; };
} }
/// <summary>
/// «[ACT]: повернуть эту карту и сбросить карту из руки → взять карту».
/// Общий скрипт для Lake Maiden, Lien, Dragon Monk, Gojo и Shout.
/// </summary>
public sealed class DrawOneScript : CardScript
{
public DrawOneScript(string nameId) => NameId = nameId;
public override string NameId { get; }
public override IEnumerable<ActivatedAbility> Activated
{
get
{
yield return new ActivatedAbility
{
CanActivate = _ => true,
RestsSelf = true,
RequiresDiscard = true,
Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 1),
};
}
}
}
/// <summary>
/// Blaster Blade: при размещении оплатить Counter Blast 2 и уничтожить Арьергард противника.
/// На Авангарде — любой, на Арьергарде — только 2 ранга или выше и при Авангарде Royal Paladin.
/// </summary>
public sealed class BlasterBladeScript : CardScript
{
public override string NameId => "blaster-blade";
public override IEnumerable<AbilityDefinition> Abilities
{
get
{
yield return new AbilityDefinition
{
Trigger = AbilityTrigger.OnPlace,
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 =>
{
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.RetireRearGuard(opponent, slot));
},
};
yield return new AbilityDefinition
{
Trigger = AbilityTrigger.OnPlace,
Optional = true,
Condition = ctx =>
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
ctx.Engine.VanguardClan(ctx.Owner) == Clan.RoyalPaladin,
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, d => d.Grade >= 2);
if (slots.Count == 0)
return;
ctx.Engine.AskUnitChoice(
ctx.Owner,
opponent,
"Выберите Арьергард противника 2 ранга или выше",
slots,
slot => ctx.Engine.RetireRearGuard(opponent, slot));
},
};
}
}
}
/// <summary> /// <summary>
/// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда. /// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда.
/// </summary> /// </summary>

View File

@ -16,6 +16,8 @@ public sealed record PassGuardCommand : GameCommand;
public sealed record ResolveTriggerCommand(int PowerTarget, int EffectTarget) : GameCommand; public sealed record ResolveTriggerCommand(int PowerTarget, int EffectTarget) : GameCommand;
public sealed record SelectTriggerCommand(int Index) : GameCommand; public sealed record SelectTriggerCommand(int Index) : GameCommand;
public sealed record ResolveAbilityCommand(bool Accept) : GameCommand; public sealed record ResolveAbilityCommand(bool Accept) : GameCommand;
public sealed record ChooseCommand(int Index) : GameCommand;
public sealed record ActivateAbilityCommand(int InstanceId, string? DiscardPrintingId = null) : GameCommand;
public sealed record EndTurnCommand : GameCommand; public sealed record EndTurnCommand : GameCommand;
public sealed record CommandResult(bool Accepted, string? Reason) public sealed record CommandResult(bool Accepted, string? Reason)

View File

@ -18,9 +18,8 @@ namespace Vanguard.Rules.Engine;
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow); /// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; /// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
/// Арьергард по умолчанию без проверок, но эффект может их выдать; /// Арьергард по умолчанию без проверок, но эффект может их выдать;
/// * способности карт подключаются скриптами (CardScript): Triggered и Continuous; /// * способности карт: Triggered, Continuous и Activated; выбор цели идёт через
/// Check Timing собирает сработавшие, активный игрок выбирает порядок, /// PendingChoice (ChooseCommand), порядок триггеров выбирает активный игрок;
/// опциональные способности ждут ResolveAbilityCommand;
/// * непрерывные эффекты пересчитываются при каждом запросе (EffectivePower); /// * непрерывные эффекты пересчитываются при каждом запросе (EffectivePower);
/// * триггер требует выбора целей через ResolveTriggerCommand: /// * триггер требует выбора целей через ResolveTriggerCommand:
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект; /// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
@ -115,9 +114,15 @@ public sealed class GameEngine
private void OfferAbility(TriggeredAbility ability) private void OfferAbility(TriggeredAbility ability)
{ {
_events.Add(new AbilityTriggeredEvent(ability.Owner, ability.Context.Source.PrintingId, ability.Definition.Trigger)); var definition = ability.Definition;
if (ability.Definition.Optional) // если стоимость оплатить нельзя, способность не предлагается
if (definition.CanPay is not null && !definition.CanPay(ability.Context))
return;
_events.Add(new AbilityTriggeredEvent(ability.Owner, ability.Context.Source.PrintingId, definition.Trigger));
if (definition.Optional)
{ {
State.PendingAbility = ability; State.PendingAbility = ability;
State.Step = Step.Ability; State.Step = Step.Ability;
@ -131,26 +136,126 @@ public sealed class GameEngine
private void ResolveAbility(TriggeredAbility ability, bool accepted) private void ResolveAbility(TriggeredAbility ability, bool accepted)
{ {
if (accepted) if (accepted)
{
ability.Definition.Pay?.Invoke(ability.Context);
ability.Definition.Resolve(ability.Context); ability.Definition.Resolve(ability.Context);
}
_events.Add(new AbilityResolvedEvent(ability.Owner, ability.Context.Source.PrintingId, accepted)); _events.Add(new AbilityResolvedEvent(ability.Owner, ability.Context.Source.PrintingId, accepted));
// способность попросила выбрать цель — ждём ChooseCommand
if (State.Step == Step.Choice)
return;
ContinueOrder();
}
private void ContinueOrder()
{
if (State.PendingOrder is { Count: > 0 } order) if (State.PendingOrder is { Count: > 0 } order)
{
if (order.Count == 1)
{ {
var next = order[0]; var next = order[0];
order.RemoveAt(0);
if (order.Count == 0)
State.PendingOrder = null; State.PendingOrder = null;
State.ActivePlayer = next.Owner; State.ActivePlayer = next.Owner;
OfferAbility(next); OfferAbility(next);
return; return;
} }
State.Step = Step.Ability;
State.ActivePlayer = order[0].Owner;
return;
}
State.Step = Step.None; State.Step = Step.None;
} }
private CommandResult Activate(int instanceId, string? discardPrintingId)
{
if (State.Phase != Phase.Main)
return CommandResult.Reject("Активируемые способности доступны только в Основной фазе.");
if (State.Step != Step.None || State.PendingAbility is not null || State.PendingChoice is not null)
return CommandResult.Reject("Сейчас нельзя активировать способности.");
var player = State.ActivePlayer;
var p = State.Players[player];
var unit = AllUnits(player).FirstOrDefault(u => u.Id == instanceId);
if (unit is null)
return CommandResult.Reject("Карта не на поле.");
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
if (!_scripts.TryGetValue(nameId, out var script))
return CommandResult.Reject("У карты нет способностей.");
var context = new AbilityContext
{
Engine = this,
Source = unit,
Owner = player,
Phase = State.Phase,
};
var ability = script.Activated.FirstOrDefault(a => a.CanActivate(context));
if (ability is null)
return CommandResult.Reject("Нет доступной способности.");
if (ability.RestsSelf && unit.Rested)
return CommandResult.Reject("Карта уже повёрнута.");
CardInstance? discard = null;
if (ability.RequiresDiscard)
{
if (discardPrintingId is null)
return CommandResult.Reject("Нужно выбрать карту для сброса.");
var index = p.Hand.FindIndex(c => c.PrintingId == discardPrintingId);
if (index < 0)
return CommandResult.Reject("Карты для сброса нет в руке.");
discard = p.Hand[index];
}
if (ability.RestsSelf)
unit.Rested = true;
if (discard is not null)
{
p.Hand.Remove(discard);
p.Drop.Add(discard);
}
ability.Resolve(context);
_events.Add(new AbilityResolvedEvent(player, unit.PrintingId, true));
return CommandResult.Ok();
}
private CommandResult Choose(int index)
{
var choice = State.PendingChoice;
if (choice is null || State.Step != Step.Choice)
return CommandResult.Reject("Сейчас нет выбора.");
if (State.ActivePlayer != choice.Player)
return CommandResult.Reject("Сейчас выбирает другой игрок.");
if (index < 0 || index >= choice.Options.Count)
return CommandResult.Reject("Некорректный вариант выбора.");
var option = choice.Options[index];
State.PendingChoice = null;
State.Step = Step.None;
choice.Resolve(option.Slot);
if (State.Step != Step.Choice)
ContinueOrder();
return CommandResult.Ok();
}
private CommandResult SelectTrigger(int index) private CommandResult SelectTrigger(int index)
{ {
var order = State.PendingOrder; var order = State.PendingOrder;
@ -261,6 +366,107 @@ public sealed class GameEngine
battle.AbilityPower += amount; battle.AbilityPower += amount;
} }
// ---------- стоимости ----------
public int FaceDownDamage(int player) =>
State.Players[player].Damage.Count(c => !c.FaceUp);
public bool CanCounterBlast(int player, int amount) =>
FaceDownDamage(player) >= amount;
public void CounterBlast(int player, int amount)
{
var flipped = 0;
foreach (var card in State.Players[player].Damage)
{
if (flipped == amount)
break;
if (card.FaceUp)
continue;
card.FaceUp = true;
flipped++;
}
}
public bool CanSoulBlast(int player, int amount) =>
State.Players[player].Soul.Count >= amount;
public void SoulBlast(int player, int amount)
{
var soul = State.Players[player].Soul;
for (var i = 0; i < amount && soul.Count > 0; i++)
{
var card = soul[0];
soul.RemoveAt(0);
State.Players[player].Drop.Add(card);
}
}
// ---------- выбор целей ----------
/// <summary>Круги Арьергарда игрока, подходящие под фильтр.</summary>
public IReadOnlyList<int> RearGuardSlots(int player, Func<CardDefinition, bool> predicate)
{
var slots = new List<int>();
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
{
var unit = State.Players[player].RearGuards[slot];
if (unit is null)
continue;
if (predicate(State.Catalog.CardOf(unit.PrintingId)))
slots.Add(slot);
}
return slots;
}
/// <summary>Останавливает разрешение и просит игрока выбрать карту на поле.</summary>
public void AskUnitChoice(
int chooser,
int targetPlayer,
string prompt,
IReadOnlyList<int> slots,
Action<int> onChosen)
{
var options = slots
.Select(slot =>
{
var unit = State.GetUnit(targetPlayer, slot)!;
return new ChoiceOption(slot, State.Catalog.CardOf(unit.PrintingId).NameEn);
})
.ToList();
State.PendingChoice = new PendingChoice
{
Player = chooser,
Prompt = prompt,
Options = options,
Resolve = onChosen,
};
State.Step = Step.Choice;
State.ActivePlayer = chooser;
}
public void RetireRearGuard(int player, int slot)
{
var unit = State.Players[player].RearGuards[slot];
if (unit is null)
return;
State.Players[player].RearGuards[slot] = null;
State.Players[player].Drop.Add(unit);
_events.Add(new UnitRetiredEvent(player, slot));
}
public Clan VanguardClan(int player)
{
var vanguard = State.Players[player].Vanguard;
return vanguard is null ? Clan.RoyalPaladin : State.Catalog.CardOf(vanguard.PrintingId).Clan;
}
// ---------- старт партии ---------- // ---------- старт партии ----------
public void Start() public void Start()
@ -332,6 +538,8 @@ public sealed class GameEngine
ResolveTriggerCommand c => ResolveTrigger(c), ResolveTriggerCommand c => ResolveTrigger(c),
SelectTriggerCommand c => SelectTrigger(c.Index), SelectTriggerCommand c => SelectTrigger(c.Index),
ResolveAbilityCommand c => ResolvePendingAbility(c.Accept), ResolveAbilityCommand c => ResolvePendingAbility(c.Accept),
ChooseCommand c => Choose(c.Index),
ActivateAbilityCommand c => Activate(c.InstanceId, c.DiscardPrintingId),
EndTurnCommand => EndTurn(), EndTurnCommand => EndTurn(),
_ => CommandResult.Reject("Неизвестная команда."), _ => CommandResult.Reject("Неизвестная команда."),
}; };
@ -404,6 +612,9 @@ public sealed class GameEngine
State.Phase = Phase.Main; State.Phase = Phase.Main;
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Main)); _events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Main));
TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null);
if (State.Step == Step.None)
TriggerFromCard(State.ActivePlayer, riddenOver, AbilityTrigger.OnRideOver, card); TriggerFromCard(State.ActivePlayer, riddenOver, AbilityTrigger.OnRideOver, card);
return CommandResult.Ok(); return CommandResult.Ok();
@ -441,6 +652,9 @@ public sealed class GameEngine
player.RearGuards[slot] = card; player.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId)); _events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null);
return CommandResult.Ok(); return CommandResult.Ok();
} }
@ -1050,6 +1264,8 @@ public sealed class GameEngine
return card; return card;
} }
public void DrawCards(int player, int count) => Draw(player, count);
private void Draw(int player, int count) private void Draw(int player, int count)
{ {
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)

View File

@ -20,6 +20,22 @@ public enum Step
Drive, Drive,
Trigger, Trigger,
Ability, Ability,
Choice,
}
/// <summary>Вариант выбора, который предлагается игроку.</summary>
public sealed record ChoiceOption(int Slot, string Label);
/// <summary>
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
/// Пока выбор не сделан, движок не продолжает разрешение способности.
/// </summary>
public sealed class PendingChoice
{
public required int Player { get; init; }
public required string Prompt { get; init; }
public required IReadOnlyList<ChoiceOption> Options { get; init; }
public required Action<int> Resolve { get; init; }
} }
public enum TriggerContext public enum TriggerContext

View File

@ -60,6 +60,9 @@ public sealed class GameState
/// <summary>Сработавшие способности ждут выбора порядка.</summary> /// <summary>Сработавшие способности ждут выбора порядка.</summary>
public List<TriggeredAbility>? PendingOrder { get; internal set; } public List<TriggeredAbility>? PendingOrder { get; internal set; }
/// <summary>Ожидание выбора цели или карты.</summary>
public PendingChoice? PendingChoice { get; internal set; }
public bool IsFinished { get; internal set; } public bool IsFinished { get; internal set; }
public int? Winner { get; internal set; } public int? Winner { get; internal set; }

View File

@ -21,6 +21,7 @@ public class GameEngineTests
private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
private const string BlasterBlade = "BT01/002EN"; private const string BlasterBlade = "BT01/002EN";
private const string Wingal = "BT01/044EN"; private const string Wingal = "BT01/044EN";
private const string Lien = "BT01/043EN"; // activated: rest + discard → draw
[Fact] [Fact]
public void Start_Places_Starting_Vanguard_And_Draws_Five() public void Start_Places_Starting_Vanguard_And_Draws_Five()
@ -624,6 +625,107 @@ public class GameEngineTests
Assert.DoesNotContain(engine.Events, e => e is UnitRetiredEvent); Assert.DoesNotContain(engine.Events, e => e is UnitRetiredEvent);
} }
[Fact]
public void BlasterBlade_On_Vanguard_Retires_Chosen_RearGuard()
{
var engine = StartedEngine();
SetVanguard(engine, 0, Marron);
GiveDamage(engine, 0, 2);
var opponent = engine.State.Players[1];
opponent.RearGuards[0] = TakeFromDeck(opponent, Gallatin);
Give(engine, 0, BlasterBlade);
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
Assert.NotNull(engine.State.PendingAbility);
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
Assert.NotNull(engine.State.PendingChoice);
Assert.Single(engine.State.PendingChoice!.Options);
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
Assert.Null(opponent.RearGuards[0]);
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
}
[Fact]
public void BlasterBlade_Is_Not_Offered_Without_Counter_Blast()
{
var engine = StartedEngine();
SetVanguard(engine, 0, Marron);
Give(engine, 0, BlasterBlade);
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
Assert.Null(engine.State.PendingAbility);
}
[Fact]
public void BlasterBlade_On_RearGuard_Requires_RoyalPaladin_Vanguard()
{
var kagero = StartedEngine();
kagero.Execute(new SkipRideCommand());
Give(kagero, 0, BlasterBlade);
kagero.Execute(new CallCommand(BlasterBlade, 0));
Assert.Null(kagero.State.PendingAbility);
var royal = StartedEngine();
SetVanguard(royal, 0, Marron);
royal.Execute(new SkipRideCommand());
GiveDamage(royal, 0, 2);
Give(royal, 0, BlasterBlade);
royal.Execute(new CallCommand(BlasterBlade, 0));
Assert.NotNull(royal.State.PendingAbility);
}
[Fact]
public void Activated_Ability_Rests_Self_Discards_And_Draws()
{
var engine = InMainPhase();
Give(engine, 0, Lien);
engine.Execute(new CallCommand(Lien, 0));
var lien = engine.State.Players[0].RearGuards[0]!;
lien.Rested = false;
Give(engine, 0, Marron);
var handBefore = engine.State.Players[0].Hand.Count;
Assert.True(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
Assert.True(lien.Rested);
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Marron);
Assert.Equal(handBefore, engine.State.Players[0].Hand.Count); // -1 сброс, +1 добор
}
[Fact]
public void Activated_Ability_Requires_Discard()
{
var engine = InMainPhase();
Give(engine, 0, Lien);
engine.Execute(new CallCommand(Lien, 0));
var lien = engine.State.Players[0].RearGuards[0]!;
lien.Rested = false;
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id)).Accepted);
}
[Fact]
public void Activated_Ability_Rejects_Rested_Card()
{
var engine = InMainPhase();
Give(engine, 0, Lien);
engine.Execute(new CallCommand(Lien, 0));
var lien = engine.State.Players[0].RearGuards[0]!;
Give(engine, 0, Marron);
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
}
[Fact] [Fact]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending() public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{ {
@ -760,6 +862,7 @@ public class GameEngineTests
(Nike, 2), (Flogal, 2), (Govannon, 2), (Nike, 2), (Flogal, 2), (Govannon, 2),
(Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 2), (Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 2),
(BlasterBlade, 2), (Wingal, 2), (BlasterBlade, 2), (Wingal, 2),
(Lien, 2),
}; };
private List<string> BuildDeck() private List<string> BuildDeck()
@ -822,6 +925,19 @@ public class GameEngineTests
player.Deck.Insert(0, cards[i]); player.Deck.Insert(0, cards[i]);
} }
private static void GiveDamage(GameEngine engine, int player, int amount)
{
var p = engine.State.Players[player];
for (var i = 0; i < amount; i++)
{
Assert.NotEmpty(p.Deck);
var card = p.Deck[0];
p.Deck.RemoveAt(0);
card.FaceUp = false;
p.Damage.Add(card);
}
}
private static void SetVanguard(GameEngine engine, int player, string printingId) private static void SetVanguard(GameEngine engine, int player, string printingId)
{ {
var p = engine.State.Players[player]; var p = engine.State.Players[player];