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()