Finish TD01/TD02 scripts and the last BT01 effects
New triggers: OnBoostedHit, OnRetireFromRC, OnOpponentRetire, OnGuard; activated abilities from hand and drop zone; superior ride from drop. Cards: Voidmaster, Hagakure, Dreadmaster, Monster Frank, Blightops, Llew, Dragon Knight Aleph, Gancelot hand effect, Deathrex second effect, Brigitte, Bors, Tristan, Starlight Unicorn, Goku, Berserk Dragon, Aermo, Joka, Rakshasa. Four tests; 111 total.
This commit is contained in:
parent
7ca53975d7
commit
d30be8cec2
|
|
@ -151,9 +151,11 @@
|
|||
- [x] RU/EN тексты для всех 94.
|
||||
- [x] Сила/щит для 14 карт, которых нет в BT01.
|
||||
- [ ] Дашборд покрытия: `scripted / tested / total`.
|
||||
— сейчас заскриптовано **42** карты: 13 базовых + 29 в `CardScriptsBT01.cs`.
|
||||
— сейчас заскриптовано **58** карт из 66 с текстом. Остались: Tejas (нужно правило атаки заднего ряда) и Yaksha (триггер из руки). Шесть карт — только напоминания о Sentinel/Heal, им скрипт не нужен.
|
||||
- [x] Триггеры `OnDriveCheck` (Asura Kaiser, Brigitte, Tristan, Goku), `OnBoostedHit` (Dreadmaster, Aermo), `OnGuard` (Hagakure), `OnRetireFromRC` (Blightops), `OnOpponentRetire` (Joka, Rakshasa).
|
||||
- [x] Способности из руки (Gancelot) и из сброса (Monster Frank).
|
||||
- [x] Возврат карты с поля в колоду: после боя (Juggernaut, Brakki) и в конце хода (Lozenge, Battleraizer).
|
||||
- [x] Триггеры `OnDriveCheck` (Asura Kaiser) и `OnLeaveGuardian` (Rock the Wall, Redshoe Milly).
|
||||
- [x] Триггер `OnLeaveGuardian` (Rock the Wall, Redshoe Milly).
|
||||
- [ ] Wyvern Strike, Tejas: атака заднего ряда не выделена — движок разрешает атаковать любой круг Арьергарда.
|
||||
|
||||
**Готово, когда:** 94/94 заскриптованы, 94/94 покрыты тестом, приёмка пройдена по чеклисту карты.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ public enum AbilityTrigger
|
|||
OnAttackDeclared,
|
||||
OnAttackHit,
|
||||
OnBoosted,
|
||||
OnBoostedHit,
|
||||
OnDriveCheck,
|
||||
OnGuard,
|
||||
OnRetireFromRC,
|
||||
OnOpponentRetire,
|
||||
OnLeaveGuardian,
|
||||
OnDamageTaken,
|
||||
}
|
||||
|
|
@ -68,6 +72,12 @@ public sealed class ActivatedAbility
|
|||
/// <summary>Стоимость: уничтожить саму карту после оплаты.</summary>
|
||||
public bool RetiresSelf { get; init; }
|
||||
|
||||
/// <summary>Способность активируется из руки, а не с поля.</summary>
|
||||
public bool FromHand { get; init; }
|
||||
|
||||
/// <summary>Способность активируется из сброса.</summary>
|
||||
public bool FromDropZone { get; init; }
|
||||
|
||||
public required Action<AbilityContext> Resolve { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,22 @@ public static class CardScripts
|
|||
["rock-the-wall"] = new DropToSoulFromGuardianScript("rock-the-wall"),
|
||||
["redshoe-milly"] = new DropToSoulFromGuardianScript("redshoe-milly"),
|
||||
["guiding-zombie"] = new GuidingZombieScript(),
|
||||
["stealth-dragon-voidmaster"] = new VoidmasterScript(),
|
||||
["stealth-beast-hagakure"] = new HagakureScript(),
|
||||
["stealth-dragon-dreadmaster"] = new DreadmasterScript(),
|
||||
["monster-frank"] = new MonsterFrankScript(),
|
||||
["assault-dragon-blightops"] = new BlightopsScript(),
|
||||
["future-knight-llew"] = new LlewScript(),
|
||||
["dragon-knight-aleph"] = new DragonKnightAlephScript(),
|
||||
["crimson-butterfly-brigitte"] = new DriveCheckGradeScript("crimson-butterfly-brigitte", Clan.RoyalPaladin),
|
||||
["knight-of-the-harp-tristan"] = new DriveCheckGradeScript("knight-of-the-harp-tristan", Clan.RoyalPaladin),
|
||||
["knight-of-conviction-bors"] = new BorsScript(),
|
||||
["starlight-unicorn"] = new StarlightUnicornScript(),
|
||||
["dragon-monk-goku"] = new GokuScript(),
|
||||
["berserk-dragon"] = new BerserkDragonScript(),
|
||||
["flame-of-hope-aermo"] = new AermoScript(),
|
||||
["demonic-dragon-madonna-joka"] = new OpponentRetireBonusScript("demonic-dragon-madonna-joka"),
|
||||
["demonic-dragon-mage-rakshasa"] = new OpponentRetireBonusScript("demonic-dragon-mage-rakshasa"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +120,25 @@ public sealed class DeathrexScript : CardScript
|
|||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 5000),
|
||||
};
|
||||
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
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.Engine.RetireRearGuard(ctx.Owner, slot));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -353,6 +353,23 @@ public sealed class GancelotScript : CardScript
|
|||
critical: 1);
|
||||
},
|
||||
};
|
||||
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
FromHand = true,
|
||||
CanActivate = _ => true,
|
||||
Resolve = ctx =>
|
||||
{
|
||||
var p = ctx.State.Players[ctx.Owner];
|
||||
p.Hand.Remove(ctx.Source);
|
||||
p.Deck.Insert(0, ctx.Source);
|
||||
|
||||
ctx.Engine.SearchDeckToHand(
|
||||
ctx.Owner,
|
||||
"Найдите Blaster Blade",
|
||||
d => d.NameId == "blaster-blade");
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -813,6 +830,235 @@ public sealed class GuidingZombieScript : CardScript
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stealth Dragon, Voidmaster: +3000 при атаке, если карт в руке больше;
|
||||
/// при попадании Counter Blast 1, если карт в руке меньше, — противник сбрасывает карту.
|
||||
/// </summary>
|
||||
public sealed class VoidmasterScript : CardScript
|
||||
{
|
||||
public override string NameId => "stealth-dragon-voidmaster";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||
Condition = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Engine.HandCount(ctx.Owner) > ctx.Engine.HandCount(1 - ctx.Owner),
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||||
};
|
||||
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackHit,
|
||||
Optional = true,
|
||||
Condition = ctx =>
|
||||
ctx.Engine.HandCount(ctx.Owner) < ctx.Engine.HandCount(1 - ctx.Owner),
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx => ctx.Engine.OpponentDiscardsChosen(
|
||||
1 - ctx.Owner,
|
||||
"Выберите карту и сбросьте её"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stealth Beast, Hagakure: попав в круг защиты, Counter Blast 1 при Авангарде Nubatama
|
||||
/// и меньшем числе карт в руке — противник сбрасывает карту.
|
||||
/// </summary>
|
||||
public sealed class HagakureScript : CardScript
|
||||
{
|
||||
public override string NameId => "stealth-beast-hagakure";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnGuard,
|
||||
Optional = true,
|
||||
Condition = ctx =>
|
||||
ctx.Engine.VanguardClan(ctx.Owner) == Clan.Nubatama &&
|
||||
ctx.Engine.HandCount(ctx.Owner) < ctx.Engine.HandCount(1 - ctx.Owner),
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx => ctx.Engine.OpponentDiscardsChosen(
|
||||
1 - ctx.Owner,
|
||||
"Выберите карту и сбросьте её"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stealth Dragon, Dreadmaster: когда атака, которую поддержала эта карта, попадает,
|
||||
/// при меньшем числе карт в руке можно Counter Blast 1, чтобы противник сбросил карту.
|
||||
/// </summary>
|
||||
public sealed class DreadmasterScript : CardScript
|
||||
{
|
||||
public override string NameId => "stealth-dragon-dreadmaster";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnBoostedHit,
|
||||
Optional = true,
|
||||
Condition = ctx =>
|
||||
ctx.Engine.HandCount(ctx.Owner) < ctx.Engine.HandCount(1 - ctx.Owner) &&
|
||||
ctx.State.Battle is { } battle &&
|
||||
ctx.State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot) is { } boosted &&
|
||||
ctx.Catalog.CardOf(boosted.PrintingId).Clan == Clan.Nubatama,
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx => ctx.Engine.OpponentDiscardsChosen(
|
||||
1 - ctx.Owner,
|
||||
"Выберите карту и сбросьте её"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monster Frank: из сброса Counter Blast 3, если Авангард 2 ранга, — райд этой картой.
|
||||
/// </summary>
|
||||
public sealed class MonsterFrankScript : CardScript
|
||||
{
|
||||
public override string NameId => "monster-frank";
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
FromDropZone = true,
|
||||
CanActivate = ctx =>
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 3) &&
|
||||
ctx.Engine.VanguardGrade(ctx.Owner) == 2,
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 3);
|
||||
ctx.Engine.RideFromDrop(ctx.Source, ctx.Owner);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assault Dragon, Blightops: попав в сброс из круга Арьергарда, Counter Blast 1 →
|
||||
/// найти Ironclad Dragon, Shieldon в руку.
|
||||
/// </summary>
|
||||
public sealed class BlightopsScript : CardScript
|
||||
{
|
||||
public override string NameId => "assault-dragon-blightops";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnRetireFromRC,
|
||||
Optional = true,
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx => ctx.Engine.SearchDeckToHand(
|
||||
ctx.Owner,
|
||||
"Найдите Ironclad Dragon, Shieldon",
|
||||
d => d.NameId == "ironclad-dragon-shieldon"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Future Knight, Llew: Counter Blast 1 и три именованные карты с кругов в душу,
|
||||
/// при Авангарде 1 ранга — найти Blaster Blade и совершить им райд.
|
||||
/// </summary>
|
||||
public sealed class LlewScript : CardScript
|
||||
{
|
||||
private static readonly string[] Cost =
|
||||
{
|
||||
"future-knight-llew",
|
||||
"barcgal",
|
||||
"flogal",
|
||||
};
|
||||
|
||||
public override string NameId => "future-knight-llew";
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx =>
|
||||
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Engine.VanguardGrade(ctx.Owner) == 1 &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 1) &&
|
||||
ctx.Engine.HasFieldUnits(ctx.Owner, Cost),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 1);
|
||||
ctx.Engine.PutFieldUnitsToSoul(ctx.Owner, Cost);
|
||||
ctx.Engine.SearchDeckToRide(
|
||||
ctx.Owner,
|
||||
"Найдите Blaster Blade",
|
||||
d => d.NameId == "blaster-blade");
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dragon Knight, Aleph: Counter Blast 1 и Bahr с Tahr с кругов в душу →
|
||||
/// найти Embodiment of Victory, Aleph и совершить им райд.
|
||||
/// </summary>
|
||||
public sealed class DragonKnightAlephScript : CardScript
|
||||
{
|
||||
private static readonly string[] Cost =
|
||||
{
|
||||
"embodiment-of-armor-bahr",
|
||||
"embodiment-of-spear-tahr",
|
||||
};
|
||||
|
||||
public override string NameId => "dragon-knight-aleph";
|
||||
|
||||
public override IEnumerable<ActivatedAbility> Activated
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new ActivatedAbility
|
||||
{
|
||||
CanActivate = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Engine.CanCounterBlast(ctx.Owner, 1) &&
|
||||
ctx.Engine.HasFieldUnits(ctx.Owner, Cost),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
ctx.Engine.CounterBlast(ctx.Owner, 1);
|
||||
ctx.Engine.PutFieldUnitsToSoul(ctx.Owner, Cost);
|
||||
ctx.Engine.SearchDeckToRide(
|
||||
ctx.Owner,
|
||||
"Найдите Embodiment of Victory, Aleph",
|
||||
d => d.NameId == "embodiment-of-victory-aleph");
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brutal Jack: Сдерживание; за Counter Blast 1 теряет его до конца хода;
|
||||
/// получает +5000 до конца боя, когда его поддерживает Nova Grappler.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
using Vanguard.Rules.Cards;
|
||||
|
||||
namespace Vanguard.Rules.Engine.Abilities;
|
||||
|
||||
/// <summary>
|
||||
/// «Когда проверка открывает карту 3 ранга нужного клана, эта карта получает
|
||||
/// +5000 до конца боя»: Crimson Butterfly, Brigitte и Knight of the Harp, Tristan.
|
||||
/// </summary>
|
||||
public sealed class DriveCheckGradeScript : CardScript
|
||||
{
|
||||
private readonly Clan _clan;
|
||||
|
||||
public DriveCheckGradeScript(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.OnDriveCheck,
|
||||
Condition = ctx =>
|
||||
ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Other is not null &&
|
||||
ctx.Catalog.CardOf(ctx.Other.PrintingId) is { Grade: 3 } checkedCard &&
|
||||
checkedCard.Clan == _clan,
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 5000),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Knight of Conviction, Bors: при атаке Counter Blast 1 → +3000 до конца боя.</summary>
|
||||
public sealed class BorsScript : CardScript
|
||||
{
|
||||
public override string NameId => "knight-of-conviction-bors";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||
Optional = true,
|
||||
CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 1),
|
||||
Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 1),
|
||||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starlight Unicorn: при размещении на (RC) выбрать другого Royal Paladin
|
||||
/// и дать ему +2000 до конца хода.
|
||||
/// </summary>
|
||||
public sealed class StarlightUnicornScript : CardScript
|
||||
{
|
||||
public override string NameId => "starlight-unicorn";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPlace,
|
||||
Condition = ctx => !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner),
|
||||
Resolve = ctx =>
|
||||
{
|
||||
var slots = new List<int>();
|
||||
|
||||
if (ctx.State.Players[ctx.Owner].Vanguard is { } vanguard
|
||||
&& vanguard != ctx.Source
|
||||
&& ctx.Catalog.CardOf(vanguard.PrintingId).Clan == Clan.RoyalPaladin)
|
||||
{
|
||||
slots.Add(-1);
|
||||
}
|
||||
|
||||
slots.AddRange(ctx.Engine
|
||||
.RearGuardSlots(ctx.Owner, d => d.Clan == Clan.RoyalPaladin)
|
||||
.Where(slot => ctx.State.GetUnit(ctx.Owner, slot) != ctx.Source));
|
||||
|
||||
if (slots.Count == 0)
|
||||
return;
|
||||
|
||||
ctx.Engine.AskUnitChoice(
|
||||
ctx.Owner,
|
||||
ctx.Owner,
|
||||
"Выберите другого Royal Paladin",
|
||||
slots,
|
||||
slot => ctx.Engine.AddPower(
|
||||
ctx.State.GetUnit(ctx.Owner, slot)!,
|
||||
2000,
|
||||
ctx.Source,
|
||||
"starlight-unicorn",
|
||||
ModifierDuration.UntilEndOfTurn));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dragon Monk, Goku: проверка открыла Kagero 3 ранга → уничтожить Арьергард
|
||||
/// противника 1 ранга или ниже.
|
||||
/// </summary>
|
||||
public sealed class GokuScript : CardScript
|
||||
{
|
||||
public override string NameId => "dragon-monk-goku";
|
||||
|
||||
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.Kagero },
|
||||
Resolve = ctx =>
|
||||
{
|
||||
var opponent = 1 - ctx.Owner;
|
||||
var slots = ctx.Engine.RearGuardSlots(opponent, d => d.Grade <= 1);
|
||||
if (slots.Count == 0)
|
||||
return;
|
||||
|
||||
ctx.Engine.AskUnitChoice(
|
||||
ctx.Owner,
|
||||
opponent,
|
||||
"Выберите Арьергард противника 1 ранга или ниже",
|
||||
slots,
|
||||
slot => ctx.Engine.RetireRearGuard(opponent, slot));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Berserk Dragon: при размещении на (VC)/(RC), если Авангард Kagero,
|
||||
/// Counter Blast 2 → уничтожить Арьергард противника 2 ранга или ниже.
|
||||
/// </summary>
|
||||
public sealed class BerserkDragonScript : CardScript
|
||||
{
|
||||
public override string NameId => "berserk-dragon";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnPlace,
|
||||
Optional = true,
|
||||
Condition = ctx => ctx.Engine.VanguardClan(ctx.Owner) == Clan.Kagero,
|
||||
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>
|
||||
/// Flame of Hope, Aermo: когда атака, которую поддержала эта карта, попадает,
|
||||
/// можно сбросить карту из руки и взять одну.
|
||||
/// </summary>
|
||||
public sealed class AermoScript : CardScript
|
||||
{
|
||||
public override string NameId => "flame-of-hope-aermo";
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnBoostedHit,
|
||||
Optional = true,
|
||||
CanPay = ctx => ctx.Engine.HandCount(ctx.Owner) > 0,
|
||||
Resolve = ctx => ctx.Engine.AskHandCardChoice(
|
||||
ctx.Owner,
|
||||
"Сбросьте карту из руки",
|
||||
null,
|
||||
card =>
|
||||
{
|
||||
var p = ctx.State.Players[ctx.Owner];
|
||||
p.Hand.Remove(card);
|
||||
p.Drop.Add(card);
|
||||
ctx.Engine.DrawCards(ctx.Owner, 1);
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Demonic Dragon Madonna, Joka и Demonic Dragon Mage, Rakshasa: в Основную фазу,
|
||||
/// когда Арьергард противника уходит в сброс, эта карта получает +3000 до конца хода.
|
||||
/// </summary>
|
||||
public sealed class OpponentRetireBonusScript : CardScript
|
||||
{
|
||||
public OpponentRetireBonusScript(string nameId) => NameId = nameId;
|
||||
|
||||
public override string NameId { get; }
|
||||
|
||||
public override IEnumerable<AbilityDefinition> Abilities
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new AbilityDefinition
|
||||
{
|
||||
Trigger = AbilityTrigger.OnOpponentRetire,
|
||||
Condition = ctx =>
|
||||
!ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) &&
|
||||
ctx.Phase == Phase.Main,
|
||||
Resolve = ctx => ctx.Engine.AddPower(
|
||||
ctx.Source,
|
||||
3000,
|
||||
ctx.Source,
|
||||
"opponent-retire",
|
||||
ModifierDuration.UntilEndOfTurn),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -148,8 +148,7 @@ public sealed class GameEngine
|
|||
}
|
||||
|
||||
/// <summary>Собирает срабатывания со всех карт на поле, активный игрок первым.</summary>
|
||||
private void TriggerAll(AbilityTrigger trigger)
|
||||
{
|
||||
private void TriggerAll(AbilityTrigger trigger) {
|
||||
var triggered = new List<TriggeredAbility>();
|
||||
|
||||
for (var owner = 0; owner < 2; owner++)
|
||||
|
|
@ -180,6 +179,37 @@ public sealed class GameEngine
|
|||
RaiseBatch(triggered);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// «Когда Арьергард противника ушёл в сброс»: срабатывания карт на поле
|
||||
/// противоположной стороны. В руке такие способности пока не ищутся.
|
||||
/// </summary>
|
||||
private void TriggerOpponentRetire(int retiredOwner)
|
||||
{
|
||||
var owner = 1 - retiredOwner;
|
||||
var triggered = new List<TriggeredAbility>();
|
||||
|
||||
foreach (var unit in AllUnits(owner))
|
||||
{
|
||||
var context = new AbilityContext
|
||||
{
|
||||
Engine = this,
|
||||
Source = unit,
|
||||
Owner = owner,
|
||||
Phase = State.Phase,
|
||||
};
|
||||
|
||||
triggered.AddRange(AbilitiesOf(unit)
|
||||
.Where(a => a.Trigger == AbilityTrigger.OnOpponentRetire
|
||||
&& (a.Condition is null || a.Condition(context)))
|
||||
.Select(a => new TriggeredAbility { Definition = a, Context = context }));
|
||||
}
|
||||
|
||||
if (triggered.Count == 0)
|
||||
return;
|
||||
|
||||
RaiseBatch(triggered);
|
||||
}
|
||||
|
||||
private void EnterMainPhase()
|
||||
{
|
||||
State.Phase = Phase.Main;
|
||||
|
|
@ -281,9 +311,25 @@ public sealed class GameEngine
|
|||
|
||||
var player = State.ActivePlayer;
|
||||
var p = State.Players[player];
|
||||
|
||||
var unit = AllUnits(player).FirstOrDefault(u => u.Id == instanceId);
|
||||
var fromHand = false;
|
||||
var fromDrop = false;
|
||||
|
||||
if (unit is null)
|
||||
return CommandResult.Reject("Карта не на поле.");
|
||||
{
|
||||
unit = p.Hand.FirstOrDefault(c => c.Id == instanceId);
|
||||
fromHand = unit is not null;
|
||||
}
|
||||
|
||||
if (unit is null)
|
||||
{
|
||||
unit = p.Drop.FirstOrDefault(c => c.Id == instanceId);
|
||||
fromDrop = unit is not null;
|
||||
}
|
||||
|
||||
if (unit is null)
|
||||
return CommandResult.Reject("Карта не на поле, в руке или в сбросе.");
|
||||
|
||||
var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
|
||||
if (!_scripts.TryGetValue(nameId, out var script))
|
||||
|
|
@ -297,10 +343,15 @@ public sealed class GameEngine
|
|||
Phase = State.Phase,
|
||||
};
|
||||
|
||||
var ability = script.Activated.FirstOrDefault(a => a.CanActivate(context));
|
||||
var ability = script.Activated.FirstOrDefault(a =>
|
||||
(fromHand ? a.FromHand : fromDrop ? a.FromDropZone : !a.FromHand && !a.FromDropZone)
|
||||
&& a.CanActivate(context));
|
||||
if (ability is null)
|
||||
return CommandResult.Reject("Нет доступной способности.");
|
||||
|
||||
if ((fromHand || fromDrop) && (ability.RestsSelf || ability.RetiresSelf))
|
||||
return CommandResult.Reject("Способность из этой зоны не оплачивается поворотом или уничтожением.");
|
||||
|
||||
if (ability.RestsSelf && unit.Rested)
|
||||
return CommandResult.Reject("Карта уже повёрнута.");
|
||||
|
||||
|
|
@ -734,6 +785,9 @@ public sealed class GameEngine
|
|||
State.Players[player].RearGuards[slot] = null;
|
||||
State.Players[player].Drop.Add(unit);
|
||||
_events.Add(new UnitRetiredEvent(player, slot));
|
||||
|
||||
TriggerFromCard(player, unit, AbilityTrigger.OnRetireFromRC, null);
|
||||
TriggerOpponentRetire(player);
|
||||
}
|
||||
|
||||
// ---------- эффекты карт ----------
|
||||
|
|
@ -1022,7 +1076,18 @@ public sealed class GameEngine
|
|||
}
|
||||
|
||||
/// <summary>Райд картой не из руки (superior ride).</summary>
|
||||
public void RideFromDeck(CardInstance card, int player)
|
||||
public void RideFromDeck(CardInstance card, int player) => RideUnit(card, player);
|
||||
|
||||
/// <summary>Райд картой из сброса (Monster Frank).</summary>
|
||||
public void RideFromDrop(CardInstance card, int player)
|
||||
{
|
||||
if (!State.Players[player].Drop.Remove(card))
|
||||
return;
|
||||
|
||||
RideUnit(card, player);
|
||||
}
|
||||
|
||||
private void RideUnit(CardInstance card, int player)
|
||||
{
|
||||
var p = State.Players[player];
|
||||
var riddenOver = p.Vanguard;
|
||||
|
|
@ -1076,6 +1141,70 @@ public sealed class GameEngine
|
|||
return vanguard is null ? Clan.RoyalPaladin : State.Catalog.CardOf(vanguard.PrintingId).Clan;
|
||||
}
|
||||
|
||||
public int VanguardGrade(int player) =>
|
||||
State.Players[player].Vanguard is { } vanguard
|
||||
? State.Catalog.CardOf(vanguard.PrintingId).Grade
|
||||
: 0;
|
||||
|
||||
/// <summary>Есть ли на своих кругах Арьергарда по одной карте каждого имени.</summary>
|
||||
public bool HasFieldUnits(int player, IReadOnlyList<string> nameIds)
|
||||
{
|
||||
var slots = new HashSet<int>();
|
||||
foreach (var nameId in nameIds)
|
||||
{
|
||||
var found = false;
|
||||
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
|
||||
{
|
||||
if (slots.Contains(slot))
|
||||
continue;
|
||||
|
||||
var unit = State.Players[player].RearGuards[slot];
|
||||
if (unit is null || State.Catalog.CardOf(unit.PrintingId).NameId != nameId)
|
||||
continue;
|
||||
|
||||
slots.Add(slot);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Положить по одной карте каждого имени со своих кругов в душу.</summary>
|
||||
public bool PutFieldUnitsToSoul(int player, IReadOnlyList<string> nameIds)
|
||||
{
|
||||
if (!HasFieldUnits(player, nameIds))
|
||||
return false;
|
||||
|
||||
var p = State.Players[player];
|
||||
var used = new HashSet<int>();
|
||||
|
||||
foreach (var nameId in nameIds)
|
||||
{
|
||||
for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++)
|
||||
{
|
||||
if (used.Contains(slot))
|
||||
continue;
|
||||
|
||||
var unit = p.RearGuards[slot];
|
||||
if (unit is null || State.Catalog.CardOf(unit.PrintingId).NameId != nameId)
|
||||
continue;
|
||||
|
||||
used.Add(slot);
|
||||
p.RearGuards[slot] = null;
|
||||
p.Soul.Add(unit);
|
||||
_events.Add(new CardMovedToSoulEvent(player, unit.PrintingId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- старт партии ----------
|
||||
|
||||
public void Start()
|
||||
|
|
@ -1385,6 +1514,7 @@ public sealed class GameEngine
|
|||
AttackerPlayer = attackerPlayer,
|
||||
AttackerSlot = attackerSlot,
|
||||
TargetSlot = targetSlot,
|
||||
BoosterSlot = boosterSlot,
|
||||
};
|
||||
|
||||
var defenderIndex = 1 - attackerPlayer;
|
||||
|
|
@ -1494,6 +1624,8 @@ public sealed class GameEngine
|
|||
_events.Add(new PerfectGuardEvent(defenderIndex, card.PrintingId, discarded.PrintingId));
|
||||
}
|
||||
|
||||
TriggerFromCard(defenderIndex, card, AbilityTrigger.OnGuard, null);
|
||||
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
|
|
@ -1829,6 +1961,44 @@ public sealed class GameEngine
|
|||
|
||||
if (hit)
|
||||
{
|
||||
// попадание: сначала «атака, которую поддержала эта карта, попала»,
|
||||
// затем «эта карта попала»; каждый шаг может встать на выбор игрока
|
||||
void AttackerHitTriggers(CardInstance? hitUnit, Action after)
|
||||
{
|
||||
TriggerFromCard(battle.AttackerPlayer, attacker, AbilityTrigger.OnAttackHit, hitUnit);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
{
|
||||
State.Continuation = after;
|
||||
return;
|
||||
}
|
||||
|
||||
after();
|
||||
}
|
||||
|
||||
void BoosterHitTriggers(CardInstance? hitUnit, Action after)
|
||||
{
|
||||
var booster = battle.BoosterSlot >= 0
|
||||
? State.GetUnit(battle.AttackerPlayer, battle.BoosterSlot)
|
||||
: null;
|
||||
|
||||
if (booster is null)
|
||||
{
|
||||
AttackerHitTriggers(hitUnit, after);
|
||||
return;
|
||||
}
|
||||
|
||||
TriggerFromCard(battle.AttackerPlayer, booster, AbilityTrigger.OnBoostedHit, hitUnit);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
{
|
||||
State.Continuation = () => AttackerHitTriggers(hitUnit, after);
|
||||
return;
|
||||
}
|
||||
|
||||
AttackerHitTriggers(hitUnit, after);
|
||||
}
|
||||
|
||||
if (battle.Interceptor is not null)
|
||||
{
|
||||
// перехватчик уже вышел из круга и уйдёт в сброс при закрытии боя,
|
||||
|
|
@ -1839,18 +2009,7 @@ public sealed class GameEngine
|
|||
{
|
||||
var damage = EffectiveCritical(attacker);
|
||||
|
||||
void AfterDamage()
|
||||
{
|
||||
TriggerFromCard(battle.AttackerPlayer, attacker, AbilityTrigger.OnAttackHit, null);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
{
|
||||
State.Continuation = () => ClearBattle(battle);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearBattle(battle);
|
||||
}
|
||||
void AfterDamage() => BoosterHitTriggers(null, () => ClearBattle(battle));
|
||||
|
||||
void ResolveDamage()
|
||||
{
|
||||
|
|
@ -1875,13 +2034,29 @@ public sealed class GameEngine
|
|||
State.Players[defenderIndex].Drop.Add(target);
|
||||
_events.Add(new UnitRetiredEvent(defenderIndex, battle.TargetSlot));
|
||||
|
||||
TriggerFromCard(battle.AttackerPlayer, attacker, AbilityTrigger.OnAttackHit, target);
|
||||
void AfterRetire()
|
||||
{
|
||||
TriggerOpponentRetire(defenderIndex);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
{
|
||||
State.Continuation = () => ClearBattle(battle);
|
||||
State.Continuation = () => BoosterHitTriggers(target, () => ClearBattle(battle));
|
||||
return;
|
||||
}
|
||||
|
||||
BoosterHitTriggers(target, () => ClearBattle(battle));
|
||||
}
|
||||
|
||||
TriggerFromCard(defenderIndex, target, AbilityTrigger.OnRetireFromRC, null);
|
||||
|
||||
if (State.Step is Step.Ability or Step.Choice)
|
||||
{
|
||||
State.Continuation = AfterRetire;
|
||||
return;
|
||||
}
|
||||
|
||||
AfterRetire();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ public sealed class BattleState
|
|||
/// <summary>Карта, вышедшая на перехват. Сила сравнения берётся по ней.</summary>
|
||||
public CardInstance? Interceptor { get; set; }
|
||||
|
||||
/// <summary>Круг карты поддержки (-1 — поддержки не было).</summary>
|
||||
public int BoosterSlot { get; set; } = -1;
|
||||
|
||||
public List<CardInstance> GuardCards { get; } = new();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,20 @@ public class GameEngineTests
|
|||
private const string Juggernaut = "BT01/020EN"; // Spike Brothers, возврат в колоду
|
||||
private const string RockTheWall = "BT01/073EN";
|
||||
private const string GuidingZombie = "BT01/078EN";
|
||||
private const string Blightops = "BT01/034EN";
|
||||
private const string MonsterFrank = "BT01/037EN";
|
||||
private const string Shieldon = "BT01/067EN";
|
||||
private const string Dka = "BT01/014EN";
|
||||
private const string Bahr = "BT01/048EN";
|
||||
private const string Tahr = "BT01/024EN";
|
||||
private const string AlephVictory = "BT01/005EN";
|
||||
private const string Gancelot = "BT01/010EN";
|
||||
private const string Voidmaster = "BT01/035EN";
|
||||
private const string BlasterBladePrint = "BT01/002EN";
|
||||
private const string Bors = "TD01/002EN";
|
||||
private const string Unicorn = "TD01/010EN";
|
||||
private const string Goku = "TD02/002EN";
|
||||
private const string Joka = "TD02/010EN";
|
||||
|
||||
[Fact]
|
||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||
|
|
@ -1359,6 +1373,214 @@ public class GameEngineTests
|
|||
Assert.Equal(3, deckBefore - engine.State.Players[0].Deck.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Monster_Frank_Rides_From_Drop_Zone()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
engine.State.Players[0].Vanguard = MakeCard(Gallatin);
|
||||
GiveDamage(engine, 0, 3);
|
||||
|
||||
var frank = MakeCard(MonsterFrank);
|
||||
engine.State.Players[0].Drop.Add(frank);
|
||||
|
||||
Assert.True(engine.Execute(new ActivateAbilityCommand(frank.Id)).Accepted);
|
||||
|
||||
Assert.Same(frank, engine.State.Players[0].Vanguard);
|
||||
Assert.DoesNotContain(engine.State.Players[0].Drop, c => c.Id == frank.Id);
|
||||
Assert.Equal(3, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gancelot_Hand_Ability_Puts_Itself_On_Top_And_Searches_Blaster_Blade()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
KeepOnly(engine, 0, "blaster-blade", 0);
|
||||
|
||||
var gancelot = MakeCard(Gancelot);
|
||||
engine.State.Players[0].Hand.Add(gancelot);
|
||||
|
||||
var blade = MakeCard(BlasterBladePrint);
|
||||
engine.State.Players[0].Deck.Insert(0, blade);
|
||||
|
||||
Assert.True(engine.Execute(new ActivateAbilityCommand(gancelot.Id)).Accepted);
|
||||
|
||||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == BlasterBladePrint);
|
||||
Assert.DoesNotContain(engine.State.Players[0].Hand, c => c.Id == gancelot.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blightops_Searches_Shieldon_When_Retired()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
KeepOnly(engine, 0, "ironclad-dragon-shieldon", 0);
|
||||
|
||||
var blightops = MakeCard(Blightops);
|
||||
engine.State.Players[0].RearGuards[0] = blightops;
|
||||
GiveDamage(engine, 0, 1);
|
||||
|
||||
engine.State.Players[0].Deck.Insert(0, MakeCard(Shieldon));
|
||||
|
||||
engine.RetireRearGuard(0, 0);
|
||||
|
||||
Assert.NotNull(engine.State.PendingAbility);
|
||||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||
|
||||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == Shieldon);
|
||||
Assert.Equal(1, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dragon_Knight_Aleph_Rides_Aleph_After_Putting_Cost_Into_Soul()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
KeepOnly(engine, 0, "embodiment-of-victory-aleph", 0);
|
||||
|
||||
var dka = MakeCard(Dka);
|
||||
engine.State.Players[0].Vanguard = dka;
|
||||
|
||||
var bahr = MakeCard(Bahr);
|
||||
var tahr = MakeCard(Tahr);
|
||||
engine.State.Players[0].RearGuards[0] = bahr;
|
||||
engine.State.Players[0].RearGuards[1] = tahr;
|
||||
|
||||
GiveDamage(engine, 0, 1);
|
||||
|
||||
var aleph = MakeCard(AlephVictory);
|
||||
engine.State.Players[0].Deck.Insert(0, aleph);
|
||||
|
||||
Assert.True(engine.Execute(new ActivateAbilityCommand(dka.Id)).Accepted);
|
||||
|
||||
Assert.Same(aleph, engine.State.Players[0].Vanguard);
|
||||
Assert.Contains(engine.State.Players[0].Soul, c => c.Id == bahr.Id);
|
||||
Assert.Contains(engine.State.Players[0].Soul, c => c.Id == tahr.Id);
|
||||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||||
Assert.Null(engine.State.Players[0].RearGuards[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Voidmaster_Makes_Opponent_Discard_When_Hand_Is_Smaller()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
engine.Execute(new SkipRideCommand());
|
||||
|
||||
var voidmaster = MakeCard(Voidmaster);
|
||||
engine.State.Players[0].Vanguard = voidmaster;
|
||||
|
||||
var p0 = engine.State.Players[0];
|
||||
var p1 = engine.State.Players[1];
|
||||
|
||||
while (p0.Hand.Count > 0)
|
||||
{
|
||||
p0.Drop.Add(p0.Hand[0]);
|
||||
p0.Hand.RemoveAt(0);
|
||||
}
|
||||
|
||||
p1.Hand.Add(MakeCard(Gallatin));
|
||||
p1.Hand.Add(MakeCard(Gallatin));
|
||||
|
||||
GiveDamage(engine, 0, 1);
|
||||
|
||||
p0.Deck.Insert(0, MakeCard(Gallatin));
|
||||
p0.Deck.Insert(1, MakeCard(Gallatin));
|
||||
p1.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.PendingAbility);
|
||||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||
|
||||
Assert.NotNull(engine.State.PendingChoice);
|
||||
var handBefore = p1.Hand.Count;
|
||||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||||
|
||||
Assert.Equal(handBefore - 1, p1.Hand.Count);
|
||||
Assert.Equal(1, p0.Damage.Count(c => c.FaceUp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bors_Gets_Power_For_Counter_Blast()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
engine.Execute(new SkipRideCommand());
|
||||
|
||||
var bors = MakeCard(Bors);
|
||||
engine.State.Players[0].Vanguard = bors;
|
||||
GiveDamage(engine, 0, 1);
|
||||
|
||||
engine.Execute(new EnterBattleCommand());
|
||||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||
|
||||
Assert.NotNull(engine.State.PendingAbility);
|
||||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||
|
||||
Assert.Equal(3000, bors.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Goku_Retires_Small_RearGuard_After_Grade3_Drive_Check()
|
||||
{
|
||||
var engine = StartedEngine();
|
||||
engine.Execute(new SkipRideCommand());
|
||||
|
||||
var goku = MakeCard(Goku);
|
||||
engine.State.Players[0].Vanguard = goku;
|
||||
|
||||
var opponent = engine.State.Players[1];
|
||||
opponent.RearGuards[0] = MakeCard(Marron);
|
||||
|
||||
engine.State.Players[0].Deck.Insert(0, MakeCard(Overlord)); // Kagero 3 ранга
|
||||
engine.State.Players[0].Deck.Insert(1, MakeCard(Gallatin));
|
||||
opponent.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.Null(opponent.RearGuards[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Starlight_Unicorn_Gives_Another_Royal_Paladin_2000()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
engine.State.Players[0].Vanguard = MakeCard(Marron);
|
||||
|
||||
var target = MakeCard(Gallatin);
|
||||
engine.State.Players[0].RearGuards[0] = target;
|
||||
|
||||
engine.State.Players[0].Hand.Add(MakeCard(Unicorn));
|
||||
Assert.True(engine.Execute(new CallCommand(Unicorn, 1)).Accepted);
|
||||
|
||||
Assert.NotNull(engine.State.PendingChoice);
|
||||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted); // Авангард, затем Gallatin
|
||||
|
||||
Assert.Equal(
|
||||
2000,
|
||||
target.Modifiers.Where(m => m.EffectId == "starlight-unicorn").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Joka_Gets_Power_When_Opponent_RearGuard_Is_Retired()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
|
||||
var joka = MakeCard(Joka);
|
||||
engine.State.Players[0].RearGuards[0] = joka;
|
||||
|
||||
engine.State.Players[1].RearGuards[1] = MakeCard(Gallatin);
|
||||
|
||||
engine.RetireRearGuard(1, 1);
|
||||
|
||||
Assert.Equal(
|
||||
3000,
|
||||
joka.Modifiers.Where(m => m.EffectId == "opponent-retire").Sum(m => m.Power));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue