Multi-target choices, action restrictions and rear-guard hit triggers
AskUnitMultiChoice asks one target at a time, offers Done once the minimum is met and resolves automatically at the maximum. CardRestriction models cannot-stand/attack/boost/intercept with source, effect id and duration, including UntilOwnerNextStand. Restraint can be overridden by effects. OnAttackHit now fires when the attack hits a rear-guard. Five tests.
This commit is contained in:
parent
1fcb3b80df
commit
ff7774b236
|
|
@ -122,6 +122,9 @@
|
||||||
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» ещё нет.
|
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» ещё нет.
|
||||||
- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
|
- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
|
||||||
- [x] Модификаторы силы/крита с источником и сроком жизни (`StatModifier`); выдача и снятие способностей эффектами (`GrantedAbility`).
|
- [x] Модификаторы силы/крита с источником и сроком жизни (`StatModifier`); выдача и снятие способностей эффектами (`GrantedAbility`).
|
||||||
|
- [x] Выбор от `min` до `max` целей на поле (`AskUnitMultiChoice`) для «до N» и «ровно N».
|
||||||
|
- [x] Запреты с источником и сроком жизни (`CardRestriction`): не вставать, не атаковать, не поддерживать, не перехватывать, не быть выбранным; снятие Сдерживания эффектом.
|
||||||
|
- [x] `OnAttackHit` срабатывает и при попадании по Арьергарду.
|
||||||
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
||||||
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
|
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
|
||||||
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,30 @@ public sealed class GameEngine
|
||||||
unit.GrantedAbilities.RemoveAll(g => predicate(g));
|
unit.GrantedAbilities.RemoveAll(g => predicate(g));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Запретить карте действие с указанием источника и срока жизни.</summary>
|
||||||
|
public void AddRestriction(
|
||||||
|
CardInstance unit,
|
||||||
|
RestrictionType type,
|
||||||
|
CardInstance source,
|
||||||
|
string effectId,
|
||||||
|
ModifierDuration duration)
|
||||||
|
{
|
||||||
|
unit.Restrictions.Add(new CardRestriction
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Source = source,
|
||||||
|
EffectId = effectId,
|
||||||
|
Duration = duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Снять запреты, подходящие под условие.</summary>
|
||||||
|
public void RemoveRestrictions(Func<CardRestriction, bool> predicate)
|
||||||
|
{
|
||||||
|
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||||
|
unit.Restrictions.RemoveAll(r => predicate(r));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- стоимости ----------
|
// ---------- стоимости ----------
|
||||||
|
|
||||||
public int FaceDownDamage(int player) =>
|
public int FaceDownDamage(int player) =>
|
||||||
|
|
@ -630,6 +654,74 @@ public sealed class GameEngine
|
||||||
State.ActivePlayer = chooser;
|
State.ActivePlayer = chooser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выбор от min до max целей на поле. Игрок выбирает по одной; когда минимум
|
||||||
|
/// набран, появляется вариант «Готово». Выбранные цели приходят одним списком.
|
||||||
|
/// </summary>
|
||||||
|
public void AskUnitMultiChoice(
|
||||||
|
int chooser,
|
||||||
|
string prompt,
|
||||||
|
IReadOnlyList<UnitTarget> targets,
|
||||||
|
int min,
|
||||||
|
int max,
|
||||||
|
Action<IReadOnlyList<UnitTarget>> onChosen)
|
||||||
|
{
|
||||||
|
var chosen = new List<UnitTarget>();
|
||||||
|
|
||||||
|
void Ask()
|
||||||
|
{
|
||||||
|
var remaining = targets.Where(t => !chosen.Contains(t)).ToList();
|
||||||
|
|
||||||
|
if (remaining.Count == 0 || chosen.Count >= max)
|
||||||
|
{
|
||||||
|
onChosen(chosen.ToList());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = remaining
|
||||||
|
.Select((target, index) =>
|
||||||
|
{
|
||||||
|
var unit = State.GetUnit(target.Player, target.Slot)!;
|
||||||
|
return new ChoiceOption(index, State.Catalog.CardOf(unit.PrintingId).NameEn);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// «Готово» появляется только когда минимум уже набран
|
||||||
|
var stopIndex = -1;
|
||||||
|
if (chosen.Count >= min)
|
||||||
|
{
|
||||||
|
stopIndex = options.Count;
|
||||||
|
options.Add(new ChoiceOption(stopIndex, "Готово"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPrompt = chosen.Count == 0
|
||||||
|
? prompt
|
||||||
|
: $"{prompt} (выбрано: {chosen.Count})";
|
||||||
|
|
||||||
|
State.PendingChoice = new PendingChoice
|
||||||
|
{
|
||||||
|
Player = chooser,
|
||||||
|
Prompt = currentPrompt,
|
||||||
|
Options = options,
|
||||||
|
Resolve = index =>
|
||||||
|
{
|
||||||
|
if (index == stopIndex)
|
||||||
|
{
|
||||||
|
onChosen(chosen.ToList());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen.Add(remaining[index]);
|
||||||
|
Ask();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
State.Step = Step.Choice;
|
||||||
|
State.ActivePlayer = chooser;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ask();
|
||||||
|
}
|
||||||
|
|
||||||
public void RetireRearGuard(int player, int slot)
|
public void RetireRearGuard(int player, int slot)
|
||||||
{
|
{
|
||||||
var unit = State.Players[player].RearGuards[slot];
|
var unit = State.Players[player].RearGuards[slot];
|
||||||
|
|
@ -1133,10 +1225,12 @@ public sealed class GameEngine
|
||||||
if (attacker.Rested)
|
if (attacker.Rested)
|
||||||
return CommandResult.Reject("Юнит уже повёрнут.");
|
return CommandResult.Reject("Юнит уже повёрнут.");
|
||||||
|
|
||||||
var attackerDef = State.Catalog.CardOf(attacker.PrintingId);
|
if (State.IsRestraint(attacker))
|
||||||
if (attackerDef.IsRestraint)
|
|
||||||
return CommandResult.Reject("Юнит со Сдерживанием не может атаковать.");
|
return CommandResult.Reject("Юнит со Сдерживанием не может атаковать.");
|
||||||
|
|
||||||
|
if (State.HasRestriction(attacker, RestrictionType.CannotAttack))
|
||||||
|
return CommandResult.Reject("Юнит не может атаковать.");
|
||||||
|
|
||||||
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
|
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
|
||||||
if (target is null)
|
if (target is null)
|
||||||
return CommandResult.Reject("Нет цели.");
|
return CommandResult.Reject("Нет цели.");
|
||||||
|
|
@ -1340,7 +1434,14 @@ public sealed class GameEngine
|
||||||
State.TurnNumber++;
|
State.TurnNumber++;
|
||||||
|
|
||||||
foreach (var unit in AllUnits(State.ActivePlayer))
|
foreach (var unit in AllUnits(State.ActivePlayer))
|
||||||
|
{
|
||||||
|
if (!State.HasRestriction(unit, RestrictionType.CannotStand))
|
||||||
unit.Rested = false;
|
unit.Rested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрет «не вставать» действует до этой фазы Подъёма и снимается после неё
|
||||||
|
foreach (var unit in AllUnits(State.ActivePlayer))
|
||||||
|
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilOwnerNextStand);
|
||||||
|
|
||||||
State.Phase = Phase.Stand;
|
State.Phase = Phase.Stand;
|
||||||
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Stand));
|
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Stand));
|
||||||
|
|
@ -1376,6 +1477,7 @@ public sealed class GameEngine
|
||||||
{
|
{
|
||||||
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfTurn);
|
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfTurn);
|
||||||
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfTurn);
|
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfTurn);
|
||||||
|
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilEndOfTurn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1547,7 +1649,6 @@ public sealed class GameEngine
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attackerDef = State.Catalog.CardOf(attacker.PrintingId);
|
|
||||||
var defenseUnit = battle.Interceptor ?? target;
|
var defenseUnit = battle.Interceptor ?? target;
|
||||||
|
|
||||||
var attackPower = EffectivePower(attacker);
|
var attackPower = EffectivePower(attacker);
|
||||||
|
|
@ -1603,6 +1704,14 @@ public sealed class GameEngine
|
||||||
State.Players[defenderIndex].RearGuards[battle.TargetSlot] = null;
|
State.Players[defenderIndex].RearGuards[battle.TargetSlot] = null;
|
||||||
State.Players[defenderIndex].Drop.Add(target);
|
State.Players[defenderIndex].Drop.Add(target);
|
||||||
_events.Add(new UnitRetiredEvent(defenderIndex, battle.TargetSlot));
|
_events.Add(new UnitRetiredEvent(defenderIndex, battle.TargetSlot));
|
||||||
|
|
||||||
|
TriggerFromCard(battle.AttackerPlayer, attacker, AbilityTrigger.OnAttackHit, target);
|
||||||
|
|
||||||
|
if (State.Step is Step.Ability or Step.Choice)
|
||||||
|
{
|
||||||
|
State.Continuation = () => ClearBattle(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1625,6 +1734,7 @@ public sealed class GameEngine
|
||||||
{
|
{
|
||||||
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfBattle);
|
unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfBattle);
|
||||||
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfBattle);
|
unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfBattle);
|
||||||
|
unit.Restrictions.RemoveAll(r => r.Duration == ModifierDuration.UntilEndOfBattle);
|
||||||
}
|
}
|
||||||
|
|
||||||
State.Battle = null;
|
State.Battle = null;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ public enum Step
|
||||||
/// <summary>Вариант выбора, который предлагается игроку.</summary>
|
/// <summary>Вариант выбора, который предлагается игроку.</summary>
|
||||||
public sealed record ChoiceOption(int Slot, string Label);
|
public sealed record ChoiceOption(int Slot, string Label);
|
||||||
|
|
||||||
|
/// <summary>Карта на поле: игрок и круг (-1 — Авангард).</summary>
|
||||||
|
public sealed record UnitTarget(int Player, int Slot);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
|
/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта.
|
||||||
/// Пока выбор не сделан, движок не продолжает разрешение способности.
|
/// Пока выбор не сделан, движок не продолжает разрешение способности.
|
||||||
|
|
@ -77,6 +80,15 @@ public sealed class CardInstance
|
||||||
|
|
||||||
/// <summary>Способности, выданные эффектами, а не напечатанные на карте.</summary>
|
/// <summary>Способности, выданные эффектами, а не напечатанные на карте.</summary>
|
||||||
public List<GrantedAbility> GrantedAbilities { get; } = new();
|
public List<GrantedAbility> GrantedAbilities { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Запреты, выданные эффектами.</summary>
|
||||||
|
public List<CardRestriction> Restrictions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переопределение Сдерживания, выданное эффектом.
|
||||||
|
/// null — как напечатано на карте.
|
||||||
|
/// </summary>
|
||||||
|
public bool? RestraintOverride { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Переопределение способности поддержки, выданное эффектом.
|
/// Переопределение способности поддержки, выданное эффектом.
|
||||||
/// null — как напечатано на карте.
|
/// null — как напечатано на карте.
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,21 @@ public sealed class GameState
|
||||||
/// Итоговая способность поддержки: выданная эффектом важнее печатной.
|
/// Итоговая способность поддержки: выданная эффектом важнее печатной.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanBoost(CardInstance unit) =>
|
public bool CanBoost(CardInstance unit) =>
|
||||||
unit.BoostOverride ?? Definition(unit).HasBoost;
|
!HasRestriction(unit, RestrictionType.CannotBoost)
|
||||||
|
&& (unit.BoostOverride ?? Definition(unit).HasBoost);
|
||||||
|
|
||||||
/// <summary>Итоговая способность перехвата: выданная эффектом важнее печатной.</summary>
|
/// <summary>Итоговая способность перехвата: выданная эффектом важнее печатной.</summary>
|
||||||
public bool CanIntercept(CardInstance unit) =>
|
public bool CanIntercept(CardInstance unit) =>
|
||||||
unit.InterceptOverride ?? Definition(unit).HasIntercept;
|
!HasRestriction(unit, RestrictionType.CannotIntercept)
|
||||||
|
&& (unit.InterceptOverride ?? Definition(unit).HasIntercept);
|
||||||
|
|
||||||
|
/// <summary>Есть ли у карты запрет указанного вида.</summary>
|
||||||
|
public bool HasRestriction(CardInstance unit, RestrictionType type) =>
|
||||||
|
unit.Restrictions.Any(r => r.Type == type);
|
||||||
|
|
||||||
|
/// <summary>Действует ли на карте Сдерживание с учётом переопределения эффектом.</summary>
|
||||||
|
public bool IsRestraint(CardInstance unit) =>
|
||||||
|
unit.RestraintOverride ?? Definition(unit).IsRestraint;
|
||||||
|
|
||||||
public CardInstance? GetUnit(int player, int slot) =>
|
public CardInstance? GetUnit(int player, int slot) =>
|
||||||
slot < 0
|
slot < 0
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,27 @@ public enum ModifierDuration
|
||||||
{
|
{
|
||||||
UntilEndOfBattle,
|
UntilEndOfBattle,
|
||||||
UntilEndOfTurn,
|
UntilEndOfTurn,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Живёт до следующей фазы Подъёма владельца карты. Нужен запретам вида
|
||||||
|
/// «не может вставать в следующую фазу Подъёма»: снимается уже после того,
|
||||||
|
/// как карта должна была встать.
|
||||||
|
/// </summary>
|
||||||
|
UntilOwnerNextStand,
|
||||||
|
|
||||||
Permanent,
|
Permanent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Постоянный запрет на действие.</summary>
|
||||||
|
public enum RestrictionType
|
||||||
|
{
|
||||||
|
CannotStand,
|
||||||
|
CannotAttack,
|
||||||
|
CannotBoost,
|
||||||
|
CannotIntercept,
|
||||||
|
CannotBeChosen,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Прибавка к силе или криту. Знает, от какой карты пришла и каким эффектом,
|
/// Прибавка к силе или криту. Знает, от какой карты пришла и каким эффектом,
|
||||||
/// поэтому её можно точечно снять, не трогая остальные.
|
/// поэтому её можно точечно снять, не трогая остальные.
|
||||||
|
|
@ -30,3 +48,12 @@ public sealed class GrantedAbility
|
||||||
public required string EffectId { get; init; }
|
public required string EffectId { get; init; }
|
||||||
public required ModifierDuration Duration { get; init; }
|
public required ModifierDuration Duration { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Запрет на действие, выданный эффектом и знающий свой источник.</summary>
|
||||||
|
public sealed class CardRestriction
|
||||||
|
{
|
||||||
|
public required RestrictionType Type { get; init; }
|
||||||
|
public required CardInstance Source { get; init; }
|
||||||
|
public required string EffectId { get; init; }
|
||||||
|
public required ModifierDuration Duration { get; init; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1040,6 +1040,131 @@ public class GameEngineTests
|
||||||
Assert.Equal(0, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
Assert.Equal(0, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiChoice_Stops_When_Player_Says_Done()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
var opponent = engine.State.Players[1];
|
||||||
|
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||||||
|
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||||||
|
opponent.RearGuards[3] = TakeFromDeck(opponent, Gallatin);
|
||||||
|
|
||||||
|
IReadOnlyList<UnitTarget>? result = null;
|
||||||
|
engine.AskUnitMultiChoice(
|
||||||
|
0,
|
||||||
|
"Выберите до двух",
|
||||||
|
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1), new UnitTarget(1, 3) },
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
chosen => result = chosen);
|
||||||
|
|
||||||
|
Assert.Equal(Step.Choice, engine.State.Step);
|
||||||
|
Assert.Equal(4, engine.State.PendingChoice!.Options.Count); // три цели и «Готово»
|
||||||
|
|
||||||
|
engine.Execute(new ChooseCommand(0));
|
||||||
|
engine.Execute(new ChooseCommand(2)); // «Готово» среди двух оставшихся
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Single(result!);
|
||||||
|
Assert.Equal(0, result![0].Slot);
|
||||||
|
Assert.Equal(Step.None, engine.State.Step);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiChoice_Resolves_Automatically_When_Max_Reached()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
var opponent = engine.State.Players[1];
|
||||||
|
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||||||
|
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||||||
|
|
||||||
|
IReadOnlyList<UnitTarget>? result = null;
|
||||||
|
engine.AskUnitMultiChoice(
|
||||||
|
0,
|
||||||
|
"Выберите ровно две",
|
||||||
|
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1) },
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
chosen => result = chosen);
|
||||||
|
|
||||||
|
Assert.Equal(2, engine.State.PendingChoice!.Options.Count); // две цели, «Готово» ещё нет
|
||||||
|
|
||||||
|
engine.Execute(new ChooseCommand(0));
|
||||||
|
Assert.Null(result); // после первой цели выбор ещё продолжается
|
||||||
|
engine.Execute(new ChooseCommand(0));
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(2, result!.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotStand_Skips_Stand_And_Expires_After_Stand_Phase()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
SetVanguard(engine, 0, Gallatin);
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
|
||||||
|
var vanguard = engine.State.Players[0].Vanguard!;
|
||||||
|
engine.AddRestriction(
|
||||||
|
vanguard,
|
||||||
|
RestrictionType.CannotStand,
|
||||||
|
vanguard,
|
||||||
|
"test",
|
||||||
|
ModifierDuration.UntilOwnerNextStand);
|
||||||
|
vanguard.Rested = true;
|
||||||
|
|
||||||
|
engine.Execute(new EndTurnCommand()); // ход переходит к игроку 1
|
||||||
|
engine.Execute(new SkipRideCommand()); // игрок 1 выходит в Основную фазу
|
||||||
|
engine.Execute(new EndTurnCommand()); // и обратно
|
||||||
|
|
||||||
|
Assert.True(vanguard.Rested);
|
||||||
|
Assert.Empty(vanguard.Restrictions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotAttack_Restriction_Blocks_Attack()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttackRearGuard();
|
||||||
|
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||||||
|
engine.AddRestriction(
|
||||||
|
attacker,
|
||||||
|
RestrictionType.CannotAttack,
|
||||||
|
attacker,
|
||||||
|
"test",
|
||||||
|
ModifierDuration.UntilEndOfTurn);
|
||||||
|
|
||||||
|
Assert.False(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Attack_Hit_On_RearGuard_Triggers_OnAttackHit()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttackRearGuard();
|
||||||
|
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||||||
|
|
||||||
|
engine.GrantAbility(
|
||||||
|
attacker,
|
||||||
|
new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnAttackHit,
|
||||||
|
Resolve = ctx => ctx.Engine.AddPower(
|
||||||
|
ctx.Source,
|
||||||
|
2000,
|
||||||
|
ctx.Source,
|
||||||
|
"hit",
|
||||||
|
ModifierDuration.UntilEndOfTurn),
|
||||||
|
},
|
||||||
|
attacker,
|
||||||
|
"test",
|
||||||
|
ModifierDuration.UntilEndOfTurn);
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(3, 0));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Null(engine.State.Players[1].RearGuards[0]);
|
||||||
|
Assert.Equal(2000, attacker.Modifiers.Where(m => m.EffectId == "hit").Sum(m => m.Power));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue