diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md
index 63f9e8f..62a6b02 100644
--- a/IMPLEMENTATION_CHECKLIST.md
+++ b/IMPLEMENTATION_CHECKLIST.md
@@ -120,6 +120,7 @@
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» ещё нет.
+- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
diff --git a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
index d685fcd..7464565 100644
--- a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
+++ b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
@@ -20,9 +20,42 @@ public static class CardScripts
["tyrant-deathrex"] = new DeathrexScript(),
["demon-eater"] = new DemonEaterScript(),
["maiden-of-libra"] = new MaidenOfLibraScript(),
+ ["knight-of-rose-morgana"] = new MorganaScript(),
};
}
+///
+/// Knight of Rose, Morgana: при атаке можно сбросить карту из руки и получить +4000 до конца хода.
+///
+public sealed class MorganaScript : CardScript
+{
+ public override string NameId => "knight-of-rose-morgana";
+
+ public override IEnumerable Abilities
+ {
+ get
+ {
+ yield return new AbilityDefinition
+ {
+ Trigger = AbilityTrigger.OnAttackDeclared,
+ Optional = true,
+ CanPay = ctx => ctx.State.Players[ctx.Owner].Hand.Count > 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.Source.TemporaryPower += 4000;
+ }),
+ };
+ }
+ }
+}
+
/// Tyrant, Deathrex: когда эта карта атакует, получает +5000 до конца боя.
public sealed class DeathrexScript : CardScript
{
diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs
index 3bfdd8e..5b7b375 100644
--- a/server/src/Vanguard.Rules/Engine/GameEngine.cs
+++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs
@@ -692,6 +692,33 @@ public sealed class GameEngine
public bool HasEmptyRearGuard(int player) =>
State.Players[player].RearGuards.Any(u => u is null);
+ /// Выбор карты из руки (стоимость, сброс, цель эффекта).
+ public void AskHandCardChoice(
+ int player,
+ string prompt,
+ Func? predicate,
+ Action onChosen)
+ {
+ var cards = State.Players[player].Hand
+ .Where(c => predicate is null || predicate(c))
+ .ToList();
+
+ if (cards.Count == 0)
+ return;
+
+ State.PendingChoice = new PendingChoice
+ {
+ Player = player,
+ Prompt = prompt,
+ Options = cards
+ .Select((card, index) => new ChoiceOption(index, State.Catalog.CardOf(card.PrintingId).NameEn))
+ .ToList(),
+ Resolve = index => onChosen(cards[index]),
+ };
+ State.Step = Step.Choice;
+ State.ActivePlayer = player;
+ }
+
public void AskEmptyRearGuardChoice(int player, string prompt, Action onChosen)
{
var slots = Enumerable.Range(0, PlayerState.RearGuardSlots)
diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
index 2ad56f4..2975f8f 100644
--- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
+++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
@@ -26,6 +26,7 @@ public class GameEngineTests
private const string Libra = "BT01/017EN"; // OTT, grade 2
private const string Deathrex = "BT01/033EN"; // Tachikaze, 10000
private const string DemonEater = "BT01/036EN"; // Dark Irregulars, 10000
+ private const string Morgana = "TD01/011EN"; // Royal Paladin, grade 1, discard +4000
[Fact]
public void Start_Places_Starting_Vanguard_And_Draws_Five()
@@ -940,6 +941,30 @@ public class GameEngineTests
Assert.NotNull(engine.State.PendingAbility);
}
+ [Fact]
+ public void Morgana_Discards_To_Gain_4000_When_Attacking()
+ {
+ var engine = StartedEngine();
+ engine.Execute(new SkipRideCommand());
+ SetVanguard(engine, 0, Morgana);
+ engine.Execute(new EnterBattleCommand());
+
+ var morgana = engine.State.Players[0].Vanguard!;
+ var handBefore = engine.State.Players[0].Hand.Count;
+
+ Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
+ Assert.NotNull(engine.State.PendingAbility);
+
+ Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
+ Assert.NotNull(engine.State.PendingChoice);
+
+ Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
+
+ Assert.Equal(4000, morgana.TemporaryPower);
+ Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
+ Assert.Equal(Step.Guard, engine.State.Step);
+ }
+
[Fact]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{
@@ -1079,6 +1104,7 @@ public class GameEngineTests
(Lien, 2),
(Cocoa, 2), (Libra, 2),
(Deathrex, 2), (DemonEater, 2),
+ (Morgana, 2),
};
private List BuildDeck()