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:
parent
5262b6b93c
commit
a1e7c250bf
|
|
@ -114,9 +114,9 @@
|
|||
- [ ] Discard, Shuffle как отдельные интенты.
|
||||
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
||||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||||
- [x] Событийная шина: `Triggered` (OnRideOver, OnBoost), `Continuous` с пересчётом при запросе; `Activated` ещё нет.
|
||||
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
||||
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
|
||||
- [ ] `Activated`-способности и выбор целей игроком.
|
||||
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
||||
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
|
||||
- [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N».
|
||||
- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace Vanguard.Rules.Engine.Abilities;
|
|||
public enum AbilityTrigger
|
||||
{
|
||||
OnRideOver,
|
||||
OnPlace,
|
||||
OnBoost,
|
||||
OnPhaseStart,
|
||||
OnAttackDeclared,
|
||||
|
|
@ -33,6 +34,12 @@ public sealed class AbilityDefinition
|
|||
public Func<AbilityContext, bool>? Condition { 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>
|
||||
public bool Optional { get; init; }
|
||||
}
|
||||
|
|
@ -45,11 +52,24 @@ public sealed class ContinuousAbility
|
|||
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 string NameId { get; }
|
||||
public virtual IEnumerable<AbilityDefinition> Abilities => Array.Empty<AbilityDefinition>();
|
||||
public virtual IEnumerable<ContinuousAbility> Continuous => Array.Empty<ContinuousAbility>();
|
||||
public virtual IEnumerable<ActivatedAbility> Activated => Array.Empty<ActivatedAbility>();
|
||||
}
|
||||
|
||||
/// <summary>Сработавшая способность, ожидающая выбора порядка или решения игрока.</summary>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,101 @@ public static class CardScripts
|
|||
["barcgal"] = new BarcgalScript(),
|
||||
["wingal"] = new WingalScript(),
|
||||
["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>
|
||||
/// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public sealed record PassGuardCommand : GameCommand;
|
|||
public sealed record ResolveTriggerCommand(int PowerTarget, int EffectTarget) : GameCommand;
|
||||
public sealed record SelectTriggerCommand(int Index) : 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 CommandResult(bool Accepted, string? Reason)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@ namespace Vanguard.Rules.Engine;
|
|||
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
|
||||
/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
|
||||
/// Арьергард по умолчанию без проверок, но эффект может их выдать;
|
||||
/// * способности карт подключаются скриптами (CardScript): Triggered и Continuous;
|
||||
/// Check Timing собирает сработавшие, активный игрок выбирает порядок,
|
||||
/// опциональные способности ждут ResolveAbilityCommand;
|
||||
/// * способности карт: Triggered, Continuous и Activated; выбор цели идёт через
|
||||
/// PendingChoice (ChooseCommand), порядок триггеров выбирает активный игрок;
|
||||
/// * непрерывные эффекты пересчитываются при каждом запросе (EffectivePower);
|
||||
/// * триггер требует выбора целей через ResolveTriggerCommand:
|
||||
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
||||
|
|
@ -115,9 +114,15 @@ public sealed class GameEngine
|
|||
|
||||
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.Step = Step.Ability;
|
||||
|
|
@ -131,26 +136,126 @@ public sealed class GameEngine
|
|||
private void ResolveAbility(TriggeredAbility ability, bool accepted)
|
||||
{
|
||||
if (accepted)
|
||||
{
|
||||
ability.Definition.Pay?.Invoke(ability.Context);
|
||||
ability.Definition.Resolve(ability.Context);
|
||||
}
|
||||
|
||||
_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 (order.Count == 1)
|
||||
{
|
||||
var next = order[0];
|
||||
order.RemoveAt(0);
|
||||
|
||||
if (order.Count == 0)
|
||||
State.PendingOrder = null;
|
||||
|
||||
State.ActivePlayer = next.Owner;
|
||||
OfferAbility(next);
|
||||
return;
|
||||
}
|
||||
|
||||
State.Step = Step.Ability;
|
||||
State.ActivePlayer = order[0].Owner;
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var order = State.PendingOrder;
|
||||
|
|
@ -261,6 +366,107 @@ public sealed class GameEngine
|
|||
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()
|
||||
|
|
@ -332,6 +538,8 @@ public sealed class GameEngine
|
|||
ResolveTriggerCommand c => ResolveTrigger(c),
|
||||
SelectTriggerCommand c => SelectTrigger(c.Index),
|
||||
ResolveAbilityCommand c => ResolvePendingAbility(c.Accept),
|
||||
ChooseCommand c => Choose(c.Index),
|
||||
ActivateAbilityCommand c => Activate(c.InstanceId, c.DiscardPrintingId),
|
||||
EndTurnCommand => EndTurn(),
|
||||
_ => CommandResult.Reject("Неизвестная команда."),
|
||||
};
|
||||
|
|
@ -404,6 +612,9 @@ public sealed class GameEngine
|
|||
State.Phase = 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);
|
||||
|
||||
return CommandResult.Ok();
|
||||
|
|
@ -441,6 +652,9 @@ public sealed class GameEngine
|
|||
player.RearGuards[slot] = card;
|
||||
|
||||
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
|
||||
|
||||
TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null);
|
||||
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
|
|
@ -1050,6 +1264,8 @@ public sealed class GameEngine
|
|||
return card;
|
||||
}
|
||||
|
||||
public void DrawCards(int player, int count) => Draw(player, count);
|
||||
|
||||
private void Draw(int player, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,22 @@ public enum Step
|
|||
Drive,
|
||||
Trigger,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ public sealed class GameState
|
|||
/// <summary>Сработавшие способности ждут выбора порядка.</summary>
|
||||
public List<TriggeredAbility>? PendingOrder { get; internal set; }
|
||||
|
||||
/// <summary>Ожидание выбора цели или карты.</summary>
|
||||
public PendingChoice? PendingChoice { get; internal set; }
|
||||
|
||||
public bool IsFinished { get; internal set; }
|
||||
public int? Winner { get; internal set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public class GameEngineTests
|
|||
private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
|
||||
private const string BlasterBlade = "BT01/002EN";
|
||||
private const string Wingal = "BT01/044EN";
|
||||
private const string Lien = "BT01/043EN"; // activated: rest + discard → draw
|
||||
|
||||
[Fact]
|
||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||
|
|
@ -624,6 +625,107 @@ public class GameEngineTests
|
|||
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]
|
||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||
{
|
||||
|
|
@ -760,6 +862,7 @@ public class GameEngineTests
|
|||
(Nike, 2), (Flogal, 2), (Govannon, 2),
|
||||
(Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 2),
|
||||
(BlasterBlade, 2), (Wingal, 2),
|
||||
(Lien, 2),
|
||||
};
|
||||
|
||||
private List<string> BuildDeck()
|
||||
|
|
@ -822,6 +925,19 @@ public class GameEngineTests
|
|||
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)
|
||||
{
|
||||
var p = engine.State.Players[player];
|
||||
|
|
|
|||
Loading…
Reference in New Issue