Drive-check and guardian triggers, return-to-deck effects, 8 more cards
Asura Kaiser reacts to grade 3 Nova Grappler drive checks; Rock the Wall and Redshoe Milly move from the guardian circle to soul; Juggernaut and Brakki return to deck after battle; Lozenge and Battleraizer return at end of turn; Guiding Zombie puts itself into soul and mills three. Four tests; 102 total.
This commit is contained in:
parent
80095da51f
commit
7ca53975d7
|
|
@ -114,7 +114,7 @@
|
||||||
- [ ] Discard, Shuffle как отдельные интенты.
|
- [ ] Discard, Shuffle как отдельные интенты.
|
||||||
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
||||||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||||||
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost, OnBoosted, OnPhaseStart, OnAttackDeclared, OnAttackHit), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost, OnBoosted, OnDriveCheck, OnLeaveGuardian, OnPhaseStart, OnAttackDeclared, OnAttackHit), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты).
|
||||||
- [ ] Событие OnDamageTaken и триггеры на получение урона.
|
- [ ] Событие OnDamageTaken и триггеры на получение урона.
|
||||||
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
|
- [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности.
|
||||||
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
||||||
|
|
@ -151,7 +151,9 @@
|
||||||
- [x] RU/EN тексты для всех 94.
|
- [x] RU/EN тексты для всех 94.
|
||||||
- [x] Сила/щит для 14 карт, которых нет в BT01.
|
- [x] Сила/щит для 14 карт, которых нет в BT01.
|
||||||
- [ ] Дашборд покрытия: `scripted / tested / total`.
|
- [ ] Дашборд покрытия: `scripted / tested / total`.
|
||||||
— сейчас заскриптовано **34** карты: 13 базовых + 21 в `CardScriptsBT01.cs`.
|
— сейчас заскриптовано **42** карты: 13 базовых + 29 в `CardScriptsBT01.cs`.
|
||||||
|
- [x] Возврат карты с поля в колоду: после боя (Juggernaut, Brakki) и в конце хода (Lozenge, Battleraizer).
|
||||||
|
- [x] Триггеры `OnDriveCheck` (Asura Kaiser) и `OnLeaveGuardian` (Rock the Wall, Redshoe Milly).
|
||||||
- [ ] Wyvern Strike, Tejas: атака заднего ряда не выделена — движок разрешает атаковать любой круг Арьергарда.
|
- [ ] Wyvern Strike, Tejas: атака заднего ряда не выделена — движок разрешает атаковать любой круг Арьергарда.
|
||||||
|
|
||||||
**Готово, когда:** 94/94 заскриптованы, 94/94 покрыты тестом, приёмка пройдена по чеклисту карты.
|
**Готово, когда:** 94/94 заскриптованы, 94/94 покрыты тестом, приёмка пройдена по чеклисту карты.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ public enum AbilityTrigger
|
||||||
OnAttackDeclared,
|
OnAttackDeclared,
|
||||||
OnAttackHit,
|
OnAttackHit,
|
||||||
OnBoosted,
|
OnBoosted,
|
||||||
|
OnDriveCheck,
|
||||||
|
OnLeaveGuardian,
|
||||||
OnDamageTaken,
|
OnDamageTaken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ public static class CardScripts
|
||||||
["ceo-amaterasu"] = new CeoAmaterasuScript(),
|
["ceo-amaterasu"] = new CeoAmaterasuScript(),
|
||||||
["mr-invincible"] = new MrInvincibleScript(),
|
["mr-invincible"] = new MrInvincibleScript(),
|
||||||
["brutal-jack"] = new BrutalJackScript(),
|
["brutal-jack"] = new BrutalJackScript(),
|
||||||
|
["asura-kaiser"] = new AsuraKaiserScript(),
|
||||||
|
["juggernaut-maximum"] = new ReturnToDeckOnAttackScript("juggernaut-maximum"),
|
||||||
|
["highspeed-brakki"] = new ReturnToDeckOnAttackScript("highspeed-brakki"),
|
||||||
|
["lozenge-magus"] = new ReturnToDeckOnBoostScript("lozenge-magus", Clan.OracleThinkTank),
|
||||||
|
["battleraizer"] = new ReturnToDeckOnBoostScript("battleraizer", Clan.NovaGrappler),
|
||||||
|
["rock-the-wall"] = new DropToSoulFromGuardianScript("rock-the-wall"),
|
||||||
|
["redshoe-milly"] = new DropToSoulFromGuardianScript("redshoe-milly"),
|
||||||
|
["guiding-zombie"] = new GuidingZombieScript(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -619,6 +619,200 @@ public sealed class MrInvincibleScript : CardScript
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asura Kaiser: −2000 без другого Nova Grappler; когда проверка открывает
|
||||||
|
/// Nova Grappler 3 ранга, можно поднять один свой Арьергард.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AsuraKaiserScript : CardScript
|
||||||
|
{
|
||||||
|
public override string NameId => "asura-kaiser";
|
||||||
|
|
||||||
|
public override IEnumerable<ContinuousAbility> Continuous
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new ContinuousAbility
|
||||||
|
{
|
||||||
|
Applies = ctx => ctx.Engine.CountFieldUnits(ctx.Owner, Clan.NovaGrappler, ctx.Source) == 0,
|
||||||
|
PowerBonus = _ => -2000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnDriveCheck,
|
||||||
|
Condition = ctx =>
|
||||||
|
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||||
|
ctx.Other is not null &&
|
||||||
|
ctx.Catalog.CardOf(ctx.Other.PrintingId) is { Grade: 3, Clan: Clan.NovaGrappler },
|
||||||
|
Resolve = ctx =>
|
||||||
|
{
|
||||||
|
var slots = ctx.Engine.RearGuardSlots(ctx.Owner, _ => true);
|
||||||
|
if (slots.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ctx.Engine.AskUnitChoice(
|
||||||
|
ctx.Owner,
|
||||||
|
ctx.Owner,
|
||||||
|
"Выберите Арьергард, чтобы поднять",
|
||||||
|
slots,
|
||||||
|
slot => ctx.State.GetUnit(ctx.Owner, slot)!.Rested = false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Juggernaut Maximum и Highspeed Brakki: на (RC) Soul Blast 1 → +5000 до конца боя,
|
||||||
|
/// в начале завершающего шага боя карта возвращается в колоду.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReturnToDeckOnAttackScript : CardScript
|
||||||
|
{
|
||||||
|
public ReturnToDeckOnAttackScript(string nameId) => NameId = nameId;
|
||||||
|
|
||||||
|
public override string NameId { get; }
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||||
|
Optional = true,
|
||||||
|
Condition = ctx => !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||||
|
CanPay = ctx => ctx.Engine.CanSoulBlast(ctx.Owner, 1),
|
||||||
|
Pay = ctx => ctx.Engine.SoulBlast(ctx.Owner, 1),
|
||||||
|
Resolve = ctx =>
|
||||||
|
{
|
||||||
|
ctx.Engine.AddBattlePower(ctx.Source, 5000);
|
||||||
|
ctx.Source.ReturnToDeck = ReturnToDeckTiming.AfterBattle;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lozenge Magus и Battleraizer: при смене можно призвать себя; при поддержке
|
||||||
|
/// усиленный получает +3000 до конца боя, а в начале Завершающей фазы карта
|
||||||
|
/// возвращается в колоду.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReturnToDeckOnBoostScript : CardScript
|
||||||
|
{
|
||||||
|
private readonly Clan _clan;
|
||||||
|
|
||||||
|
public ReturnToDeckOnBoostScript(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.OnRideOver,
|
||||||
|
Optional = true,
|
||||||
|
Condition = ctx =>
|
||||||
|
ctx.Other is not null &&
|
||||||
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == _clan,
|
||||||
|
Resolve = ctx => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner),
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnBoost,
|
||||||
|
Condition = ctx => ctx.Other is not null,
|
||||||
|
Resolve = ctx =>
|
||||||
|
{
|
||||||
|
ctx.Engine.AddBattlePower(ctx.Other!, 3000, ctx.Source);
|
||||||
|
ctx.Source.ReturnToDeck = ReturnToDeckTiming.EndOfTurn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rock the Wall и Redshoe Milly: попав в сброс из круга защиты, карта уходит в душу.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DropToSoulFromGuardianScript : CardScript
|
||||||
|
{
|
||||||
|
public DropToSoulFromGuardianScript(string nameId) => NameId = nameId;
|
||||||
|
|
||||||
|
public override string NameId { get; }
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnLeaveGuardian,
|
||||||
|
Resolve = ctx => ctx.Engine.MoveDropToSoul(ctx.Owner, ctx.Source),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guiding Zombie: при смене можно призвать себя; на (RC) положить себя в душу
|
||||||
|
/// и отправить три верхние карты колоды в сброс.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GuidingZombieScript : CardScript
|
||||||
|
{
|
||||||
|
public override string NameId => "guiding-zombie";
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnRideOver,
|
||||||
|
Optional = true,
|
||||||
|
Condition = ctx =>
|
||||||
|
ctx.Other is not null &&
|
||||||
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.Granblue,
|
||||||
|
Resolve = ctx => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<ActivatedAbility> Activated
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new ActivatedAbility
|
||||||
|
{
|
||||||
|
CanActivate = _ => true,
|
||||||
|
Resolve = ctx =>
|
||||||
|
{
|
||||||
|
var p = ctx.State.Players[ctx.Owner];
|
||||||
|
var slot = Array.IndexOf(p.RearGuards, ctx.Source);
|
||||||
|
if (slot < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
p.RearGuards[slot] = null;
|
||||||
|
p.Soul.Add(ctx.Source);
|
||||||
|
ctx.Engine.MillTop(ctx.Owner, 3);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Brutal Jack: Сдерживание; за Counter Blast 1 теряет его до конца хода;
|
/// Brutal Jack: Сдерживание; за Counter Blast 1 теряет его до конца хода;
|
||||||
/// получает +5000 до конца боя, когда его поддерживает Nova Grappler.
|
/// получает +5000 до конца боя, когда его поддерживает Nova Grappler.
|
||||||
|
|
|
||||||
|
|
@ -815,6 +815,46 @@ public sealed class GameEngine
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Положить верхние карты колоды в сброс.</summary>
|
||||||
|
public void MillTop(int player, int count)
|
||||||
|
{
|
||||||
|
var p = State.Players[player];
|
||||||
|
for (var i = 0; i < count && p.Deck.Count > 0; i++)
|
||||||
|
{
|
||||||
|
var card = p.Deck[0];
|
||||||
|
p.Deck.RemoveAt(0);
|
||||||
|
p.Drop.Add(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Переместить карту из сброса в душу (Rock the Wall, Redshoe Milly).</summary>
|
||||||
|
public bool MoveDropToSoul(int player, CardInstance card)
|
||||||
|
{
|
||||||
|
var p = State.Players[player];
|
||||||
|
if (!p.Drop.Remove(card))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
p.Soul.Add(card);
|
||||||
|
_events.Add(new CardMovedToSoulEvent(player, card.PrintingId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Вернуть карту с поля в колоду и перемешать. Авангард так вернуть нельзя.</summary>
|
||||||
|
public bool ReturnUnitToDeck(int player, CardInstance unit)
|
||||||
|
{
|
||||||
|
var p = State.Players[player];
|
||||||
|
var slot = Array.IndexOf(p.RearGuards, unit);
|
||||||
|
if (slot < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
p.RearGuards[slot] = null;
|
||||||
|
unit.ReturnToDeck = null;
|
||||||
|
p.Deck.Add(unit);
|
||||||
|
ShuffleDeck(player);
|
||||||
|
_events.Add(new UnitReturnedToDeckEvent(player, unit.PrintingId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- поиск в колоде ----------
|
// ---------- поиск в колоде ----------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1521,6 +1561,16 @@ public sealed class GameEngine
|
||||||
State.Phase = Phase.End;
|
State.Phase = Phase.End;
|
||||||
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.End));
|
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.End));
|
||||||
|
|
||||||
|
// «в начале Завершающей фазы вернуть в колоду»
|
||||||
|
foreach (var player in new[] { 0, 1 })
|
||||||
|
{
|
||||||
|
foreach (var unit in AllUnits(player).ToList())
|
||||||
|
{
|
||||||
|
if (unit.ReturnToDeck == ReturnToDeckTiming.EndOfTurn)
|
||||||
|
ReturnUnitToDeck(player, unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ClearTurnModifiers();
|
ClearTurnModifiers();
|
||||||
|
|
||||||
if (State.IsFinished)
|
if (State.IsFinished)
|
||||||
|
|
@ -1638,6 +1688,16 @@ public sealed class GameEngine
|
||||||
State.Players[player].Hand.Add(card);
|
State.Players[player].Hand.Add(card);
|
||||||
_events.Add(new CardDrawnEvent(player));
|
_events.Add(new CardDrawnEvent(player));
|
||||||
battle.ChecksRemaining--;
|
battle.ChecksRemaining--;
|
||||||
|
|
||||||
|
var checkedAttacker = State.GetUnit(player, battle.AttackerSlot);
|
||||||
|
TriggerFromCard(player, checkedAttacker, AbilityTrigger.OnDriveCheck, card);
|
||||||
|
|
||||||
|
if (State.Step is Step.Ability or Step.Choice)
|
||||||
|
{
|
||||||
|
State.Continuation = () => ContinueDriveChecks(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1833,11 +1893,25 @@ public sealed class GameEngine
|
||||||
|
|
||||||
private void ClearBattle(BattleState battle)
|
private void ClearBattle(BattleState battle)
|
||||||
{
|
{
|
||||||
var defender = State.Players[1 - battle.AttackerPlayer];
|
var defenderIndex = 1 - battle.AttackerPlayer;
|
||||||
|
var defender = State.Players[defenderIndex];
|
||||||
|
|
||||||
|
var leftGuardian = new List<CardInstance>();
|
||||||
foreach (var guard in battle.GuardCards)
|
foreach (var guard in battle.GuardCards)
|
||||||
{
|
{
|
||||||
defender.Guardian.Remove(guard);
|
defender.Guardian.Remove(guard);
|
||||||
defender.Drop.Add(guard);
|
defender.Drop.Add(guard);
|
||||||
|
leftGuardian.Add(guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// карты, которые должны вернуться в колоду сразу после боя
|
||||||
|
foreach (var player in new[] { 0, 1 })
|
||||||
|
{
|
||||||
|
foreach (var unit in AllUnits(player).ToList())
|
||||||
|
{
|
||||||
|
if (unit.ReturnToDeck == ReturnToDeckTiming.AfterBattle)
|
||||||
|
ReturnUnitToDeck(player, unit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||||
|
|
@ -1851,6 +1925,10 @@ public sealed class GameEngine
|
||||||
State.PendingTrigger = null;
|
State.PendingTrigger = null;
|
||||||
State.Step = Step.None;
|
State.Step = Step.None;
|
||||||
State.ActivePlayer = battle.AttackerPlayer;
|
State.ActivePlayer = battle.AttackerPlayer;
|
||||||
|
|
||||||
|
// бой уже закрыт: эффекты «попала в сброс из (GC)» срабатывают после
|
||||||
|
foreach (var guard in leftGuardian)
|
||||||
|
TriggerFromCard(defenderIndex, guard, AbilityTrigger.OnLeaveGuardian, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DealDamage(int player, int amount)
|
private void DealDamage(int player, int amount)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ public sealed record SearchFailedEvent(int Player, string Query) : GameEvent;
|
||||||
public sealed record CardRevealedEvent(int Player, string PrintingId) : GameEvent;
|
public sealed record CardRevealedEvent(int Player, string PrintingId) : GameEvent;
|
||||||
public sealed record CardReturnedToDeckEvent(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 CardDiscardedEvent(int Player, string PrintingId) : GameEvent;
|
||||||
|
public sealed record UnitReturnedToDeckEvent(int Player, string PrintingId) : GameEvent;
|
||||||
|
public sealed record CardMovedToSoulEvent(int Player, string PrintingId) : GameEvent;
|
||||||
public sealed record GameFinishedEvent(int Winner) : GameEvent;
|
public sealed record GameFinishedEvent(int Winner) : GameEvent;
|
||||||
public sealed record AbilityTriggeredEvent(int Player, string PrintingId, AbilityTrigger Trigger) : GameEvent;
|
public sealed record AbilityTriggeredEvent(int Player, string PrintingId, AbilityTrigger Trigger) : GameEvent;
|
||||||
public sealed record AbilityResolvedEvent(int Player, string PrintingId, bool Accepted) : GameEvent;
|
public sealed record AbilityResolvedEvent(int Player, string PrintingId, bool Accepted) : GameEvent;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,13 @@ public enum TriggerContext
|
||||||
DamageCheck,
|
DamageCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Когда карта, находившаяся на поле, возвращается в колоду.</summary>
|
||||||
|
public enum ReturnToDeckTiming
|
||||||
|
{
|
||||||
|
AfterBattle,
|
||||||
|
EndOfTurn,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Триггер, который ждёт выбора целей от владельца.</summary>
|
/// <summary>Триггер, который ждёт выбора целей от владельца.</summary>
|
||||||
public sealed class PendingTrigger
|
public sealed class PendingTrigger
|
||||||
{
|
{
|
||||||
|
|
@ -84,6 +91,9 @@ public sealed class CardInstance
|
||||||
/// <summary>Запреты, выданные эффектами.</summary>
|
/// <summary>Запреты, выданные эффектами.</summary>
|
||||||
public List<CardRestriction> Restrictions { get; } = new();
|
public List<CardRestriction> Restrictions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Вернуть карту в колоду после боя или в конце хода (Juggernaut, Lozenge).</summary>
|
||||||
|
public ReturnToDeckTiming? ReturnToDeck { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Переопределение Сдерживания, выданное эффектом.
|
/// Переопределение Сдерживания, выданное эффектом.
|
||||||
/// null — как напечатано на карте.
|
/// null — как напечатано на карте.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ public class GameEngineTests
|
||||||
private const string HellSpider = "BT01/039EN"; // Megacolony, запрет вставать
|
private const string HellSpider = "BT01/039EN"; // Megacolony, запрет вставать
|
||||||
private const string BrutalJack = "BT01/029EN"; // Nova Grappler, Сдерживание
|
private const string BrutalJack = "BT01/029EN"; // Nova Grappler, Сдерживание
|
||||||
private const string ToughBoy = "BT01/060EN"; // Nova Grappler, grade 1
|
private const string ToughBoy = "BT01/060EN"; // Nova Grappler, grade 1
|
||||||
|
private const string Asura = "BT01/008EN"; // Nova Grappler, grade 3
|
||||||
|
private const string Juggernaut = "BT01/020EN"; // Spike Brothers, возврат в колоду
|
||||||
|
private const string RockTheWall = "BT01/073EN";
|
||||||
|
private const string GuidingZombie = "BT01/078EN";
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||||
|
|
@ -1270,6 +1274,91 @@ public class GameEngineTests
|
||||||
Assert.Empty(overlord.GrantedAbilities);
|
Assert.Empty(overlord.GrantedAbilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Asura_Kaiser_Stands_RearGuard_On_Grade3_Drive_Check()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
|
||||||
|
var asura = MakeCard(Asura);
|
||||||
|
engine.State.Players[0].Vanguard = asura;
|
||||||
|
|
||||||
|
var rear = MakeCard(ToughBoy);
|
||||||
|
engine.State.Players[0].RearGuards[0] = rear;
|
||||||
|
rear.Rested = true;
|
||||||
|
|
||||||
|
// проверка открывает Mr. Invincible (Nova Grappler 3 ранга), затем обычную карту
|
||||||
|
engine.State.Players[0].Deck.Insert(0, MakeCard("BT01/028EN"));
|
||||||
|
engine.State.Players[0].Deck.Insert(1, MakeCard(Gallatin));
|
||||||
|
engine.State.Players[1].Deck.Insert(0, MakeCard(Gallatin)); // проверка урона без триггера
|
||||||
|
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.NotNull(engine.State.PendingChoice);
|
||||||
|
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||||||
|
|
||||||
|
Assert.False(rear.Rested);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Juggernaut_Returns_To_Deck_After_Battle()
|
||||||
|
{
|
||||||
|
var engine = InMainPhase();
|
||||||
|
var juggernaut = MakeCard(Juggernaut);
|
||||||
|
engine.State.Players[0].RearGuards[3] = juggernaut;
|
||||||
|
juggernaut.Rested = false;
|
||||||
|
engine.SoulCharge(0, 1);
|
||||||
|
|
||||||
|
StackDeck(engine.State.Players[1], Gallatin);
|
||||||
|
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(3, -1)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingAbility);
|
||||||
|
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(engine.State.Players[0].RearGuards[3]);
|
||||||
|
Assert.Contains(engine.State.Players[0].Deck, c => c.PrintingId == Juggernaut);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rock_The_Wall_Moves_To_Soul_After_Leaving_Guardian()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttack();
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||||
|
|
||||||
|
engine.State.Players[1].Hand.Add(MakeCard(RockTheWall));
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new GuardCommand(RockTheWall)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
var defender = engine.State.Players[1];
|
||||||
|
Assert.Contains(defender.Soul, c => c.PrintingId == RockTheWall);
|
||||||
|
Assert.DoesNotContain(defender.Drop, c => c.PrintingId == RockTheWall);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Guiding_Zombie_Puts_Itself_Into_Soul_And_Mills_Three()
|
||||||
|
{
|
||||||
|
var engine = InMainPhase();
|
||||||
|
var zombie = MakeCard(GuidingZombie);
|
||||||
|
engine.State.Players[0].Hand.Add(zombie);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new CallCommand(GuidingZombie, 0)).Accepted);
|
||||||
|
zombie.Rested = false;
|
||||||
|
|
||||||
|
var deckBefore = engine.State.Players[0].Deck.Count;
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ActivateAbilityCommand(zombie.Id)).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||||||
|
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == GuidingZombie);
|
||||||
|
Assert.Equal(3, deckBefore - engine.State.Players[0].Deck.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue